[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in ~/spec/r... #4
21 changed files with 991 additions and 443 deletions
3
docs/ref/wails-v3/go.mod
Normal file
3
docs/ref/wails-v3/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/wailsapp/wails/v3
|
||||
|
||||
go 1.26.0
|
||||
9
docs/ref/wails-v3/src/application/assets/index.html
Normal file
9
docs/ref/wails-v3/src/application/assets/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Wails Assets Placeholder</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
40
go.mod
40
go.mod
|
|
@ -9,69 +9,33 @@ require (
|
|||
forge.lthn.ai/core/go-log v0.0.4
|
||||
forge.lthn.ai/core/go-webview v0.1.7
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/leaanthony/u v1.1.1
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
|
||||
)
|
||||
|
||||
replace github.com/wailsapp/wails/v3 => ./stubs/wails
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.17.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/lmittmann/tint v1.1.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/samber/lo v1.53.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
128
go.sum
128
go.sum
|
|
@ -1,5 +1,3 @@
|
|||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
forge.lthn.ai/core/config v0.1.8 h1:xP2hys7T94QGVF/OTh84/Zr5Dm/dL/0vzjht8zi+LOg=
|
||||
forge.lthn.ai/core/config v0.1.8/go.mod h1:8epZrkwoCt+5ayrqdinOUU/+w6UoxOyv9ZrdgVOgYfQ=
|
||||
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||
|
|
@ -10,130 +8,40 @@ forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
|||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
forge.lthn.ai/core/go-webview v0.1.7 h1:9+aEHeAvNcPX8Zwr+UGu0/T+menRm5T1YOmqZ9dawDc=
|
||||
forge.lthn.ai/core/go-webview v0.1.7/go.mod h1:5n1tECD1wBV/uFZRY9ZjfPFO5TYZrlaR3mQFwvO2nek=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
|
|
@ -142,60 +50,24 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -588,7 +588,11 @@ func (s *Service) windowService() *window.Service {
|
|||
|
||||
// OpenWindow creates a new window via IPC.
|
||||
func (s *Service) OpenWindow(options ...window.WindowOption) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskOpenWindow{Options: options})
|
||||
spec, err := window.ApplyOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = s.Core().PERFORM(window.TaskOpenWindow{Window: spec})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -661,97 +665,41 @@ func (s *Service) CloseWindow(name string) error {
|
|||
}
|
||||
|
||||
// RestoreWindow restores a maximized/minimized window.
|
||||
// Uses direct Manager access (no IPC task for restore yet).
|
||||
func (s *Service) RestoreWindow(name string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.RestoreWindow", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.RestoreWindow", "window not found: "+name, nil)
|
||||
}
|
||||
pw.Restore()
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskRestore{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowVisibility shows or hides a window.
|
||||
// Uses direct Manager access (no IPC task for visibility yet).
|
||||
func (s *Service) SetWindowVisibility(name string, visible bool) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SetWindowVisibility", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.SetWindowVisibility", "window not found: "+name, nil)
|
||||
}
|
||||
pw.SetVisibility(visible)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetVisibility{Name: name, Visible: visible})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowAlwaysOnTop sets whether a window stays on top.
|
||||
// Uses direct Manager access (no IPC task for always-on-top yet).
|
||||
func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SetWindowAlwaysOnTop", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.SetWindowAlwaysOnTop", "window not found: "+name, nil)
|
||||
}
|
||||
pw.SetAlwaysOnTop(alwaysOnTop)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowTitle changes a window's title.
|
||||
// Uses direct Manager access (no IPC task for title yet).
|
||||
func (s *Service) SetWindowTitle(name string, title string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SetWindowTitle", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.SetWindowTitle", "window not found: "+name, nil)
|
||||
}
|
||||
pw.SetTitle(title)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetTitle{Name: name, Title: title})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowFullscreen sets a window to fullscreen mode.
|
||||
// Uses direct Manager access (no IPC task for fullscreen yet).
|
||||
func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SetWindowFullscreen", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.SetWindowFullscreen", "window not found: "+name, nil)
|
||||
}
|
||||
if fullscreen {
|
||||
pw.Fullscreen()
|
||||
} else {
|
||||
pw.UnFullscreen()
|
||||
}
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskFullscreen{Name: name, Fullscreen: fullscreen})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowBackgroundColour sets the background colour of a window.
|
||||
// Uses direct Manager access (no IPC task for background colour yet).
|
||||
func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SetWindowBackgroundColour", "window service not available", nil)
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.SetWindowBackgroundColour", "window not found: "+name, nil)
|
||||
}
|
||||
pw.SetBackgroundColour(r, g, b, a)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetBackgroundColour{
|
||||
Name: name, Red: r, Green: g, Blue: b, Alpha: a,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFocusedWindow returns the name of the currently focused window.
|
||||
|
|
@ -818,12 +766,14 @@ func (s *Service) CreateWindow(options CreateWindowOptions) (*window.WindowInfo,
|
|||
return nil, coreerr.E("display.CreateWindow", "window name is required", nil)
|
||||
}
|
||||
result, _, err := s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName(options.Name),
|
||||
window.WithTitle(options.Title),
|
||||
window.WithURL(options.URL),
|
||||
window.WithSize(options.Width, options.Height),
|
||||
window.WithPosition(options.X, options.Y),
|
||||
Window: &window.Window{
|
||||
Name: options.Name,
|
||||
Title: options.Title,
|
||||
URL: options.URL,
|
||||
Width: options.Width,
|
||||
Height: options.Height,
|
||||
X: options.X,
|
||||
Y: options.Y,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -837,143 +787,68 @@ func (s *Service) CreateWindow(options CreateWindowOptions) (*window.WindowInfo,
|
|||
|
||||
// SaveLayout saves the current window arrangement as a named layout.
|
||||
func (s *Service) SaveLayout(name string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SaveLayout", "window service not available", nil)
|
||||
}
|
||||
states := make(map[string]window.WindowState)
|
||||
for _, n := range ws.Manager().List() {
|
||||
if pw, ok := ws.Manager().Get(n); ok {
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
states[n] = window.WindowState{X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised()}
|
||||
}
|
||||
}
|
||||
return ws.Manager().Layout().SaveLayout(name, states)
|
||||
_, _, err := s.Core().PERFORM(window.TaskSaveLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// RestoreLayout applies a saved layout.
|
||||
func (s *Service) RestoreLayout(name string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.RestoreLayout", "window service not available", nil)
|
||||
}
|
||||
layout, ok := ws.Manager().Layout().GetLayout(name)
|
||||
if !ok {
|
||||
return coreerr.E("display.RestoreLayout", "layout not found: "+name, nil)
|
||||
}
|
||||
for wName, state := range layout.Windows {
|
||||
if pw, ok := ws.Manager().Get(wName); ok {
|
||||
pw.SetPosition(state.X, state.Y)
|
||||
pw.SetSize(state.Width, state.Height)
|
||||
if state.Maximized {
|
||||
pw.Maximise()
|
||||
} else {
|
||||
pw.Restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskRestoreLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// ListLayouts returns all saved layout names with metadata.
|
||||
func (s *Service) ListLayouts() []window.LayoutInfo {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
result, handled, _ := s.Core().QUERY(window.QueryLayoutList{})
|
||||
if !handled {
|
||||
return nil
|
||||
}
|
||||
return ws.Manager().Layout().ListLayouts()
|
||||
layouts, _ := result.([]window.LayoutInfo)
|
||||
return layouts
|
||||
}
|
||||
|
||||
// DeleteLayout removes a saved layout by name.
|
||||
func (s *Service) DeleteLayout(name string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.DeleteLayout", "window service not available", nil)
|
||||
}
|
||||
ws.Manager().Layout().DeleteLayout(name)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskDeleteLayout{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLayout returns a specific layout by name.
|
||||
func (s *Service) GetLayout(name string) *window.Layout {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
result, handled, _ := s.Core().QUERY(window.QueryLayoutGet{Name: name})
|
||||
if !handled {
|
||||
return nil
|
||||
}
|
||||
layout, ok := ws.Manager().Layout().GetLayout(name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &layout
|
||||
layout, _ := result.(*window.Layout)
|
||||
return layout
|
||||
}
|
||||
|
||||
// --- Tiling/snapping delegation ---
|
||||
|
||||
// TileWindows arranges windows in a tiled layout.
|
||||
func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.TileWindows", "window service not available", nil)
|
||||
}
|
||||
screenWidth, screenHeight := s.primaryScreenSize()
|
||||
return ws.Manager().TileWindows(mode, windowNames, screenWidth, screenHeight)
|
||||
_, _, err := s.Core().PERFORM(window.TaskTileWindows{Mode: mode.String(), Windows: windowNames})
|
||||
return err
|
||||
}
|
||||
|
||||
// SnapWindow snaps a window to a screen edge or corner.
|
||||
func (s *Service) SnapWindow(name string, position window.SnapPosition) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.SnapWindow", "window service not available", nil)
|
||||
}
|
||||
screenWidth, screenHeight := s.primaryScreenSize()
|
||||
return ws.Manager().SnapWindow(name, position, screenWidth, screenHeight)
|
||||
}
|
||||
|
||||
func (s *Service) primaryScreenSize() (int, int) {
|
||||
const fallbackWidth = 1920
|
||||
const fallbackHeight = 1080
|
||||
|
||||
result, handled, err := s.Core().QUERY(screen.QueryPrimary{})
|
||||
if err != nil || !handled {
|
||||
return fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
primary, ok := result.(*screen.Screen)
|
||||
if !ok || primary == nil {
|
||||
return fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
width := primary.WorkArea.Width
|
||||
height := primary.WorkArea.Height
|
||||
if width <= 0 || height <= 0 {
|
||||
width = primary.Bounds.Width
|
||||
height = primary.Bounds.Height
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
return width, height
|
||||
_, _, err := s.Core().PERFORM(window.TaskSnapWindow{Name: name, Position: position.String()})
|
||||
return err
|
||||
}
|
||||
|
||||
// StackWindows arranges windows in a cascade pattern.
|
||||
func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.StackWindows", "window service not available", nil)
|
||||
}
|
||||
return ws.Manager().StackWindows(windowNames, offsetX, offsetY)
|
||||
_, _, err := s.Core().PERFORM(window.TaskStackWindows{Windows: windowNames, OffsetX: offsetX, OffsetY: offsetY})
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyWorkflowLayout applies a predefined layout for a specific workflow.
|
||||
func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return coreerr.E("display.ApplyWorkflowLayout", "window service not available", nil)
|
||||
}
|
||||
screenWidth, screenHeight := s.primaryScreenSize()
|
||||
return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), screenWidth, screenHeight)
|
||||
_, _, err := s.Core().PERFORM(window.TaskApplyWorkflow{
|
||||
Workflow: workflow.String(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEventManager returns the event manager for WebSocket event subscriptions.
|
||||
|
|
@ -1022,11 +897,12 @@ func ptr[T any](v T) *T { return &v }
|
|||
|
||||
func (s *Service) handleNewWorkspace() {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName("workspace-new"),
|
||||
window.WithTitle("New Workspace"),
|
||||
window.WithURL("/workspace/new"),
|
||||
window.WithSize(500, 400),
|
||||
Window: &window.Window{
|
||||
Name: "workspace-new",
|
||||
Title: "New Workspace",
|
||||
URL: "/workspace/new",
|
||||
Width: 500,
|
||||
Height: 400,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1045,11 +921,12 @@ func (s *Service) handleListWorkspaces() {
|
|||
|
||||
func (s *Service) handleNewFile() {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle("New File - Editor"),
|
||||
window.WithURL("/#/developer/editor?new=true"),
|
||||
window.WithSize(1200, 800),
|
||||
Window: &window.Window{
|
||||
Name: "editor",
|
||||
Title: "New File - Editor",
|
||||
URL: "/#/developer/editor?new=true",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1069,11 +946,12 @@ func (s *Service) handleOpenFile() {
|
|||
return
|
||||
}
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle(paths[0] + " - Editor"),
|
||||
window.WithURL("/#/developer/editor?file=" + paths[0]),
|
||||
window.WithSize(1200, 800),
|
||||
Window: &window.Window{
|
||||
Name: "editor",
|
||||
Title: paths[0] + " - Editor",
|
||||
URL: "/#/developer/editor?file=" + paths[0],
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1081,21 +959,23 @@ func (s *Service) handleOpenFile() {
|
|||
func (s *Service) handleSaveFile() { _ = s.Core().ACTION(ActionIDECommand{Command: "save"}) }
|
||||
func (s *Service) handleOpenEditor() {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle("Editor"),
|
||||
window.WithURL("/#/developer/editor"),
|
||||
window.WithSize(1200, 800),
|
||||
Window: &window.Window{
|
||||
Name: "editor",
|
||||
Title: "Editor",
|
||||
URL: "/#/developer/editor",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
},
|
||||
})
|
||||
}
|
||||
func (s *Service) handleOpenTerminal() {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{
|
||||
window.WithName("terminal"),
|
||||
window.WithTitle("Terminal"),
|
||||
window.WithURL("/#/developer/terminal"),
|
||||
window.WithSize(800, 500),
|
||||
Window: &window.Window{
|
||||
Name: "terminal",
|
||||
Title: "Terminal",
|
||||
URL: "/#/developer/terminal",
|
||||
Width: 800,
|
||||
Height: 500,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ func TestServiceConclave_Good(t *testing.T) {
|
|||
|
||||
// Open a window via IPC
|
||||
result, handled, err := c.PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{window.WithName("main")},
|
||||
Window: &window.Window{Name: "main"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
|
@ -413,7 +413,7 @@ func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
|
|||
// Open a window — this should trigger ActionWindowOpened
|
||||
// which HandleIPCEvents should convert to a WS event
|
||||
result, handled, err := c.PERFORM(window.TaskOpenWindow{
|
||||
Options: []window.WindowOption{window.WithName("test")},
|
||||
Window: &window.Window{Name: "test"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,43 @@ func (s *Subsystem) layoutSnap(_ context.Context, _ *mcp.CallToolRequest, input
|
|||
return nil, LayoutSnapOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_stack ---
|
||||
|
||||
type LayoutStackInput struct {
|
||||
Windows []string `json:"windows,omitempty"`
|
||||
OffsetX int `json:"offsetX"`
|
||||
OffsetY int `json:"offsetY"`
|
||||
}
|
||||
type LayoutStackOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutStack(_ context.Context, _ *mcp.CallToolRequest, input LayoutStackInput) (*mcp.CallToolResult, LayoutStackOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskStackWindows{Windows: input.Windows, OffsetX: input.OffsetX, OffsetY: input.OffsetY})
|
||||
if err != nil {
|
||||
return nil, LayoutStackOutput{}, err
|
||||
}
|
||||
return nil, LayoutStackOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_workflow ---
|
||||
|
||||
type LayoutWorkflowInput struct {
|
||||
Workflow string `json:"workflow"`
|
||||
Windows []string `json:"windows,omitempty"`
|
||||
}
|
||||
type LayoutWorkflowOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutWorkflow(_ context.Context, _ *mcp.CallToolRequest, input LayoutWorkflowInput) (*mcp.CallToolResult, LayoutWorkflowOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskApplyWorkflow{Workflow: input.Workflow, Windows: input.Windows})
|
||||
if err != nil {
|
||||
return nil, LayoutWorkflowOutput{}, err
|
||||
}
|
||||
return nil, LayoutWorkflowOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerLayoutTools(server *mcp.Server) {
|
||||
|
|
@ -146,4 +183,6 @@ func (s *Subsystem) registerLayoutTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "layout_get", Description: "Get a specific layout by name"}, s.layoutGet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_tile", Description: "Tile windows in a grid arrangement"}, s.layoutTile)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_snap", Description: "Snap a window to a screen edge or corner"}, s.layoutSnap)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_stack", Description: "Stack windows in a cascade pattern"}, s.layoutStack)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_workflow", Description: "Apply a preset workflow layout"}, s.layoutWorkflow)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/gui/pkg/screen"
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -109,6 +110,34 @@ func (s *Subsystem) screenWorkAreas(_ context.Context, _ *mcp.CallToolRequest, _
|
|||
return nil, ScreenWorkAreasOutput{WorkAreas: areas}, nil
|
||||
}
|
||||
|
||||
// --- screen_for_window ---
|
||||
|
||||
type ScreenForWindowInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type ScreenForWindowOutput struct {
|
||||
Screen *screen.Screen `json:"screen"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) screenForWindow(_ context.Context, _ *mcp.CallToolRequest, input ScreenForWindowInput) (*mcp.CallToolResult, ScreenForWindowOutput, error) {
|
||||
result, _, err := s.core.QUERY(window.QueryWindowByName{Name: input.Name})
|
||||
if err != nil {
|
||||
return nil, ScreenForWindowOutput{}, err
|
||||
}
|
||||
info, _ := result.(*window.WindowInfo)
|
||||
if info == nil {
|
||||
return nil, ScreenForWindowOutput{}, nil
|
||||
}
|
||||
centerX := info.X + info.Width/2
|
||||
centerY := info.Y + info.Height/2
|
||||
screenResult, _, err := s.core.QUERY(screen.QueryAtPoint{X: centerX, Y: centerY})
|
||||
if err != nil {
|
||||
return nil, ScreenForWindowOutput{}, err
|
||||
}
|
||||
scr, _ := screenResult.(*screen.Screen)
|
||||
return nil, ScreenForWindowOutput{Screen: scr}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerScreenTools(server *mcp.Server) {
|
||||
|
|
@ -117,4 +146,5 @@ func (s *Subsystem) registerScreenTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "screen_primary", Description: "Get the primary screen"}, s.screenPrimary)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_at_point", Description: "Get the screen at a specific point"}, s.screenAtPoint)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_work_areas", Description: "Get work areas for all screens"}, s.screenWorkAreas)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_for_window", Description: "Get the screen containing a window"}, s.screenForWindow)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,22 +89,17 @@ type WindowCreateOutput struct {
|
|||
}
|
||||
|
||||
func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, input WindowCreateInput) (*mcp.CallToolResult, WindowCreateOutput, error) {
|
||||
options := []window.WindowOption{
|
||||
window.WithName(input.Name),
|
||||
}
|
||||
if input.Title != "" {
|
||||
options = append(options, window.WithTitle(input.Title))
|
||||
}
|
||||
if input.URL != "" {
|
||||
options = append(options, window.WithURL(input.URL))
|
||||
}
|
||||
if input.Width > 0 || input.Height > 0 {
|
||||
options = append(options, window.WithSize(input.Width, input.Height))
|
||||
}
|
||||
if input.X != 0 || input.Y != 0 {
|
||||
options = append(options, window.WithPosition(input.X, input.Y))
|
||||
}
|
||||
result, _, err := s.core.PERFORM(window.TaskOpenWindow{Options: options})
|
||||
result, _, err := s.core.PERFORM(window.TaskOpenWindow{
|
||||
Window: &window.Window{
|
||||
Name: input.Name,
|
||||
Title: input.Title,
|
||||
URL: input.URL,
|
||||
Width: input.Width,
|
||||
Height: input.Height,
|
||||
X: input.X,
|
||||
Y: input.Y,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, WindowCreateOutput{}, err
|
||||
}
|
||||
|
|
@ -281,6 +276,27 @@ func (s *Subsystem) windowTitle(_ context.Context, _ *mcp.CallToolRequest, input
|
|||
return nil, WindowTitleOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- window_title_get ---
|
||||
|
||||
type WindowTitleGetInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type WindowTitleGetOutput struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) windowTitleGet(_ context.Context, _ *mcp.CallToolRequest, input WindowTitleGetInput) (*mcp.CallToolResult, WindowTitleGetOutput, error) {
|
||||
result, _, err := s.core.QUERY(window.QueryWindowByName{Name: input.Name})
|
||||
if err != nil {
|
||||
return nil, WindowTitleGetOutput{}, err
|
||||
}
|
||||
info, _ := result.(*window.WindowInfo)
|
||||
if info == nil {
|
||||
return nil, WindowTitleGetOutput{}, nil
|
||||
}
|
||||
return nil, WindowTitleGetOutput{Title: info.Title}, nil
|
||||
}
|
||||
|
||||
// --- window_visibility ---
|
||||
|
||||
type WindowVisibilityInput struct {
|
||||
|
|
@ -299,6 +315,47 @@ func (s *Subsystem) windowVisibility(_ context.Context, _ *mcp.CallToolRequest,
|
|||
return nil, WindowVisibilityOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- window_always_on_top ---
|
||||
|
||||
type WindowAlwaysOnTopInput struct {
|
||||
Name string `json:"name"`
|
||||
AlwaysOnTop bool `json:"alwaysOnTop"`
|
||||
}
|
||||
type WindowAlwaysOnTopOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) windowAlwaysOnTop(_ context.Context, _ *mcp.CallToolRequest, input WindowAlwaysOnTopInput) (*mcp.CallToolResult, WindowAlwaysOnTopOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskSetAlwaysOnTop{Name: input.Name, AlwaysOnTop: input.AlwaysOnTop})
|
||||
if err != nil {
|
||||
return nil, WindowAlwaysOnTopOutput{}, err
|
||||
}
|
||||
return nil, WindowAlwaysOnTopOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- window_background_colour ---
|
||||
|
||||
type WindowBackgroundColourInput struct {
|
||||
Name string `json:"name"`
|
||||
Red uint8 `json:"red"`
|
||||
Green uint8 `json:"green"`
|
||||
Blue uint8 `json:"blue"`
|
||||
Alpha uint8 `json:"alpha"`
|
||||
}
|
||||
type WindowBackgroundColourOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) windowBackgroundColour(_ context.Context, _ *mcp.CallToolRequest, input WindowBackgroundColourInput) (*mcp.CallToolResult, WindowBackgroundColourOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskSetBackgroundColour{
|
||||
Name: input.Name, Red: input.Red, Green: input.Green, Blue: input.Blue, Alpha: input.Alpha,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, WindowBackgroundColourOutput{}, err
|
||||
}
|
||||
return nil, WindowBackgroundColourOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- window_fullscreen ---
|
||||
|
||||
type WindowFullscreenInput struct {
|
||||
|
|
@ -333,6 +390,9 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "window_restore", Description: "Restore a maximised or minimised window"}, s.windowRestore)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_focus", Description: "Bring a window to the front"}, s.windowFocus)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_title", Description: "Set the title of a window"}, s.windowTitle)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_title_get", Description: "Get the title of a window"}, s.windowTitleGet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_visibility", Description: "Show or hide a window"}, s.windowVisibility)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_always_on_top", Description: "Pin a window above others"}, s.windowAlwaysOnTop)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_background_colour", Description: "Set a window background colour"}, s.windowBackgroundColour)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_fullscreen", Description: "Set a window to fullscreen mode"}, s.windowFullscreen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ func (m *Manager) SetMenu(items []TrayMenuItem) error {
|
|||
if m.tray == nil {
|
||||
return fmt.Errorf("tray not initialised")
|
||||
}
|
||||
menu := m.buildMenu(items)
|
||||
menu := m.platform.NewMenu()
|
||||
m.buildMenu(menu, items)
|
||||
m.tray.SetMenu(menu)
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMenu recursively builds a PlatformMenu from TrayMenuItem descriptors.
|
||||
func (m *Manager) buildMenu(items []TrayMenuItem) PlatformMenu {
|
||||
menu := m.platform.NewMenu()
|
||||
func (m *Manager) buildMenu(menu PlatformMenu, items []TrayMenuItem) {
|
||||
for _, item := range items {
|
||||
if item.Type == "separator" {
|
||||
menu.AddSeparator()
|
||||
|
|
@ -45,7 +45,6 @@ func (m *Manager) buildMenu(items []TrayMenuItem) PlatformMenu {
|
|||
})
|
||||
}
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
// RegisterCallback registers a callback for a menu action ID.
|
||||
|
|
|
|||
|
|
@ -87,23 +87,26 @@ func TestManager_GetInfo_Good(t *testing.T) {
|
|||
|
||||
func TestManager_Build_Submenu_Recursive_Good(t *testing.T) {
|
||||
m, p := newTestManager()
|
||||
items := []MenuItem{
|
||||
require.NoError(t, m.Setup("Core", "Core"))
|
||||
|
||||
items := []TrayMenuItem{
|
||||
{
|
||||
Label: "Parent",
|
||||
Children: []MenuItem{
|
||||
Submenu: []TrayMenuItem{
|
||||
{Label: "Child 1"},
|
||||
{Label: "Child 2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
menu := m.Build(items)
|
||||
assert.NotNil(t, menu)
|
||||
require.NoError(t, m.SetMenu(items))
|
||||
require.Len(t, p.menus, 1)
|
||||
require.Len(t, p.menus[0].items, 1)
|
||||
assert.Equal(t, "Parent", p.menus[0].items[0])
|
||||
require.Len(t, p.menus[0].subs, 1)
|
||||
require.Len(t, p.menus[0].subs[0].items, 2)
|
||||
assert.Equal(t, "Child 1", p.menus[0].subs[0].items[0])
|
||||
assert.Equal(t, "Child 2", p.menus[0].subs[0].items[1])
|
||||
|
||||
menu := p.menus[0]
|
||||
require.Len(t, menu.items, 1)
|
||||
assert.Equal(t, "Parent", menu.items[0])
|
||||
require.Len(t, menu.subs, 1)
|
||||
require.Len(t, menu.subs[0].items, 2)
|
||||
assert.Equal(t, "Child 1", menu.subs[0].items[0])
|
||||
assert.Equal(t, "Child 2", menu.subs[0].items[1])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ type QueryWindowByName struct{ Name string }
|
|||
|
||||
type QueryConfig struct{}
|
||||
|
||||
type TaskOpenWindow struct{ Options []WindowOption }
|
||||
type TaskOpenWindow struct {
|
||||
Window *Window
|
||||
Options []WindowOption
|
||||
}
|
||||
|
||||
type TaskCloseWindow struct{ Name string }
|
||||
|
||||
|
|
@ -44,6 +47,19 @@ type TaskSetTitle struct {
|
|||
Title string
|
||||
}
|
||||
|
||||
type TaskSetAlwaysOnTop struct {
|
||||
Name string
|
||||
AlwaysOnTop bool
|
||||
}
|
||||
|
||||
type TaskSetBackgroundColour struct {
|
||||
Name string
|
||||
Red uint8
|
||||
Green uint8
|
||||
Blue uint8
|
||||
Alpha uint8
|
||||
}
|
||||
|
||||
type TaskSetVisibility struct {
|
||||
Name string
|
||||
Visible bool
|
||||
|
|
@ -69,11 +85,22 @@ type TaskTileWindows struct {
|
|||
Windows []string // window names; empty = all
|
||||
}
|
||||
|
||||
type TaskStackWindows struct {
|
||||
Windows []string // window names; empty = all
|
||||
OffsetX int
|
||||
OffsetY int
|
||||
}
|
||||
|
||||
type TaskSnapWindow struct {
|
||||
Name string // window name
|
||||
Position string // "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "center"
|
||||
}
|
||||
|
||||
type TaskApplyWorkflow struct {
|
||||
Workflow string
|
||||
Windows []string // window names; empty = all
|
||||
}
|
||||
|
||||
type TaskSaveConfig struct{ Config map[string]any }
|
||||
|
||||
type ActionWindowOpened struct{ Name string }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type MockWindow struct {
|
|||
width, height, x, y int
|
||||
maximised, focused bool
|
||||
visible, alwaysOnTop bool
|
||||
backgroundColour [4]uint8
|
||||
closed bool
|
||||
eventHandlers []func(WindowEvent)
|
||||
fileDropHandlers []func(paths []string, targetID string)
|
||||
|
|
@ -47,7 +48,7 @@ func (w *MockWindow) IsFocused() bool { return w.focused }
|
|||
func (w *MockWindow) SetTitle(title string) { w.title = title }
|
||||
func (w *MockWindow) SetPosition(x, y int) { w.x = x; w.y = y }
|
||||
func (w *MockWindow) SetSize(width, height int) { w.width = width; w.height = height }
|
||||
func (w *MockWindow) SetBackgroundColour(r, g, b, a uint8) {}
|
||||
func (w *MockWindow) SetBackgroundColour(r, g, b, a uint8) { w.backgroundColour = [4]uint8{r, g, b, a} }
|
||||
func (w *MockWindow) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *MockWindow) Maximise() { w.maximised = true }
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type mockWindow struct {
|
|||
width, height, x, y int
|
||||
maximised, focused bool
|
||||
visible, alwaysOnTop bool
|
||||
backgroundColour [4]uint8
|
||||
closed bool
|
||||
minimised bool
|
||||
fullscreened bool
|
||||
|
|
@ -47,7 +48,7 @@ func (w *mockWindow) IsFocused() bool { return w.focused }
|
|||
func (w *mockWindow) SetTitle(title string) { w.title = title }
|
||||
func (w *mockWindow) SetPosition(x, y int) { w.x = x; w.y = y }
|
||||
func (w *mockWindow) SetSize(width, height int) { w.width = width; w.height = height }
|
||||
func (w *mockWindow) SetBackgroundColour(r, g, b, a uint8) {}
|
||||
func (w *mockWindow) SetBackgroundColour(r, g, b, a uint8) { w.backgroundColour = [4]uint8{r, g, b, a} }
|
||||
func (w *mockWindow) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *mockWindow) Maximise() { w.maximised = true }
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return nil, true, s.taskRestore(t.Name)
|
||||
case TaskSetTitle:
|
||||
return nil, true, s.taskSetTitle(t.Name, t.Title)
|
||||
case TaskSetAlwaysOnTop:
|
||||
return nil, true, s.taskSetAlwaysOnTop(t.Name, t.AlwaysOnTop)
|
||||
case TaskSetBackgroundColour:
|
||||
return nil, true, s.taskSetBackgroundColour(t.Name, t.Red, t.Green, t.Blue, t.Alpha)
|
||||
case TaskSetVisibility:
|
||||
return nil, true, s.taskSetVisibility(t.Name, t.Visible)
|
||||
case TaskFullscreen:
|
||||
|
|
@ -138,42 +142,60 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return nil, true, nil
|
||||
case TaskTileWindows:
|
||||
return nil, true, s.taskTileWindows(t.Mode, t.Windows)
|
||||
case TaskStackWindows:
|
||||
return nil, true, s.taskStackWindows(t.Windows, t.OffsetX, t.OffsetY)
|
||||
case TaskSnapWindow:
|
||||
return nil, true, s.taskSnapWindow(t.Name, t.Position)
|
||||
case TaskApplyWorkflow:
|
||||
return nil, true, s.taskApplyWorkflow(t.Workflow, t.Windows)
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) primaryScreenSize() (int, int) {
|
||||
func (s *Service) primaryScreenArea() (int, int, int, int) {
|
||||
const fallbackX = 0
|
||||
const fallbackY = 0
|
||||
const fallbackWidth = 1920
|
||||
const fallbackHeight = 1080
|
||||
|
||||
result, handled, err := s.Core().QUERY(screen.QueryPrimary{})
|
||||
if err != nil || !handled {
|
||||
return fallbackWidth, fallbackHeight
|
||||
return fallbackX, fallbackY, fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
primary, ok := result.(*screen.Screen)
|
||||
if !ok || primary == nil {
|
||||
return fallbackWidth, fallbackHeight
|
||||
return fallbackX, fallbackY, fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
x := primary.WorkArea.X
|
||||
y := primary.WorkArea.Y
|
||||
width := primary.WorkArea.Width
|
||||
height := primary.WorkArea.Height
|
||||
if width <= 0 || height <= 0 {
|
||||
x = primary.Bounds.X
|
||||
y = primary.Bounds.Y
|
||||
width = primary.Bounds.Width
|
||||
height = primary.Bounds.Height
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return fallbackWidth, fallbackHeight
|
||||
return fallbackX, fallbackY, fallbackWidth, fallbackHeight
|
||||
}
|
||||
|
||||
return width, height
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) {
|
||||
pw, err := s.manager.Open(t.Options...)
|
||||
var (
|
||||
pw PlatformWindow
|
||||
err error
|
||||
)
|
||||
if t.Window != nil {
|
||||
pw, err = s.manager.Create(t.Window)
|
||||
} else {
|
||||
pw, err = s.manager.Open(t.Options...)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
|
@ -302,6 +324,24 @@ func (s *Service) taskSetTitle(name, title string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetAlwaysOnTop(name string, alwaysOnTop bool) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.SetAlwaysOnTop(alwaysOnTop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetBackgroundColour(name string, red, green, blue, alpha uint8) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.SetBackgroundColour(red, green, blue, alpha)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskSetVisibility(name string, visible bool) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
|
|
@ -350,7 +390,10 @@ func (s *Service) taskRestoreLayout(name string) error {
|
|||
pw.SetSize(state.Width, state.Height)
|
||||
if state.Maximized {
|
||||
pw.Maximise()
|
||||
} else {
|
||||
pw.Restore()
|
||||
}
|
||||
s.manager.State().CaptureState(pw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -371,8 +414,16 @@ func (s *Service) taskTileWindows(mode string, names []string) error {
|
|||
if len(names) == 0 {
|
||||
names = s.manager.List()
|
||||
}
|
||||
screenWidth, screenHeight := s.primaryScreenSize()
|
||||
return s.manager.TileWindows(tm, names, screenWidth, screenHeight)
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.TileWindows(tm, names, screenWidth, screenHeight, originX, originY)
|
||||
}
|
||||
|
||||
func (s *Service) taskStackWindows(names []string, offsetX, offsetY int) error {
|
||||
if len(names) == 0 {
|
||||
names = s.manager.List()
|
||||
}
|
||||
originX, originY, _, _ := s.primaryScreenArea()
|
||||
return s.manager.StackWindows(names, offsetX, offsetY, originX, originY)
|
||||
}
|
||||
|
||||
var snapPosMap = map[string]SnapPosition{
|
||||
|
|
@ -388,8 +439,27 @@ func (s *Service) taskSnapWindow(name, position string) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("unknown snap position: %s", position)
|
||||
}
|
||||
screenWidth, screenHeight := s.primaryScreenSize()
|
||||
return s.manager.SnapWindow(name, pos, screenWidth, screenHeight)
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.SnapWindow(name, pos, screenWidth, screenHeight, originX, originY)
|
||||
}
|
||||
|
||||
var workflowLayoutMap = map[string]WorkflowLayout{
|
||||
"coding": WorkflowCoding,
|
||||
"debugging": WorkflowDebugging,
|
||||
"presenting": WorkflowPresenting,
|
||||
"side-by-side": WorkflowSideBySide,
|
||||
}
|
||||
|
||||
func (s *Service) taskApplyWorkflow(workflow string, names []string) error {
|
||||
layout, ok := workflowLayoutMap[workflow]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown workflow layout: %s", workflow)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
names = s.manager.List()
|
||||
}
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.ApplyWorkflow(layout, names, screenWidth, screenHeight, originX, originY)
|
||||
}
|
||||
|
||||
// Manager returns the underlying window Manager for direct access.
|
||||
|
|
|
|||
|
|
@ -97,3 +97,38 @@ func TestTaskSnapWindow_UsesPrimaryScreenSize(t *testing.T) {
|
|||
assert.Equal(t, 1000, info.Width)
|
||||
assert.Equal(t, 1000, info.Height)
|
||||
}
|
||||
|
||||
func TestTaskTileWindows_UsesPrimaryWorkAreaOrigin(t *testing.T) {
|
||||
_, c := newTestWindowServiceWithScreen(t, []screen.Screen{
|
||||
{
|
||||
ID: "1", Name: "Primary", IsPrimary: true,
|
||||
Bounds: screen.Rect{X: 0, Y: 0, Width: 2000, Height: 1000},
|
||||
WorkArea: screen.Rect{X: 100, Y: 50, Width: 2000, Height: 1000},
|
||||
},
|
||||
})
|
||||
|
||||
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}})
|
||||
require.NoError(t, err)
|
||||
_, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
result, _, err := c.QUERY(QueryWindowByName{Name: "left"})
|
||||
require.NoError(t, err)
|
||||
left := result.(*WindowInfo)
|
||||
assert.Equal(t, 100, left.X)
|
||||
assert.Equal(t, 50, left.Y)
|
||||
assert.Equal(t, 1000, left.Width)
|
||||
assert.Equal(t, 1000, left.Height)
|
||||
|
||||
result, _, err = c.QUERY(QueryWindowByName{Name: "right"})
|
||||
require.NoError(t, err)
|
||||
right := result.(*WindowInfo)
|
||||
assert.Equal(t, 1100, right.X)
|
||||
assert.Equal(t, 50, right.Y)
|
||||
assert.Equal(t, 1000, right.Width)
|
||||
assert.Equal(t, 1000, right.Height)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func TestRegister_Good(t *testing.T) {
|
|||
func TestTaskOpenWindow_Good(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
result, handled, err := c.PERFORM(TaskOpenWindow{
|
||||
Options: []WindowOption{WithName("test"), WithURL("/")},
|
||||
Window: &Window{Name: "test", URL: "/"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
|
@ -39,6 +39,17 @@ func TestTaskOpenWindow_Good(t *testing.T) {
|
|||
assert.Equal(t, "test", info.Name)
|
||||
}
|
||||
|
||||
func TestTaskOpenWindow_OptionsFallback_Good(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
result, handled, err := c.PERFORM(TaskOpenWindow{
|
||||
Options: []WindowOption{WithName("test-fallback"), WithURL("/")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
info := result.(WindowInfo)
|
||||
assert.Equal(t, "test-fallback", info.Name)
|
||||
}
|
||||
|
||||
func TestTaskOpenWindow_Bad(t *testing.T) {
|
||||
// No window service registered — PERFORM returns handled=false
|
||||
c, err := core.New(core.WithServiceLock())
|
||||
|
|
@ -274,6 +285,54 @@ func TestTaskSetTitle_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskSetAlwaysOnTop ---
|
||||
|
||||
func TestTaskSetAlwaysOnTop_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "test", AlwaysOnTop: true})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
mw := pw.(*mockWindow)
|
||||
assert.True(t, mw.alwaysOnTop)
|
||||
}
|
||||
|
||||
func TestTaskSetAlwaysOnTop_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "nonexistent", AlwaysOnTop: true})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskSetBackgroundColour ---
|
||||
|
||||
func TestTaskSetBackgroundColour_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetBackgroundColour{
|
||||
Name: "test", Red: 10, Green: 20, Blue: 30, Alpha: 40,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
mw := pw.(*mockWindow)
|
||||
assert.Equal(t, [4]uint8{10, 20, 30, 40}, mw.backgroundColour)
|
||||
}
|
||||
|
||||
func TestTaskSetBackgroundColour_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetBackgroundColour{Name: "nonexistent", Red: 1, Green: 2, Blue: 3, Alpha: 4})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskSetVisibility ---
|
||||
|
||||
func TestTaskSetVisibility_Good(t *testing.T) {
|
||||
|
|
@ -401,6 +460,16 @@ func TestTaskRestoreLayout_Good(t *testing.T) {
|
|||
x2, y2 := pw2.Position()
|
||||
assert.Equal(t, 0, x2)
|
||||
assert.Equal(t, 0, y2)
|
||||
|
||||
editorState, ok := svc.Manager().State().GetState("editor")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 0, editorState.X)
|
||||
assert.Equal(t, 0, editorState.Y)
|
||||
|
||||
terminalState, ok := svc.Manager().State().GetState("terminal")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 0, terminalState.X)
|
||||
assert.Equal(t, 0, terminalState.Y)
|
||||
}
|
||||
|
||||
func TestTaskRestoreLayout_Bad(t *testing.T) {
|
||||
|
|
@ -409,3 +478,45 @@ func TestTaskRestoreLayout_Bad(t *testing.T) {
|
|||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskStackWindows ---
|
||||
|
||||
func TestTaskStackWindows_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s1"), WithSize(800, 600)}})
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s2"), WithSize(800, 600)}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskStackWindows{Windows: []string{"s1", "s2"}, OffsetX: 25, OffsetY: 35})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
pw, ok := svc.Manager().Get("s2")
|
||||
require.True(t, ok)
|
||||
x, y := pw.Position()
|
||||
assert.Equal(t, 25, x)
|
||||
assert.Equal(t, 35, y)
|
||||
}
|
||||
|
||||
// --- TaskApplyWorkflow ---
|
||||
|
||||
func TestTaskApplyWorkflow_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600)}})
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600)}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskApplyWorkflow{Workflow: "side-by-side"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
editor, ok := svc.Manager().Get("editor")
|
||||
require.True(t, ok)
|
||||
x, y := editor.Position()
|
||||
assert.Equal(t, 0, x)
|
||||
assert.Equal(t, 0, y)
|
||||
|
||||
terminal, ok := svc.Manager().Get("terminal")
|
||||
require.True(t, ok)
|
||||
x, y = terminal.Position()
|
||||
assert.Equal(t, 960, x)
|
||||
assert.Equal(t, 0, y)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,16 @@ const (
|
|||
SnapCenter
|
||||
)
|
||||
|
||||
var snapPositionNames = map[SnapPosition]string{
|
||||
SnapLeft: "left", SnapRight: "right",
|
||||
SnapTop: "top", SnapBottom: "bottom",
|
||||
SnapTopLeft: "top-left", SnapTopRight: "top-right",
|
||||
SnapBottomLeft: "bottom-left", SnapBottomRight: "bottom-right",
|
||||
SnapCenter: "center",
|
||||
}
|
||||
|
||||
func (p SnapPosition) String() string { return snapPositionNames[p] }
|
||||
|
||||
// WorkflowLayout is a predefined arrangement for common tasks.
|
||||
type WorkflowLayout int
|
||||
|
||||
|
|
@ -61,8 +71,26 @@ var workflowNames = map[WorkflowLayout]string{
|
|||
|
||||
func (w WorkflowLayout) String() string { return workflowNames[w] }
|
||||
|
||||
func layoutOrigin(origin []int) (int, int) {
|
||||
if len(origin) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
if len(origin) == 1 {
|
||||
return origin[0], 0
|
||||
}
|
||||
return origin[0], origin[1]
|
||||
}
|
||||
|
||||
func (m *Manager) captureState(pw PlatformWindow) {
|
||||
if m.state == nil || pw == nil {
|
||||
return
|
||||
}
|
||||
m.state.CaptureState(pw)
|
||||
}
|
||||
|
||||
// TileWindows arranges the named windows in the given mode across the screen area.
|
||||
func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH int) error {
|
||||
func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH int, origin ...int) error {
|
||||
originX, originY := layoutOrigin(origin)
|
||||
windows := make([]PlatformWindow, 0, len(names))
|
||||
for _, name := range names {
|
||||
pw, ok := m.Get(name)
|
||||
|
|
@ -81,8 +109,9 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in
|
|||
case TileModeLeftRight:
|
||||
w := screenW / len(windows)
|
||||
for i, pw := range windows {
|
||||
pw.SetPosition(i*w, 0)
|
||||
pw.SetPosition(originX+i*w, originY)
|
||||
pw.SetSize(w, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeGrid:
|
||||
cols := 2
|
||||
|
|
@ -95,55 +124,65 @@ func (m *Manager) TileWindows(mode TileMode, names []string, screenW, screenH in
|
|||
col := i % cols
|
||||
rows := (len(windows) + cols - 1) / cols
|
||||
cellH := screenH / rows
|
||||
pw.SetPosition(col*cellW, row*cellH)
|
||||
pw.SetPosition(originX+col*cellW, originY+row*cellH)
|
||||
pw.SetSize(cellW, cellH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeLeftHalf:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(halfW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeRightHalf:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(halfW, 0)
|
||||
pw.SetPosition(originX+halfW, originY)
|
||||
pw.SetSize(halfW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeTopHalf:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(screenW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeBottomHalf:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(0, halfH)
|
||||
pw.SetPosition(originX, originY+halfH)
|
||||
pw.SetSize(screenW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeTopLeft:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(halfW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeTopRight:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(halfW, 0)
|
||||
pw.SetPosition(originX+halfW, originY)
|
||||
pw.SetSize(halfW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeBottomLeft:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(0, halfH)
|
||||
pw.SetPosition(originX, originY+halfH)
|
||||
pw.SetSize(halfW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case TileModeBottomRight:
|
||||
for _, pw := range windows {
|
||||
pw.SetPosition(halfW, halfH)
|
||||
pw.SetPosition(originX+halfW, originY+halfH)
|
||||
pw.SetSize(halfW, halfH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SnapWindow snaps a window to a screen edge/corner/centre.
|
||||
func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int) error {
|
||||
func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int, origin ...int) error {
|
||||
originX, originY := layoutOrigin(origin)
|
||||
pw, ok := m.Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("window.Manager.SnapWindow", "window not found: "+name, nil)
|
||||
|
|
@ -153,50 +192,54 @@ func (m *Manager) SnapWindow(name string, pos SnapPosition, screenW, screenH int
|
|||
|
||||
switch pos {
|
||||
case SnapLeft:
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(halfW, screenH)
|
||||
case SnapRight:
|
||||
pw.SetPosition(halfW, 0)
|
||||
pw.SetPosition(originX+halfW, originY)
|
||||
pw.SetSize(halfW, screenH)
|
||||
case SnapTop:
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(screenW, halfH)
|
||||
case SnapBottom:
|
||||
pw.SetPosition(0, halfH)
|
||||
pw.SetPosition(originX, originY+halfH)
|
||||
pw.SetSize(screenW, halfH)
|
||||
case SnapTopLeft:
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(halfW, halfH)
|
||||
case SnapTopRight:
|
||||
pw.SetPosition(halfW, 0)
|
||||
pw.SetPosition(originX+halfW, originY)
|
||||
pw.SetSize(halfW, halfH)
|
||||
case SnapBottomLeft:
|
||||
pw.SetPosition(0, halfH)
|
||||
pw.SetPosition(originX, originY+halfH)
|
||||
pw.SetSize(halfW, halfH)
|
||||
case SnapBottomRight:
|
||||
pw.SetPosition(halfW, halfH)
|
||||
pw.SetPosition(originX+halfW, originY+halfH)
|
||||
pw.SetSize(halfW, halfH)
|
||||
case SnapCenter:
|
||||
cw, ch := pw.Size()
|
||||
pw.SetPosition((screenW-cw)/2, (screenH-ch)/2)
|
||||
pw.SetPosition(originX+(screenW-cw)/2, originY+(screenH-ch)/2)
|
||||
}
|
||||
m.captureState(pw)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StackWindows cascades windows with an offset.
|
||||
func (m *Manager) StackWindows(names []string, offsetX, offsetY int) error {
|
||||
func (m *Manager) StackWindows(names []string, offsetX, offsetY int, origin ...int) error {
|
||||
originX, originY := layoutOrigin(origin)
|
||||
for i, name := range names {
|
||||
pw, ok := m.Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("window.Manager.StackWindows", "window not found: "+name, nil)
|
||||
}
|
||||
pw.SetPosition(i*offsetX, i*offsetY)
|
||||
pw.SetPosition(originX+i*offsetX, originY+i*offsetY)
|
||||
m.captureState(pw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyWorkflow arranges windows in a predefined workflow layout.
|
||||
func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW, screenH int) error {
|
||||
func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW, screenH int, origin ...int) error {
|
||||
originX, originY := layoutOrigin(origin)
|
||||
if len(names) == 0 {
|
||||
return coreerr.E("window.Manager.ApplyWorkflow", "no windows for workflow", nil)
|
||||
}
|
||||
|
|
@ -206,36 +249,41 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW
|
|||
// 70/30 split — main editor + terminal
|
||||
mainW := screenW * 70 / 100
|
||||
if pw, ok := m.Get(names[0]); ok {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(mainW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
if len(names) > 1 {
|
||||
if pw, ok := m.Get(names[1]); ok {
|
||||
pw.SetPosition(mainW, 0)
|
||||
pw.SetPosition(originX+mainW, originY)
|
||||
pw.SetSize(screenW-mainW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
}
|
||||
case WorkflowDebugging:
|
||||
// 60/40 split
|
||||
mainW := screenW * 60 / 100
|
||||
if pw, ok := m.Get(names[0]); ok {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(mainW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
if len(names) > 1 {
|
||||
if pw, ok := m.Get(names[1]); ok {
|
||||
pw.SetPosition(mainW, 0)
|
||||
pw.SetPosition(originX+mainW, originY)
|
||||
pw.SetSize(screenW-mainW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
}
|
||||
case WorkflowPresenting:
|
||||
// Maximise first window
|
||||
if pw, ok := m.Get(names[0]); ok {
|
||||
pw.SetPosition(0, 0)
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(screenW, screenH)
|
||||
m.captureState(pw)
|
||||
}
|
||||
case WorkflowSideBySide:
|
||||
return m.TileWindows(TileModeLeftRight, names, screenW, screenH)
|
||||
return m.TileWindows(TileModeLeftRight, names, screenW, screenH, originX, originY)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
3
stubs/wails/go.mod
Normal file
3
stubs/wails/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/wailsapp/wails/v3
|
||||
|
||||
go 1.26.0
|
||||
363
stubs/wails/pkg/application/application.go
Normal file
363
stubs/wails/pkg/application/application.go
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// Context mirrors the callback context type exposed by Wails.
|
||||
type Context struct{}
|
||||
|
||||
// Logger is a minimal logger surface used by the GUI packages.
|
||||
type Logger struct{}
|
||||
|
||||
func (l Logger) Info(message string, args ...any) {}
|
||||
|
||||
// RGBA stores a colour with alpha.
|
||||
type RGBA struct {
|
||||
Red, Green, Blue, Alpha uint8
|
||||
}
|
||||
|
||||
// NewRGBA constructs an RGBA value.
|
||||
func NewRGBA(red, green, blue, alpha uint8) RGBA {
|
||||
return RGBA{Red: red, Green: green, Blue: blue, Alpha: alpha}
|
||||
}
|
||||
|
||||
// MenuRole identifies a platform menu role.
|
||||
type MenuRole int
|
||||
|
||||
const (
|
||||
AppMenu MenuRole = iota
|
||||
FileMenu
|
||||
EditMenu
|
||||
ViewMenu
|
||||
WindowMenu
|
||||
HelpMenu
|
||||
)
|
||||
|
||||
// MenuItem is a minimal menu item implementation.
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Accelerator string
|
||||
Tooltip string
|
||||
Checked bool
|
||||
Enabled bool
|
||||
onClick func(*Context)
|
||||
}
|
||||
|
||||
func (mi *MenuItem) SetAccelerator(accel string) { mi.Accelerator = accel }
|
||||
func (mi *MenuItem) SetTooltip(text string) { mi.Tooltip = text }
|
||||
func (mi *MenuItem) SetChecked(checked bool) { mi.Checked = checked }
|
||||
func (mi *MenuItem) SetEnabled(enabled bool) { mi.Enabled = enabled }
|
||||
func (mi *MenuItem) OnClick(fn func(*Context)) { mi.onClick = fn }
|
||||
|
||||
// Menu is a minimal menu tree used by the GUI wrappers.
|
||||
type Menu struct {
|
||||
Items []*MenuItem
|
||||
}
|
||||
|
||||
func NewMenu() *Menu { return &Menu{} }
|
||||
|
||||
func (m *Menu) Add(label string) *MenuItem {
|
||||
item := &MenuItem{Label: label, Enabled: true}
|
||||
m.Items = append(m.Items, item)
|
||||
return item
|
||||
}
|
||||
|
||||
func (m *Menu) AddSeparator() {
|
||||
m.Items = append(m.Items, &MenuItem{Label: "---"})
|
||||
}
|
||||
|
||||
func (m *Menu) AddSubmenu(label string) *Menu {
|
||||
submenu := &Menu{}
|
||||
m.Items = append(m.Items, &MenuItem{Label: label})
|
||||
return submenu
|
||||
}
|
||||
|
||||
func (m *Menu) AddRole(role MenuRole) {
|
||||
m.Items = append(m.Items, &MenuItem{Label: role.String(), Enabled: true})
|
||||
}
|
||||
|
||||
func (role MenuRole) String() string {
|
||||
switch role {
|
||||
case AppMenu:
|
||||
return "app"
|
||||
case FileMenu:
|
||||
return "file"
|
||||
case EditMenu:
|
||||
return "edit"
|
||||
case ViewMenu:
|
||||
return "view"
|
||||
case WindowMenu:
|
||||
return "window"
|
||||
case HelpMenu:
|
||||
return "help"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MenuManager owns the application menu.
|
||||
type MenuManager struct {
|
||||
applicationMenu *Menu
|
||||
}
|
||||
|
||||
func (m *MenuManager) SetApplicationMenu(menu *Menu) { m.applicationMenu = menu }
|
||||
|
||||
// SystemTray represents a tray instance.
|
||||
type SystemTray struct {
|
||||
icon []byte
|
||||
templateIcon []byte
|
||||
tooltip string
|
||||
label string
|
||||
menu *Menu
|
||||
attachedWindow *WebviewWindow
|
||||
}
|
||||
|
||||
func (t *SystemTray) SetIcon(data []byte) { t.icon = append([]byte(nil), data...) }
|
||||
func (t *SystemTray) SetTemplateIcon(data []byte) { t.templateIcon = append([]byte(nil), data...) }
|
||||
func (t *SystemTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *SystemTray) SetLabel(text string) { t.label = text }
|
||||
func (t *SystemTray) SetMenu(menu *Menu) { t.menu = menu }
|
||||
func (t *SystemTray) AttachWindow(w *WebviewWindow) {
|
||||
t.attachedWindow = w
|
||||
}
|
||||
|
||||
// SystemTrayManager creates tray instances.
|
||||
type SystemTrayManager struct{}
|
||||
|
||||
func (m *SystemTrayManager) New() *SystemTray { return &SystemTray{} }
|
||||
|
||||
// WindowEventContext carries drag-and-drop details for a window event.
|
||||
type WindowEventContext struct {
|
||||
droppedFiles []string
|
||||
dropDetails *DropTargetDetails
|
||||
}
|
||||
|
||||
func (c *WindowEventContext) DroppedFiles() []string {
|
||||
return append([]string(nil), c.droppedFiles...)
|
||||
}
|
||||
|
||||
func (c *WindowEventContext) DropTargetDetails() *DropTargetDetails {
|
||||
if c.dropDetails == nil {
|
||||
return nil
|
||||
}
|
||||
details := *c.dropDetails
|
||||
return &details
|
||||
}
|
||||
|
||||
// DropTargetDetails mirrors the fields consumed by the GUI wrappers.
|
||||
type DropTargetDetails struct {
|
||||
ElementID string
|
||||
}
|
||||
|
||||
// WindowEvent mirrors the event object passed to window callbacks.
|
||||
type WindowEvent struct {
|
||||
ctx *WindowEventContext
|
||||
}
|
||||
|
||||
func (e *WindowEvent) Context() *WindowEventContext {
|
||||
if e.ctx == nil {
|
||||
e.ctx = &WindowEventContext{}
|
||||
}
|
||||
return e.ctx
|
||||
}
|
||||
|
||||
// WebviewWindowOptions configures a window instance.
|
||||
type WebviewWindowOptions struct {
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Width, Height int
|
||||
X, Y int
|
||||
MinWidth, MinHeight int
|
||||
MaxWidth, MaxHeight int
|
||||
Frameless bool
|
||||
Hidden bool
|
||||
AlwaysOnTop bool
|
||||
DisableResize bool
|
||||
EnableFileDrop bool
|
||||
BackgroundColour RGBA
|
||||
}
|
||||
|
||||
// WebviewWindow is a lightweight, in-memory window implementation.
|
||||
type WebviewWindow struct {
|
||||
mu sync.RWMutex
|
||||
opts WebviewWindowOptions
|
||||
title string
|
||||
x, y int
|
||||
width, height int
|
||||
maximised bool
|
||||
focused bool
|
||||
visible bool
|
||||
alwaysOnTop bool
|
||||
fullscreen bool
|
||||
closed bool
|
||||
eventHandlers map[events.WindowEventType][]func(*WindowEvent)
|
||||
}
|
||||
|
||||
func newWebviewWindow(options WebviewWindowOptions) *WebviewWindow {
|
||||
return &WebviewWindow{
|
||||
opts: options,
|
||||
title: options.Title,
|
||||
x: options.X,
|
||||
y: options.Y,
|
||||
width: options.Width,
|
||||
height: options.Height,
|
||||
visible: !options.Hidden,
|
||||
alwaysOnTop: options.AlwaysOnTop,
|
||||
eventHandlers: make(map[events.WindowEventType][]func(*WindowEvent)),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Name() string { return w.opts.Name }
|
||||
func (w *WebviewWindow) Title() string {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.title
|
||||
}
|
||||
func (w *WebviewWindow) Position() (int, int) {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.x, w.y
|
||||
}
|
||||
func (w *WebviewWindow) Size() (int, int) {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.width, w.height
|
||||
}
|
||||
func (w *WebviewWindow) IsMaximised() bool {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.maximised
|
||||
}
|
||||
func (w *WebviewWindow) IsFocused() bool {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.focused
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) SetTitle(title string) {
|
||||
w.mu.Lock()
|
||||
w.title = title
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) SetPosition(x, y int) {
|
||||
w.mu.Lock()
|
||||
w.x = x
|
||||
w.y = y
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) SetSize(width, height int) {
|
||||
w.mu.Lock()
|
||||
w.width = width
|
||||
w.height = height
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) SetBackgroundColour(colour RGBA) {}
|
||||
|
||||
func (w *WebviewWindow) SetAlwaysOnTop(alwaysOnTop bool) {
|
||||
w.mu.Lock()
|
||||
w.alwaysOnTop = alwaysOnTop
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Maximise() {
|
||||
w.mu.Lock()
|
||||
w.maximised = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Restore() {
|
||||
w.mu.Lock()
|
||||
w.maximised = false
|
||||
w.fullscreen = false
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Minimise() {}
|
||||
|
||||
func (w *WebviewWindow) Focus() {
|
||||
w.mu.Lock()
|
||||
w.focused = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Close() {
|
||||
w.mu.Lock()
|
||||
w.closed = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Show() {
|
||||
w.mu.Lock()
|
||||
w.visible = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Hide() {
|
||||
w.mu.Lock()
|
||||
w.visible = false
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Fullscreen() {
|
||||
w.mu.Lock()
|
||||
w.fullscreen = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) UnFullscreen() {
|
||||
w.mu.Lock()
|
||||
w.fullscreen = false
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) OnWindowEvent(eventType events.WindowEventType, callback func(event *WindowEvent)) func() {
|
||||
w.mu.Lock()
|
||||
w.eventHandlers[eventType] = append(w.eventHandlers[eventType], callback)
|
||||
w.mu.Unlock()
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// WindowManager manages in-memory windows.
|
||||
type WindowManager struct {
|
||||
mu sync.RWMutex
|
||||
windows []*WebviewWindow
|
||||
}
|
||||
|
||||
func (wm *WindowManager) NewWithOptions(options WebviewWindowOptions) *WebviewWindow {
|
||||
window := newWebviewWindow(options)
|
||||
wm.mu.Lock()
|
||||
wm.windows = append(wm.windows, window)
|
||||
wm.mu.Unlock()
|
||||
return window
|
||||
}
|
||||
|
||||
func (wm *WindowManager) GetAll() []any {
|
||||
wm.mu.RLock()
|
||||
defer wm.mu.RUnlock()
|
||||
out := make([]any, 0, len(wm.windows))
|
||||
for _, window := range wm.windows {
|
||||
out = append(out, window)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// App is the top-level application object used by the GUI packages.
|
||||
type App struct {
|
||||
Logger Logger
|
||||
Window WindowManager
|
||||
Menu MenuManager
|
||||
SystemTray SystemTrayManager
|
||||
}
|
||||
|
||||
func (a *App) Quit() {}
|
||||
|
||||
func (a *App) NewMenu() *Menu {
|
||||
return NewMenu()
|
||||
}
|
||||
30
stubs/wails/pkg/events/events.go
Normal file
30
stubs/wails/pkg/events/events.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package events
|
||||
|
||||
// WindowEventType identifies a window event emitted by the application layer.
|
||||
type WindowEventType int
|
||||
|
||||
const (
|
||||
WindowFocus WindowEventType = iota
|
||||
WindowLostFocus
|
||||
WindowDidMove
|
||||
WindowDidResize
|
||||
WindowClosing
|
||||
WindowFilesDropped
|
||||
)
|
||||
|
||||
// Common matches the event namespace used by the real Wails package.
|
||||
var Common = struct {
|
||||
WindowFocus WindowEventType
|
||||
WindowLostFocus WindowEventType
|
||||
WindowDidMove WindowEventType
|
||||
WindowDidResize WindowEventType
|
||||
WindowClosing WindowEventType
|
||||
WindowFilesDropped WindowEventType
|
||||
}{
|
||||
WindowFocus: WindowFocus,
|
||||
WindowLostFocus: WindowLostFocus,
|
||||
WindowDidMove: WindowDidMove,
|
||||
WindowDidResize: WindowDidResize,
|
||||
WindowClosing: WindowClosing,
|
||||
WindowFilesDropped: WindowFilesDropped,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue