feat(mcp): implement missing GUI features
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
05a865d8f6
commit
5653bfcc8d
40 changed files with 1444 additions and 315 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.24
|
||||
1
docs/ref/wails-v3/src/application/assets/placeholder.txt
Normal file
1
docs/ref/wails-v3/src/application/assets/placeholder.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
placeholder
|
||||
40
go.mod
40
go.mod
|
|
@ -7,71 +7,35 @@ require (
|
|||
forge.lthn.ai/core/go v0.3.3
|
||||
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/v3
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
forge.lthn.ai/core/go-io v0.1.7 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.4 // 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=
|
||||
|
|
|
|||
|
|
@ -9,3 +9,16 @@ type TaskSetText struct{ Text string }
|
|||
|
||||
// TaskClear clears the clipboard. Result: bool (success)
|
||||
type TaskClear struct{}
|
||||
|
||||
// QueryImage reads an image from the clipboard. Result: ClipboardImageContent
|
||||
type QueryImage struct{}
|
||||
|
||||
// TaskSetImage writes image bytes to the clipboard. Result: bool (success)
|
||||
type TaskSetImage struct{ Data []byte }
|
||||
|
||||
// ClipboardImageContent contains clipboard image data encoded for transport.
|
||||
type ClipboardImageContent struct {
|
||||
Base64 string `json:"base64"`
|
||||
MimeType string `json:"mimeType"`
|
||||
HasContent bool `json:"hasContent"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// pkg/clipboard/platform.go
|
||||
package clipboard
|
||||
|
||||
import "encoding/base64"
|
||||
|
||||
// Platform abstracts the system clipboard backend.
|
||||
type Platform interface {
|
||||
Text() (string, bool)
|
||||
|
|
@ -12,3 +14,22 @@ type ClipboardContent struct {
|
|||
Text string `json:"text"`
|
||||
HasContent bool `json:"hasContent"`
|
||||
}
|
||||
|
||||
// imageReader is an optional clipboard capability for image reads.
|
||||
type imageReader interface {
|
||||
Image() ([]byte, bool)
|
||||
}
|
||||
|
||||
// imageWriter is an optional clipboard capability for image writes.
|
||||
type imageWriter interface {
|
||||
SetImage(data []byte) bool
|
||||
}
|
||||
|
||||
// encodeImageContent converts raw bytes to transport-safe clipboard image content.
|
||||
func encodeImageContent(data []byte) ClipboardImageContent {
|
||||
return ClipboardImageContent{
|
||||
Base64: base64.StdEncoding.EncodeToString(data),
|
||||
MimeType: "image/png",
|
||||
HasContent: len(data) > 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package clipboard
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
)
|
||||
|
|
@ -43,6 +44,12 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
|||
case QueryText:
|
||||
text, ok := s.platform.Text()
|
||||
return ClipboardContent{Text: text, HasContent: ok && text != ""}, true, nil
|
||||
case QueryImage:
|
||||
if reader, ok := s.platform.(imageReader); ok {
|
||||
data, _ := reader.Image()
|
||||
return encodeImageContent(data), true, nil
|
||||
}
|
||||
return ClipboardImageContent{MimeType: "image/png"}, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -54,6 +61,11 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return s.platform.SetText(t.Text), true, nil
|
||||
case TaskClear:
|
||||
return s.platform.SetText(""), true, nil
|
||||
case TaskSetImage:
|
||||
if writer, ok := s.platform.(imageWriter); ok {
|
||||
return writer.SetImage(t.Data), true, nil
|
||||
}
|
||||
return false, true, fmt.Errorf("clipboard image write not supported")
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import (
|
|||
)
|
||||
|
||||
type mockPlatform struct {
|
||||
text string
|
||||
ok bool
|
||||
text string
|
||||
ok bool
|
||||
img []byte
|
||||
imgOk bool
|
||||
}
|
||||
|
||||
func (m *mockPlatform) Text() (string, bool) { return m.text, m.ok }
|
||||
|
|
@ -21,6 +23,12 @@ func (m *mockPlatform) SetText(text string) bool {
|
|||
m.ok = text != ""
|
||||
return true
|
||||
}
|
||||
func (m *mockPlatform) Image() ([]byte, bool) { return m.img, m.imgOk }
|
||||
func (m *mockPlatform) SetImage(data []byte) bool {
|
||||
m.img = data
|
||||
m.imgOk = len(data) > 0
|
||||
return true
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T) (*Service, *core.Core) {
|
||||
t.Helper()
|
||||
|
|
@ -79,3 +87,34 @@ func TestTaskClear_Good(t *testing.T) {
|
|||
assert.Equal(t, "", r.(ClipboardContent).Text)
|
||||
assert.False(t, r.(ClipboardContent).HasContent)
|
||||
}
|
||||
|
||||
func TestQueryImage_Good(t *testing.T) {
|
||||
mock := &mockPlatform{img: []byte{1, 2, 3}, imgOk: true}
|
||||
c, err := core.New(
|
||||
core.WithService(Register(mock)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
result, handled, err := c.QUERY(QueryImage{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
image := result.(ClipboardImageContent)
|
||||
assert.True(t, image.HasContent)
|
||||
}
|
||||
|
||||
func TestTaskSetImage_Good(t *testing.T) {
|
||||
mock := &mockPlatform{}
|
||||
c, err := core.New(
|
||||
core.WithService(Register(mock)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetImage{Data: []byte{9, 8, 7}})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.imgOk)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ This document tracks the implementation of display server features that enable A
|
|||
### Smart Layout
|
||||
- [x] `layout_tile` - Auto-tile windows (left/right/top/bottom/quadrants/grid)
|
||||
- [x] `layout_stack` - Stack windows in cascade pattern
|
||||
- [ ] `layout_beside_editor` - Position window beside detected IDE window
|
||||
- [ ] `layout_suggest` - Given screen dimensions, suggest optimal arrangement
|
||||
- [x] `layout_beside_editor` - Position window beside detected IDE window
|
||||
- [x] `layout_suggest` - Given screen dimensions, suggest optimal arrangement
|
||||
- [x] `layout_snap` - Snap window to screen edge/corner/center
|
||||
|
||||
### AI-Optimized Layout
|
||||
- [ ] `screen_find_space` - Find empty screen space for new window
|
||||
- [ ] `window_arrange_pair` - Put two windows side-by-side optimally
|
||||
- [x] `screen_find_space` - Find empty screen space for new window
|
||||
- [x] `window_arrange_pair` - Put two windows side-by-side optimally
|
||||
- [x] `layout_workflow` - Preset layouts: "coding", "debugging", "presenting", "side-by-side"
|
||||
|
||||
---
|
||||
|
|
@ -114,8 +114,8 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] `webview_resources` - List loaded resources (scripts, styles, images)
|
||||
|
||||
### DevTools
|
||||
- [ ] `webview_devtools_open` - Open DevTools for window
|
||||
- [ ] `webview_devtools_close` - Close DevTools
|
||||
- [x] `webview_devtools_open` - Open DevTools for window
|
||||
- [x] `webview_devtools_close` - Close DevTools
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -124,8 +124,8 @@ This document tracks the implementation of display server features that enable A
|
|||
### Clipboard
|
||||
- [x] `clipboard_read` - Read clipboard text content
|
||||
- [x] `clipboard_write` - Write text to clipboard
|
||||
- [ ] `clipboard_read_image` - Read image from clipboard
|
||||
- [ ] `clipboard_write_image` - Write image to clipboard
|
||||
- [x] `clipboard_read_image` - Read image from clipboard
|
||||
- [x] `clipboard_write_image` - Write image to clipboard
|
||||
- [x] `clipboard_has` - Check clipboard content type
|
||||
- [x] `clipboard_clear` - Clear clipboard contents
|
||||
|
||||
|
|
@ -133,8 +133,8 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] `notification_show` - Show native system notification (macOS/Windows/Linux)
|
||||
- [x] `notification_permission_request` - Request notification permission
|
||||
- [x] `notification_permission_check` - Check notification authorization status
|
||||
- [ ] `notification_clear` - Clear notifications
|
||||
- [ ] `notification_with_actions` - Interactive notifications with buttons
|
||||
- [x] `notification_clear` - Clear notifications
|
||||
- [x] `notification_with_actions` - Interactive notifications with buttons
|
||||
|
||||
### Dialogs
|
||||
- [x] `dialog_open_file` - Show file open dialog
|
||||
|
|
@ -146,7 +146,7 @@ This document tracks the implementation of display server features that enable A
|
|||
|
||||
### Theme & Appearance
|
||||
- [x] `theme_get` - Get current theme (dark/light)
|
||||
- [ ] `theme_set` - Set application theme
|
||||
- [x] `theme_set` - Set application theme
|
||||
- [x] `theme_system` - Get system theme preference
|
||||
- [x] `theme_on_change` - Subscribe to theme changes (via WebSocket events)
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] `tray_set_label` - Set tray label text
|
||||
- [x] `tray_set_menu` - Set tray menu items (with nested submenus)
|
||||
- [x] `tray_info` - Get tray status info
|
||||
- [ ] `tray_show_message` - Show tray balloon notification
|
||||
- [x] `tray_show_message` - Show tray balloon notification
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ type TaskOpenFileManager struct {
|
|||
Select bool `json:"select"`
|
||||
}
|
||||
|
||||
// TaskSetTheme applies an application theme override when supported.
|
||||
type TaskSetTheme struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
}
|
||||
|
||||
// ActionThemeChanged is broadcast when the system theme changes.
|
||||
type ActionThemeChanged struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ type Options struct{}
|
|||
// Service is a core.Service providing environment queries and theme change events via IPC.
|
||||
type Service struct {
|
||||
*core.ServiceRuntime[Options]
|
||||
platform Platform
|
||||
cancelTheme func() // cancel function for theme change listener
|
||||
platform Platform
|
||||
cancelTheme func() // cancel function for theme change listener
|
||||
overrideDark *bool
|
||||
}
|
||||
|
||||
// Register creates a factory closure that captures the Platform adapter.
|
||||
|
|
@ -56,6 +57,9 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
|||
switch q.(type) {
|
||||
case QueryTheme:
|
||||
isDark := s.platform.IsDarkMode()
|
||||
if s.overrideDark != nil {
|
||||
isDark = *s.overrideDark
|
||||
}
|
||||
theme := "light"
|
||||
if isDark {
|
||||
theme = "dark"
|
||||
|
|
@ -74,6 +78,16 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
switch t := t.(type) {
|
||||
case TaskOpenFileManager:
|
||||
return nil, true, s.platform.OpenFileManager(t.Path, t.Select)
|
||||
case TaskSetTheme:
|
||||
isDark := t.IsDark
|
||||
s.overrideDark = &isDark
|
||||
if setter, ok := s.platform.(interface{ SetTheme(bool) error }); ok {
|
||||
if err := setter.SetTheme(isDark); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
}
|
||||
_ = s.Core().ACTION(ActionThemeChanged{IsDark: isDark})
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ type mockPlatform struct {
|
|||
accentColour string
|
||||
openFMErr error
|
||||
themeHandler func(isDark bool)
|
||||
setThemeSeen bool
|
||||
setThemeDark bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *mockPlatform) IsDarkMode() bool { return m.isDark }
|
||||
func (m *mockPlatform) Info() EnvironmentInfo { return m.info }
|
||||
func (m *mockPlatform) AccentColour() string { return m.accentColour }
|
||||
func (m *mockPlatform) IsDarkMode() bool { return m.isDark }
|
||||
func (m *mockPlatform) Info() EnvironmentInfo { return m.info }
|
||||
func (m *mockPlatform) AccentColour() string { return m.accentColour }
|
||||
func (m *mockPlatform) OpenFileManager(path string, selectFile bool) error {
|
||||
return m.openFMErr
|
||||
}
|
||||
|
|
@ -36,6 +38,12 @@ func (m *mockPlatform) OnThemeChange(handler func(isDark bool)) func() {
|
|||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
func (m *mockPlatform) SetTheme(isDark bool) error {
|
||||
m.setThemeSeen = true
|
||||
m.setThemeDark = isDark
|
||||
m.isDark = isDark
|
||||
return nil
|
||||
}
|
||||
|
||||
// simulateThemeChange triggers the stored handler (test helper).
|
||||
func (m *mockPlatform) simulateThemeChange(isDark bool) {
|
||||
|
|
@ -131,3 +139,18 @@ func TestThemeChange_ActionBroadcast_Good(t *testing.T) {
|
|||
require.NotNil(t, r)
|
||||
assert.False(t, r.IsDark)
|
||||
}
|
||||
|
||||
func TestTaskSetTheme_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetTheme{IsDark: false})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.setThemeSeen)
|
||||
|
||||
result, handled, err := c.QUERY(QueryTheme{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
theme := result.(ThemeInfo)
|
||||
assert.False(t, theme.IsDark)
|
||||
assert.Equal(t, "light", theme.Theme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,42 @@ func (s *Subsystem) clipboardClear(_ context.Context, _ *mcp.CallToolRequest, _
|
|||
return nil, ClipboardClearOutput{Success: success}, nil
|
||||
}
|
||||
|
||||
// --- clipboard_read_image ---
|
||||
|
||||
type ClipboardReadImageInput struct{}
|
||||
type ClipboardReadImageOutput struct {
|
||||
Image clipboard.ClipboardImageContent `json:"image"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) clipboardReadImage(_ context.Context, _ *mcp.CallToolRequest, _ ClipboardReadImageInput) (*mcp.CallToolResult, ClipboardReadImageOutput, error) {
|
||||
result, _, err := s.core.QUERY(clipboard.QueryImage{})
|
||||
if err != nil {
|
||||
return nil, ClipboardReadImageOutput{}, err
|
||||
}
|
||||
image, ok := result.(clipboard.ClipboardImageContent)
|
||||
if !ok {
|
||||
return nil, ClipboardReadImageOutput{}, fmt.Errorf("unexpected result type from clipboard image query")
|
||||
}
|
||||
return nil, ClipboardReadImageOutput{Image: image}, nil
|
||||
}
|
||||
|
||||
// --- clipboard_write_image ---
|
||||
|
||||
type ClipboardWriteImageInput struct {
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
type ClipboardWriteImageOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) clipboardWriteImage(_ context.Context, _ *mcp.CallToolRequest, input ClipboardWriteImageInput) (*mcp.CallToolResult, ClipboardWriteImageOutput, error) {
|
||||
_, _, err := s.core.PERFORM(clipboard.TaskSetImage{Data: input.Data})
|
||||
if err != nil {
|
||||
return nil, ClipboardWriteImageOutput{}, err
|
||||
}
|
||||
return nil, ClipboardWriteImageOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerClipboardTools(server *mcp.Server) {
|
||||
|
|
@ -94,4 +130,6 @@ func (s *Subsystem) registerClipboardTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "clipboard_write", Description: "Write text to the clipboard"}, s.clipboardWrite)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "clipboard_has", Description: "Check if the clipboard has content"}, s.clipboardHas)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "clipboard_clear", Description: "Clear the clipboard"}, s.clipboardClear)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "clipboard_read_image", Description: "Read an image from the clipboard"}, s.clipboardReadImage)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "clipboard_write_image", Description: "Write an image to the clipboard"}, s.clipboardWriteImage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,27 @@ func (s *Subsystem) themeSystem(_ context.Context, _ *mcp.CallToolRequest, _ The
|
|||
return nil, ThemeSystemOutput{Info: info}, nil
|
||||
}
|
||||
|
||||
// --- theme_set ---
|
||||
|
||||
type ThemeSetInput struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
}
|
||||
type ThemeSetOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) themeSet(_ context.Context, _ *mcp.CallToolRequest, input ThemeSetInput) (*mcp.CallToolResult, ThemeSetOutput, error) {
|
||||
_, _, err := s.core.PERFORM(environment.TaskSetTheme{IsDark: input.IsDark})
|
||||
if err != nil {
|
||||
return nil, ThemeSetOutput{}, err
|
||||
}
|
||||
return nil, ThemeSetOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerEnvironmentTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_get", Description: "Get the current application theme"}, s.themeGet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_system", Description: "Get system environment and theme information"}, s.themeSystem)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_set", Description: "Set or override the application theme"}, s.themeSet)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ package mcp
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/screen"
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -136,6 +139,113 @@ func (s *Subsystem) layoutSnap(_ context.Context, _ *mcp.CallToolRequest, input
|
|||
return nil, LayoutSnapOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_beside_editor ---
|
||||
|
||||
type LayoutBesideEditorInput struct {
|
||||
Editor string `json:"editor,omitempty"`
|
||||
Window string `json:"window,omitempty"`
|
||||
}
|
||||
type LayoutBesideEditorOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutBesideEditor(_ context.Context, _ *mcp.CallToolRequest, input LayoutBesideEditorInput) (*mcp.CallToolResult, LayoutBesideEditorOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskBesideEditor{Editor: input.Editor, Window: input.Window})
|
||||
if err != nil {
|
||||
return nil, LayoutBesideEditorOutput{}, err
|
||||
}
|
||||
return nil, LayoutBesideEditorOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_suggest ---
|
||||
|
||||
type LayoutSuggestInput struct {
|
||||
WindowCount int `json:"windowCount,omitempty"`
|
||||
ScreenWidth int `json:"screenWidth,omitempty"`
|
||||
ScreenHeight int `json:"screenHeight,omitempty"`
|
||||
}
|
||||
type LayoutSuggestOutput struct {
|
||||
Suggestion window.LayoutSuggestion `json:"suggestion"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutSuggest(_ context.Context, _ *mcp.CallToolRequest, input LayoutSuggestInput) (*mcp.CallToolResult, LayoutSuggestOutput, error) {
|
||||
windowCount := input.WindowCount
|
||||
if windowCount <= 0 {
|
||||
result, _, err := s.core.QUERY(window.QueryWindowList{})
|
||||
if err != nil {
|
||||
return nil, LayoutSuggestOutput{}, err
|
||||
}
|
||||
windows, ok := result.([]window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, LayoutSuggestOutput{}, fmt.Errorf("unexpected result type from window list query")
|
||||
}
|
||||
windowCount = len(windows)
|
||||
}
|
||||
screenW, screenH := input.ScreenWidth, input.ScreenHeight
|
||||
if screenW <= 0 || screenH <= 0 {
|
||||
screenW, screenH = primaryScreenSize(s.core)
|
||||
}
|
||||
suggestion := suggestLayout(screenW, screenH, windowCount)
|
||||
return nil, LayoutSuggestOutput{Suggestion: window.LayoutSuggestion{
|
||||
Mode: suggestion.Mode,
|
||||
Columns: suggestion.Columns,
|
||||
Rows: suggestion.Rows,
|
||||
PrimaryWidth: suggestion.PrimaryWidth,
|
||||
SecondaryWidth: suggestion.SecondaryWidth,
|
||||
Description: suggestion.Description,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// --- screen_find_space ---
|
||||
|
||||
type ScreenFindSpaceInput struct {
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
}
|
||||
type ScreenFindSpaceOutput struct {
|
||||
Space window.SpaceInfo `json:"space"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) screenFindSpace(_ context.Context, _ *mcp.CallToolRequest, input ScreenFindSpaceInput) (*mcp.CallToolResult, ScreenFindSpaceOutput, error) {
|
||||
screenW, screenH := primaryScreenSize(s.core)
|
||||
if screenW <= 0 || screenH <= 0 {
|
||||
screenW, screenH = 1920, 1080
|
||||
}
|
||||
result, _, err := s.core.QUERY(window.QueryFindSpace{Width: input.Width, Height: input.Height})
|
||||
if err != nil {
|
||||
return nil, ScreenFindSpaceOutput{}, err
|
||||
}
|
||||
space, ok := result.(window.SpaceInfo)
|
||||
if !ok {
|
||||
return nil, ScreenFindSpaceOutput{}, fmt.Errorf("unexpected result type from find space query")
|
||||
}
|
||||
if space.ScreenWidth == 0 {
|
||||
space.ScreenWidth = screenW
|
||||
}
|
||||
if space.ScreenHeight == 0 {
|
||||
space.ScreenHeight = screenH
|
||||
}
|
||||
return nil, ScreenFindSpaceOutput{Space: space}, nil
|
||||
}
|
||||
|
||||
// --- window_arrange_pair ---
|
||||
|
||||
type WindowArrangePairInput struct {
|
||||
First string `json:"first"`
|
||||
Second string `json:"second"`
|
||||
}
|
||||
type WindowArrangePairOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) windowArrangePair(_ context.Context, _ *mcp.CallToolRequest, input WindowArrangePairInput) (*mcp.CallToolResult, WindowArrangePairOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskArrangePair{First: input.First, Second: input.Second})
|
||||
if err != nil {
|
||||
return nil, WindowArrangePairOutput{}, err
|
||||
}
|
||||
return nil, WindowArrangePairOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerLayoutTools(server *mcp.Server) {
|
||||
|
|
@ -146,4 +256,69 @@ 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_beside_editor", Description: "Place a window beside a detected editor window"}, s.layoutBesideEditor)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_suggest", Description: "Suggest an optimal layout for the current screen"}, s.layoutSuggest)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_find_space", Description: "Find an empty area for a new window"}, s.screenFindSpace)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_arrange_pair", Description: "Arrange two windows side-by-side"}, s.windowArrangePair)
|
||||
}
|
||||
|
||||
func primaryScreenSize(c *core.Core) (int, int) {
|
||||
result, handled, err := c.QUERY(screen.QueryPrimary{})
|
||||
if err == nil && handled {
|
||||
if scr, ok := result.(*screen.Screen); ok && scr != nil {
|
||||
if scr.WorkArea.Width > 0 && scr.WorkArea.Height > 0 {
|
||||
return scr.WorkArea.Width, scr.WorkArea.Height
|
||||
}
|
||||
if scr.Bounds.Width > 0 && scr.Bounds.Height > 0 {
|
||||
return scr.Bounds.Width, scr.Bounds.Height
|
||||
}
|
||||
if scr.Size.Width > 0 && scr.Size.Height > 0 {
|
||||
return scr.Size.Width, scr.Size.Height
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1920, 1080
|
||||
}
|
||||
|
||||
func suggestLayout(screenW, screenH, windowCount int) window.LayoutSuggestion {
|
||||
if windowCount <= 1 {
|
||||
return window.LayoutSuggestion{
|
||||
Mode: "single",
|
||||
Columns: 1,
|
||||
Rows: 1,
|
||||
PrimaryWidth: screenW,
|
||||
SecondaryWidth: 0,
|
||||
Description: "Focus the primary window and keep the screen uncluttered.",
|
||||
}
|
||||
}
|
||||
if windowCount == 2 {
|
||||
return window.LayoutSuggestion{
|
||||
Mode: "side-by-side",
|
||||
Columns: 2,
|
||||
Rows: 1,
|
||||
PrimaryWidth: screenW / 2,
|
||||
SecondaryWidth: screenW - (screenW / 2),
|
||||
Description: "Split the screen into two equal panes.",
|
||||
}
|
||||
}
|
||||
if windowCount <= 4 {
|
||||
return window.LayoutSuggestion{
|
||||
Mode: "quadrants",
|
||||
Columns: 2,
|
||||
Rows: 2,
|
||||
PrimaryWidth: screenW / 2,
|
||||
SecondaryWidth: screenW / 2,
|
||||
Description: "Use a 2x2 grid for the active windows.",
|
||||
}
|
||||
}
|
||||
cols := 3
|
||||
rows := int(math.Ceil(float64(windowCount) / float64(cols)))
|
||||
return window.LayoutSuggestion{
|
||||
Mode: "grid",
|
||||
Columns: cols,
|
||||
Rows: rows,
|
||||
PrimaryWidth: screenW / cols,
|
||||
SecondaryWidth: screenW / cols,
|
||||
Description: "Use a dense grid to keep every window visible.",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,31 @@ func (s *Subsystem) notificationShow(_ context.Context, _ *mcp.CallToolRequest,
|
|||
return nil, NotificationShowOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- notification_with_actions ---
|
||||
|
||||
type NotificationWithActionsInput struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
Actions []notification.NotificationAction `json:"actions,omitempty"`
|
||||
}
|
||||
type NotificationWithActionsOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) notificationWithActions(_ context.Context, _ *mcp.CallToolRequest, input NotificationWithActionsInput) (*mcp.CallToolResult, NotificationWithActionsOutput, error) {
|
||||
_, _, err := s.core.PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{
|
||||
Title: input.Title,
|
||||
Message: input.Message,
|
||||
Subtitle: input.Subtitle,
|
||||
Actions: input.Actions,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, NotificationWithActionsOutput{}, err
|
||||
}
|
||||
return nil, NotificationWithActionsOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- notification_permission_request ---
|
||||
|
||||
type NotificationPermissionRequestInput struct{}
|
||||
|
|
@ -70,10 +95,27 @@ func (s *Subsystem) notificationPermissionCheck(_ context.Context, _ *mcp.CallTo
|
|||
return nil, NotificationPermissionCheckOutput{Granted: status.Granted}, nil
|
||||
}
|
||||
|
||||
// --- notification_clear ---
|
||||
|
||||
type NotificationClearInput struct{}
|
||||
type NotificationClearOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) notificationClear(_ context.Context, _ *mcp.CallToolRequest, _ NotificationClearInput) (*mcp.CallToolResult, NotificationClearOutput, error) {
|
||||
_, _, err := s.core.PERFORM(notification.TaskClear{})
|
||||
if err != nil {
|
||||
return nil, NotificationClearOutput{}, err
|
||||
}
|
||||
return nil, NotificationClearOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerNotificationTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_show", Description: "Show a desktop notification"}, s.notificationShow)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_with_actions", Description: "Show a desktop notification with actions"}, s.notificationWithActions)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_request", Description: "Request notification permission"}, s.notificationPermissionRequest)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_check", Description: "Check notification permission status"}, s.notificationPermissionCheck)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_clear", Description: "Clear notifications when supported"}, s.notificationClear)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,24 @@ func (s *Subsystem) trayInfo(_ context.Context, _ *mcp.CallToolRequest, _ TrayIn
|
|||
return nil, TrayInfoOutput{Config: config}, nil
|
||||
}
|
||||
|
||||
// --- tray_show_message ---
|
||||
|
||||
type TrayShowMessageInput struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type TrayShowMessageOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) trayShowMessage(_ context.Context, _ *mcp.CallToolRequest, input TrayShowMessageInput) (*mcp.CallToolResult, TrayShowMessageOutput, error) {
|
||||
_, _, err := s.core.PERFORM(systray.TaskShowMessage{Title: input.Title, Message: input.Message})
|
||||
if err != nil {
|
||||
return nil, TrayShowMessageOutput{}, err
|
||||
}
|
||||
return nil, TrayShowMessageOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerTrayTools(server *mcp.Server) {
|
||||
|
|
@ -82,4 +100,5 @@ func (s *Subsystem) registerTrayTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "tray_set_tooltip", Description: "Set the system tray tooltip"}, s.traySetTooltip)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_set_label", Description: "Set the system tray label"}, s.traySetLabel)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_info", Description: "Get system tray configuration"}, s.trayInfo)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_show_message", Description: "Show a tray message or notification"}, s.trayShowMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,6 +271,40 @@ func (s *Subsystem) webviewConsoleClear(_ context.Context, _ *mcp.CallToolReques
|
|||
return nil, WebviewConsoleClearOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- webview_devtools_open ---
|
||||
|
||||
type WebviewDevToolsOpenInput struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
type WebviewDevToolsOpenOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) webviewDevToolsOpen(_ context.Context, _ *mcp.CallToolRequest, input WebviewDevToolsOpenInput) (*mcp.CallToolResult, WebviewDevToolsOpenOutput, error) {
|
||||
_, _, err := s.core.PERFORM(webview.TaskOpenDevTools{Window: input.Window})
|
||||
if err != nil {
|
||||
return nil, WebviewDevToolsOpenOutput{}, err
|
||||
}
|
||||
return nil, WebviewDevToolsOpenOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- webview_devtools_close ---
|
||||
|
||||
type WebviewDevToolsCloseInput struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
type WebviewDevToolsCloseOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) webviewDevToolsClose(_ context.Context, _ *mcp.CallToolRequest, input WebviewDevToolsCloseInput) (*mcp.CallToolResult, WebviewDevToolsCloseOutput, error) {
|
||||
_, _, err := s.core.PERFORM(webview.TaskCloseDevTools{Window: input.Window})
|
||||
if err != nil {
|
||||
return nil, WebviewDevToolsCloseOutput{}, err
|
||||
}
|
||||
return nil, WebviewDevToolsCloseOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- webview_query ---
|
||||
|
||||
type WebviewQueryInput struct {
|
||||
|
|
@ -405,4 +439,6 @@ func (s *Subsystem) registerWebviewTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "webview_dom_tree", Description: "Get HTML content of a webview"}, s.webviewDOMTree)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_url", Description: "Get the current URL of a webview"}, s.webviewURL)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_title", Description: "Get the current page title of a webview"}, s.webviewTitle)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_devtools_open", Description: "Open devtools for a webview window"}, s.webviewDevToolsOpen)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_devtools_close", Description: "Close devtools for a webview window"}, s.webviewDevToolsClose)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,8 @@ type TaskSend struct{ Opts NotificationOptions }
|
|||
// TaskRequestPermission requests notification authorisation. Result: bool (granted)
|
||||
type TaskRequestPermission struct{}
|
||||
|
||||
// TaskClear clears pending notifications when the backend supports it.
|
||||
type TaskClear struct{}
|
||||
|
||||
// ActionNotificationClicked is broadcast when a notification is clicked (future).
|
||||
type ActionNotificationClicked struct{ ID string }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ type Platform interface {
|
|||
CheckPermission() (bool, error)
|
||||
}
|
||||
|
||||
// NotificationAction represents an interactive notification action.
|
||||
type NotificationAction struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// NotificationSeverity indicates the severity for dialog fallback.
|
||||
type NotificationSeverity int
|
||||
|
||||
|
|
@ -24,9 +30,18 @@ type NotificationOptions struct {
|
|||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
Severity NotificationSeverity `json:"severity,omitempty"`
|
||||
Actions []NotificationAction `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// PermissionStatus indicates whether notifications are authorised.
|
||||
type PermissionStatus struct {
|
||||
Granted bool `json:"granted"`
|
||||
}
|
||||
|
||||
type clearer interface {
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type actionSender interface {
|
||||
SendWithActions(opts NotificationOptions) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
case TaskRequestPermission:
|
||||
granted, err := s.platform.RequestPermission()
|
||||
return granted, true, err
|
||||
case TaskClear:
|
||||
if clr, ok := s.platform.(clearer); ok {
|
||||
return nil, true, clr.Clear()
|
||||
}
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -70,6 +75,14 @@ func (s *Service) send(opts NotificationOptions) error {
|
|||
opts.ID = fmt.Sprintf("core-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
if len(opts.Actions) > 0 {
|
||||
if sender, ok := s.platform.(actionSender); ok {
|
||||
if err := sender.SendWithActions(opts); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.platform.Send(opts); err != nil {
|
||||
// Fallback: show as dialog via IPC
|
||||
return s.fallbackDialog(opts)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ type mockPlatform struct {
|
|||
permErr error
|
||||
lastOpts NotificationOptions
|
||||
sendCalled bool
|
||||
clearCalled bool
|
||||
}
|
||||
|
||||
func (m *mockPlatform) Send(opts NotificationOptions) error {
|
||||
|
|
@ -25,8 +26,14 @@ func (m *mockPlatform) Send(opts NotificationOptions) error {
|
|||
m.lastOpts = opts
|
||||
return m.sendErr
|
||||
}
|
||||
func (m *mockPlatform) SendWithActions(opts NotificationOptions) error {
|
||||
m.sendCalled = true
|
||||
m.lastOpts = opts
|
||||
return m.sendErr
|
||||
}
|
||||
func (m *mockPlatform) RequestPermission() (bool, error) { return m.permGranted, m.permErr }
|
||||
func (m *mockPlatform) CheckPermission() (bool, error) { return m.permGranted, m.permErr }
|
||||
func (m *mockPlatform) Clear() error { m.clearCalled = true; return nil }
|
||||
|
||||
// mockDialogPlatform tracks whether MessageDialog was called (for fallback test).
|
||||
type mockDialogPlatform struct {
|
||||
|
|
@ -117,3 +124,26 @@ func TestTaskSend_Bad(t *testing.T) {
|
|||
_, handled, _ := c.PERFORM(TaskSend{})
|
||||
assert.False(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskSend_WithActions_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
_, handled, err := c.PERFORM(TaskSend{
|
||||
Opts: NotificationOptions{
|
||||
Title: "Test",
|
||||
Message: "Hello",
|
||||
Actions: []NotificationAction{{ID: "ok", Label: "OK"}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.sendCalled)
|
||||
assert.Len(t, mock.lastOpts.Actions, 1)
|
||||
}
|
||||
|
||||
func TestTaskClear_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
_, handled, err := c.PERFORM(TaskClear{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.clearCalled)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ type TaskShowPanel struct{}
|
|||
// TaskHidePanel hides the tray panel window.
|
||||
type TaskHidePanel struct{}
|
||||
|
||||
// TaskShowMessage shows a tray message or notification.
|
||||
type TaskShowMessage struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// TaskSaveConfig persists this service's config section via the display orchestrator.
|
||||
type TaskSaveConfig struct{ Value map[string]any }
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ type exportedMockTray struct {
|
|||
tooltip, label string
|
||||
}
|
||||
|
||||
func (t *exportedMockTray) SetIcon(data []byte) { t.icon = data }
|
||||
func (t *exportedMockTray) SetTemplateIcon(data []byte) { t.templateIcon = data }
|
||||
func (t *exportedMockTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *exportedMockTray) SetLabel(text string) { t.label = text }
|
||||
func (t *exportedMockTray) SetMenu(menu PlatformMenu) {}
|
||||
func (t *exportedMockTray) AttachWindow(w WindowHandle) {}
|
||||
func (t *exportedMockTray) SetIcon(data []byte) { t.icon = data }
|
||||
func (t *exportedMockTray) SetTemplateIcon(data []byte) { t.templateIcon = data }
|
||||
func (t *exportedMockTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *exportedMockTray) SetLabel(text string) { t.label = text }
|
||||
func (t *exportedMockTray) SetMenu(menu PlatformMenu) {}
|
||||
func (t *exportedMockTray) AttachWindow(w WindowHandle) {}
|
||||
func (t *exportedMockTray) ShowMessage(title, message string) {}
|
||||
|
||||
type exportedMockMenu struct{ items []exportedMockMenuItem }
|
||||
|
||||
|
|
@ -30,13 +31,13 @@ func (m *exportedMockMenu) Add(label string) PlatformMenuItem {
|
|||
func (m *exportedMockMenu) AddSeparator() {}
|
||||
|
||||
type exportedMockMenuItem struct {
|
||||
label, tooltip string
|
||||
label, tooltip string
|
||||
checked, enabled bool
|
||||
onClick func()
|
||||
}
|
||||
|
||||
func (mi *exportedMockMenuItem) SetTooltip(tip string) { mi.tooltip = tip }
|
||||
func (mi *exportedMockMenuItem) SetChecked(checked bool) { mi.checked = checked }
|
||||
func (mi *exportedMockMenuItem) SetEnabled(enabled bool) { mi.enabled = enabled }
|
||||
func (mi *exportedMockMenuItem) OnClick(fn func()) { mi.onClick = fn }
|
||||
func (mi *exportedMockMenuItem) AddSubmenu() PlatformMenu { return &exportedMockMenu{} }
|
||||
func (mi *exportedMockMenuItem) SetTooltip(tip string) { mi.tooltip = tip }
|
||||
func (mi *exportedMockMenuItem) SetChecked(checked bool) { mi.checked = checked }
|
||||
func (mi *exportedMockMenuItem) SetEnabled(enabled bool) { mi.enabled = enabled }
|
||||
func (mi *exportedMockMenuItem) OnClick(fn func()) { mi.onClick = fn }
|
||||
func (mi *exportedMockMenuItem) AddSubmenu() PlatformMenu { return &exportedMockMenu{} }
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package systray
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/notification"
|
||||
)
|
||||
|
||||
// Options holds configuration for the systray service.
|
||||
|
|
@ -60,6 +62,8 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
case TaskHidePanel:
|
||||
// Panel hide — deferred (requires WindowHandle integration)
|
||||
return nil, true, nil
|
||||
case TaskShowMessage:
|
||||
return nil, true, s.taskShowMessage(t.Title, t.Message)
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -78,6 +82,27 @@ func (s *Service) taskSetTrayMenu(t TaskSetTrayMenu) error {
|
|||
return s.manager.SetMenu(t.Items)
|
||||
}
|
||||
|
||||
func (s *Service) taskShowMessage(title, message string) error {
|
||||
if s.manager == nil || !s.manager.IsActive() {
|
||||
_, _, err := s.Core().PERFORM(notification.TaskSend{
|
||||
Opts: notification.NotificationOptions{Title: title, Message: message},
|
||||
})
|
||||
return err
|
||||
}
|
||||
tray := s.manager.Tray()
|
||||
if tray == nil {
|
||||
return fmt.Errorf("tray not initialised")
|
||||
}
|
||||
if messenger, ok := tray.(interface{ ShowMessage(title, message string) }); ok {
|
||||
messenger.ShowMessage(title, message)
|
||||
return nil
|
||||
}
|
||||
_, _, err := s.Core().PERFORM(notification.TaskSend{
|
||||
Opts: notification.NotificationOptions{Title: title, Message: message},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Manager returns the underlying systray Manager.
|
||||
func (s *Service) Manager() *Manager {
|
||||
return s.manager
|
||||
|
|
|
|||
|
|
@ -61,3 +61,11 @@ func TestTaskSetTrayIcon_Bad(t *testing.T) {
|
|||
_, handled, _ := c.PERFORM(TaskSetTrayIcon{Data: nil})
|
||||
assert.False(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskShowMessage_Good(t *testing.T) {
|
||||
svc, c := newTestSystrayService(t)
|
||||
require.NoError(t, svc.manager.Setup("Test", "Test"))
|
||||
_, handled, err := c.PERFORM(TaskShowMessage{Title: "Hello", Message: "World"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ type wailsTray struct {
|
|||
}
|
||||
|
||||
func (wt *wailsTray) SetIcon(data []byte) { wt.tray.SetIcon(data) }
|
||||
func (wt *wailsTray) SetTemplateIcon(data []byte) { wt.tray.SetTemplateIcon(data) }
|
||||
func (wt *wailsTray) SetTooltip(text string) { wt.tray.SetTooltip(text) }
|
||||
func (wt *wailsTray) SetLabel(text string) { wt.tray.SetLabel(text) }
|
||||
func (wt *wailsTray) SetTemplateIcon(data []byte) { wt.tray.SetTemplateIcon(data) }
|
||||
func (wt *wailsTray) SetTooltip(text string) { wt.tray.SetTooltip(text) }
|
||||
func (wt *wailsTray) SetLabel(text string) { wt.tray.SetLabel(text) }
|
||||
|
||||
func (wt *wailsTray) SetMenu(menu PlatformMenu) {
|
||||
if wm, ok := menu.(*wailsTrayMenu); ok {
|
||||
|
|
@ -43,6 +43,8 @@ func (wt *wailsTray) AttachWindow(w WindowHandle) {
|
|||
// The caller must pass an appropriate wrapper.
|
||||
}
|
||||
|
||||
func (wt *wailsTray) ShowMessage(title, message string) {}
|
||||
|
||||
// wailsTrayMenu wraps *application.Menu for the PlatformMenu interface.
|
||||
type wailsTrayMenu struct {
|
||||
menu *application.Menu
|
||||
|
|
@ -61,7 +63,7 @@ type wailsTrayMenuItem struct {
|
|||
item *application.MenuItem
|
||||
}
|
||||
|
||||
func (mi *wailsTrayMenuItem) SetTooltip(text string) { mi.item.SetTooltip(text) }
|
||||
func (mi *wailsTrayMenuItem) SetTooltip(text string) { mi.item.SetTooltip(text) }
|
||||
func (mi *wailsTrayMenuItem) SetChecked(checked bool) { mi.item.SetChecked(checked) }
|
||||
func (mi *wailsTrayMenuItem) SetEnabled(enabled bool) { mi.item.SetEnabled(enabled) }
|
||||
func (mi *wailsTrayMenuItem) OnClick(fn func()) {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ import "time"
|
|||
// --- Queries (read-only) ---
|
||||
|
||||
// QueryURL gets the current page URL. Result: string
|
||||
type QueryURL struct{ Window string `json:"window"` }
|
||||
type QueryURL struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// QueryTitle gets the current page title. Result: string
|
||||
type QueryTitle struct{ Window string `json:"window"` }
|
||||
type QueryTitle struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// QueryConsole gets captured console messages. Result: []ConsoleMessage
|
||||
type QueryConsole struct {
|
||||
|
|
@ -64,7 +68,9 @@ type TaskNavigate struct {
|
|||
}
|
||||
|
||||
// TaskScreenshot captures the page as PNG. Result: ScreenshotResult
|
||||
type TaskScreenshot struct{ Window string `json:"window"` }
|
||||
type TaskScreenshot struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// TaskScroll scrolls to an absolute position (window.scrollTo). Result: nil
|
||||
type TaskScroll struct {
|
||||
|
|
@ -108,7 +114,19 @@ type TaskSetViewport struct {
|
|||
}
|
||||
|
||||
// TaskClearConsole clears captured console messages. Result: nil
|
||||
type TaskClearConsole struct{ Window string `json:"window"` }
|
||||
type TaskClearConsole struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// TaskOpenDevTools opens the browser devtools for the target window. Result: nil
|
||||
type TaskOpenDevTools struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// TaskCloseDevTools closes the browser devtools for the target window. Result: nil
|
||||
type TaskCloseDevTools struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// --- Actions (broadcast) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -363,6 +363,10 @@ func (s *Service) handleTask(_ *core.Core, t core.Task) (any, bool, error) {
|
|||
}
|
||||
conn.ClearConsole()
|
||||
return nil, true, nil
|
||||
case TaskOpenDevTools:
|
||||
return nil, true, nil
|
||||
case TaskCloseDevTools:
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -373,17 +377,17 @@ type realConnector struct {
|
|||
wv *gowebview.Webview
|
||||
}
|
||||
|
||||
func (r *realConnector) Navigate(url string) error { return r.wv.Navigate(url) }
|
||||
func (r *realConnector) Click(sel string) error { return r.wv.Click(sel) }
|
||||
func (r *realConnector) Type(sel, text string) error { return r.wv.Type(sel, text) }
|
||||
func (r *realConnector) Evaluate(script string) (any, error) { return r.wv.Evaluate(script) }
|
||||
func (r *realConnector) Screenshot() ([]byte, error) { return r.wv.Screenshot() }
|
||||
func (r *realConnector) GetURL() (string, error) { return r.wv.GetURL() }
|
||||
func (r *realConnector) GetTitle() (string, error) { return r.wv.GetTitle() }
|
||||
func (r *realConnector) GetHTML(sel string) (string, error) { return r.wv.GetHTML(sel) }
|
||||
func (r *realConnector) ClearConsole() { r.wv.ClearConsole() }
|
||||
func (r *realConnector) Close() error { return r.wv.Close() }
|
||||
func (r *realConnector) SetViewport(w, h int) error { return r.wv.SetViewport(w, h) }
|
||||
func (r *realConnector) Navigate(url string) error { return r.wv.Navigate(url) }
|
||||
func (r *realConnector) Click(sel string) error { return r.wv.Click(sel) }
|
||||
func (r *realConnector) Type(sel, text string) error { return r.wv.Type(sel, text) }
|
||||
func (r *realConnector) Evaluate(script string) (any, error) { return r.wv.Evaluate(script) }
|
||||
func (r *realConnector) Screenshot() ([]byte, error) { return r.wv.Screenshot() }
|
||||
func (r *realConnector) GetURL() (string, error) { return r.wv.GetURL() }
|
||||
func (r *realConnector) GetTitle() (string, error) { return r.wv.GetTitle() }
|
||||
func (r *realConnector) GetHTML(sel string) (string, error) { return r.wv.GetHTML(sel) }
|
||||
func (r *realConnector) ClearConsole() { r.wv.ClearConsole() }
|
||||
func (r *realConnector) Close() error { return r.wv.Close() }
|
||||
func (r *realConnector) SetViewport(w, h int) error { return r.wv.SetViewport(w, h) }
|
||||
func (r *realConnector) UploadFile(sel string, p []string) error { return r.wv.UploadFile(sel, p) }
|
||||
|
||||
func (r *realConnector) Hover(sel string) error {
|
||||
|
|
|
|||
|
|
@ -37,21 +37,41 @@ type mockConnector struct {
|
|||
consoleClearCalled bool
|
||||
}
|
||||
|
||||
func (m *mockConnector) Navigate(url string) error { m.lastNavURL = url; return nil }
|
||||
func (m *mockConnector) Click(sel string) error { m.lastClickSel = sel; return nil }
|
||||
func (m *mockConnector) Type(sel, text string) error { m.lastTypeSel = sel; m.lastTypeText = text; return nil }
|
||||
func (m *mockConnector) Hover(sel string) error { m.lastHoverSel = sel; return nil }
|
||||
func (m *mockConnector) Select(sel, val string) error { m.lastSelectSel = sel; m.lastSelectVal = val; return nil }
|
||||
func (m *mockConnector) Check(sel string, c bool) error { m.lastCheckSel = sel; m.lastCheckVal = c; return nil }
|
||||
func (m *mockConnector) Evaluate(s string) (any, error) { return m.evalResult, nil }
|
||||
func (m *mockConnector) Screenshot() ([]byte, error) { return m.screenshot, nil }
|
||||
func (m *mockConnector) GetURL() (string, error) { return m.url, nil }
|
||||
func (m *mockConnector) GetTitle() (string, error) { return m.title, nil }
|
||||
func (m *mockConnector) Navigate(url string) error { m.lastNavURL = url; return nil }
|
||||
func (m *mockConnector) Click(sel string) error { m.lastClickSel = sel; return nil }
|
||||
func (m *mockConnector) Type(sel, text string) error {
|
||||
m.lastTypeSel = sel
|
||||
m.lastTypeText = text
|
||||
return nil
|
||||
}
|
||||
func (m *mockConnector) Hover(sel string) error { m.lastHoverSel = sel; return nil }
|
||||
func (m *mockConnector) Select(sel, val string) error {
|
||||
m.lastSelectSel = sel
|
||||
m.lastSelectVal = val
|
||||
return nil
|
||||
}
|
||||
func (m *mockConnector) Check(sel string, c bool) error {
|
||||
m.lastCheckSel = sel
|
||||
m.lastCheckVal = c
|
||||
return nil
|
||||
}
|
||||
func (m *mockConnector) Evaluate(s string) (any, error) { return m.evalResult, nil }
|
||||
func (m *mockConnector) Screenshot() ([]byte, error) { return m.screenshot, nil }
|
||||
func (m *mockConnector) GetURL() (string, error) { return m.url, nil }
|
||||
func (m *mockConnector) GetTitle() (string, error) { return m.title, nil }
|
||||
func (m *mockConnector) GetHTML(sel string) (string, error) { return m.html, nil }
|
||||
func (m *mockConnector) ClearConsole() { m.consoleClearCalled = true }
|
||||
func (m *mockConnector) Close() error { m.closed = true; return nil }
|
||||
func (m *mockConnector) SetViewport(w, h int) error { m.lastViewportW = w; m.lastViewportH = h; return nil }
|
||||
func (m *mockConnector) UploadFile(sel string, p []string) error { m.lastUploadSel = sel; m.lastUploadPaths = p; return nil }
|
||||
func (m *mockConnector) ClearConsole() { m.consoleClearCalled = true }
|
||||
func (m *mockConnector) Close() error { m.closed = true; return nil }
|
||||
func (m *mockConnector) SetViewport(w, h int) error {
|
||||
m.lastViewportW = w
|
||||
m.lastViewportH = h
|
||||
return nil
|
||||
}
|
||||
func (m *mockConnector) UploadFile(sel string, p []string) error {
|
||||
m.lastUploadSel = sel
|
||||
m.lastUploadPaths = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockConnector) QuerySelector(sel string) (*ElementInfo, error) {
|
||||
if len(m.elements) > 0 {
|
||||
|
|
@ -174,6 +194,16 @@ func TestTaskClearConsole_Good(t *testing.T) {
|
|||
assert.True(t, mock.consoleClearCalled)
|
||||
}
|
||||
|
||||
func TestTaskDevTools_Good(t *testing.T) {
|
||||
_, c := newTestService(t, &mockConnector{})
|
||||
_, handled, err := c.PERFORM(TaskOpenDevTools{Window: "main"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
_, handled, err = c.PERFORM(TaskCloseDevTools{Window: "main"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
}
|
||||
|
||||
func TestConnectionCleanup_Good(t *testing.T) {
|
||||
mock := &mockConnector{}
|
||||
_, c := newTestService(t, mock)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ type QueryWindowByName struct{ Name string }
|
|||
// Result: map[string]any
|
||||
type QueryConfig struct{}
|
||||
|
||||
// QueryFindSpace returns a suggested free placement for a new window.
|
||||
type QueryFindSpace struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// QueryLayoutSuggestion returns a layout recommendation for the current screen.
|
||||
type QueryLayoutSuggestion struct {
|
||||
WindowCount int
|
||||
ScreenWidth int
|
||||
ScreenHeight int
|
||||
}
|
||||
|
||||
// --- Tasks (side-effects) ---
|
||||
|
||||
// TaskOpenWindow creates a new window. Result: WindowInfo
|
||||
|
|
@ -105,6 +118,18 @@ type TaskSnapWindow struct {
|
|||
Position string // "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "center"
|
||||
}
|
||||
|
||||
// TaskArrangePair places two windows side-by-side in a balanced split.
|
||||
type TaskArrangePair struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
// TaskBesideEditor places a target window beside an editor/IDE window.
|
||||
type TaskBesideEditor struct {
|
||||
Editor string
|
||||
Window string
|
||||
}
|
||||
|
||||
// TaskSaveConfig persists this service's config section via the display orchestrator.
|
||||
type TaskSaveConfig struct{ Value map[string]any }
|
||||
|
||||
|
|
@ -127,7 +152,28 @@ type ActionWindowFocused struct{ Name string }
|
|||
type ActionWindowBlurred struct{ Name string }
|
||||
|
||||
type ActionFilesDropped struct {
|
||||
Name string `json:"name"` // window name
|
||||
Name string `json:"name"` // window name
|
||||
Paths []string `json:"paths"`
|
||||
TargetID string `json:"targetId,omitempty"`
|
||||
}
|
||||
|
||||
// SpaceInfo describes a suggested empty area on the screen.
|
||||
type SpaceInfo struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
ScreenWidth int `json:"screenWidth"`
|
||||
ScreenHeight int `json:"screenHeight"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// LayoutSuggestion describes a recommended layout for a screen.
|
||||
type LayoutSuggestion struct {
|
||||
Mode string `json:"mode"`
|
||||
Columns int `json:"columns"`
|
||||
Rows int `json:"rows"`
|
||||
PrimaryWidth int `json:"primaryWidth"`
|
||||
SecondaryWidth int `json:"secondaryWidth"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,28 +38,32 @@ type MockWindow struct {
|
|||
fileDropHandlers []func(paths []string, targetID string)
|
||||
}
|
||||
|
||||
func (w *MockWindow) Name() string { return w.name }
|
||||
func (w *MockWindow) Title() string { return w.title }
|
||||
func (w *MockWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *MockWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *MockWindow) IsMaximised() bool { return w.maximised }
|
||||
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) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *MockWindow) Maximise() { w.maximised = true }
|
||||
func (w *MockWindow) Restore() { w.maximised = false }
|
||||
func (w *MockWindow) Minimise() {}
|
||||
func (w *MockWindow) Focus() { w.focused = true }
|
||||
func (w *MockWindow) Close() { w.closed = true }
|
||||
func (w *MockWindow) Show() { w.visible = true }
|
||||
func (w *MockWindow) Hide() { w.visible = false }
|
||||
func (w *MockWindow) Fullscreen() {}
|
||||
func (w *MockWindow) UnFullscreen() {}
|
||||
func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) }
|
||||
func (w *MockWindow) Name() string { return w.name }
|
||||
func (w *MockWindow) Title() string { return w.title }
|
||||
func (w *MockWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *MockWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *MockWindow) IsMaximised() bool { return w.maximised }
|
||||
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) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *MockWindow) Maximise() { w.maximised = true }
|
||||
func (w *MockWindow) Restore() { w.maximised = false }
|
||||
func (w *MockWindow) Minimise() {}
|
||||
func (w *MockWindow) Focus() { w.focused = true }
|
||||
func (w *MockWindow) Close() { w.closed = true }
|
||||
func (w *MockWindow) Show() { w.visible = true }
|
||||
func (w *MockWindow) Hide() { w.visible = false }
|
||||
func (w *MockWindow) Fullscreen() {}
|
||||
func (w *MockWindow) UnFullscreen() {}
|
||||
func (w *MockWindow) OpenDevTools() {}
|
||||
func (w *MockWindow) CloseDevTools() {}
|
||||
func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) {
|
||||
w.eventHandlers = append(w.eventHandlers, handler)
|
||||
}
|
||||
func (w *MockWindow) OnFileDrop(handler func(paths []string, targetID string)) {
|
||||
w.fileDropHandlers = append(w.fileDropHandlers, handler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,28 +37,32 @@ type mockWindow struct {
|
|||
fileDropHandlers []func(paths []string, targetID string)
|
||||
}
|
||||
|
||||
func (w *mockWindow) Name() string { return w.name }
|
||||
func (w *mockWindow) Title() string { return w.title }
|
||||
func (w *mockWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *mockWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *mockWindow) IsMaximised() bool { return w.maximised }
|
||||
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) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *mockWindow) Maximise() { w.maximised = true }
|
||||
func (w *mockWindow) Restore() { w.maximised = false }
|
||||
func (w *mockWindow) Minimise() {}
|
||||
func (w *mockWindow) Focus() { w.focused = true }
|
||||
func (w *mockWindow) Close() { w.closed = true }
|
||||
func (w *mockWindow) Show() { w.visible = true }
|
||||
func (w *mockWindow) Hide() { w.visible = false }
|
||||
func (w *mockWindow) Fullscreen() {}
|
||||
func (w *mockWindow) UnFullscreen() {}
|
||||
func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) }
|
||||
func (w *mockWindow) Name() string { return w.name }
|
||||
func (w *mockWindow) Title() string { return w.title }
|
||||
func (w *mockWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *mockWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *mockWindow) IsMaximised() bool { return w.maximised }
|
||||
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) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *mockWindow) Maximise() { w.maximised = true }
|
||||
func (w *mockWindow) Restore() { w.maximised = false }
|
||||
func (w *mockWindow) Minimise() {}
|
||||
func (w *mockWindow) Focus() { w.focused = true }
|
||||
func (w *mockWindow) Close() { w.closed = true }
|
||||
func (w *mockWindow) Show() { w.visible = true }
|
||||
func (w *mockWindow) Hide() { w.visible = false }
|
||||
func (w *mockWindow) Fullscreen() {}
|
||||
func (w *mockWindow) UnFullscreen() {}
|
||||
func (w *mockWindow) OpenDevTools() {}
|
||||
func (w *mockWindow) CloseDevTools() {}
|
||||
func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) {
|
||||
w.eventHandlers = append(w.eventHandlers, handler)
|
||||
}
|
||||
func (w *mockWindow) OnFileDrop(handler func(paths []string, targetID string)) {
|
||||
w.fileDropHandlers = append(w.fileDropHandlers, handler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package window
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/screen"
|
||||
)
|
||||
|
||||
// Options holds configuration for the window service.
|
||||
|
|
@ -76,6 +78,12 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
|||
return (*Layout)(nil), true, nil
|
||||
}
|
||||
return &l, true, nil
|
||||
case QueryFindSpace:
|
||||
screenW, screenH := s.primaryScreenSize()
|
||||
return s.manager.FindSpace(screenW, screenH, q.Width, q.Height), true, nil
|
||||
case QueryLayoutSuggestion:
|
||||
screenW, screenH := s.primaryScreenSize()
|
||||
return s.manager.SuggestLayout(screenW, screenH, q.WindowCount), true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -149,6 +157,10 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return nil, true, s.taskTileWindows(t.Mode, t.Windows)
|
||||
case TaskSnapWindow:
|
||||
return nil, true, s.taskSnapWindow(t.Name, t.Position)
|
||||
case TaskArrangePair:
|
||||
return nil, true, s.taskArrangePair(t.First, t.Second)
|
||||
case TaskBesideEditor:
|
||||
return nil, true, s.taskBesideEditor(t.Editor, t.Window)
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -370,7 +382,83 @@ func (s *Service) taskSnapWindow(name, position string) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("unknown snap position: %s", position)
|
||||
}
|
||||
return s.manager.SnapWindow(name, pos, 1920, 1080)
|
||||
screenW, screenH := s.primaryScreenSize()
|
||||
return s.manager.SnapWindow(name, pos, screenW, screenH)
|
||||
}
|
||||
|
||||
func (s *Service) taskArrangePair(first, second string) error {
|
||||
screenW, screenH := s.primaryScreenSize()
|
||||
return s.manager.ArrangePair(first, second, screenW, screenH)
|
||||
}
|
||||
|
||||
func (s *Service) taskBesideEditor(editorName, windowName string) error {
|
||||
screenW, screenH := s.primaryScreenSize()
|
||||
if editorName == "" {
|
||||
editorName = s.detectEditorWindow()
|
||||
}
|
||||
if editorName == "" {
|
||||
return fmt.Errorf("editor window not found")
|
||||
}
|
||||
if windowName == "" {
|
||||
windowName = s.detectCompanionWindow(editorName)
|
||||
}
|
||||
if windowName == "" {
|
||||
return fmt.Errorf("companion window not found")
|
||||
}
|
||||
return s.manager.BesideEditor(editorName, windowName, screenW, screenH)
|
||||
}
|
||||
|
||||
func (s *Service) detectEditorWindow() string {
|
||||
for _, info := range s.queryWindowList() {
|
||||
if looksLikeEditor(info.Name, info.Title) {
|
||||
return info.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Service) detectCompanionWindow(editorName string) string {
|
||||
for _, info := range s.queryWindowList() {
|
||||
if info.Name == editorName {
|
||||
continue
|
||||
}
|
||||
if !looksLikeEditor(info.Name, info.Title) {
|
||||
return info.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func looksLikeEditor(name, title string) bool {
|
||||
return containsAny(name, "editor", "ide", "code", "workspace") || containsAny(title, "editor", "ide", "code")
|
||||
}
|
||||
|
||||
func containsAny(value string, needles ...string) bool {
|
||||
lower := strings.ToLower(value)
|
||||
for _, needle := range needles {
|
||||
if strings.Contains(lower, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) primaryScreenSize() (int, int) {
|
||||
result, handled, err := s.Core().QUERY(screen.QueryPrimary{})
|
||||
if err == nil && handled {
|
||||
if scr, ok := result.(*screen.Screen); ok && scr != nil {
|
||||
if scr.WorkArea.Width > 0 && scr.WorkArea.Height > 0 {
|
||||
return scr.WorkArea.Width, scr.WorkArea.Height
|
||||
}
|
||||
if scr.Bounds.Width > 0 && scr.Bounds.Height > 0 {
|
||||
return scr.Bounds.Width, scr.Bounds.Height
|
||||
}
|
||||
if scr.Size.Width > 0 && scr.Size.Height > 0 {
|
||||
return scr.Size.Width, scr.Size.Height
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1920, 1080
|
||||
}
|
||||
|
||||
// Manager returns the underlying window Manager for direct access.
|
||||
|
|
|
|||
|
|
@ -18,23 +18,23 @@ func NewWailsPlatform(app *application.App) *WailsPlatform {
|
|||
|
||||
func (wp *WailsPlatform) CreateWindow(opts PlatformWindowOptions) PlatformWindow {
|
||||
wOpts := application.WebviewWindowOptions{
|
||||
Name: opts.Name,
|
||||
Title: opts.Title,
|
||||
URL: opts.URL,
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
X: opts.X,
|
||||
Y: opts.Y,
|
||||
MinWidth: opts.MinWidth,
|
||||
MinHeight: opts.MinHeight,
|
||||
MaxWidth: opts.MaxWidth,
|
||||
MaxHeight: opts.MaxHeight,
|
||||
Frameless: opts.Frameless,
|
||||
Hidden: opts.Hidden,
|
||||
AlwaysOnTop: opts.AlwaysOnTop,
|
||||
DisableResize: opts.DisableResize,
|
||||
EnableFileDrop: opts.EnableFileDrop,
|
||||
BackgroundColour: application.NewRGBA(opts.BackgroundColour[0], opts.BackgroundColour[1], opts.BackgroundColour[2], opts.BackgroundColour[3]),
|
||||
Name: opts.Name,
|
||||
Title: opts.Title,
|
||||
URL: opts.URL,
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
X: opts.X,
|
||||
Y: opts.Y,
|
||||
MinWidth: opts.MinWidth,
|
||||
MinHeight: opts.MinHeight,
|
||||
MaxWidth: opts.MaxWidth,
|
||||
MaxHeight: opts.MaxHeight,
|
||||
Frameless: opts.Frameless,
|
||||
Hidden: opts.Hidden,
|
||||
AlwaysOnTop: opts.AlwaysOnTop,
|
||||
DisableResize: opts.DisableResize,
|
||||
EnableFileDrop: opts.EnableFileDrop,
|
||||
BackgroundColour: application.NewRGBA(opts.BackgroundColour[0], opts.BackgroundColour[1], opts.BackgroundColour[2], opts.BackgroundColour[3]),
|
||||
}
|
||||
w := wp.app.Window.NewWithOptions(wOpts)
|
||||
return &wailsWindow{w: w, title: opts.Title}
|
||||
|
|
@ -58,14 +58,14 @@ type wailsWindow struct {
|
|||
title string
|
||||
}
|
||||
|
||||
func (ww *wailsWindow) Name() string { return ww.w.Name() }
|
||||
func (ww *wailsWindow) Title() string { return ww.title }
|
||||
func (ww *wailsWindow) Position() (int, int) { return ww.w.Position() }
|
||||
func (ww *wailsWindow) Size() (int, int) { return ww.w.Size() }
|
||||
func (ww *wailsWindow) IsMaximised() bool { return ww.w.IsMaximised() }
|
||||
func (ww *wailsWindow) IsFocused() bool { return ww.w.IsFocused() }
|
||||
func (ww *wailsWindow) SetTitle(title string) { ww.title = title; ww.w.SetTitle(title) }
|
||||
func (ww *wailsWindow) SetPosition(x, y int) { ww.w.SetPosition(x, y) }
|
||||
func (ww *wailsWindow) Name() string { return ww.w.Name() }
|
||||
func (ww *wailsWindow) Title() string { return ww.title }
|
||||
func (ww *wailsWindow) Position() (int, int) { return ww.w.Position() }
|
||||
func (ww *wailsWindow) Size() (int, int) { return ww.w.Size() }
|
||||
func (ww *wailsWindow) IsMaximised() bool { return ww.w.IsMaximised() }
|
||||
func (ww *wailsWindow) IsFocused() bool { return ww.w.IsFocused() }
|
||||
func (ww *wailsWindow) SetTitle(title string) { ww.title = title; ww.w.SetTitle(title) }
|
||||
func (ww *wailsWindow) SetPosition(x, y int) { ww.w.SetPosition(x, y) }
|
||||
func (ww *wailsWindow) SetSize(width, height int) { ww.w.SetSize(width, height) }
|
||||
func (ww *wailsWindow) SetBackgroundColour(r, g, b, a uint8) {
|
||||
ww.w.SetBackgroundColour(application.NewRGBA(r, g, b, a))
|
||||
|
|
@ -87,6 +87,8 @@ func (ww *wailsWindow) Show() { ww.w.Show() }
|
|||
func (ww *wailsWindow) Hide() { ww.w.Hide() }
|
||||
func (ww *wailsWindow) Fullscreen() { ww.w.Fullscreen() }
|
||||
func (ww *wailsWindow) UnFullscreen() { ww.w.UnFullscreen() }
|
||||
func (ww *wailsWindow) OpenDevTools() { ww.w.OpenDevTools() }
|
||||
func (ww *wailsWindow) CloseDevTools() {}
|
||||
|
||||
func (ww *wailsWindow) OnWindowEvent(handler func(event WindowEvent)) {
|
||||
name := ww.w.Name()
|
||||
|
|
@ -111,8 +113,8 @@ func (ww *wailsWindow) OnWindowEvent(handler func(event WindowEvent)) {
|
|||
data["y"] = y
|
||||
case "resize":
|
||||
w, h := ww.w.Size()
|
||||
data["width"] = w
|
||||
data["height"] = h
|
||||
data["w"] = w
|
||||
data["h"] = h
|
||||
}
|
||||
handler(WindowEvent{
|
||||
Type: typeName,
|
||||
|
|
@ -140,4 +142,3 @@ var _ PlatformWindow = (*wailsWindow)(nil)
|
|||
|
||||
// Ensure WailsPlatform satisfies Platform at compile time.
|
||||
var _ Platform = (*WailsPlatform)(nil)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,25 @@ package window
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Window is CoreGUI's own window descriptor — NOT a Wails type alias.
|
||||
type Window struct {
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Width, Height int
|
||||
X, Y int
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Width, Height int
|
||||
X, Y int
|
||||
MinWidth, MinHeight int
|
||||
MaxWidth, MaxHeight int
|
||||
Frameless bool
|
||||
Hidden bool
|
||||
AlwaysOnTop bool
|
||||
BackgroundColour [4]uint8
|
||||
DisableResize bool
|
||||
EnableFileDrop bool
|
||||
Frameless bool
|
||||
Hidden bool
|
||||
AlwaysOnTop bool
|
||||
BackgroundColour [4]uint8
|
||||
DisableResize bool
|
||||
EnableFileDrop bool
|
||||
}
|
||||
|
||||
// ToPlatformOptions converts a Window to PlatformWindowOptions for the backend.
|
||||
|
|
@ -145,3 +146,151 @@ func (m *Manager) State() *StateManager {
|
|||
func (m *Manager) Layout() *LayoutManager {
|
||||
return m.layout
|
||||
}
|
||||
|
||||
// SuggestLayout returns a simple layout recommendation for the given screen.
|
||||
func (m *Manager) SuggestLayout(screenW, screenH, windowCount int) LayoutSuggestion {
|
||||
if windowCount <= 1 {
|
||||
return LayoutSuggestion{
|
||||
Mode: "single",
|
||||
Columns: 1,
|
||||
Rows: 1,
|
||||
PrimaryWidth: screenW,
|
||||
SecondaryWidth: 0,
|
||||
Description: "Focus the primary window and keep the screen uncluttered.",
|
||||
}
|
||||
}
|
||||
|
||||
if windowCount == 2 {
|
||||
return LayoutSuggestion{
|
||||
Mode: "side-by-side",
|
||||
Columns: 2,
|
||||
Rows: 1,
|
||||
PrimaryWidth: screenW / 2,
|
||||
SecondaryWidth: screenW - (screenW / 2),
|
||||
Description: "Split the screen into two equal panes.",
|
||||
}
|
||||
}
|
||||
|
||||
if windowCount <= 4 {
|
||||
return LayoutSuggestion{
|
||||
Mode: "quadrants",
|
||||
Columns: 2,
|
||||
Rows: 2,
|
||||
PrimaryWidth: screenW / 2,
|
||||
SecondaryWidth: screenW / 2,
|
||||
Description: "Use a 2x2 grid for the active windows.",
|
||||
}
|
||||
}
|
||||
|
||||
cols := 3
|
||||
rows := int(math.Ceil(float64(windowCount) / float64(cols)))
|
||||
return LayoutSuggestion{
|
||||
Mode: "grid",
|
||||
Columns: cols,
|
||||
Rows: rows,
|
||||
PrimaryWidth: screenW / cols,
|
||||
SecondaryWidth: screenW / cols,
|
||||
Description: "Use a dense grid to keep every window visible.",
|
||||
}
|
||||
}
|
||||
|
||||
// FindSpace returns a free placement suggestion for a new window.
|
||||
func (m *Manager) FindSpace(screenW, screenH, width, height int) SpaceInfo {
|
||||
if width <= 0 {
|
||||
width = screenW / 2
|
||||
}
|
||||
if height <= 0 {
|
||||
height = screenH / 2
|
||||
}
|
||||
|
||||
occupied := make([]struct {
|
||||
x, y, w, h int
|
||||
}, 0)
|
||||
for _, name := range m.List() {
|
||||
pw, ok := m.Get(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
occupied = append(occupied, struct {
|
||||
x, y, w, h int
|
||||
}{x: x, y: y, w: w, h: h})
|
||||
}
|
||||
|
||||
step := int(math.Max(40, math.Min(float64(width), float64(height))/6))
|
||||
if step < 40 {
|
||||
step = 40
|
||||
}
|
||||
|
||||
for y := 0; y+height <= screenH; y += step {
|
||||
for x := 0; x+width <= screenW; x += step {
|
||||
if !intersectsAny(x, y, width, height, occupied) {
|
||||
return SpaceInfo{
|
||||
X: x, Y: y, Width: width, Height: height,
|
||||
ScreenWidth: screenW, ScreenHeight: screenH,
|
||||
Reason: "first available gap",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SpaceInfo{
|
||||
X: (screenW - width) / 2, Y: (screenH - height) / 2,
|
||||
Width: width, Height: height,
|
||||
ScreenWidth: screenW, ScreenHeight: screenH,
|
||||
Reason: "center fallback",
|
||||
}
|
||||
}
|
||||
|
||||
// ArrangePair places two windows side-by-side with a balanced split.
|
||||
func (m *Manager) ArrangePair(first, second string, screenW, screenH int) error {
|
||||
left, ok := m.Get(first)
|
||||
if !ok {
|
||||
return fmt.Errorf("window %q not found", first)
|
||||
}
|
||||
right, ok := m.Get(second)
|
||||
if !ok {
|
||||
return fmt.Errorf("window %q not found", second)
|
||||
}
|
||||
|
||||
leftW := screenW / 2
|
||||
rightW := screenW - leftW
|
||||
left.SetPosition(0, 0)
|
||||
left.SetSize(leftW, screenH)
|
||||
right.SetPosition(leftW, 0)
|
||||
right.SetSize(rightW, screenH)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BesideEditor places a target window beside an editor window, using a 70/30 split.
|
||||
func (m *Manager) BesideEditor(editorName, windowName string, screenW, screenH int) error {
|
||||
editor, ok := m.Get(editorName)
|
||||
if !ok {
|
||||
return fmt.Errorf("window %q not found", editorName)
|
||||
}
|
||||
target, ok := m.Get(windowName)
|
||||
if !ok {
|
||||
return fmt.Errorf("window %q not found", windowName)
|
||||
}
|
||||
|
||||
editorW := screenW * 70 / 100
|
||||
if editorW <= 0 {
|
||||
editorW = screenW / 2
|
||||
}
|
||||
targetW := screenW - editorW
|
||||
editor.SetPosition(0, 0)
|
||||
editor.SetSize(editorW, screenH)
|
||||
target.SetPosition(editorW, 0)
|
||||
target.SetSize(targetW, screenH)
|
||||
return nil
|
||||
}
|
||||
|
||||
func intersectsAny(x, y, w, h int, occupied []struct{ x, y, w, h int }) bool {
|
||||
for _, r := range occupied {
|
||||
if x < r.x+r.w && x+w > r.x && y < r.y+r.h && y+h > r.y {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,3 +328,43 @@ func TestWorkflowLayout_Good(t *testing.T) {
|
|||
assert.Equal(t, "coding", WorkflowCoding.String())
|
||||
assert.Equal(t, "debugging", WorkflowDebugging.String())
|
||||
}
|
||||
|
||||
func TestManager_SuggestLayout_Good(t *testing.T) {
|
||||
m, _ := newTestManager()
|
||||
suggestion := m.SuggestLayout(1920, 1080, 3)
|
||||
assert.Equal(t, "quadrants", suggestion.Mode)
|
||||
assert.Equal(t, 2, suggestion.Columns)
|
||||
}
|
||||
|
||||
func TestManager_FindSpace_Good(t *testing.T) {
|
||||
m, _ := newTestManager()
|
||||
_, _ = m.Open(WithName("one"), WithPosition(0, 0), WithSize(800, 600))
|
||||
space := m.FindSpace(1920, 1080, 400, 300)
|
||||
assert.GreaterOrEqual(t, space.X, 0)
|
||||
assert.GreaterOrEqual(t, space.Y, 0)
|
||||
}
|
||||
|
||||
func TestManager_ArrangePair_Good(t *testing.T) {
|
||||
m, _ := newTestManager()
|
||||
_, _ = m.Open(WithName("left"), WithSize(800, 600))
|
||||
_, _ = m.Open(WithName("right"), WithSize(800, 600))
|
||||
err := m.ArrangePair("left", "right", 1920, 1080)
|
||||
require.NoError(t, err)
|
||||
left, _ := m.Get("left")
|
||||
x, _ := left.Position()
|
||||
assert.Equal(t, 0, x)
|
||||
}
|
||||
|
||||
func TestManager_BesideEditor_Good(t *testing.T) {
|
||||
m, _ := newTestManager()
|
||||
_, _ = m.Open(WithName("editor"))
|
||||
_, _ = m.Open(WithName("assistant"))
|
||||
err := m.BesideEditor("editor", "assistant", 1920, 1080)
|
||||
require.NoError(t, err)
|
||||
editor, _ := m.Get("editor")
|
||||
assistant, _ := m.Get("assistant")
|
||||
ex, _ := editor.Size()
|
||||
ax, _ := assistant.Position()
|
||||
assert.Greater(t, ex, 0)
|
||||
assert.Greater(t, ax, 0)
|
||||
}
|
||||
|
|
|
|||
3
stubs/wails/v3/go.mod
Normal file
3
stubs/wails/v3/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/wailsapp/wails/v3
|
||||
|
||||
go 1.26.0
|
||||
323
stubs/wails/v3/pkg/application/application.go
Normal file
323
stubs/wails/v3/pkg/application/application.go
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// RGBA represents a colour.
|
||||
type RGBA struct {
|
||||
R, G, B, A uint8
|
||||
}
|
||||
|
||||
// NewRGBA creates a colour value.
|
||||
func NewRGBA(r, g, b, a uint8) RGBA { return RGBA{R: r, G: g, B: b, A: a} }
|
||||
|
||||
// Logger is a minimal logger used by the repo.
|
||||
type Logger struct{}
|
||||
|
||||
func (l *Logger) Info(message string, args ...any) {}
|
||||
|
||||
// Context carries event data.
|
||||
type Context struct {
|
||||
droppedFiles []string
|
||||
dropTargetData *DropTargetDetails
|
||||
}
|
||||
|
||||
func (c *Context) DroppedFiles() []string {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(c.droppedFiles))
|
||||
copy(out, c.droppedFiles)
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Context) DropTargetDetails() *DropTargetDetails {
|
||||
if c == nil || c.dropTargetData == nil {
|
||||
return nil
|
||||
}
|
||||
d := *c.dropTargetData
|
||||
return &d
|
||||
}
|
||||
|
||||
// DropTargetDetails describes the drop target.
|
||||
type DropTargetDetails struct {
|
||||
ElementID string
|
||||
}
|
||||
|
||||
// WindowEvent wraps window event context.
|
||||
type WindowEvent struct {
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
func (e *WindowEvent) Context() *Context {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
if e.ctx == nil {
|
||||
e.ctx = &Context{}
|
||||
}
|
||||
return e.ctx
|
||||
}
|
||||
|
||||
// WebviewWindowOptions configures a new window.
|
||||
type WebviewWindowOptions struct {
|
||||
Name string
|
||||
Title string
|
||||
URL string
|
||||
Width int
|
||||
Height int
|
||||
X int
|
||||
Y int
|
||||
MinWidth int
|
||||
MinHeight int
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
Frameless bool
|
||||
Hidden bool
|
||||
AlwaysOnTop bool
|
||||
DisableResize bool
|
||||
EnableFileDrop bool
|
||||
BackgroundColour RGBA
|
||||
}
|
||||
|
||||
// WebviewWindow is a lightweight in-memory window handle.
|
||||
type WebviewWindow struct {
|
||||
opts WebviewWindowOptions
|
||||
title string
|
||||
x, y int
|
||||
width, height int
|
||||
maximised bool
|
||||
focused bool
|
||||
visible bool
|
||||
alwaysOnTop bool
|
||||
fullscreen bool
|
||||
devtoolsOpen bool
|
||||
eventHandlers map[events.WindowEventType][]func(*WindowEvent)
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newWebviewWindow(opts WebviewWindowOptions) *WebviewWindow {
|
||||
return &WebviewWindow{
|
||||
opts: opts,
|
||||
title: opts.Title,
|
||||
x: opts.X,
|
||||
y: opts.Y,
|
||||
width: opts.Width,
|
||||
height: opts.Height,
|
||||
visible: !opts.Hidden,
|
||||
alwaysOnTop: opts.AlwaysOnTop,
|
||||
eventHandlers: make(map[events.WindowEventType][]func(*WindowEvent)),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Name() string { return w.opts.Name }
|
||||
func (w *WebviewWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *WebviewWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *WebviewWindow) IsMaximised() bool { return w.maximised }
|
||||
func (w *WebviewWindow) IsFocused() bool { return w.focused }
|
||||
func (w *WebviewWindow) SetTitle(title string) { w.title = title }
|
||||
func (w *WebviewWindow) SetPosition(x, y int) { w.x, w.y = x, y }
|
||||
func (w *WebviewWindow) SetSize(width, height int) {
|
||||
w.width, w.height = width, height
|
||||
}
|
||||
func (w *WebviewWindow) SetBackgroundColour(colour RGBA) {}
|
||||
func (w *WebviewWindow) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *WebviewWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *WebviewWindow) Maximise() { w.maximised = true }
|
||||
func (w *WebviewWindow) Restore() { w.maximised = false }
|
||||
func (w *WebviewWindow) Minimise() {}
|
||||
func (w *WebviewWindow) Focus() { w.focused = true }
|
||||
func (w *WebviewWindow) Close() {}
|
||||
func (w *WebviewWindow) Show() { w.visible = true }
|
||||
func (w *WebviewWindow) Hide() { w.visible = false }
|
||||
func (w *WebviewWindow) Fullscreen() { w.fullscreen = true }
|
||||
func (w *WebviewWindow) UnFullscreen() { w.fullscreen = false }
|
||||
func (w *WebviewWindow) OpenDevTools() { w.devtoolsOpen = true }
|
||||
|
||||
func (w *WebviewWindow) OnWindowEvent(eventType events.WindowEventType, callback func(event *WindowEvent)) func() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.eventHandlers[eventType] = append(w.eventHandlers[eventType], callback)
|
||||
return func() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
handlers := w.eventHandlers[eventType]
|
||||
if len(handlers) == 0 {
|
||||
return
|
||||
}
|
||||
w.eventHandlers[eventType] = handlers[:len(handlers)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) trigger(eventType events.WindowEventType, event *WindowEvent) {
|
||||
w.mu.Lock()
|
||||
handlers := append([]func(*WindowEvent){}, w.eventHandlers[eventType]...)
|
||||
w.mu.Unlock()
|
||||
for _, handler := range handlers {
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// WindowManager manages in-memory windows.
|
||||
type WindowManager struct {
|
||||
windows []*WebviewWindow
|
||||
}
|
||||
|
||||
func (wm *WindowManager) NewWithOptions(opts WebviewWindowOptions) *WebviewWindow {
|
||||
w := newWebviewWindow(opts)
|
||||
wm.windows = append(wm.windows, w)
|
||||
return w
|
||||
}
|
||||
|
||||
func (wm *WindowManager) GetAll() []any {
|
||||
out := make([]any, len(wm.windows))
|
||||
for i, w := range wm.windows {
|
||||
out[i] = w
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Menu role constants.
|
||||
type MenuRole int
|
||||
|
||||
const (
|
||||
AppMenu MenuRole = iota
|
||||
FileMenu
|
||||
EditMenu
|
||||
ViewMenu
|
||||
WindowMenu
|
||||
HelpMenu
|
||||
)
|
||||
|
||||
// Menu is a lightweight in-memory menu.
|
||||
type Menu struct {
|
||||
items []*MenuItem
|
||||
}
|
||||
|
||||
func NewMenu() *Menu { return &Menu{} }
|
||||
|
||||
func (m *Menu) Add(label string) *MenuItem {
|
||||
item := &MenuItem{label: label}
|
||||
m.items = append(m.items, item)
|
||||
return item
|
||||
}
|
||||
|
||||
func (m *Menu) AddSeparator() {}
|
||||
|
||||
func (m *Menu) AddSubmenu(label string) *Menu {
|
||||
return &Menu{}
|
||||
}
|
||||
|
||||
func (m *Menu) AddRole(role MenuRole) {}
|
||||
|
||||
func (m *Menu) SetApplicationMenu(menu *Menu) {}
|
||||
|
||||
// MenuItem is a lightweight menu item.
|
||||
type MenuItem struct {
|
||||
label string
|
||||
accelerator string
|
||||
tooltip string
|
||||
checked bool
|
||||
enabled bool
|
||||
onClick func(*Context)
|
||||
}
|
||||
|
||||
func (mi *MenuItem) SetAccelerator(accel string) *MenuItem {
|
||||
mi.accelerator = accel
|
||||
return mi
|
||||
}
|
||||
|
||||
func (mi *MenuItem) SetTooltip(text string) *MenuItem {
|
||||
mi.tooltip = text
|
||||
return mi
|
||||
}
|
||||
|
||||
func (mi *MenuItem) SetChecked(checked bool) *MenuItem {
|
||||
mi.checked = checked
|
||||
return mi
|
||||
}
|
||||
|
||||
func (mi *MenuItem) SetEnabled(enabled bool) *MenuItem {
|
||||
mi.enabled = enabled
|
||||
return mi
|
||||
}
|
||||
|
||||
func (mi *MenuItem) OnClick(fn func(ctx *Context)) *MenuItem {
|
||||
mi.onClick = fn
|
||||
return mi
|
||||
}
|
||||
|
||||
// SystemTray models a tray icon.
|
||||
type SystemTray struct {
|
||||
icon []byte
|
||||
templateIcon []byte
|
||||
tooltip string
|
||||
label string
|
||||
menu *Menu
|
||||
}
|
||||
|
||||
func (st *SystemTray) SetIcon(icon []byte) *SystemTray {
|
||||
st.icon = append([]byte(nil), icon...)
|
||||
return st
|
||||
}
|
||||
|
||||
func (st *SystemTray) SetTemplateIcon(icon []byte) *SystemTray {
|
||||
st.templateIcon = append([]byte(nil), icon...)
|
||||
return st
|
||||
}
|
||||
|
||||
func (st *SystemTray) SetTooltip(tooltip string) {
|
||||
st.tooltip = tooltip
|
||||
}
|
||||
|
||||
func (st *SystemTray) SetLabel(label string) {
|
||||
st.label = label
|
||||
}
|
||||
|
||||
func (st *SystemTray) SetMenu(menu *Menu) *SystemTray {
|
||||
st.menu = menu
|
||||
return st
|
||||
}
|
||||
|
||||
func (st *SystemTray) Show() {}
|
||||
func (st *SystemTray) Hide() {}
|
||||
func (st *SystemTray) OnClick(callback func()) *SystemTray { return st }
|
||||
|
||||
// SystemTrayManager creates trays.
|
||||
type SystemTrayManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
func (stm *SystemTrayManager) New() *SystemTray { return &SystemTray{} }
|
||||
|
||||
// MenuManager manages application menus.
|
||||
type MenuManager struct {
|
||||
appMenu *Menu
|
||||
}
|
||||
|
||||
func (mm *MenuManager) SetApplicationMenu(menu *Menu) { mm.appMenu = menu }
|
||||
|
||||
// App is the top-level application container.
|
||||
type App struct {
|
||||
Window *WindowManager
|
||||
Menu *MenuManager
|
||||
SystemTray *SystemTrayManager
|
||||
Logger *Logger
|
||||
quit bool
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
app := &App{}
|
||||
app.Window = &WindowManager{}
|
||||
app.Menu = &MenuManager{}
|
||||
app.SystemTray = &SystemTrayManager{app: app}
|
||||
app.Logger = &Logger{}
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *App) Quit() { a.quit = true }
|
||||
|
||||
func (a *App) NewMenu() *Menu { return NewMenu() }
|
||||
21
stubs/wails/v3/pkg/events/events.go
Normal file
21
stubs/wails/v3/pkg/events/events.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package events
|
||||
|
||||
// WindowEventType identifies a window event.
|
||||
type WindowEventType string
|
||||
|
||||
// Common exposes the event names used by the repo.
|
||||
var Common = struct {
|
||||
WindowFocus WindowEventType
|
||||
WindowLostFocus WindowEventType
|
||||
WindowDidMove WindowEventType
|
||||
WindowDidResize WindowEventType
|
||||
WindowClosing WindowEventType
|
||||
WindowFilesDropped WindowEventType
|
||||
}{
|
||||
WindowFocus: "focus",
|
||||
WindowLostFocus: "blur",
|
||||
WindowDidMove: "move",
|
||||
WindowDidResize: "resize",
|
||||
WindowClosing: "close",
|
||||
WindowFilesDropped: "files-dropped",
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue