Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7cb9bf0
Update main.go: add ctrl-alt-[u|i|j|k] on resizing to 4 corners
phoeagon Aug 31, 2025
6973c0a
Update main.go
phoeagon Aug 31, 2025
fb91abe
Update README.md
phoeagon Aug 31, 2025
918f628
Update snap.go: add resize function and maxheight
phoeagon Aug 31, 2025
ecd0eb0
Update main.go
phoeagon Aug 31, 2025
ebd2e58
Update README.md
phoeagon Aug 31, 2025
aefbeb7
Update README.md: add max height hotkey
phoeagon Aug 31, 2025
0f779f9
Update README.md: fix hotkey script on max height
phoeagon Aug 31, 2025
b745638
add a conf.go to load yaml config
phoeagon Sep 2, 2025
bb6713f
minor formatting fixes. removes some debug outputs
phoeagon Sep 2, 2025
a48be35
update README.me: include description of config.yaml
phoeagon Sep 2, 2025
b45e3cc
Create go.yml
phoeagon Sep 7, 2025
41329a4
Update ci.yml
phoeagon Sep 7, 2025
02a7228
Update ci.yml
phoeagon Sep 7, 2025
09f1e37
Update ci.yml: switch new ways to set cache paths in ci.yml
phoeagon Sep 7, 2025
33ac7fb
Update ci.yml: fix typo: use go 1.20 not 1.2
phoeagon Sep 7, 2025
57f7b4d
Create release.yml: build and release
phoeagon Sep 7, 2025
35d1945
Update release.yml
phoeagon Sep 7, 2025
5df4a18
Update release.yml: fix ident
phoeagon Sep 7, 2025
db3b31e
Update release.yml: fix release setup
phoeagon Sep 7, 2025
8bb6486
Update release.yml: test building for windows artifacts using ubuntu-…
phoeagon Sep 7, 2025
b5509ea
Update release.yml: set a tag upon input, which is required for the r…
phoeagon Sep 7, 2025
f816c52
Update release.yml: update to auto release
phoeagon Sep 7, 2025
bb90211
Update release.yml: remove zip. not necessary,
phoeagon Sep 7, 2025
d2b37d4
adds matching against more keys defined in keymap.go by name. also fi…
phoeagon Sep 7, 2025
e9aaa12
update README: explains a few more hotkeys
phoeagon Sep 7, 2025
5d2a822
Update ci.yml: fix go-version to "1.20" instead of 1.20 suspecting in…
phoeagon Sep 7, 2025
86fbf79
minor fix to satisfy gofmt -s -d .
phoeagon Sep 7, 2025
17206ff
fix for go mod tidy
phoeagon Sep 7, 2025
583d3b8
Update ci.yml: remove --skip-publish which is no longer supported
phoeagon Sep 7, 2025
aeca165
Update ci.yml: removes unrecognized flag
phoeagon Sep 7, 2025
dcce140
Update ci.yml: minor fix: merge env section
phoeagon Sep 7, 2025
91acec0
update .goreleaser.yaml to v2 format
phoeagon Sep 7, 2025
a710b10
fix goreleaser.yaml: update to v2 format
phoeagon Sep 7, 2025
6fa940b
updates RectangleWin to automatically emit a config yaml at %HOME%/.c…
phoeagon Sep 22, 2025
d51082b
Add a tray menu item to open editor on the config file in %HOME%. A c…
phoeagon Sep 22, 2025
91b99e7
update github workflow: no longer release config.yaml because this ca…
phoeagon Sep 22, 2025
8bcd939
updates to README: separate config.yaml specific hotkey to a differen…
phoeagon Sep 22, 2025
b0f8ddb
Merge branch 'ahmetb:main' into main
phoeagon Nov 8, 2025
79f7683
minor fix: go has a different syntax on multiple cases going to the s…
phoeagon Nov 16, 2025
c10fffc
indentation fix to satisfy gofmt. somehow go fmt doesn't mind
phoeagon Nov 16, 2025
349dcd9
Merge branch 'ahmetb:main' into main
phoeagon Nov 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -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 .

30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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/
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ only using hotkeys:

- **Always On Top (toggle)**: <kbd>Win</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>

The following are also available if you use the attached `config.yaml`:
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>&uarr;</kbd>: maximize window height.

## Why?

It seems that no window snapping utility for Windows is capable of letting
Expand All @@ -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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some major things I always wanted to do:

  1. the path to config file is something well known, like %HOME%/.config/rectanglewin/config.yaml

  2. the program should create the default config file if it's missing (we can have a nice config file with inline comments etc).

  3. the tray icon has an item called "Preferences" that launches a notepad.exe with the config file.

Stretch goal: I suspect we'll add new configs over time --we gotta find a way to update the config file with the "new" defaults the program ships with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Updated the files (please see #14) to:

  1. automatically drop this config.yaml if missing at %HOME%/.config/RectangleWin/config.yaml
  2. load from aforementioned path
  3. Add an option to load notepad.exe on that file

I think it may take more work to add a "reload" option in the context menu, due to this process registering hotkeys, which must happen after parent process quits, introducing needs for additional interprocess synchronization.


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:
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>&larr;</kbd>: snap to left half
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>&rarr;</kbd>: snap to right half
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>&uarr;</kbd>: snap to upper half
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>&darr;</kbd>: snap to bottom half

- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>U</kbd>: top-left ½, ⅔ and ⅓
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>I</kbd>: top-right ½, ⅔ and ⅓
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>J</kbd>: bottom-left ½, ⅔ and ⅓
- <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>K</kbd>: bottom-right ½, ⅔ and ⅓

## Development (Install from source)

With Go 1.17+ installed, clone this repository and run:
Expand Down
212 changes: 212 additions & 0 deletions conf.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading