From 7f11f8598cf1ba8159ce64181c511f3aac054c4a Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 4 Oct 2026 20:12:04 -0700 Subject: [PATCH] slab: auto-tile Chrome below its native minimum size --- .../Sources/SlabMenubar/AXTiler.swift | 6 +++- .../Sources/SlabMenubar/AppDelegate.swift | 32 ++++++++++++++--- .../SlabMenubar/ChromeTileScript.swift | 36 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 slab/menubar-swift/Sources/SlabMenubar/ChromeTileScript.swift diff --git a/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift b/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift index 6be1ccc76e..7d7469d7c6 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift @@ -43,6 +43,7 @@ enum AXTiler { let iterm: [Window] let terminal: [Window] let acPanes: [Window] + let chrome: [Window] /// Stage windows: a game stream (GeForce NOW) that wants one big /// column of its own rather than a grid cell. Only the first stage /// window is staged; any extra ones fall into the ordinary grid. @@ -52,7 +53,7 @@ enum AXTiler { /// binaries with no bundle identifier. let wizards: [Window] - var all: [Window] { iterm + terminal + acPanes + wizards + stage } + var all: [Window] { iterm + terminal + acPanes + chrome + wizards + stage } var signature: [CGWindowID] { all.map(\.id).sorted() } } @@ -126,6 +127,7 @@ enum AXTiler { acPanes: windowRefs(bundleId: "computer.aesthetic.app", requireStandardSubrole: false, liveWindows: liveWindows) + easelWindowRefs(liveWindows: liveWindows), + chrome: windowRefs(bundleId: "com.google.Chrome", liveWindows: liveWindows), stage: stageBundleIDs.flatMap { windowRefs(bundleId: $0, liveWindows: liveWindows) }, @@ -147,6 +149,8 @@ enum AXTiler { + windowRefs(bundleId: "computer.aesthetic.app", requireStandardSubrole: false, liveWindows: liveWindows, requireGeometry: false) + easelWindowRefs(liveWindows: liveWindows, requireGeometry: false) + + windowRefs(bundleId: "com.google.Chrome", liveWindows: liveWindows, + requireGeometry: false) + wizardWindowRefs(liveWindows: liveWindows, requireGeometry: false) + stageBundleIDs.flatMap { windowRefs(bundleId: $0, liveWindows: liveWindows, requireGeometry: false) diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index 42625161b3..db0831015b 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -3977,6 +3977,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { let window: AXUIElement let id: CGWindowID let isTerminal: Bool + let isChrome: Bool let bounds: (left: Int, top: Int, right: Int, bottom: Int) } private struct AXPass { @@ -3990,11 +3991,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { @discardableResult private static func axTilePass(snapshot: AXTiler.Snapshot, geom: ScreenGeom, textSize: TextSize) -> AXPass? { - NSLog("🧩 [tile] windows=%d ids=%@ iterm=%d term=%d acpane=%d wizard=%d stage=%d", + NSLog("🧩 [tile] windows=%d ids=%@ iterm=%d term=%d acpane=%d chrome=%d wizard=%d stage=%d", snapshot.all.count, snapshot.signature.map(String.init).joined(separator: ","), snapshot.iterm.count, snapshot.terminal.count, snapshot.acPanes.count, - snapshot.wizards.count, snapshot.stage.count) + snapshot.chrome.count, snapshot.wizards.count, snapshot.stage.count) // A stage window (GeForce NOW) is an ordinary, equal grid cell first. // Only when the app clamps above its cell — its configured floor is // bigger than the grid can offer — does it get a column of its own, @@ -4069,6 +4070,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { pick = localityAssignment(windowCenters: windowCenters, cellCenters: cellCenters) } let terminalIDs = Set(snapshot.terminal.map(\.id)) + let chromeIDs = Set(snapshot.chrome.map(\.id)) + var chromeCorrections: [ChromeTileScript.Placement] = [] var placements: [AXPlacement] = [] var terminalPlacements: [AXPlacement] = [] var misfitTerminalIDs: [CGWindowID] = [] @@ -4082,6 +4085,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { all[wi].element, left: cell.left, top: cell.top, right: cell.right, bottom: cell.bottom) let isTerminal = terminalIDs.contains(all[wi].id) + let isChrome = chromeIDs.contains(all[wi].id) + if isChrome, let residual, + residual.width > 1 || residual.height > 1, + let frame = AXTiler.frame(all[wi].element) { + chromeCorrections.append(.init( + current: (Int(frame.minX.rounded()), Int(frame.minY.rounded()), + Int(frame.maxX.rounded()), Int(frame.maxY.rounded())), + target: cell)) + } if isTerminal, let residual, abs(residual.width) > 1 || abs(residual.height) > 1 { @@ -4095,11 +4107,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { overflow = true } let placement = AXPlacement(window: all[wi].element, id: all[wi].id, - isTerminal: isTerminal, bounds: cell) + isTerminal: isTerminal, isChrome: isChrome, bounds: cell) placements.append(placement) if isTerminal { terminalPlacements.append(placement) } } if let stagePlacement { placements.append(stagePlacement) } + if !chromeCorrections.isEmpty { + let result = ShellRunner.run("/usr/bin/osascript", args: ["-e", + ChromeTileScript.make(placements: chromeCorrections)], timeout: 3) + NSLog("🧩 [tile-chrome] requested=%d applied=%@ status=%d", + chromeCorrections.count, + result.output.trimmingCharacters(in: .whitespacesAndNewlines), result.status) + } return (AXPass(nIterm: snapshot.iterm.count, nTerm: snapshot.terminal.count, fontSize: layout.fontSize, placements: placements, terminalPlacements: terminalPlacements, @@ -4148,11 +4167,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { height: geom.height) } return AXPlacement(window: stage.element, id: stage.id, - isTerminal: false, bounds: cell) + isTerminal: false, isChrome: false, bounds: cell) } private static func repin(_ pass: AXPass, includeTerminal: Bool = true) { - for placement in pass.placements where includeTerminal || !placement.isTerminal { + // Even an identical AX write expands a Chrome window below its minimum. + // Its final AppleScript bounds were already applied in the tile pass. + for placement in pass.placements + where !placement.isChrome && (includeTerminal || !placement.isTerminal) { let b = placement.bounds AXTiler.setFrameFitting(placement.window, left: b.left, top: b.top, right: b.right, bottom: b.bottom) diff --git a/slab/menubar-swift/Sources/SlabMenubar/ChromeTileScript.swift b/slab/menubar-swift/Sources/SlabMenubar/ChromeTileScript.swift new file mode 100644 index 0000000000..aae7c6134a --- /dev/null +++ b/slab/menubar-swift/Sources/SlabMenubar/ChromeTileScript.swift @@ -0,0 +1,36 @@ +import Foundation + +/// Chrome's AX resize clamps to its UI minimum; its AppleScript bounds setter +/// accepts smaller windows. Chrome's script IDs are NOT CGWindowIDs, so resolve +/// each target by its measured frame, refusing ambiguous or missing matches. +enum ChromeTileScript { + struct Placement { + let current: (left: Int, top: Int, right: Int, bottom: Int) + let target: (left: Int, top: Int, right: Int, bottom: Int) + } + + static func make(placements: [Placement]) -> String { + guard !placements.isEmpty else { return "" } + var lines = [ + "set applied to 0", + "if application id \"com.google.Chrome\" is running then", + " tell application id \"com.google.Chrome\"", + ] + for p in placements { + let c = p.current + let t = p.target + lines += [ + " try", + " set matches to every window whose bounds is {\(c.left), \(c.top), \(c.right), \(c.bottom)} and minimized is false", + " if (count matches) is 1 then", + " set w to item 1 of matches", + " set bounds of w to {\(t.left), \(t.top), \(t.right), \(t.bottom)}", + " set applied to applied + 1", + " end if", + " end try", + ] + } + lines += [" end tell", "end if", "return applied"] + return lines.joined(separator: "\n") + } +} -- 2.51.2