Skip to content

Instantly share code, notes, and snippets.

@lilith
Created October 25, 2025 00:06
Show Gist options
  • Save lilith/238b2bb8050f427b39eddad3d8e35f49 to your computer and use it in GitHub Desktop.
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)
; 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