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