diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..53caa24 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,28 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: | + go env -w GOOS=windows + go build -ldflags -H=windowsgui . + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a231fbd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release +on: + push: + tags: + - "v*" + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: | + go env -w GOOS=windows + GOOS=windows go build -ldflags -H=windowsgui . + mkdir build/ + mv RectangleWin.exe build/RectangleWin.exe + + - name: Create GitHub Release + uses: fnkr/github-action-ghr@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GHR_PATH: build/ diff --git a/README.md b/README.md index 820ad4b..4b7ab28 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ only using hotkeys: - **Always On Top (toggle)**: Win+Alt+A +The following are also available if you use the attached `config.yaml`: + - Ctrl + Alt + Shift + : maximize window height. + ## Why? It seems that no window snapping utility for Windows is capable of letting @@ -58,6 +61,88 @@ are not offering enough hotkey support. - Configurable shortcuts: I don't need these and it will likely require a pop-up UI, so I will probably not get to this. +## Configuration + +Configuration could be achieved by changing `config.yaml` in the same folder. + +This file has the following structure: + +``` +keybindings: + - modifier: + - Ctrl + - Alt + key: UP_ARROW + bindfeature: moveToTop + + - modifier: + - Ctrl + - Alt + key: DOWN_ARROW + bindfeature: moveToBottom + +``` + +Each element of `keybindings` define a feature to bind keys. +Here are some valid `modifier`s: + ++ Ctrl ++ Alt ++ Shift ++ Win + +Here are some valid `key`: + +``` +/* case insensitive*/ +a +b +... +c + +0 +... +9 + +UP_ARROW +DOWN_ARROW +LEFT_ARROW +RIGHT_ARROW + ++ +"-" // This one has to be escaped +``` + +Here are some valid `bindfeature`s: + +``` +// moveToTop +// moveToBottom +// moveToLeft +// moveToRight +// moveToTopLeft +// moveToTopRight +// moveToBottomLeft +// moveToBottomRight +// makeSmaller +// makeLarger +// makeFullHeight +``` + +See `conf.go` for more details. + +The `config.example.yaml` contains an example. In addition, this YAML makes the following hotkey assignments +available: + - Ctrl + Alt + : snap to left half + - Ctrl + Alt + : snap to right half + - Ctrl + Alt + : snap to upper half + - Ctrl + Alt + : snap to bottom half + + - Ctrl + Alt + U: top-left ½, ⅔ and ⅓ + - Ctrl + Alt + I: top-right ½, ⅔ and ⅓ + - Ctrl + Alt + J: bottom-left ½, ⅔ and ⅓ + - Ctrl + Alt + K: bottom-right ½, ⅔ and ⅓ + ## Development (Install from source) With Go 1.17+ installed, clone this repository and run: diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..2b9627f --- /dev/null +++ b/conf.go @@ -0,0 +1,212 @@ +package main + +import ( + _ "embed" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "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"` +} + +// 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{ + { + Modifier: []string{"Ctrl", "Alt"}, + Key: "UP_ARROW", + KeyCode: 0x26, + BindFeature: "moveToTop", + }, + }, +} + +//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) { + switch strings.ToLower(keyName) { + case "ctrl": + return MOD_CONTROL, nil + case "alt": + return MOD_ALT, nil + case "shift": + return MOD_SHIFT, nil + case "win", "meta", "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 + } + for id, v := range keyNames { + lv := strings.ToLower(v) + if lv == k || lv == (k+" key") { + return int32(id), nil + } + } + 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 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 + 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", configFilePath, 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/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..6b25a56 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,70 @@ +# This is a sample config file for RectangleWin. +# See conf.go for details. +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 + diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..24e8407 --- /dev/null +++ b/config.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 + 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..b0c5ac7 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,108 @@ 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) }})) + case "makeLarger": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { + if _, err := resize(w32.GetForegroundWindow(), makeLarger); err != nil { + fmt.Printf("warn: resize: %v\n", err) + return + } + }})) + case "makeSmaller": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { + if _, err := resize(w32.GetForegroundWindow(), makeSmaller); err != nil { + fmt.Printf("warn: resize: %v\n", err) + return + } + }})) + case "makeFullHeight": + id += 1 + hks = append(hks, (HotKey{ + id: id, + mod: int(keyBinding.CombinedMod) | MOD_NOREPEAT, + vk: int(keyBinding.KeyCode), + callback: func() { + if _, err := resize(w32.GetForegroundWindow(), maxHeight); err != nil { + fmt.Printf("warn: resize: %v\n", err) + return + } + }})) + default: + continue + } + } + var failedHotKeys []HotKey for _, hk := range hks { if !RegisterHotKey(hk) { diff --git a/snap.go b/snap.go index 5626d4f..7553871 100644 --- a/snap.go +++ b/snap.go @@ -70,6 +70,36 @@ func topLeftHalf(disp, _ w32.RECT) w32.RECT { return merge(toLeft(disp, 1, func topLeftTwoThirds(disp, _ w32.RECT) w32.RECT { return merge(toLeft(disp, 2, 3), toTop(disp, 1, 2)) } func topLeftOneThirds(disp, _ w32.RECT) w32.RECT { return merge(toLeft(disp, 1, 3), toTop(disp, 1, 2)) } +func maxHeight(disp, cur w32.RECT) w32.RECT { + return w32.RECT{Left: cur.Left, Right: cur.Right, Top: disp.Top, Bottom: disp.Bottom} +} +func min(a, b int32) int32 { + if a < b { + return a + } + return b +} +func max(a, b int32) int32 { + if a > b { + return a + } + return b +} + +// sign = 1 for positive. sign = -1 for negative. +func resizeByPercent(disp, cur w32.RECT, sign int32) w32.RECT { + delta_x := (disp.Left - disp.Right) * sign / 20 + delta_y := (disp.Top - disp.Bottom) * sign / 20 + + return w32.RECT{ + Left: max(disp.Left, cur.Left+delta_x), + Right: min(disp.Right, cur.Right-delta_x), + Top: max(disp.Top, cur.Top+delta_y), + Bottom: min(disp.Bottom, cur.Bottom-delta_y)} +} +func makeLarger(disp, cur w32.RECT) w32.RECT { return resizeByPercent(disp, cur, 1) } +func makeSmaller(disp, cur w32.RECT) w32.RECT { return resizeByPercent(disp, cur, -1) } + func topRightHalf(disp, _ w32.RECT) w32.RECT { return merge(toRight(disp, 1, 2), toTop(disp, 1, 2)) } func topRightTwoThirds(disp, _ w32.RECT) w32.RECT { return merge(toRight(disp, 2, 3), toTop(disp, 1, 2)) 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