diff --git a/go.mod b/go.mod index af4cee7..21d98ab 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,18 @@ go 1.26.0 require ( forge.lthn.ai/core/go v0.2.2 + forge.lthn.ai/core/go-config v0.1.2 github.com/gorilla/websocket v1.5.3 + github.com/leaanthony/u v1.1.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 ) require ( dario.cat/mergo v1.0.2 // indirect + forge.lthn.ai/core/go-io v0.0.5 // indirect + forge.lthn.ai/core/go-log v0.0.1 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect @@ -22,10 +27,12 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/ebitengine/purego v0.9.1 // 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.7.0 // indirect github.com/go-git/go-git/v5 v5.16.4 // 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/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect @@ -34,23 +41,29 @@ require ( github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect - github.com/leaanthony/u v1.1.1 // indirect github.com/lmittmann/tint v1.1.2 // 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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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.52.0 // 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 - golang.org/x/crypto v0.47.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a5b76c3..c211f8d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= forge.lthn.ai/core/go v0.2.2 h1:JCWaFfiG+agb0f7b5DO1g+h40x6nb4UydxJ7D+oZk5k= forge.lthn.ai/core/go v0.2.2/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc= +forge.lthn.ai/core/go-config v0.1.2 h1:W2Rf7PhVeugTjHZ9XbEr1yMCYY6r1r5fyUmQcM2C9xY= +forge.lthn.ai/core/go-config v0.1.2/go.mod h1:6jZZ0XQaSSiWiZjiNfibMEfSPehsBGwfToh//QGhN7E= +forge.lthn.ai/core/go-io v0.0.5 h1:oSyngKTkB1gR5fEWYKXftTg9FxwnpddSiCq2dlwfImE= +forge.lthn.ai/core/go-io v0.0.5/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0= +forge.lthn.ai/core/go-log v0.0.1 h1:x/E6EfF9vixzqiLHQOl2KT25HyBcMc9qiBkomqVlpPg= +forge.lthn.ai/core/go-log v0.0.1/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -33,6 +39,10 @@ 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= @@ -47,6 +57,8 @@ github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRko 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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -87,6 +99,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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= @@ -101,6 +115,8 @@ 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.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -108,22 +124,34 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq 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= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +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= +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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= @@ -136,14 +164,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= diff --git a/pkg/display/display.go b/pkg/display/display.go index e3aad27..765b4e1 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -3,8 +3,11 @@ package display import ( "context" "fmt" + "os" + "path/filepath" "runtime" + "forge.lthn.ai/core/go-config" "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/menu" "forge.lthn.ai/core/gui/pkg/systray" @@ -28,6 +31,7 @@ type Service struct { app App config Options configData map[string]map[string]any + cfg *config.Config // go-config instance for file persistence notifier *notifications.NotificationService events *WSEventManager } @@ -149,13 +153,33 @@ func (s *Service) handleTrayAction(actionID string) { } } +func guiConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".core", "gui", "config.yaml") + } + return filepath.Join(home, ".core", "gui", "config.yaml") +} + func (s *Service) loadConfig() { - // In-memory defaults. go-config integration is deferred work. - if s.configData == nil { - s.configData = map[string]map[string]any{ - "window": {}, - "systray": {}, - "menu": {}, + if s.cfg != nil { + return // Already loaded (e.g., via loadConfigFrom in tests) + } + s.loadConfigFrom(guiConfigPath()) +} + +func (s *Service) loadConfigFrom(path string) { + cfg, err := config.New(config.WithPath(path)) + if err != nil { + // Non-critical — continue with empty configData + return + } + s.cfg = cfg + + for _, section := range []string{"window", "systray", "menu"} { + var data map[string]any + if err := cfg.Get(section, &data); err == nil && data != nil { + s.configData[section] = data } } } @@ -177,18 +201,29 @@ func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) switch t := t.(type) { case window.TaskSaveConfig: s.configData["window"] = t.Value + s.persistSection("window", t.Value) return nil, true, nil case systray.TaskSaveConfig: s.configData["systray"] = t.Value + s.persistSection("systray", t.Value) return nil, true, nil case menu.TaskSaveConfig: s.configData["menu"] = t.Value + s.persistSection("menu", t.Value) return nil, true, nil default: return nil, false, nil } } +func (s *Service) persistSection(key string, value map[string]any) { + if s.cfg == nil { + return + } + _ = s.cfg.Set(key, value) + _ = s.cfg.Commit() +} + // --- Service accessors --- // windowService returns the window service from Core, or nil if not registered. diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 59f8008..657d252 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -2,6 +2,8 @@ package display import ( "context" + "os" + "path/filepath" "testing" "forge.lthn.ai/core/go/pkg/core" @@ -448,3 +450,68 @@ func TestWSEventManager_SetupWindowEventListeners_Good(t *testing.T) { // Verify theme handler was registered assert.Len(t, es.themeHandlers, 1) } + +// --- Config file loading tests --- + +func TestLoadConfig_Good(t *testing.T) { + // Create temp config file + dir := t.TempDir() + cfgPath := filepath.Join(dir, ".core", "gui", "config.yaml") + require.NoError(t, os.MkdirAll(filepath.Dir(cfgPath), 0o755)) + require.NoError(t, os.WriteFile(cfgPath, []byte(` +window: + default_width: 1280 + default_height: 720 +systray: + tooltip: "Test App" +menu: + show_dev_tools: false +`), 0o644)) + + s, _ := New() + s.loadConfigFrom(cfgPath) + + // Verify configData was populated from file + assert.Equal(t, 1280, s.configData["window"]["default_width"]) + assert.Equal(t, "Test App", s.configData["systray"]["tooltip"]) + assert.Equal(t, false, s.configData["menu"]["show_dev_tools"]) +} + +func TestLoadConfig_Bad_MissingFile(t *testing.T) { + s, _ := New() + s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml")) + + // Should not panic, configData stays at empty defaults + assert.Empty(t, s.configData["window"]) + assert.Empty(t, s.configData["systray"]) + assert.Empty(t, s.configData["menu"]) +} + +func TestHandleConfigTask_Persists_Good(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.yaml") + + s, _ := New() + s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet) + + // Simulate a TaskSaveConfig through the handler + c, _ := core.New( + core.WithService(func(c *core.Core) (any, error) { + s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) + return s, nil + }), + core.WithServiceLock(), + ) + c.ServiceStartup(context.Background(), nil) + + _, handled, err := c.PERFORM(window.TaskSaveConfig{ + Value: map[string]any{"default_width": 1920}, + }) + require.NoError(t, err) + assert.True(t, handled) + + // Verify file was written + data, err := os.ReadFile(cfgPath) + require.NoError(t, err) + assert.Contains(t, string(data), "default_width") +} diff --git a/pkg/menu/service.go b/pkg/menu/service.go index 62d32ad..2e8ac26 100644 --- a/pkg/menu/service.go +++ b/pkg/menu/service.go @@ -12,9 +12,10 @@ type Options struct{} // Service is a core.Service managing application menus via IPC. type Service struct { *core.ServiceRuntime[Options] - manager *Manager - platform Platform - items []MenuItem // last-set menu items for QueryGetAppMenu + manager *Manager + platform Platform + items []MenuItem // last-set menu items for QueryGetAppMenu + showDevTools bool } // OnStartup queries config and registers IPC handlers. @@ -31,7 +32,16 @@ func (s *Service) OnStartup(ctx context.Context) error { } func (s *Service) applyConfig(cfg map[string]any) { - // Apply config — e.g., show_dev_tools + if v, ok := cfg["show_dev_tools"]; ok { + if show, ok := v.(bool); ok { + s.showDevTools = show + } + } +} + +// ShowDevTools returns whether developer tools menu items should be shown. +func (s *Service) ShowDevTools() bool { + return s.showDevTools } // HandleIPCEvents is auto-discovered and registered by core.WithService. diff --git a/pkg/systray/service.go b/pkg/systray/service.go index 6dc2ec9..70eaa04 100644 --- a/pkg/systray/service.go +++ b/pkg/systray/service.go @@ -14,6 +14,7 @@ type Service struct { *core.ServiceRuntime[Options] manager *Manager platform Platform + iconPath string } // OnStartup queries config and registers IPC handlers. @@ -29,12 +30,17 @@ func (s *Service) OnStartup(ctx context.Context) error { } func (s *Service) applyConfig(cfg map[string]any) { - // Apply config — tooltip, icon path, etc. tooltip, _ := cfg["tooltip"].(string) if tooltip == "" { tooltip = "Core" } _ = s.manager.Setup(tooltip, tooltip) + + if iconPath, ok := cfg["icon"].(string); ok && iconPath != "" { + // Icon loading is deferred to when assets are available. + // Store the path for later use. + s.iconPath = iconPath + } } // HandleIPCEvents is auto-discovered and registered by core.WithService. diff --git a/pkg/window/service.go b/pkg/window/service.go index b409907..d038400 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -38,8 +38,21 @@ func (s *Service) OnStartup(ctx context.Context) error { } func (s *Service) applyConfig(cfg map[string]any) { - // Apply config to manager defaults — future expansion. - // e.g., default_width, default_height, state_file path. + if w, ok := cfg["default_width"]; ok { + if _, ok := w.(int); ok { + // TODO: s.manager.SetDefaultWidth(width) — add when Manager API is extended + } + } + if h, ok := cfg["default_height"]; ok { + if _, ok := h.(int); ok { + // TODO: s.manager.SetDefaultHeight(height) — add when Manager API is extended + } + } + if sf, ok := cfg["state_file"]; ok { + if _, ok := sf.(string); ok { + // TODO: s.manager.State().SetPath(stateFile) — add when StateManager API is extended + } + } } // HandleIPCEvents is auto-discovered and registered by core.WithService.