Created
October 25, 2025 00:06
-
-
Save lilith/238b2bb8050f427b39eddad3d8e35f49 to your computer and use it in GitHub Desktop.
Move windows around to span or shift quadrants in a square monitor setup (two stacked landscape, or two vertical side-by-side)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ; AutoHotkey v2 — Span active window across ALL monitors with cycling states | |
| ; Fully configurable keybindings | |
| ; ===== CONFIGURATION ===== | |
| class Config { | |
| ; Monitor layout - "horizontal" (side-by-side) or "vertical" (stacked) | |
| static MonitorLayout := "horizontal" ; "horizontal" or "vertical" | |
| ; Main cycling hotkeys | |
| static CycleUpHotkey := "#^Up" ; Win+Ctrl+Up | |
| static CycleDownHotkey := "#^Down" ; Win+Ctrl+Down | |
| ; Monitor switching hotkeys (next/previous in array) | |
| static NextMonitorHotkey := "#^Right" ; Win+Ctrl+Right | |
| static PreviousMonitorHotkey := "#^Left" ; Win+Ctrl+Left | |
| ; Instant span all hotkey | |
| static InstantSpanHotkey := "#^Space" ; Win+Ctrl+Spacebar (any key) | |
| ; Debug info hotkey | |
| static DebugHotkey := "#^m" ; Win+Ctrl+M | |
| ; Set to false to disable specific features | |
| static EnableCycling := true | |
| static EnableMonitorSwitching := true | |
| static EnableInstantSpan := true | |
| static EnableDebug := true | |
| } | |
| ; ===== DESIGN SPECIFICATION ===== | |
| ; QUADRANT-BASED WINDOW MANAGEMENT | |
| ; | |
| ; System divides the combined monitor area into a 2x2 grid of quadrants. | |
| ; Requirements: | |
| ; - All monitors must have identical resolution and orientation | |
| ; - Monitors are arranged either horizontally (side-by-side) or vertically (stacked) | |
| ; | |
| ; Quadrant Notation: (col, row, width, height) in grid units | |
| ; - col, row: 0-indexed starting position | |
| ; - width, height: span in grid units (1 or 2) | |
| ; | |
| ; For HORIZONTAL layout (side-by-side monitors): | |
| ; Monitor 1 = columns 0, Monitor 2 = column 1 | |
| ; Top half = row 0, Bottom half = row 1 | |
| ; | |
| ; For VERTICAL layout (stacked monitors): | |
| ; Monitor 1 = row 0, Monitor 2 = row 1 | |
| ; Left half = column 0, Right half = column 1 | |
| ; | |
| ; STATE TRANSITIONS: | |
| ; | |
| ; LEFT/RIGHT (horizontal movement): | |
| ; (0,0,1,2) <=> (0,0,2,2) <=> (1,0,1,2) ; Full height, moving horizontally | |
| ; (0,0,1,1) <=> (0,0,2,1) <=> (1,0,1,1) ; Top half, moving horizontally | |
| ; (0,1,1,1) <=> (0,1,2,1) <=> (1,1,1,1) ; Bottom half, moving horizontally | |
| ; | |
| ; UP/DOWN (vertical movement): | |
| ; Within single column: | |
| ; (x,0,1,1) <=> (x,0,1,2) <=> (x,1,1,1) where x = 0 or 1 | |
| ; When spanning both columns: | |
| ; (0,0,2,1) <=> (0,0,2,2) <=> (0,1,2,1) | |
| ; | |
| ; ALGORITHM: | |
| ; - Detect current quadrant state from window geometry | |
| ; - Apply directional rules to compute next quadrant | |
| ; - Convert quadrant to absolute screen coordinates | |
| ; - Apply new geometry to window | |
| ; ===== DPI AWARENESS ===== | |
| try { | |
| DllCall("SetProcessDpiAwarenessContext", "ptr", -4, "ptr") | |
| } catch { | |
| try { | |
| DllCall("Shcore\SetProcessDpiAwareness", "int", 2) | |
| } catch { | |
| DllCall("User32\SetProcessDPIAware") | |
| } | |
| } | |
| ; ===== QUADRANT STATE REPRESENTATION ===== | |
| class QuadrantState { | |
| col := 0 ; Starting column (0 or 1) | |
| row := 0 ; Starting row (0 or 1) | |
| width := 1 ; Width in grid units (1 or 2) | |
| height := 1 ; Height in grid units (1 or 2) | |
| __New(col, row, width, height) { | |
| this.col := col | |
| this.row := row | |
| this.width := width | |
| this.height := height | |
| } | |
| ToString() { | |
| return Format("({1},{2},{3},{4})", this.col, this.row, this.width, this.height) | |
| } | |
| Equals(other) { | |
| return (this.col = other.col && this.row = other.row && | |
| this.width = other.width && this.height = other.height) | |
| } | |
| MoveLeft() { | |
| ; Moving left (decreasing column) | |
| if (this.width = 2) { | |
| ; Spanning both columns -> move to left column | |
| return QuadrantState(0, this.row, 1, this.height) | |
| } else if (this.col = 1) { | |
| ; Right column -> span both | |
| return QuadrantState(0, this.row, 2, this.height) | |
| } else { | |
| ; Already at left column -> span both | |
| return QuadrantState(0, this.row, 2, this.height) | |
| } | |
| } | |
| MoveRight() { | |
| ; Moving right (increasing column) | |
| if (this.width = 2) { | |
| ; Spanning both columns -> move to right column | |
| return QuadrantState(1, this.row, 1, this.height) | |
| } else if (this.col = 0) { | |
| ; Left column -> span both | |
| return QuadrantState(0, this.row, 2, this.height) | |
| } else { | |
| ; Already at right column -> span both | |
| return QuadrantState(0, this.row, 2, this.height) | |
| } | |
| } | |
| MoveUp() { | |
| ; Moving up (decreasing row) | |
| if (this.height = 2) { | |
| ; Spanning both rows -> move to top row | |
| return QuadrantState(this.col, 0, this.width, 1) | |
| } else if (this.row = 1) { | |
| ; Bottom row -> span both | |
| return QuadrantState(this.col, 0, this.width, 2) | |
| } else { | |
| ; Already at top row -> span both | |
| return QuadrantState(this.col, 0, this.width, 2) | |
| } | |
| } | |
| MoveDown() { | |
| ; Moving down (increasing row) | |
| if (this.height = 2) { | |
| ; Spanning both rows -> move to bottom row | |
| return QuadrantState(this.col, 1, this.width, 1) | |
| } else if (this.row = 0) { | |
| ; Top row -> span both | |
| return QuadrantState(this.col, 0, this.width, 2) | |
| } else { | |
| ; Already at bottom row -> span both | |
| return QuadrantState(this.col, 0, this.width, 2) | |
| } | |
| } | |
| } | |
| ; ===== STATE MEMORY FOR INSTANT SPAN TOGGLE ===== | |
| global g_SavedStates := Map() ; Maps hwnd to saved QuadrantState | |
| ; ===== HOTKEY REGISTRATION ===== | |
| RegisterHotkeys() { | |
| if (Config.EnableCycling && Config.CycleUpHotkey != "") { | |
| Hotkey Config.CycleUpHotkey, (*) => CycleWindowState("up") | |
| } | |
| if (Config.EnableCycling && Config.CycleDownHotkey != "") { | |
| Hotkey Config.CycleDownHotkey, (*) => CycleWindowState("down") | |
| } | |
| if (Config.EnableMonitorSwitching && Config.NextMonitorHotkey != "") { | |
| Hotkey Config.NextMonitorHotkey, (*) => CycleWindowState("right") | |
| } | |
| if (Config.EnableMonitorSwitching && Config.PreviousMonitorHotkey != "") { | |
| Hotkey Config.PreviousMonitorHotkey, (*) => CycleWindowState("left") | |
| } | |
| if (Config.EnableInstantSpan && Config.InstantSpanHotkey != "") { | |
| Hotkey Config.InstantSpanHotkey, (*) => InstantSpanAll() | |
| } | |
| if (Config.EnableDebug && Config.DebugHotkey != "") { | |
| Hotkey Config.DebugHotkey, (*) => ShowDebugInfo() | |
| } | |
| } | |
| ; Register all hotkeys on startup | |
| RegisterHotkeys() | |
| ; ===== MAIN LOGIC ===== | |
| CycleWindowState(direction) { | |
| hwnd := WinExist("A") | |
| if !hwnd | |
| return | |
| WinRestore(hwnd) | |
| ; Validate monitor setup | |
| if (!ValidateMonitorSetup()) { | |
| MsgBox "Error: All monitors must have the same resolution and orientation.", "Monitor Setup Error", 16 | |
| return | |
| } | |
| currentQuadrant := DetectQuadrantState(hwnd) | |
| nextQuadrant := "" | |
| switch direction { | |
| case "up": nextQuadrant := currentQuadrant.MoveUp() | |
| case "down": nextQuadrant := currentQuadrant.MoveDown() | |
| case "left": nextQuadrant := currentQuadrant.MoveLeft() | |
| case "right": nextQuadrant := currentQuadrant.MoveRight() | |
| default: return | |
| } | |
| ApplyQuadrantState(hwnd, nextQuadrant) | |
| } | |
| InstantSpanAll() { | |
| hwnd := WinExist("A") | |
| if !hwnd | |
| return | |
| WinRestore(hwnd) | |
| if (!ValidateMonitorSetup()) { | |
| MsgBox "Error: All monitors must have the same resolution and orientation.", "Monitor Setup Error", 16 | |
| return | |
| } | |
| currentQuadrant := DetectQuadrantState(hwnd) | |
| fullSpanQuadrant := QuadrantState(0, 0, 2, 2) | |
| ; Check if currently in full span state | |
| if (currentQuadrant.Equals(fullSpanQuadrant)) { | |
| ; Already in full span - restore previous state if available | |
| if (g_SavedStates.Has(hwnd)) { | |
| savedQuadrant := g_SavedStates[hwnd] | |
| ApplyQuadrantState(hwnd, savedQuadrant) | |
| ; Remove saved state after restoring | |
| g_SavedStates.Delete(hwnd) | |
| } | |
| } else { | |
| ; Not in full span - save current state and span all | |
| g_SavedStates[hwnd] := currentQuadrant | |
| ApplyQuadrantState(hwnd, fullSpanQuadrant) | |
| } | |
| } | |
| ; ===== QUADRANT DETECTION & APPLICATION ===== | |
| DetectQuadrantState(hwnd) { | |
| pos := GetWindowGeometry(hwnd) | |
| grid := GetQuadrantGrid() | |
| tolerance := 10 | |
| ; Try to match current window position to a quadrant | |
| for col in [0, 1] { | |
| for row in [0, 1] { | |
| for w in [1, 2] { | |
| for h in [1, 2] { | |
| ; Skip invalid quadrants | |
| if (col + w > 2 || row + h > 2) | |
| continue | |
| testQuadrant := QuadrantState(col, row, w, h) | |
| expectedPos := QuadrantToGeometry(testQuadrant, grid) | |
| if (GeometryMatches(pos, expectedPos, tolerance)) | |
| return testQuadrant | |
| } | |
| } | |
| } | |
| } | |
| ; Default to full span | |
| return QuadrantState(0, 0, 2, 2) | |
| } | |
| ApplyQuadrantState(hwnd, quadrant) { | |
| grid := GetQuadrantGrid() | |
| geometry := QuadrantToGeometry(quadrant, grid) | |
| SetWindowGeometry(hwnd, geometry) | |
| } | |
| ; ===== QUADRANT GRID CALCULATIONS ===== | |
| GetQuadrantGrid() { | |
| ; Returns grid defining the 2x2 quadrant system | |
| ; Grid has: originX, originY, quadrantWidth, quadrantHeight | |
| monitors := GetMonitorArray() | |
| if (monitors.Length = 1) { | |
| ; Single monitor - divide it into 2x2 | |
| mon := monitors[1] | |
| return { | |
| originX: mon.x, | |
| originY: mon.y, | |
| quadrantWidth: mon.w / 2, | |
| quadrantHeight: mon.h / 2 | |
| } | |
| } | |
| ; Two monitors - arrangement depends on layout | |
| mon1 := monitors[1] | |
| mon2 := monitors[2] | |
| if (Config.MonitorLayout = "vertical") { | |
| ; Stacked vertically | |
| return { | |
| originX: mon1.x, | |
| originY: mon1.y, | |
| quadrantWidth: mon1.w / 2, | |
| quadrantHeight: mon1.h | |
| } | |
| } else { | |
| ; Side by side (horizontal) | |
| return { | |
| originX: mon1.x, | |
| originY: mon1.y, | |
| quadrantWidth: mon1.w, | |
| quadrantHeight: mon1.h / 2 | |
| } | |
| } | |
| } | |
| QuadrantToGeometry(quadrant, grid) { | |
| ; Convert quadrant coordinates to absolute screen geometry | |
| x := grid.originX + (quadrant.col * grid.quadrantWidth) | |
| y := grid.originY + (quadrant.row * grid.quadrantHeight) | |
| w := quadrant.width * grid.quadrantWidth | |
| h := quadrant.height * grid.quadrantHeight | |
| return {x: x, y: y, w: w, h: h} | |
| } | |
| ; ===== MONITOR VALIDATION ===== | |
| ValidateMonitorSetup() { | |
| monitors := GetMonitorArray() | |
| if (monitors.Length < 2) | |
| return true ; Single monitor always valid | |
| if (monitors.Length > 2) { | |
| ; More than 2 monitors not supported | |
| return false | |
| } | |
| mon1 := monitors[1] | |
| mon2 := monitors[2] | |
| ; Check if resolutions match | |
| if (mon1.w != mon2.w || mon1.h != mon2.h) | |
| return false | |
| ; Verify layout matches configuration | |
| if (Config.MonitorLayout = "vertical") { | |
| ; Should be stacked (same X, different Y) | |
| if (mon1.x != mon2.x) | |
| return false | |
| } else { | |
| ; Should be side-by-side (same Y, different X) | |
| if (mon1.y != mon2.y) | |
| return false | |
| } | |
| return true | |
| } | |
| ; ===== GEOMETRY CALCULATIONS ===== | |
| GetWindowGeometry(hwnd) { | |
| WinGetPos(&x, &y, &w, &h, hwnd) | |
| offsets := GetDWMFrameOffsets(hwnd) | |
| return { | |
| x: x + offsets.left, | |
| y: y + offsets.top, | |
| w: w - offsets.left - offsets.right, | |
| h: h - offsets.top - offsets.bottom | |
| } | |
| } | |
| SetWindowGeometry(hwnd, geometry) { | |
| offsets := GetDWMFrameOffsets(hwnd) | |
| x := geometry.x - offsets.left | |
| y := geometry.y - offsets.top | |
| w := geometry.w + offsets.left + offsets.right | |
| h := geometry.h + offsets.top + offsets.bottom | |
| flags := 0x0004 | 0x0010 | 0x0400 ; NOZORDER | NOACTIVATE | NOSENDCHANGING | |
| DllCall("User32\SetWindowPos", "ptr", hwnd, "ptr", 0, | |
| "int", x, "int", y, "int", w, "int", h, "uint", flags) | |
| } | |
| GeometryMatches(actual, expected, tolerance) { | |
| return (Abs(actual.x - expected.x) <= tolerance && | |
| Abs(actual.y - expected.y) <= tolerance && | |
| Abs(actual.w - expected.w) <= tolerance && | |
| Abs(actual.h - expected.h) <= tolerance) | |
| } | |
| ; ===== MONITOR UTILITIES ===== | |
| GetMonitorArray() { | |
| count := MonitorGetCount() | |
| monitors := [] | |
| loop count { | |
| MonitorGetWorkArea(A_Index, &l, &t, &r, &b) | |
| monitors.Push({x: l, y: t, w: r - l, h: b - t, index: A_Index}) | |
| } | |
| ; Sort monitors based on layout | |
| if (Config.MonitorLayout = "vertical") { | |
| ; Sort by Y position (top to bottom) | |
| loop monitors.Length - 1 { | |
| i := A_Index | |
| loop monitors.Length - i { | |
| j := A_Index + i | |
| if (monitors[i].y > monitors[j].y) { | |
| temp := monitors[i] | |
| monitors[i] := monitors[j] | |
| monitors[j] := temp | |
| } | |
| } | |
| } | |
| } else { | |
| ; Sort by X position (left to right) | |
| loop monitors.Length - 1 { | |
| i := A_Index | |
| loop monitors.Length - i { | |
| j := A_Index + i | |
| if (monitors[i].x > monitors[j].x) { | |
| temp := monitors[i] | |
| monitors[i] := monitors[j] | |
| monitors[j] := temp | |
| } | |
| } | |
| } | |
| } | |
| return monitors | |
| } | |
| GetDWMFrameOffsets(hwnd) { | |
| rect := Buffer(16, 0) | |
| if (DllCall("dwmapi\DwmGetWindowAttribute", "ptr", hwnd, "uint", 9, "ptr", rect, "uint", 16) != 0) | |
| return {left: 0, top: 0, right: 0, bottom: 0} | |
| winRect := Buffer(16, 0) | |
| DllCall("GetWindowRect", "ptr", hwnd, "ptr", winRect) | |
| return { | |
| left: NumGet(rect, 0, "int") - NumGet(winRect, 0, "int"), | |
| top: NumGet(rect, 4, "int") - NumGet(winRect, 4, "int"), | |
| right: NumGet(winRect, 8, "int") - NumGet(rect, 8, "int"), | |
| bottom: NumGet(winRect, 12, "int") - NumGet(rect, 12, "int") | |
| } | |
| } | |
| ; ===== DEBUG ===== | |
| ShowDebugInfo() { | |
| hwnd := WinExist("A") | |
| monitors := GetMonitorArray() | |
| msg := Format("=== Configuration ===`n") | |
| msg .= Format("Monitor Layout: {1}`n", Config.MonitorLayout) | |
| msg .= Format("Cycle Up: {1} ({2})`n", | |
| Config.EnableCycling ? "Enabled" : "Disabled", Config.CycleUpHotkey) | |
| msg .= Format("Cycle Down: {1} ({2})`n", | |
| Config.EnableCycling ? "Enabled" : "Disabled", Config.CycleDownHotkey) | |
| msg .= Format("Next Monitor: {1} ({2})`n", | |
| Config.EnableMonitorSwitching ? "Enabled" : "Disabled", Config.NextMonitorHotkey) | |
| msg .= Format("Previous Monitor: {1} ({2})`n", | |
| Config.EnableMonitorSwitching ? "Enabled" : "Disabled", Config.PreviousMonitorHotkey) | |
| msg .= Format("Instant Span: {1} ({2})`n", | |
| Config.EnableInstantSpan ? "Enabled" : "Disabled", Config.InstantSpanHotkey) | |
| msg .= Format("Debug: {1} ({2})`n`n", | |
| Config.EnableDebug ? "Enabled" : "Disabled", Config.DebugHotkey) | |
| msg .= Format("=== Monitor Array ({1} monitors) ===`n", monitors.Length) | |
| for index, mon in monitors { | |
| msg .= Format("Monitor {1}: x={2} y={3} w={4} h={5}`n", | |
| index, mon.x, mon.y, mon.w, mon.h) | |
| } | |
| msg .= Format("`nMonitor setup valid: {1}`n`n", ValidateMonitorSetup() ? "Yes" : "No") | |
| if (hwnd) { | |
| pos := GetWindowGeometry(hwnd) | |
| currentQuadrant := DetectQuadrantState(hwnd) | |
| grid := GetQuadrantGrid() | |
| msg .= Format("=== Current Window ===`n") | |
| msg .= Format("Position: x={1} y={2} w={3} h={4}`n", pos.x, pos.y, pos.w, pos.h) | |
| msg .= Format("Quadrant State: {1}`n", currentQuadrant.ToString()) | |
| msg .= Format("`nGrid: origin=({1},{2}) quadSize=({3}x{4})", | |
| grid.originX, grid.originY, grid.quadrantWidth, grid.quadrantHeight) | |
| ; Show saved state if exists | |
| if (g_SavedStates.Has(hwnd)) { | |
| savedQuadrant := g_SavedStates[hwnd] | |
| msg .= Format("`nSaved State: {1}", savedQuadrant.ToString()) | |
| } | |
| } | |
| MsgBox msg | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment