From 9f3f63f42dd9dc7491109fc8affa7eadaa05e0ae Mon Sep 17 00:00:00 2001 From: phoeagon Date: Mon, 8 Sep 2025 22:58:12 -0700 Subject: [PATCH 1/6] Create a YAML based configuration to customize key binding --- .goreleaser.yaml | 12 ++-- conf.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 9 +++ go.sum | 42 +++++++++++- main.go | 69 ++++++++++++++++++++ systemwindow.go | 2 +- 6 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 conf.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f17857c..3a0a20c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,11 +1,9 @@ +version: 2 + builds: - - goos: + - id: win + goos: - windows ldflags: - -H=windowsgui -archives: -- format: binary - name_template: "{{ .ProjectName }}-{{ .Arch }}-v{{.Version}}" - replacements: - 386: x86 - amd64: x64 \ No newline at end of file + binary: "{{ .ProjectName }}-{{- if eq .Arch \"386\" }}x86{{ else if eq .Arch \"amd64\" }}x64{{ else }}{{ .Arch }}{{ end }}-v{{ .Version }}" \ No newline at end of file diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..db43d17 --- /dev/null +++ b/conf.go @@ -0,0 +1,162 @@ +package main + +import ( + "errors" + "fmt" + "strings" +) +import ( + "github.com/davecgh/go-spew/spew" + "github.com/golobby/config/v3" + "github.com/golobby/config/v3/pkg/feeder" + "github.com/gonutz/w32/v2" +) + +type KeyBinding struct { + // A repeated value of key modifiers. + // Valid values include: + // SHIFT, ALT, CTRL, WIN (SUPER/META). + Modifier []string `yaml: "modifier"` + // When this is set, this overrides Modifier + ModifierCode []int32 + // Calculated bitwise OR result of modifiers + CombinedMod int32 + // Valid values are: + // A - Z, 0 - 9, UP_ARROW, =, - + // Anything not covered here could be set directly via KeyCode + Key string `yaml: "key"` + // Automatically converted from Key. + // When this is set, it overrides Key. + KeyCode int32 `yaml: "key_code"` + // The feature in RectangleWin to bind to. + // Valid values: + // moveToTop + // moveToBottom + // moveToLeft + // moveToRight + // moveToTopLeft + // moveToTopRight + // moveToBottomLeft + // moveToBottomRight + // makeSmaller + // makeLarger + // makeFullHeight + // + BindFeature string `yaml: "bindfeature"` +} + +type Configuration struct { + Keybindings []KeyBinding `yaml: "key_binding"` +} + +var DEFAULT_CONF = Configuration{ + Keybindings: []KeyBinding{ + { + Modifier: []string{"Ctrl", "Alt"}, + Key: "UP_ARROW", + KeyCode: 0x26, + BindFeature: "moveToTop", + }, + }, +} + +var DEFAULT_CONF_NAME = "config.yaml" + +func convertModifier(keyName string) (int32, error) { + switch strings.ToLower(keyName) { + case "ctrl": + return MOD_CONTROL, nil + case "alt": + return MOD_ALT, nil + case "shift": + return MOD_SHIFT, nil + case "win": + case "meta": + case "super": + return MOD_WIN, nil + default: + return 0, errors.New("invalid keyname") + } + return 0, errors.New("unreachable") +} + +func convertKeyCode(key string) (int32, error) { + k := strings.ToLower(key) + if len(k) == 1 { + if k[0] >= 'a' && k[0] <= 'z' { + return int32(k[0]) - 32, nil + } + if k[0] >= '0' && k[0] <= '9' { + return int32(k[0]), nil + } + } + switch k { + case "up_arrow": + return w32.VK_UP, nil + case "down_arrow": + return w32.VK_DOWN, nil + case "left_arrow": + return w32.VK_LEFT, nil + case "right_arrow": + return w32.VK_RIGHT, nil + case "-": + return 189, nil + case "=": + return 187, nil + default: + return 0, errors.New("Unknown key") + } +} + +func bitwiseOr(nums []int32) int32 { + if len(nums) == 0 { + return 0 + } + result := nums[0] + for _, n := range nums[1:] { + result |= n // bitwise OR + } + return result +} + +func fetchConfiguration() Configuration { + spew.Dump(DEFAULT_CONF) + // Create a Configuration file. + myConfig := Configuration{} + + // Yaml feeder + yamlFeeder := feeder.Yaml{Path: DEFAULT_CONF_NAME} + c := config.New() + c.AddFeeder(yamlFeeder) + c.AddStruct(&myConfig) + + err := c.Feed() + if err != nil { + fmt.Printf("warn: invalid config files found: %s %v\n", DEFAULT_CONF_NAME, err) + return DEFAULT_CONF + } + + for i := range myConfig.Keybindings { + if len(myConfig.Keybindings[i].ModifierCode) == 0 { + for _, mod := range myConfig.Keybindings[i].Modifier { + if modCode, err := convertModifier(mod); err == nil { + myConfig.Keybindings[i].ModifierCode = append(myConfig.Keybindings[i].ModifierCode, modCode) + } else { + fmt.Printf("warn: invalid key name %s", mod) + continue + } + } + } + myConfig.Keybindings[i].CombinedMod = bitwiseOr(myConfig.Keybindings[i].ModifierCode) + if myConfig.Keybindings[i].KeyCode == 0 { + if key, err := convertKeyCode(myConfig.Keybindings[i].Key); err == nil { + myConfig.Keybindings[i].KeyCode = key + } else { + fmt.Printf("warn: invalid key string %s", myConfig.Keybindings[i].Key) + continue + } + } + } + spew.Dump(myConfig) + return myConfig +} diff --git a/go.mod b/go.mod index 9155b76..7908785 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,17 @@ module github.com/ahmetb/RectangleWin go 1.17 require ( + github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 + github.com/davecgh/go-spew v1.1.1 github.com/getlantern/systray v1.1.0 + github.com/golobby/config/v3 v3.4.2 github.com/gonutz/w32/v2 v2.2.2 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e ) require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab // indirect github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect @@ -16,5 +21,9 @@ require ( github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect github.com/go-stack/stack v1.8.0 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/golobby/dotenv v1.3.2 // indirect + github.com/golobby/env/v2 v2.2.4 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 21b34fd..5a7104e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4= +github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY= +github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k= +github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= @@ -16,15 +24,45 @@ github.com/getlantern/systray v1.1.0 h1:U0wCEqseLi2ok1fE6b88gJklzriavPJixZysZPkZ github.com/getlantern/systray v1.1.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +github.com/golobby/config/v3 v3.4.2 h1:oIOSo24mC0A8f93ZTL24NDNw0hZ3Tbb34wc1ckn2CsA= +github.com/golobby/config/v3 v3.4.2/go.mod h1:3go9UVPb3bBNrH7qidd4vd1HbsAAwIYqcQJgGmAa044= +github.com/golobby/dotenv v1.3.2 h1:9vA8XqXXIB3cX/5xQ1CTbOCPegioHtHXIxeFng+uOqQ= +github.com/golobby/dotenv v1.3.2/go.mod h1:9MMVXqzLNluhVxCv3X/DLYBNUb289f05tr+df1+7278= +github.com/golobby/env/v2 v2.2.4 h1:sjdTe+bScPRWUIA1AQH95RHv52jM5Mns2XHwLyEbkzk= +github.com/golobby/env/v2 v2.2.4/go.mod h1:HDJW+dHHwLxkb8FZMjBTBiZUFl1iAA4F9YX15kBC84c= github.com/gonutz/w32/v2 v2.2.2 h1:y6Y337TpuCXjYdFTq5p5NmcujEdAQiTB43kisovMk+0= github.com/gonutz/w32/v2 v2.2.2/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index cf678f7..dbe7524 100644 --- a/main.go +++ b/main.go @@ -29,11 +29,13 @@ import ( "github.com/gonutz/w32/v2" "github.com/ahmetb/RectangleWin/w32ex" + "github.com/apenwarr/fixconsole" ) var lastResized w32.HWND func main() { + err := fixconsole.FixConsoleIfNeeded() runtime.LockOSThread() // since we bind hotkeys etc that need to dispatch their message here if !w32ex.SetProcessDPIAware() { panic("failed to set DPI aware") @@ -87,6 +89,7 @@ func main() { (HotKey{id: 2, mod: MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_RIGHT, callback: func() { cycleEdgeFuncs(1) }}), (HotKey{id: 3, mod: MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_UP, callback: func() { cycleEdgeFuncs(2) }}), (HotKey{id: 4, mod: MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_DOWN, callback: func() { cycleEdgeFuncs(3) }}), + // Corner func #1 (HotKey{id: 5, mod: MOD_CONTROL | MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_LEFT, callback: func() { cycleCornerFuncs(0) }}), (HotKey{id: 6, mod: MOD_CONTROL | MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_UP, callback: func() { cycleCornerFuncs(1) }}), (HotKey{id: 7, mod: MOD_CONTROL | MOD_ALT | MOD_WIN | MOD_NOREPEAT, vk: w32.VK_DOWN, callback: func() { cycleCornerFuncs(2) }}), @@ -115,6 +118,72 @@ func main() { }}), } + myConfig := fetchConfiguration() + // start from id 200 + id := 200 + for _, keyBinding := range myConfig.Keybindings { + switch keyBinding.BindFeature { + case "moveToTop": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleEdgeFuncs(2) }})) + case "moveToBottom": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleEdgeFuncs(3) }})) + case "moveToLeft": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleEdgeFuncs(0) }})) + case "moveToRight": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleEdgeFuncs(1) }})) + case "moveToTopLeft": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleCornerFuncs(0) }})) + case "moveToTopRight": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleCornerFuncs(1) }})) + case "moveToBottomLeft": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleCornerFuncs(2) }})) + case "moveToBottomRight": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { cycleCornerFuncs(3) }})) + default: + continue + } + } + var failedHotKeys []HotKey for _, hk := range hks { if !RegisterHotKey(hk) { diff --git a/systemwindow.go b/systemwindow.go index 1f60653..298b3a8 100644 --- a/systemwindow.go +++ b/systemwindow.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, From 2d66fe2f8e151c11ed462e0d72c7e0824ad84267 Mon Sep 17 00:00:00 2001 From: phoeagon Date: Sun, 21 Sep 2025 20:30:33 -0700 Subject: [PATCH 2/6] updates RectangleWin to automatically emit a config yaml at %HOME%/.config/RectangleWin/config.yaml if missing, instad of the current folder --- conf.go | 51 ++++++++++++++++++++++++++++++++-- config.example.yaml | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 config.example.yaml diff --git a/conf.go b/conf.go index db43d17..178661f 100644 --- a/conf.go +++ b/conf.go @@ -1,8 +1,12 @@ package main import ( + _ "embed" "errors" "fmt" + "io/ioutil" + "os" + "path/filepath" "strings" ) import ( @@ -49,6 +53,9 @@ type Configuration struct { Keybindings []KeyBinding `yaml: "key_binding"` } +// This mini config is returned if we can't load a valid file +// and cannot write the detailed example yaml config.example.yaml +// into the expected path at %HOME% var DEFAULT_CONF = Configuration{ Keybindings: []KeyBinding{ { @@ -60,6 +67,11 @@ var DEFAULT_CONF = Configuration{ }, } +//go:embed config.example.yaml +var configExampleYaml []byte + +// Expected config path at %HOME%/.config/RectangleWin/config.yaml +var DEFAULT_CONF_PATH_PREFIX = ".config/RectangleWin/" var DEFAULT_CONF_NAME = "config.yaml" func convertModifier(keyName string) (int32, error) { @@ -119,20 +131,55 @@ func bitwiseOr(nums []int32) int32 { return result } +func getValidConfigPathOrCreate() string { + homeDir := os.Getenv("HOME") + if homeDir == "" { + homeDir = os.Getenv("USERPROFILE") + } + if homeDir == "" { + // Give up generating a valid path. + // read or write the conf in current folder. + return DEFAULT_CONF_NAME + } + configDir := filepath.Join(homeDir, DEFAULT_CONF_PATH_PREFIX) + err := os.MkdirAll(configDir, 0755) + if err != nil { + fmt.Printf("Error creating directory under user's home folder: %s", err) + // read or write the conf in current folder + return DEFAULT_CONF_NAME + } + configPath := filepath.Join(configDir, DEFAULT_CONF_NAME) + return configPath +} + +func maybeDropExampleConfigFile(target string) { + // Check if the file exists, if not, create it with some content + if _, err := os.Stat(target); os.IsNotExist(err) { + // Create the file and write the sample content + err := ioutil.WriteFile(target, configExampleYaml, 0644) + if err != nil { + fmt.Println("Failed to create file created: %s %v", target, err) + } + fmt.Println("File created: %s", target) + } +} + func fetchConfiguration() Configuration { spew.Dump(DEFAULT_CONF) // Create a Configuration file. myConfig := Configuration{} // Yaml feeder - yamlFeeder := feeder.Yaml{Path: DEFAULT_CONF_NAME} + configFilePath := getValidConfigPathOrCreate() + maybeDropExampleConfigFile(configFilePath) + yamlFeeder := feeder.Yaml{Path: configFilePath} c := config.New() c.AddFeeder(yamlFeeder) c.AddStruct(&myConfig) err := c.Feed() if err != nil { - fmt.Printf("warn: invalid config files found: %s %v\n", DEFAULT_CONF_NAME, err) + fmt.Printf("warn: invalid config files found: %s %v\n", configFilePath, err) return DEFAULT_CONF } diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..24e8407 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,68 @@ +keybindings: + - modifier: + - Ctrl + - Alt + key: UP_ARROW + bindfeature: moveToTop + + - modifier: + - Ctrl + - Alt + key: DOWN_ARROW + bindfeature: moveToBottom + + - modifier: + - Ctrl + - Alt + key: LEFT_ARROW + bindfeature: moveToLeft + + - modifier: + - Ctrl + - Alt + key: RIGHT_ARROW + bindfeature: moveToRight + + - modifier: + - Ctrl + - Alt + key: U + bindfeature: moveToTopLeft + + - modifier: + - Ctrl + - Alt + key: I + bindfeature: moveToTopRight + + - modifier: + - Ctrl + - Alt + key: J + bindfeature: moveToBottomLeft + + - modifier: + - Ctrl + - Alt + key: K + bindfeature: moveToBottomRight + + - modifier: + - Ctrl + - Alt + key: = + bindfeature: makeLarger + + - modifier: + - Ctrl + - Alt + key: "-" + bindfeature: makeSmaller + + - modifier: + - Ctrl + - Alt + - Shift + key: UP_ARROW + bindfeature: makeFullHeight + From 3cbb7005635ce36f97042fa94fc3b12146a615b9 Mon Sep 17 00:00:00 2001 From: phoeagon Date: Sun, 21 Sep 2025 20:52:03 -0700 Subject: [PATCH 3/6] Add a tray menu item to open editor on the config file in %HOME%. A caveat of this is that there's no simple way to reload after changing and saving it in notepad.exe, due to the need for a more complex synchronization for this reload --- config.example.yaml | 2 ++ tray.go | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/config.example.yaml b/config.example.yaml index 24e8407..6b25a56 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,3 +1,5 @@ +# This is a sample config file for RectangleWin. +# See conf.go for details. keybindings: - modifier: - Ctrl diff --git a/tray.go b/tray.go index b45aae1..9d311b8 100644 --- a/tray.go +++ b/tray.go @@ -17,9 +17,9 @@ package main import ( _ "embed" "fmt" - "github.com/getlantern/systray" "github.com/gonutz/w32/v2" + "os/exec" ) //go:embed assets/tray_icon.ico @@ -78,6 +78,25 @@ func onReady() { systray.AddSeparator() + mConfig := systray.AddMenuItem("Configuration", "") + go func() { + <-mConfig.ClickedCh + fmt.Println("opening editor for default config") + configFilePath := getValidConfigPathOrCreate() + maybeDropExampleConfigFile(configFilePath) + cmd := exec.Command("notepad.exe", configFilePath) + err := cmd.Start() + if err != nil { + showMessageBox(fmt.Sprintf("Failed to open config file %s\n%v", configFilePath, err)) + } + // TODO add a better way to reload current program. + // Reloading programmatically is non-trivial because this program registers + // hotkeys, so it much synchronize to start the child process, but quit + // parent before the child starts to register hotkeys + }() + + systray.AddSeparator() + mQuit := systray.AddMenuItem("Quit", "") go func() { <-mQuit.ClickedCh From ddd9db2eb5a52adf6af5fe66aaef48db8d003fde Mon Sep 17 00:00:00 2001 From: phoeagon Date: Sun, 21 Sep 2025 22:15:04 -0700 Subject: [PATCH 4/6] minor changes: avoid dropping config file if folder under %HOME% isn't available. --- conf.go | 18 ++++++++++-------- config.example.yaml | 2 -- tray.go | 10 +++++++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/conf.go b/conf.go index 178661f..553b3b2 100644 --- a/conf.go +++ b/conf.go @@ -131,7 +131,7 @@ func bitwiseOr(nums []int32) int32 { return result } -func getValidConfigPathOrCreate() string { +func getValidConfigPathOrCreate() (string, error) { homeDir := os.Getenv("HOME") if homeDir == "" { homeDir = os.Getenv("USERPROFILE") @@ -139,17 +139,17 @@ func getValidConfigPathOrCreate() string { if homeDir == "" { // Give up generating a valid path. // read or write the conf in current folder. - return DEFAULT_CONF_NAME + return DEFAULT_CONF_NAME, errors.New("Failed to find user home directory") } - configDir := filepath.Join(homeDir, DEFAULT_CONF_PATH_PREFIX) + configDir := filepath.Join(homeDir, filepath.FromSlash(DEFAULT_CONF_PATH_PREFIX)) err := os.MkdirAll(configDir, 0755) if err != nil { fmt.Printf("Error creating directory under user's home folder: %s", err) // read or write the conf in current folder - return DEFAULT_CONF_NAME + return DEFAULT_CONF_NAME, errors.New("Failed to create folders under user's home directory") } configPath := filepath.Join(configDir, DEFAULT_CONF_NAME) - return configPath + return configPath, nil } func maybeDropExampleConfigFile(target string) { @@ -170,14 +170,16 @@ func fetchConfiguration() Configuration { myConfig := Configuration{} // Yaml feeder - configFilePath := getValidConfigPathOrCreate() - maybeDropExampleConfigFile(configFilePath) + configFilePath, err := getValidConfigPathOrCreate() + if err == nil { + maybeDropExampleConfigFile(configFilePath) + } yamlFeeder := feeder.Yaml{Path: configFilePath} c := config.New() c.AddFeeder(yamlFeeder) c.AddStruct(&myConfig) - err := c.Feed() + err = c.Feed() if err != nil { fmt.Printf("warn: invalid config files found: %s %v\n", configFilePath, err) return DEFAULT_CONF diff --git a/config.example.yaml b/config.example.yaml index 6b25a56..24e8407 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,5 +1,3 @@ -# This is a sample config file for RectangleWin. -# See conf.go for details. keybindings: - modifier: - Ctrl diff --git a/tray.go b/tray.go index 9d311b8..a9ccb84 100644 --- a/tray.go +++ b/tray.go @@ -82,10 +82,14 @@ func onReady() { go func() { <-mConfig.ClickedCh fmt.Println("opening editor for default config") - configFilePath := getValidConfigPathOrCreate() - maybeDropExampleConfigFile(configFilePath) + configFilePath, err := getValidConfigPathOrCreate() + if err != nil { + showMessageBox(fmt.Sprintf( + "Can't locate config path under user home directory %s\n%v", configFilePath, err)) + return + } cmd := exec.Command("notepad.exe", configFilePath) - err := cmd.Start() + err = cmd.Start() if err != nil { showMessageBox(fmt.Sprintf("Failed to open config file %s\n%v", configFilePath, err)) } From cb4feee62ccff7574c10fc03a1de866fa434d830 Mon Sep 17 00:00:00 2001 From: phoeagon Date: Sun, 21 Sep 2025 22:43:30 -0700 Subject: [PATCH 5/6] use simpler dependencies for yaml parsing: use gopkg.in/yaml.v3 instead of golobby/config; also remove spew which is for debugging purposes only; also use fmt.Errorf(...) for errors --- conf.go | 27 ++++++++++++--------------- go.mod | 11 ++++------- go.sum | 10 ---------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/conf.go b/conf.go index 553b3b2..6ca8780 100644 --- a/conf.go +++ b/conf.go @@ -10,10 +10,8 @@ import ( "strings" ) import ( - "github.com/davecgh/go-spew/spew" - "github.com/golobby/config/v3" - "github.com/golobby/config/v3/pkg/feeder" "github.com/gonutz/w32/v2" + "gopkg.in/yaml.v3" ) type KeyBinding struct { @@ -87,7 +85,7 @@ func convertModifier(keyName string) (int32, error) { case "super": return MOD_WIN, nil default: - return 0, errors.New("invalid keyname") + return 0, fmt.Errorf("invalid keyname: %s", keyName) } return 0, errors.New("unreachable") } @@ -116,7 +114,7 @@ func convertKeyCode(key string) (int32, error) { case "=": return 187, nil default: - return 0, errors.New("Unknown key") + return 0, fmt.Errorf("Unknown key %s", key) } } @@ -146,7 +144,7 @@ func getValidConfigPathOrCreate() (string, error) { if err != nil { fmt.Printf("Error creating directory under user's home folder: %s", err) // read or write the conf in current folder - return DEFAULT_CONF_NAME, errors.New("Failed to create folders under user's home directory") + return DEFAULT_CONF_NAME, fmt.Errorf("Failed to create folders under user's home directory: %s", configDir) } configPath := filepath.Join(configDir, DEFAULT_CONF_NAME) return configPath, nil @@ -165,7 +163,6 @@ func maybeDropExampleConfigFile(target string) { } func fetchConfiguration() Configuration { - spew.Dump(DEFAULT_CONF) // Create a Configuration file. myConfig := Configuration{} @@ -174,14 +171,15 @@ func fetchConfiguration() Configuration { if err == nil { maybeDropExampleConfigFile(configFilePath) } - yamlFeeder := feeder.Yaml{Path: configFilePath} - c := config.New() - c.AddFeeder(yamlFeeder) - c.AddStruct(&myConfig) - - err = c.Feed() + data, err := os.ReadFile(configFilePath) if err != nil { - fmt.Printf("warn: invalid config files found: %s %v\n", configFilePath, err) + fmt.Printf("Failed to load config file at expected path %s\n", configFilePath) + // use the last-ditch config + return DEFAULT_CONF + } + + if err := yaml.Unmarshal(data, &myConfig); err != nil { + showMessageBox("Failed to parse config file at %s.\n") return DEFAULT_CONF } @@ -206,6 +204,5 @@ func fetchConfiguration() Configuration { } } } - spew.Dump(myConfig) return myConfig } diff --git a/go.mod b/go.mod index 7908785..316f34b 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,13 @@ go 1.17 require ( github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 - github.com/davecgh/go-spew v1.1.1 github.com/getlantern/systray v1.1.0 - github.com/golobby/config/v3 v3.4.2 github.com/gonutz/w32/v2 v2.2.2 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e + gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/BurntSushi/toml v1.2.1 // indirect github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab // indirect github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect @@ -21,9 +19,8 @@ require ( github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/golobby/cast v1.3.3 // indirect - github.com/golobby/dotenv v1.3.2 // indirect - github.com/golobby/env/v2 v2.2.4 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/stretchr/testify v1.8.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 5a7104e..865e525 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY= github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k= @@ -24,14 +22,6 @@ github.com/getlantern/systray v1.1.0 h1:U0wCEqseLi2ok1fE6b88gJklzriavPJixZysZPkZ github.com/getlantern/systray v1.1.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= -github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= -github.com/golobby/config/v3 v3.4.2 h1:oIOSo24mC0A8f93ZTL24NDNw0hZ3Tbb34wc1ckn2CsA= -github.com/golobby/config/v3 v3.4.2/go.mod h1:3go9UVPb3bBNrH7qidd4vd1HbsAAwIYqcQJgGmAa044= -github.com/golobby/dotenv v1.3.2 h1:9vA8XqXXIB3cX/5xQ1CTbOCPegioHtHXIxeFng+uOqQ= -github.com/golobby/dotenv v1.3.2/go.mod h1:9MMVXqzLNluhVxCv3X/DLYBNUb289f05tr+df1+7278= -github.com/golobby/env/v2 v2.2.4 h1:sjdTe+bScPRWUIA1AQH95RHv52jM5Mns2XHwLyEbkzk= -github.com/golobby/env/v2 v2.2.4/go.mod h1:HDJW+dHHwLxkb8FZMjBTBiZUFl1iAA4F9YX15kBC84c= github.com/gonutz/w32/v2 v2.2.2 h1:y6Y337TpuCXjYdFTq5p5NmcujEdAQiTB43kisovMk+0= github.com/gonutz/w32/v2 v2.2.2/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= From 129db08b5c37f588a5fe3f32a3a5fbe4cbc0b322 Mon Sep 17 00:00:00 2001 From: phoeagon Date: Sat, 15 Nov 2025 17:26:28 -0800 Subject: [PATCH 6/6] minor syntax fix: go has a syntax in switch-cases where multiple cases correspond to the same block --- conf.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/conf.go b/conf.go index 6ca8780..9a095dd 100644 --- a/conf.go +++ b/conf.go @@ -80,9 +80,7 @@ func convertModifier(keyName string) (int32, error) { return MOD_ALT, nil case "shift": return MOD_SHIFT, nil - case "win": - case "meta": - case "super": + case "win", "meta", "super": return MOD_WIN, nil default: return 0, fmt.Errorf("invalid keyname: %s", keyName)