From c4f810f5f4f17ef388ad34d4701d0644ec8470cf Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 28 May 2026 16:59:14 -0700 Subject: [PATCH] slab/menubar: add Tiny text size mode for tightest tile density MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Far/Near becomes a three-state enum (Far/Near/Tiny). Tiny scales the auto-fit font to ~40% with a 4pt floor — for cramming many panes onto one screen at the edge of legibility. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Sources/SlabMenubar/AppDelegate.swift | 39 ++++++++++++++----- .../Sources/SlabMenubar/MenuBuilder.swift | 12 ++++-- .../Sources/SlabMenubar/Paths.swift | 4 ++ .../Sources/SlabMenubar/StateSnapshot.swift | 20 +++++++++- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index ea7545b69f..2b8aff73b9 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -630,7 +630,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { guard let n = sender.representedObject as? Int, n > 0 else { return } let liveIds = Set(state.claudeSessions.map { $0.sessionId }) let tile = state.autoTile - let near = state.nearText + let textSize = state.textSize // Snapshot screen geometry on main; NSScreen reads off-main can // return nil on first hit and were the silent failure mode for the // first auto-tile pass. @@ -643,7 +643,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } return } - let layout = geom.flatMap { Self.computeTileLayout(count: entries.count, geom: $0, near: near) } + let layout = geom.flatMap { Self.computeTileLayout(count: entries.count, geom: $0, size: textSize) } let term = Self.preferredTerminalApp() for (i, entry) in entries.enumerated() { let cell = layout?.cellAt(index: i) @@ -676,7 +676,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { // janitor — once the pid dies, the marker file gets reaped. let payloads = sessions.map { (sid: $0.sessionId, cwd: $0.cwd, pid: $0.claudePid) } let geom = state.autoTile ? Self.screenGeom() : nil - let layout = geom.flatMap { Self.computeTileLayout(count: payloads.count, geom: $0, near: state.nearText) } + let layout = geom.flatMap { Self.computeTileLayout(count: payloads.count, geom: $0, size: state.textSize) } DispatchQueue.global(qos: .userInitiated).async { // Resolve the target terminal before the SIGTERM — once the old // windows tear down, the open-app detection would skew. @@ -718,12 +718,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { let dir = (path as NSString).deletingLastPathComponent try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) FileManager.default.createFile(atPath: path, contents: nil) + try? FileManager.default.removeItem(atPath: Paths.tinyTextFlag) + refresh() + tileNow() + } + + @objc func setTextTiny() { + let path = Paths.tinyTextFlag + let dir = (path as NSString).deletingLastPathComponent + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) + FileManager.default.createFile(atPath: path, contents: nil) + try? FileManager.default.removeItem(atPath: Paths.nearTextFlag) refresh() tileNow() } @objc func setTextFar() { try? FileManager.default.removeItem(atPath: Paths.nearTextFlag) + try? FileManager.default.removeItem(atPath: Paths.tinyTextFlag) refresh() tileNow() } @@ -1455,7 +1467,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { /// spread the windows across those rows as evenly as possible; rows that /// come up a window short just get wider panels. Font size scales down /// for denser layouts so even N=10 stays legible. - private static func computeTileLayout(count: Int, geom: ScreenGeom, near: Bool = false) -> TileLayout? { + private static func computeTileLayout(count: Int, geom: ScreenGeom, size: TextSize = .far) -> TileLayout? { guard count > 0 else { return nil } // Near-square: same density feel as the old grid, but the per-row @@ -1479,10 +1491,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { let fontByH = Double(innerH) / (24.0 * 1.2) let fontByW = Double(innerW) / (80.0 * 0.6) let raw = min(fontByH, fontByW) - // Near = "I'm sitting close, give me density" → 60% of the - // legible-from-typical-distance size, floored at 6pt. - let scaled = near ? raw * 0.6 : raw - let fontSize = max(near ? 6 : 8, Int(scaled.rounded())) + // Far = legible from a typical sitting distance (the auto-fit + // baseline). Near ≈ 60% — denser when sitting close. Tiny ≈ 40% + // — for cramming many panes at the edge of legibility. + let scale: Double + let floor: Int + switch size { + case .far: scale = 1.0; floor = 8 + case .near: scale = 0.6; floor = 6 + case .tiny: scale = 0.4; floor = 4 + } + let fontSize = max(floor, Int((raw * scale).rounded())) return TileLayout( rowCounts: rowCounts, @@ -1503,7 +1522,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { /// un-themed (the iTerm2-only port intentionally dropped Terminal decor). @objc func tileNow() { guard let geom = Self.screenGeom() else { return } - let near = state.nearText + let textSize = state.textSize DispatchQueue.global(qos: .userInitiated).async { // Size the grid to everything on screen across both apps. The // `is running` guard never launches a quit Terminal.app, so a @@ -1512,7 +1531,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { let nTerm = Self.windowCount(app: "Terminal") let n = nIterm + nTerm guard n > 0 else { return } - guard let layout = Self.computeTileLayout(count: n, geom: geom, near: near) else { return } + guard let layout = Self.computeTileLayout(count: n, geom: geom, size: textSize) else { return } // Reset decor memo so the next refresh re-themes every iTerm2 // window from scratch (a re-pack invalidates prior placement). diff --git a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift index d41e4f3736..fd83ec2db3 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift @@ -309,18 +309,22 @@ enum MenuBuilder { tileNow.keyEquivalentModifierMask = [.command, .option] sub.addItem(tileNow) - // Text size — radio pair so the active mode is visible at a glance. - // Near = denser (close to screen), Far = auto-fit "suitable" size. + // Text size — radio trio so the active mode is visible at a glance. + // Far = auto-fit "suitable", Near = denser, Tiny = edge-of-legibility. let textParent = NSMenuItem(title: "Text size", action: nil, keyEquivalent: "") let textSub = NSMenu() let farItem = NSMenuItem(title: "Far (suitable)", action: #selector(AppDelegate.setTextFar), keyEquivalent: "") farItem.target = target - farItem.state = state.nearText ? .off : .on + farItem.state = state.textSize == .far ? .on : .off textSub.addItem(farItem) let nearItem = NSMenuItem(title: "Near (small)", action: #selector(AppDelegate.setTextNear), keyEquivalent: "") nearItem.target = target - nearItem.state = state.nearText ? .on : .off + nearItem.state = state.textSize == .near ? .on : .off textSub.addItem(nearItem) + let tinyItem = NSMenuItem(title: "Tiny (tightest)", action: #selector(AppDelegate.setTextTiny), keyEquivalent: "") + tinyItem.target = target + tinyItem.state = state.textSize == .tiny ? .on : .off + textSub.addItem(tinyItem) textParent.submenu = textSub sub.addItem(textParent) diff --git a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift index 6fdfce4834..ebbebe84fc 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift @@ -71,6 +71,10 @@ enum Paths { /// "comfortable from typical distance" size — for sitting close to the /// screen and wanting more content per pane. static var nearTextFlag: String { "\(slabHome)/state/tile-near" } + /// When this file exists, the tile font shrinks further still — for + /// cramming many panes onto one screen at the edge of legibility. + /// Tiny takes priority over near when both are set. + static var tinyTextFlag: String { "\(slabHome)/state/tile-tiny" } /// When this file exists, each Terminal window matching a live Claude /// session is re-themed by status (working/awaiting), so a wall of /// terminals reads as a status display at a glance. diff --git a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift index add459d363..11e9febe0a 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift @@ -14,6 +14,14 @@ struct PopRender { var startedAt: Double // ms since epoch } +/// How aggressively to shrink the tile font. Far = "comfortable from +/// typical distance" (the auto-fit baseline). Near = ~60% — denser when +/// you're sitting close. Tiny = ~40% — for cramming many panes onto one +/// screen at the edge of legibility. +enum TextSize { + case far, near, tiny +} + struct StateSnapshot { var lidClosed: Bool = false var sleepDisabled: Bool = false @@ -22,7 +30,7 @@ struct StateSnapshot { var ambientActive: Bool = false var muted: Bool = false var autoTile: Bool = false - var nearText: Bool = false + var textSize: TextSize = .far var themeByStatus: Bool = false var forceBright: Bool = false var tailnetPeers: [TailnetPeer] = [] @@ -68,7 +76,15 @@ struct StateSnapshot { s.ambientActive = FileManager.default.fileExists(atPath: Paths.ambientFlag) s.muted = FileManager.default.fileExists(atPath: Paths.muteFlag) s.autoTile = FileManager.default.fileExists(atPath: Paths.autoTileFlag) - s.nearText = FileManager.default.fileExists(atPath: Paths.nearTextFlag) + // Tiny wins over near if both flags somehow coexist on disk — + // matches the menu handlers, which only ever leave one set. + if FileManager.default.fileExists(atPath: Paths.tinyTextFlag) { + s.textSize = .tiny + } else if FileManager.default.fileExists(atPath: Paths.nearTextFlag) { + s.textSize = .near + } else { + s.textSize = .far + } s.themeByStatus = FileManager.default.fileExists(atPath: Paths.themeByStatusFlag) s.forceBright = FileManager.default.fileExists(atPath: Paths.forceBrightFlag) s.tailnetPeers = TailnetPeer.query() -- 2.51.2