Skip to content

Instantly share code, notes, and snippets.

@niteria
Created March 21, 2026 23:58
Show Gist options
  • Select an option

  • Save niteria/af6de2044204462d7978f68de2013cf2 to your computer and use it in GitHub Desktop.

Select an option

Save niteria/af6de2044204462d7978f68de2013cf2 to your computer and use it in GitHub Desktop.
commit d0c445c801ec208cb79b37cf0c3461fb958d1352
Author: Bartosz Nitka <niteria@gmail.com>
Date: Sun Mar 22 00:37:54 2026 +0100
feat(hyprland): implement smart directional moves with monitor spillover
Replaces the previous ALT+SHIFT+Left/Right hy3 bindings with a new
`hypr-smart-move` Go binary.
The tool attempts a normal hy3 internal move first, then intelligently
detects whether it succeeded by comparing normalized `hy3:debugnodes`
output. If the window hit a monitor edge, it automatically performs
`movewindow mon:<direction>` instead.
This provides a much smoother experience when moving windows across
monitors. Also includes significant cleanup of the keybindings file
(removed stale comments and whitespace).
diff --git a/omarchy-nix/modules/home-manager/hyprland/bindings.nix b/omarchy-nix/modules/home-manager/hyprland/bindings.nix
index f6d9f43..cd643d1 100644
--- a/omarchy-nix/modules/home-manager/hyprland/bindings.nix
+++ b/omarchy-nix/modules/home-manager/hyprland/bindings.nix
@@ -5,6 +5,18 @@
}:
let
cfg = config.omarchy;
+
+ hyprSmartMove = pkgs.buildGoModule {
+ pname = "hypr-smart-move";
+ version = "1.0";
+ src = ./.;
+ vendorHash = null; # no external deps
+ # Only build this one file
+ buildPhase = ''
+ go build -o $out/bin/hypr-smart-move ${./hypr-smart-move.go}
+ '';
+ doCheck = false;
+ };
in
{
wayland.windowManager.hyprland.settings = {
@@ -16,27 +28,24 @@ in
"5, monitor:DP-1, persistent:true"
"6, monitor:HDMI-A-2, persistent:true"
];
+
bind = cfg.quick_app_bindings ++ [
"SUPER, space, exec, vicinae toggle"
- # "SUPER, space, exec, walker"
+ # "SUPER, space, exec, walker"
"ALT SHIFT, SPACE, exec, pkill -SIGUSR1 waybar"
"SUPER CTRL, SPACE, exec, ~/.local/share/omarchy/bin/omarchy-bg-next"
# "SUPER SHIFT CTRL, SPACE, exec, ~/.local/share/omarchy/bin/omarchy-theme-next"
-
"ALT SHIFT, Q, killactive,"
-
# End active session
"SUPER, ESCAPE, exec, hyprlock"
"SUPER SHIFT, ESCAPE, exit,"
"SUPER CTRL, ESCAPE, exec, reboot"
"SUPER SHIFT CTRL, ESCAPE, exec, systemctl poweroff"
-
# Move focus with mainMod + arrow keys
"ALT, left, hy3:movefocus, l"
"ALT, right, hy3:movefocus, r"
"ALT, up, hy3:movefocus, u"
"ALT, down, hy3:movefocus, d"
-
# Switch workspaces with mainMod + [0-9]
"ALT, 1, workspace, 1"
"ALT, 2, workspace, 2"
@@ -48,7 +57,6 @@ in
"ALT, 8, workspace, 8"
"ALT, 9, workspace, 9"
"ALT, 0, workspace, 10"
-
"SUPER, q, workspace, 1"
"SUPER, w, workspace, 2"
"SUPER, e, workspace, 3"
@@ -59,7 +67,6 @@ in
"SUPER, i, workspace, 8"
"SUPER, o, workspace, 9"
"SUPER, p, workspace, 10"
-
# Move active window to a workspace with mainMod + SHIFT + [0-9]
"ALT SHIFT, 1, movetoworkspace, 1"
"ALT SHIFT, 2, movetoworkspace, 2"
@@ -71,7 +78,6 @@ in
"ALT SHIFT, 8, movetoworkspace, 8"
"ALT SHIFT, 9, movetoworkspace, 9"
"ALT SHIFT, 0, movetoworkspace, 10"
-
"SUPER SHIFT, q, movetoworkspace, 1"
"SUPER SHIFT, w, movetoworkspace, 2"
"SUPER SHIFT, e, movetoworkspace, 3"
@@ -82,40 +88,32 @@ in
"SUPER SHIFT, i, movetoworkspace, 8"
"SUPER SHIFT, o, movetoworkspace, 9"
"SUPER SHIFT, p, movetoworkspace, 10"
-
- # Swap active window with the one next to it with mainMod + SHIFT + arrow keys
- "ALT SHIFT, left, hy3:movewindow, l"
- "ALT SHIFT, right, hy3:movewindow, r"
+ # === SMART DIRECTIONAL MOVE (now compiled Go) ===
+ "ALT SHIFT, left, exec, ${hyprSmartMove}/bin/hypr-smart-move l"
+ "ALT SHIFT, right, exec, ${hyprSmartMove}/bin/hypr-smart-move r"
"ALT SHIFT, up, hy3:movewindow, u"
"ALT SHIFT, down, hy3:movewindow, d"
-
# Resize active window
"ALT, minus, resizeactive, -100 0"
"ALT, equal, resizeactive, 100 0"
"ALT SHIFT, minus, resizeactive, 0 -100"
"ALT SHIFT, equal, resizeactive, 0 100"
-
# Scroll through existing workspaces with mainMod + scroll
"SUPER, mouse_down, workspace, e+1"
"SUPER, mouse_up, workspace, e-1"
-
# Control Apple Display brightness
"CTRL, F1, exec, ~/.local/share/omarchy/bin/apple-display-brightness -5000"
"CTRL, F2, exec, ~/.local/share/omarchy/bin/apple-display-brightness +5000"
"SHIFT CTRL, F2, exec, ~/.local/share/omarchy/bin/apple-display-brightness +60000"
-
# Super workspace floating layer
"SUPER, S, togglespecialworkspace, magic"
"SUPER SHIFT, S, movetoworkspace, special:magic"
-
# Screenshots with satty editing
", PRINT, exec, hyprshot -m region --clipboard-only && satty --filename - --fullscreen --output-filename ~/Pictures/Screenshots/satty-$(date '+%Y%m%d-%H%M%S').png"
"SHIFT, PRINT, exec, hyprshot -m window --clipboard-only && satty --filename - --fullscreen --output-filename ~/Pictures/Screenshots/satty-$(date '+%Y%m%d-%H%M%S').png"
"CTRL, PRINT, exec, hyprshot -m output --clipboard-only && satty --filename - --fullscreen --output-filename ~/Pictures/Screenshots/satty-$(date '+%Y%m%d-%H%M%S').png"
-
# Color picker
"ALT, PRINT, exec, hyprpicker -a"
-
# Clipse
"CTRL ALT, V, exec, ghostty --class clipse -e clipse"
# Layout controls with hy3
@@ -125,19 +123,16 @@ in
"ALT, S, hy3:changegroup, toggletab"
"ALT, R, hy3:changefocus, raise"
"ALT SHIFT, G, hy3:changegroup, opposite"
-
# This is the mic key on my keyboard
"SUPER, C, exec, ~/.local/share/omarchy/bin/hypermodern-waytoggle -s 1,3 -t 4,6 -f DP-1"
];
bindm = [
- # Move/resize windows with mainMod + LMB/RMB and dragging
"ALT, mouse:272, movewindow"
"ALT, mouse:273, resizewindow"
];
bindel = [
- # Laptop multimedia keys for volume and LCD brightness
",XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"
",XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"
",XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
@@ -147,7 +142,6 @@ in
];
bindl = [
- # Requires playerctl
", XF86AudioNext, exec, playerctl next"
", XF86AudioPause, exec, playerctl play-pause"
", XF86AudioPlay, exec, playerctl play-pause"
diff --git a/omarchy-nix/modules/home-manager/hyprland/hypr-smart-move.go b/omarchy-nix/modules/home-manager/hyprland/hypr-smart-move.go
new file mode 100644
index 0000000..589d663
--- /dev/null
+++ b/omarchy-nix/modules/home-manager/hyprland/hypr-smart-move.go
@@ -0,0 +1,133 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+)
+
+type Logger struct {
+ enabled bool
+}
+
+func (l *Logger) log(msg string) {
+ if !l.enabled {
+ return
+ }
+ ts := time.Now().Format("2006-01-02 15:04:05")
+ logPath := filepath.Join(os.Getenv("HOME"), ".cache", "hypr-smart-move.log")
+ f, _ := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if f != nil {
+ defer f.Close()
+ fmt.Fprintf(f, "=== %s ===\n%s\n=== END ===\n\n", ts, msg)
+ }
+}
+
+func runHyprctl(args []string) (string, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, "hyprctl", args...)
+ var stdout bytes.Buffer
+ cmd.Stdout = &stdout
+
+ if err := cmd.Run(); err != nil {
+ return "", fmt.Errorf("hyprctl %v failed: %w", args, err)
+ }
+ return strings.TrimSpace(stdout.String()), nil
+}
+
+func getActiveMonitor() int {
+ out, err := runHyprctl([]string{"activewindow", "-j"})
+ if err != nil || out == "" {
+ return -1
+ }
+ var data struct {
+ Monitor int `json:"monitor"`
+ }
+ if json.Unmarshal([]byte(out), &data) != nil {
+ return -1
+ }
+ return data.Monitor
+}
+
+func normalize(nodes, winID string) string {
+ protected := strings.ReplaceAll(nodes, winID, "__ACTIVE_WINDOW__")
+ re := regexp.MustCompile(`0x[0-9a-fA-F]+`)
+ normalized := re.ReplaceAllString(protected, "[ADDR]")
+ return strings.ReplaceAll(normalized, "__ACTIVE_WINDOW__", winID)
+}
+
+func main() {
+ direction := "r"
+ enableLog := false
+
+ flag.BoolVar(&enableLog, "log", false, "Enable logging")
+ flag.BoolVar(&enableLog, "L", false, "Enable logging (short)")
+ flag.Parse()
+
+ // Positional argument (exactly like the original Python script)
+ if len(flag.Args()) > 0 {
+ direction = flag.Args()[0]
+ }
+
+ dirArg := "r"
+ if direction == "l" || direction == "left" {
+ dirArg = "l"
+ }
+
+ logger := &Logger{enabled: enableLog}
+ logger.log(fmt.Sprintf("Called with direction='%s' → '%s'", direction, dirArg))
+
+ winJSON, err := runHyprctl([]string{"activewindow", "-j"})
+ if err != nil {
+ logger.log("Failed to get active window")
+ fmt.Fprintf(os.Stderr, "hypr-smart-move: %v\n", err)
+ return
+ }
+
+ type Window struct {
+ Address string `json:"address"`
+ }
+ var win Window
+ if json.Unmarshal([]byte(winJSON), &win) != nil || win.Address == "" {
+ logger.log("No active window found")
+ return
+ }
+
+ logger.log(fmt.Sprintf("Stable window ID: %s", win.Address))
+
+ beforeRaw, _ := runHyprctl([]string{"dispatch", "hy3:debugnodes"})
+ beforeNorm := normalize(beforeRaw, win.Address)
+
+ logger.log(fmt.Sprintf("→ hy3:movewindow %s", dirArg))
+ _, _ = runHyprctl([]string{"dispatch", "hy3:movewindow", dirArg})
+
+ time.Sleep(100 * time.Millisecond)
+
+ afterRaw, _ := runHyprctl([]string{"dispatch", "hy3:debugnodes"})
+ afterNorm := normalize(afterRaw, win.Address)
+
+ if beforeNorm == afterNorm {
+ logger.log("→ NORMALIZED TREE IDENTICAL → TRUE EDGE HIT")
+ oldMon := getActiveMonitor()
+ logger.log(fmt.Sprintf("→ Trying spill to mon:%s", dirArg))
+
+ _, _ = runHyprctl([]string{"dispatch", "movewindow", "mon:" + dirArg})
+ time.Sleep(50 * time.Millisecond)
+
+ if newMon := getActiveMonitor(); newMon != oldMon && newMon != -1 {
+ logger.log(fmt.Sprintf("→ Spill succeeded (monitor %d → %d)", oldMon, newMon))
+ }
+ } else {
+ logger.log("→ Tree changed → internal move/tab cycle succeeded")
+ }
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment