Created
July 21, 2026 15:07
-
-
Save spongeboy/ad98fa74e2e8367d12ab03073f70f5ec to your computer and use it in GitHub Desktop.
PatSnap
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
| ; PATSNAP - Pat shaped Windows Snapping. To copy behaviour of Moom in OSX | |
| ; Waits for key press (1/2/3/4), snaps window to specified width | |
| ; 1 = 50% Left | 2 = 50% Right | 3 = 75% Left | 4 = 75% Right | |
| #Requires AutoHotkey v2.0 | |
| #SingleInstance Force | |
| ; --- Startup Message --- | |
| TrayTip("See C:\Users\pat_m\personal\projects\asdf\Docs\autohotkey", "PATSNAP") | |
| ^+z:: { | |
| ToolTip("PATSNAP. Esc to exit.") | |
| ; Loop: wait for a single key each time; exit on any unexpected key | |
| Loop { | |
| ih := InputHook("L1 T10", "{Escape}{Left}{Right}{Space}{1}{2}{3}{4}") | |
| ih.Start() | |
| ih.Wait() | |
| if (ih.EndReason = "Timeout") { | |
| ; No whitelisted key or char yet; if any other key is currently down, exit | |
| if (UnexpectedKeyPressed()) | |
| break | |
| continue | |
| } | |
| key := ih.EndKey ; Will be one of the EndKeys, or empty for other chars | |
| if (key = "") { | |
| ; Any non-whitelisted character exits snap mode | |
| break | |
| } | |
| if (key = "1") { | |
| SnapLeft50() | |
| continue | |
| } | |
| if (key = "2") { | |
| SnapRight50() | |
| continue | |
| } | |
| if (key = "3") { | |
| SnapLeft75() | |
| continue | |
| } | |
| if (key = "4") { | |
| SnapRight75() | |
| continue | |
| } | |
| if (key = "Space") { | |
| SnapFull() | |
| continue | |
| } | |
| if (key = "Left") { | |
| MoveToMonitor(-1) | |
| continue | |
| } | |
| if (key = "Right") { | |
| MoveToMonitor(1) | |
| continue | |
| } | |
| if (key = "Escape") { | |
| break | |
| } | |
| ; Fallback: any other key exits | |
| break | |
| } | |
| ToolTip("") | |
| ;TrayTip("Snap mode exited", "Window Snapper", 16) | |
| } | |
| ; ##################################### | |
| ; ### Function Definitions ### | |
| ; ##################################### | |
| SnapLeft50() { | |
| ShowDebug("Key '1' pressed - Snapping 50% LEFT") | |
| SnapWindow(true, 75) | |
| } | |
| SnapRight50() { | |
| ShowDebug("Key '2' pressed - Snapping 50% RIGHT") | |
| SnapWindow(false, 75) | |
| } | |
| SnapLeft75() { | |
| ShowDebug("Key '3' pressed - Snapping 75% LEFT") | |
| SnapWindow(true, 50) | |
| } | |
| SnapRight75() { | |
| ShowDebug("Key '4' pressed - Snapping 75% RIGHT") | |
| SnapWindow(false, 50) | |
| } | |
| SnapFull() { | |
| ShowDebug("Key space pressed - Snapping 100%") | |
| SnapWindow(false, 100) | |
| } | |
| ; --- Move Window to Next/Previous Monitor --- | |
| MoveToMonitor(direction) { | |
| ShowDebug("Moving window to " . (direction > 0 ? "next" : "previous") . " monitor") | |
| ; Get the active window | |
| hActive := WinExist("A") | |
| if (!hActive) { | |
| ShowError("No active window found") | |
| return | |
| } | |
| ; Don't move system windows | |
| sClass := WinGetClass("ahk_id " . hActive) | |
| if (sClass = "Progman" || sClass = "WorkerW" || sClass = "Shell_TrayWnd") { | |
| ShowDebug("Skipping system window: " . sClass) | |
| return | |
| } | |
| ; Get current window position and size | |
| WinGetPos(&winX, &winY, &winW, &winH, "ahk_id " . hActive) | |
| ; Get the current monitor | |
| MONITOR_DEFAULTTONEAREST := 0x00000002 | |
| hCurrentMonitor := DllCall("MonitorFromWindow", "Ptr", hActive, "UInt", MONITOR_DEFAULTTONEAREST, "Ptr") | |
| ; Get monitor count | |
| monitorCount := SysGet(80) ; SM_CMONITORS | |
| if (monitorCount <= 1) { | |
| ShowDebug("Only one monitor detected") | |
| return | |
| } | |
| ; Find which monitor number the window is currently on | |
| currentMonitorNum := 0 | |
| Loop monitorCount { | |
| MonitorGet(A_Index, &mLeft, &mTop, &mRight, &mBottom) | |
| ; Get monitor handle for this monitor | |
| MONITORINFO := Buffer(40, 0) | |
| NumPut("UInt", 40, MONITORINFO, 0) | |
| ; Create a point in the center of this monitor | |
| centerX := mLeft + (mRight - mLeft) // 2 | |
| centerY := mTop + (mBottom - mTop) // 2 | |
| ; Get the monitor handle at this point | |
| hTestMonitor := DllCall("MonitorFromPoint", "Int64", (centerY << 32) | (centerX & 0xFFFFFFFF), "UInt", MONITOR_DEFAULTTONEAREST, "Ptr") | |
| if (hTestMonitor = hCurrentMonitor) { | |
| currentMonitorNum := A_Index | |
| break | |
| } | |
| } | |
| if (currentMonitorNum = 0) { | |
| ShowError("Could not determine current monitor") | |
| return | |
| } | |
| ; Calculate target monitor number (wrap around) | |
| targetMonitorNum := currentMonitorNum + direction | |
| if (targetMonitorNum > monitorCount) | |
| targetMonitorNum := 1 | |
| else if (targetMonitorNum < 1) | |
| targetMonitorNum := monitorCount | |
| ShowDebug("Moving from monitor " . currentMonitorNum . " to monitor " . targetMonitorNum) | |
| ; Get target monitor work area | |
| MonitorGetWorkArea(targetMonitorNum, &workLeft, &workTop, &workRight, &workBottom) | |
| ; Calculate target position (try to maintain relative position) | |
| MonitorGetWorkArea(currentMonitorNum, &currWorkLeft, &currWorkTop, &currWorkRight, &currWorkBottom) | |
| currWorkW := currWorkRight - currWorkLeft | |
| currWorkH := currWorkBottom - currWorkTop | |
| targetWorkW := workRight - workLeft | |
| targetWorkH := workBottom - workTop | |
| ; Calculate relative position on current monitor (0.0 to 1.0) | |
| relX := (winX - currWorkLeft) / currWorkW | |
| relY := (winY - currWorkTop) / currWorkH | |
| ; Calculate size ratio | |
| relW := winW / currWorkW | |
| relH := winH / currWorkH | |
| ; Apply to target monitor | |
| newX := workLeft + Round(relX * targetWorkW) | |
| newY := workTop + Round(relY * targetWorkH) | |
| newW := Round(relW * targetWorkW) | |
| newH := Round(relH * targetWorkH) | |
| ; Move the window | |
| try { | |
| WinMove(newX, newY, newW, newH, "ahk_id " . hActive) | |
| ShowDebug("Window moved successfully to monitor " . targetMonitorNum) | |
| } catch as err { | |
| ShowError("Failed to move window - Error: " . err.Message) | |
| return | |
| } | |
| } | |
| ; --- Main Worker Function for Snapping --- | |
| SnapWindow(isLeft, widthPercent) { | |
| ShowDebug("SnapWindow called - Direction: " . (isLeft ? "Left" : "Right") . " Width: " . widthPercent . "%") | |
| ; 1. Get the handle of the currently active window | |
| hActive := WinExist("A") | |
| ShowDebug("Window Handle: " . hActive) | |
| if (!hActive) { | |
| ShowError("No active window found") | |
| return | |
| } | |
| ; Don't snap if it's the desktop or taskbar | |
| sClass := WinGetClass("ahk_id " . hActive) | |
| ShowDebug("Window Class: " . sClass) | |
| ; Only skip actual desktop and taskbar windows | |
| if (sClass = "Progman" || sClass = "WorkerW" || sClass = "Shell_TrayWnd") { | |
| ShowDebug("Skipping system window: " . sClass) | |
| return | |
| } | |
| ; Restore window if maximized | |
| minMax := WinGetMinMax("ahk_id " . hActive) | |
| if (minMax = 1) { | |
| ShowDebug("Window is maximized, restoring...") | |
| WinRestore("ahk_id " . hActive) | |
| Sleep(100) | |
| } | |
| ; 2. Get the monitor handle associated with the active window | |
| MONITOR_DEFAULTTONEAREST := 0x00000002 | |
| hMonitor := DllCall("MonitorFromWindow", "Ptr", hActive, "UInt", MONITOR_DEFAULTTONEAREST, "Ptr") | |
| ShowDebug("Monitor Handle: " . hMonitor) | |
| if (!hMonitor) { | |
| ShowError("Failed to get monitor handle") | |
| return | |
| } | |
| ; 3. Get monitor information using direct WinAPI call | |
| ; Create MONITORINFO structure (40 bytes) | |
| MONITORINFO := Buffer(40, 0) | |
| NumPut("UInt", 40, MONITORINFO, 0) ; cbSize | |
| ; Call GetMonitorInfo | |
| result := DllCall("GetMonitorInfo", "Ptr", hMonitor, "Ptr", MONITORINFO) | |
| if (!result) { | |
| ShowError("Failed to get monitor information") | |
| return | |
| } | |
| ShowDebug("Monitor info retrieved") | |
| ; 4. Extract monitor dimensions | |
| ; Structure: cbSize(4) + rcMonitor(16) + rcWork(16) + dwFlags(4) | |
| ; rcMonitor: left(4), top(4), right(4), bottom(4) at offsets 4,8,12,16 | |
| ; rcWork: left(4), top(4), right(4), bottom(4) at offsets 20,24,28,32 | |
| iMonLeft := NumGet(MONITORINFO, 4, "Int") | |
| iMonTop := NumGet(MONITORINFO, 8, "Int") | |
| iMonRight := NumGet(MONITORINFO, 12, "Int") | |
| iMonBottom := NumGet(MONITORINFO, 16, "Int") | |
| ShowDebug("Full Monitor: L=" . iMonLeft . " T=" . iMonTop . " R=" . iMonRight . " B=" . iMonBottom) | |
| ; Work area (excludes taskbar) | |
| iWorkLeft := NumGet(MONITORINFO, 20, "Int") | |
| iWorkTop := NumGet(MONITORINFO, 24, "Int") | |
| iWorkRight := NumGet(MONITORINFO, 28, "Int") | |
| iWorkBottom := NumGet(MONITORINFO, 32, "Int") | |
| ShowDebug("Work Area: L=" . iWorkLeft . " T=" . iWorkTop . " R=" . iWorkRight . " B=" . iWorkBottom) | |
| ; Use work area if valid, otherwise fall back to full monitor | |
| if (iWorkRight > iWorkLeft && iWorkBottom > iWorkTop) { | |
| iMonitorX := iWorkLeft | |
| iMonitorY := iWorkTop | |
| iMonitorW := iWorkRight - iWorkLeft | |
| iMonitorH := iWorkBottom - iWorkTop | |
| ShowDebug("Using Work Area") | |
| } else { | |
| iMonitorX := iMonLeft | |
| iMonitorY := iMonTop | |
| iMonitorW := iMonRight - iMonLeft | |
| iMonitorH := iMonBottom - iMonTop | |
| ShowDebug("Using Full Monitor Area") | |
| } | |
| ShowDebug("Final: X=" . iMonitorX . " Y=" . iMonitorY . " W=" . iMonitorW . " H=" . iMonitorH) | |
| ; Check if dimensions are valid | |
| if (iMonitorW <= 0 || iMonitorH <= 0) { | |
| ShowError("Invalid monitor dimensions: W=" . iMonitorW . " H=" . iMonitorH) | |
| return | |
| } | |
| ; 5. Calculate the new position and size based on percentage | |
| iWindowW := Floor(iMonitorW * widthPercent / 100) | |
| iNewY := iMonitorY | |
| if (isLeft) { | |
| ; Left Side: Starts at the monitor's work area X position | |
| iNewX := iMonitorX | |
| } else { | |
| ; Right Side: Starts at the monitor's X position PLUS the remaining width | |
| iNewX := iMonitorX + (iMonitorW - iWindowW) | |
| } | |
| ShowDebug("Moving window to: X=" . iNewX . " Y=" . iNewY . " W=" . iWindowW . " H=" . iMonitorH) | |
| ; 6. Move and resize the active window | |
| try { | |
| WinMove(iNewX, iNewY, iWindowW, iMonitorH, "ahk_id " . hActive) | |
| ShowDebug("Window snapped successfully!") | |
| } catch as err { | |
| ShowError("Failed to move window - Error: " . err.Message) | |
| return | |
| } | |
| } | |
| ; --- Error Display Function --- | |
| ShowError(sMessage) { | |
| ToolTip("ERROR: " . sMessage) | |
| Sleep(2000) | |
| ToolTip() | |
| } | |
| ; --- Debug Display Function --- | |
| ShowDebug(sMessage) { | |
| ;ToolTip("DEBUG: " . sMessage) | |
| ;Sleep(1500) | |
| } | |
| ; Returns true if any unexpected key (not handled by snap mode) is detected as down | |
| UnexpectedKeyPressed() { | |
| ; Virtual-key ranges to check broadly | |
| vkRanges := [ [0x08,0x2F], [0x5B,0xFF] ] | |
| ; Allowed keys' virtual key codes | |
| allowedVK := Map( | |
| 0x31, true, ; '1' | |
| 0x32, true, ; '2' | |
| 0x33, true, ; '3' | |
| 0x34, true, ; '4' | |
| 0x20, true, ; Space | |
| 0x25, true, ; Left | |
| 0x27, true, ; Right | |
| 0x1B, true, ; Escape | |
| ; Modifiers (ignore if still held from hotkey): Shift/Ctrl/Alt and L/R variants | |
| 0x10, true, ; Shift | |
| 0x11, true, ; Control | |
| 0x12, true, ; Alt (Menu) | |
| 0xA0, true, ; LShift | |
| 0xA1, true, ; RShift | |
| 0xA2, true, ; LControl | |
| 0xA3, true, ; RControl | |
| 0xA4, true, ; LMenu (Alt) | |
| 0xA5, true ; RMenu (Alt) | |
| ) | |
| for range in vkRanges { | |
| start := range[1] | |
| finish := range[2] | |
| vk := start | |
| while (vk <= finish) { | |
| if !allowedVK.Has(vk) { | |
| state := DllCall("GetAsyncKeyState", "Int", vk, "Short") | |
| if (state & 0x8000) { | |
| return true | |
| } | |
| } | |
| vk++ | |
| } | |
| } | |
| return false | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment