Snap 8 Windows into a Grid on an Ultrawide in One Command — a Swift + AppleScript Convergence Loop and 5 Traps

Snap 8 Windows into a Grid on an Ultrawide in One Command — a Swift + AppleScript Convergence Loop and 5 Traps

Lily Posted on Jul 22 • Originally published at dev.to This is part of my "Claude Code environment" series. Last time, in The Environment-Degradation Sentinel, I wrote about self-diagnosing a Claude Code environment. This time it's a different kind of workflow cost. Picture eight Terminal.app windows scattered across an ultrawide monitor. A single command snaps them into a 4×2 grid. It looks like "just line up windows with osascript," but there are traps across three layers: coordinate-system conversion, AppleScript's asynchronous behavior, and macOS's row rounding. In this article I'll explain the two design cores and the five traps I hit during implementation, with real code. The problem: dragging windows into place cost me a minute When I line up multiple Claude Code sessions on a 3440×1440 ultrawide, the windows open in random positions every time. Fullscreen split isn't enough. Mission Control can't pin positions. In the end I was spending a minute every morning dragging all eight windows by mouse into a 4×2 arrangement. Writing the position and size directly in AppleScript should have been the end of it. But once I actually started writing, three things became clear: There are two coordinate systems (Cocoa's Y-up and screen coordinates' Y-down) set position gets silently ignored in some situations The window's height doesn't end up at the value I specified I'll walk through the design of a script that solves all of these together (~/.claude/scripts/terminal-grid8.sh). Design core ①: Convert visibleFrame to global coordinates in Swift and hand it to the shell AppleScript's position of window returns global coordinates (top-left origin, Y-down). Meanwhile macOS's NSScreen.visibleFrame uses the Cocoa coordinate system (Y-up). Mixing the two makes the math go wrong. I do the conversion all at once in Swift and hand it off via the shell variables GX GY GW GH. GEOM=$(swift - <<'EOF' import AppKit let screens = NSScreen.screens guard let main = screens.first else { exit(1) } // 最も横長の画面を対象にする(ウルトラワイド)。同率ならメイン以外を優先 let target = screens.max(by: { $0.frame.width < $1.frame.width })! let v = target.visibleFrame // Dock除外(Cocoa座標: 左下原点Y上向き) let mainH = main.frame.height var topY = mainH - (v.origin.y + v.height) // グローバル(左上原点)座標へ変換 var h = v.height // visibleFrameがメニューバーを反映しない画面がある → 手動で差し引く if v.height == target.frame.height { topY += 30 h -= 30 } print("\(Int(v.origin.x)) \(Int(topY)) \(Int(v.width)) \(Int(h))") EOF ) read -r GX GY GW GH <<< "$GEOM" Enter fullscreen mode Exit fullscreen mode The key is the conversion formula mainH - (v.origin.y + v.height). Cocoa's visibleFrame.origin.y is the distance from the bottom edge of the main screen (Y-up), so to turn it into a global "distance from the top-left (Y-down)," you subtract from the height of the main screen. The if v.height == target.frame.height branch is a fallback that emerged from real measurement. On non-main screens, visibleFrame can return the same value as the frame without subtracting the 30px menu bar. In that case I subtract it manually on the Swift side (trap ④, below). The cell dimensions are a simple integer division. CW=$(( GW / COLS )) # 列幅 CH=$(( GH / ROWS )) # 行高 Enter fullscreen mode Exit fullscreen mode Design core ②: AppleScript's convergence loop The backbone of the convergence loop is that it re-scans indexes on every pass — not by name, not by ID. -- 収束ループ: 毎パス再スキャンして未配置の窓を1枚だけ動かす repeat with pass from 1 to (nCells * 3) set occupied to {} repeat nCells times set end of occupied to false end repeat set moverIdx to 0 set n to count windows -- 1) 対象画面上の窓を走査し、セル一致済みをマーク・最初の未配置窓を記録 repeat with i from 1 to n try set p to position of window i set px to item 1 of p set py to item 2 of p on error set px to -99999 set py to -99999 end try if px ≥ (gx - tol) and px < (gx + nCols * cw) and py ≥ (gy - tol) and py < (gy + nRows * ch) then -- セル一致判定 ...(tol=40px の誤差許容) if (not matched) and moverIdx = 0 then set moverIdx to i end if end repeat -- 2) 未配置窓が無ければ収束 if moverIdx = 0 then exit repeat -- 3) 最初の空きセルへ移動 (size→delay→position→delay→size、delay 必須) set size of window moverIdx to {cw, ch} delay 0.2 set position of window moverIdx to {tx, ty} delay 0.2 set size of window moverIdx to {cw, ch} delay 0.2 -- 読み返して position を最終補正(リサイズでのドリフト対策) set p2 to position of window moverIdx if (item 1 of p2) is not tx or (item 2 of p2) is not ty then set position of window moverIdx to {tx, ty} delay 0.2 end if end repeat Enter fullscreen mode Exit fullscreen mode There are two reasons I designed this as "move exactly one window and re-scan as long as any remain unplaced," rather than "move all the windows at once with a forEach": Name references don't work (trap ①), so you can't hold an object reference across passes Moving one window changes the cells' occupancy state, so you have to redo the scan Even in the worst case (all 8 windows are out of place), nCells * 3 = 24 passes are enough to converge. :::message If you drop delay 0.2, set position is silently ignored (trap ②). AppleScript doesn't throw an error either. You need the five steps size → delay → position → delay → size, plus the fallback that reads back and re-corrects. ::: After the convergence loop, there's a normalization pass that unifies the bottom row's y by actual height. -- 最下段yの正規化: 行丸めでセル高を超える実高のクランプ揺れ対策 set bottomEdge to gy + nRows * ch set lastRowTop to gy + (nRows - 1) * ch repeat with i from 1 to (count windows) try set p to position of window i -- 最下段の窓だけを対象に実高で下端揃え if py ≥ (lastRowTop - tol) and py < bottomEdge then set s to size of window i set wh to item 2 of s -- 実際のピクセル高 set targetY to bottomEdge - wh if py is not targetY then set position of window i to {bestX, targetY} delay 0.2 end if end if end try end repeat Enter fullscreen mode Exit fullscreen mode The traps I hit ① Window name references break down while the spinner is rotating Terminal.app replaces the title with spinner characters (⠐⠂✳) while a tab is loading. If you reference a window by name right after a session starts, you get -1728 (object not found). Stop referencing by name; operate directly by the window i index on every pass. ② A position set right after size is ignored without a delay If you put set position of window i to {tx, ty} right after a resize, the coordinate change arrives before macOS's window server has finished the previous resize, and it gets dropped. Put delay 0.2 in two places (after size and after position), then read back and correct. ③ Terminal's row-height rounding keeps the cell height off the specified value Terminal.app rounds a window's height to "rows × row height." For example, even if you specify a cell height of 705px, the actual value becomes 44 rows × 16px = 704px, or 45 rows × 16px = 720px. This error, combined with macOS's boundary clamping, shifts the bottom row's windows by a few px and makes the cell-match check fail. Run a "bottom-edge alignment by actual height" normalization pass after the convergence loop to absorb it. ④ visibleFrame sometimes doesn't subtract the menu bar On non-main screens, NSScreen.visibleFrame can return the same value as NSScreen.frame (observed in practice). In that case the computed top edge of the grid overlaps the menu bar. On the Swift side, apply topY += 30 / h -= 30 manually, conditioned on v.height == target.frame.height. if v.height == target.frame.height { topY += 30 h -= 30 } Enter fullscreen mode Exit fullscreen mode ⑤ Transient errors (-10006 / -1728) recover naturally on retry While a window is initializing or its name is changing, you get -10006 (invalid index) or -1728. If you insert delay 0.3 in an on error and fall through to the next pass, the convergence loop re-scans that same window on the following pass and picks it up. The key is not to abort immediately on an error. on error delay 0.3 -- 過渡エラーは次パスで再試行 end try Enter fullscreen mode Exit fullscreen mode Usage and verification ~/.claude/scripts/terminal-grid8.sh Enter fullscreen mode Exit fullscreen mode One command aligns Terminal.app's 8 windows. You can change the number of divisions with just the two lines at the top. COLS=4 ROWS=2 Enter fullscreen mode Exit fullscreen mode If you want 6 divisions, rewrite it to COLS=3 ROWS=2. The final report prints each window's x,y WxH title. On a 3440-wide screen, success means x converges to the four values 0/860/1720/2580, and y converges to one value each for the top and bottom rows. Wrap-up Cocoa Y-up → global Y-down conversion is handled by the single Swift line mainH - (v.origin.y + v.height) and handed to the shell AppleScript's convergence loop repeats "move one window and re-scan" instead of a foreach. Drop name references and operate by index on every pass If you omit delay 0.2, set position is silently ignored The y drift from Terminal's row-height rounding is normalized by a post-convergence "bottom-edge alignment by actual height" pass Because some screens don't reflect the menu bar in visibleFrame, add a manual 30px correction conditioned on height == frame.height Next time, I'll write about an environment-alignment hook that fires on Claude Code startup using this script as a trigger — placing Terminal windows, the status line, and Obsidian into fixed positions all at once from launchd. Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio · X · GitHub*

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.