native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476import CoreGraphicsimport Foundation
struct CanvasCardLayout: Codable, Equatable, Hashable, Sendable { var positionX: CGFloat var positionY: CGFloat var width: CGFloat var height: CGFloat
var position: CGPoint { get { CGPoint(x: positionX, y: positionY) } set { positionX = newValue.x positionY = newValue.y } }
var size: CGSize { get { CGSize(width: width, height: height) } set { width = newValue.width height = newValue.height } }
/// Card size used on small screens (≈14" MacBook Pro). On a small viewport /// the canvas must zoom out to fit a multi-card grid, which shrinks rendered /// text; smaller cards keep the fit-to-view scale — and thus text — larger. static let minDefaultSize = CGSize(width: 800, height: 550) /// Card size used on large screens (≈27" display and up), where fit-to-view /// caps at 1.0 so a larger card simply shows more content at native size. static let maxDefaultSize = CGSize(width: 1000, height: 680) /// Reference screen widths (logical points, default scaling) mapped to the /// interpolation endpoints above. static let minDefaultScreenWidth: CGFloat = 1512 // 14" MacBook Pro static let maxDefaultScreenWidth: CGFloat = 2560 // 27" / Studio Display
/// Size new cards adopt when no explicit size is given. Equal to /// `maxDefaultSize`, used only for transient fallbacks; creation paths pass /// an explicit `adaptiveDefaultSize(forScreenWidth:)` instead. static let defaultSize = maxDefaultSize
/// Linearly interpolate the default card size between `minDefaultSize` /// (14"-class screens) and `maxDefaultSize` (27"-class and larger) by screen /// width, clamped at both ends. static func adaptiveDefaultSize(forScreenWidth screenWidth: CGFloat) -> CGSize { let span = maxDefaultScreenWidth - minDefaultScreenWidth let fraction = span > 0 ? min(max((screenWidth - minDefaultScreenWidth) / span, 0), 1) : 1 return CGSize( width: minDefaultSize.width + (maxDefaultSize.width - minDefaultSize.width) * fraction, height: minDefaultSize.height + (maxDefaultSize.height - minDefaultSize.height) * fraction ) }
init(position: CGPoint, size: CGSize = Self.defaultSize) { self.positionX = position.x self.positionY = position.y self.width = size.width self.height = size.height }}
// MARK: - Card Packing
struct CanvasCardPacker { var spacing: CGFloat var titleBarHeight: CGFloat
struct CardInfo { var key: String var size: CGSize }
struct PackResult { var layouts: [String: CanvasCardLayout] var boundingSize: CGSize }
/// The maximum card count for exhaustive row-break enumeration. private static let exhaustiveLimit = 20
/// Pack cards to maximize the fitToView scale — cards appear as large as /// possible on screen. /// /// Two strategies compete: **waterfall** (equal-width columns, cards drop /// into the shortest column — great for varying heights) and **row-break** /// (cards flow left-to-right with centered rows — great for varying widths). /// The configuration with the highest `min(vW/bW, vH/bH)` wins. func pack(cards: [CardInfo], targetRatio: CGFloat) -> PackResult { guard !cards.isEmpty, targetRatio > 0 else { return PackResult(layouts: [:], boundingSize: .zero) }
let columnWidth = cards.map(\.size.width).max()! var bestScale: CGFloat = -1 var bestArea = CGFloat.infinity // Positive = waterfall column count, negative = row-break mask (offset by -1). var bestTag = 1
// Strategy 1: Waterfall — try all column counts. for cols in 1...cards.count { let (boxW, boxH) = waterfallBoundingSize(cards: cards, columns: cols, columnWidth: columnWidth) let scale = min(targetRatio / boxW, 1.0 / boxH) let area = boxW * boxH if scale > bestScale || (scale == bestScale && area < bestArea) { bestScale = scale bestArea = area bestTag = cols } }
// Strategy 2: Row-break — try all row configurations (exhaustive for small N). if cards.count <= Self.exhaustiveLimit { for mask in 0..<(1 << (cards.count - 1)) { let (boxW, boxH) = rowBreakBoundingSize(cards: cards, breakMask: mask) let scale = min(targetRatio / boxW, 1.0 / boxH) let area = boxW * boxH if scale > bestScale || (scale == bestScale && area < bestArea) { bestScale = scale bestArea = area bestTag = -(mask + 1) } } }
if bestTag > 0 { return waterfallPack(cards: cards, columns: bestTag, columnWidth: columnWidth) } else { return rowBreakLayout(cards: cards, breakMask: -(bestTag + 1)) } }
// MARK: - Waterfall layout
/// Compute bounding size for a waterfall layout without building layouts. private func waterfallBoundingSize( cards: [CardInfo], columns: Int, columnWidth: CGFloat ) -> (CGFloat, CGFloat) { var colHeights = Array(repeating: spacing, count: columns) for card in cards { let col = colHeights.enumerated().min(by: { $0.element < $1.element })!.offset colHeights[col] += card.size.height + titleBarHeight + spacing } let totalWidth = spacing + CGFloat(columns) * (columnWidth + spacing) let totalHeight = colHeights.max() ?? spacing return (totalWidth, totalHeight) }
/// Place cards into equal-width columns, each card going to the shortest /// column. Cards are horizontally centered within their column. private func waterfallPack( cards: [CardInfo], columns: Int, columnWidth: CGFloat ) -> PackResult { var colHeights = Array(repeating: spacing, count: columns) var layouts: [String: CanvasCardLayout] = [:]
for card in cards { let col = colHeights.enumerated().min(by: { $0.element < $1.element })!.offset let cardHeight = card.size.height + titleBarHeight let colLeft = spacing + CGFloat(col) * (columnWidth + spacing)
layouts[card.key] = CanvasCardLayout( position: CGPoint( x: colLeft + columnWidth / 2, y: colHeights[col] + cardHeight / 2 ), size: card.size )
colHeights[col] += cardHeight + spacing }
let totalWidth = spacing + CGFloat(columns) * (columnWidth + spacing) let totalHeight = colHeights.max() ?? spacing
return PackResult( layouts: layouts, boundingSize: CGSize(width: totalWidth, height: totalHeight) ) }
// MARK: - Row-break layout
/// Compute bounding size for a row-break configuration without allocating. private func rowBreakBoundingSize(cards: [CardInfo], breakMask: Int) -> (CGFloat, CGFloat) { var maxWidth = spacing var totalHeight = spacing var rowWidth = spacing var rowHeight: CGFloat = 0
for idx in 0..<cards.count { if idx > 0 && (breakMask & (1 << (idx - 1))) != 0 { maxWidth = max(maxWidth, rowWidth) totalHeight += rowHeight + spacing rowWidth = spacing rowHeight = 0 } rowWidth += cards[idx].size.width + spacing rowHeight = max(rowHeight, cards[idx].size.height + titleBarHeight) }
maxWidth = max(maxWidth, rowWidth) totalHeight += rowHeight + spacing return (maxWidth, totalHeight) }
/// Build card layouts from a row-break mask. Rows are centered horizontally. private func rowBreakLayout(cards: [CardInfo], breakMask: Int) -> PackResult { var rows: [[Int]] = [[0]] for idx in 1..<cards.count { if breakMask & (1 << (idx - 1)) != 0 { rows.append([idx]) } else { rows[rows.count - 1].append(idx) } }
let rowWidths = rows.map { row -> CGFloat in row.reduce(spacing) { $0 + cards[$1].size.width + spacing } } let maxRowWidth = rowWidths.max() ?? 0
var layouts: [String: CanvasCardLayout] = [:] var posY = spacing
for (rowIndex, row) in rows.enumerated() { let rowHeight = row.map { cards[$0].size.height + titleBarHeight }.max() ?? 0 let xOffset = (maxRowWidth - rowWidths[rowIndex]) / 2 var posX = spacing + xOffset
for idx in row { let card = cards[idx] let cardHeight = card.size.height + titleBarHeight layouts[card.key] = CanvasCardLayout( position: CGPoint( x: posX + card.size.width / 2, y: posY + cardHeight / 2 ), size: card.size ) posX += card.size.width + spacing }
posY += rowHeight + spacing }
return PackResult( layouts: layouts, boundingSize: CGSize(width: maxRowWidth, height: posY) ) }}
// MARK: - Tile Layout
/// Resizes cards to tile and fill the entire viewport, like an automatic window/// manager. Cards are laid into a balanced grid whose orientation follows the/// viewport: wide viewports spread cards into rows (left-to-right), tall/// viewports stack them into columns (top-to-bottom). Each line independently/// fills its full extent, so a 2-card row gets ½-width cards while a 3-card row/// gets ⅓-width cards — maximizing the area every card uses.struct CanvasTileLayout { var spacing: CGFloat var titleBarHeight: CGFloat
/// Distribute `count` cards across `floor(√count)` lines, packing the extra /// cards into the later lines. e.g. 5 → `[2, 3]`, 7 → `[3, 4]`, 9 → `[3, 3, 3]`. static func lineCounts(for count: Int) -> [Int] { guard count > 0 else { return [] } let lines = max(1, Int(Double(count).squareRoot().rounded(.down))) let base = count / lines let remainder = count % lines // Earlier lines get `base`; the last `remainder` lines get one extra. return (0..<lines).map { index in index < lines - remainder ? base : base + 1 } }
/// Resize and position `keys` to tile and fill `viewport`. Returns an empty /// dictionary when there is nothing to lay out (the caller treats this as a /// no-op, matching `CanvasCardPacker.pack`). /// /// The layout is built in a `viewport × zoom` frame and laid out edge-to-edge, /// so `fitToView` reproduces a scale of `1 / zoom`. `zoom` stays `1` (native /// scale, identical to single-card framing) while the tiled cards are already /// at least `comfortableSize`; once the grid shrinks cards below that, `zoom` /// grows so each card keeps a comfortable terminal surface (more rows/columns /// at smaller on-screen text) instead of a few oversized glyphs. The constant /// `spacing` lives in this scaled frame, so the on-screen gap (`spacing × scale`) /// tightens automatically as more cards are tiled. func layout(keys: [String], viewport: CGSize, comfortableSize: CGSize) -> [String: CanvasCardLayout] { guard !keys.isEmpty, viewport.width > 0, viewport.height > 0 else { return [:] }
let counts = Self.lineCounts(for: keys.count) let landscape = viewport.width >= viewport.height let lineCount = CGFloat(counts.count) let maxPerLine = CGFloat(counts.max() ?? 1)
// Enlarge the frame so the smallest card surface reaches `comfortableSize`; // `fitToView` later scales the whole frame back down to the viewport. let zoom: CGFloat if landscape { // Lines are rows: a card's width ≈ frameWidth / maxPerLine, height ≈ frameHeight / lineCount. zoom = max( 1, max(comfortableSize.width * maxPerLine / viewport.width, comfortableSize.height * lineCount / viewport.height) ) } else { // Lines are columns: width ≈ frameWidth / lineCount, height ≈ frameHeight / maxPerLine. zoom = max( 1, max(comfortableSize.width * lineCount / viewport.width, comfortableSize.height * maxPerLine / viewport.height) ) } let frame = CGSize(width: viewport.width * zoom, height: viewport.height * zoom)
var layouts: [String: CanvasCardLayout] = [:] var cursor = 0
if landscape { // Lines are rows: split the height evenly, fill each row's width. let rows = counts.count let rowVisualHeight = (frame.height - CGFloat(rows + 1) * spacing) / CGFloat(rows) var originY = spacing for cardsInRow in counts { let cardWidth = (frame.width - CGFloat(cardsInRow + 1) * spacing) / CGFloat(cardsInRow) var originX = spacing for _ in 0..<cardsInRow { layouts[keys[cursor]] = CanvasCardLayout( position: CGPoint(x: originX + cardWidth / 2, y: originY + rowVisualHeight / 2), size: CGSize(width: cardWidth, height: rowVisualHeight - titleBarHeight) ) originX += cardWidth + spacing cursor += 1 } originY += rowVisualHeight + spacing } } else { // Lines are columns: split the width evenly, fill each column's height. let cols = counts.count let colVisualWidth = (frame.width - CGFloat(cols + 1) * spacing) / CGFloat(cols) var originX = spacing for cardsInColumn in counts { let cardVisualHeight = (frame.height - CGFloat(cardsInColumn + 1) * spacing) / CGFloat(cardsInColumn) var originY = spacing for _ in 0..<cardsInColumn { layouts[keys[cursor]] = CanvasCardLayout( position: CGPoint(x: originX + colVisualWidth / 2, y: originY + cardVisualHeight / 2), size: CGSize(width: colVisualWidth, height: cardVisualHeight - titleBarHeight) ) originY += cardVisualHeight + spacing cursor += 1 } originX += colVisualWidth + spacing } }
return layouts }}
@MainActor@Observablefinal class CanvasLayoutStore { private static let storageKey = "canvasCardLayouts"
/// Whether auto-arrange has run in this app session. Resets on app launch. static var hasAutoArrangedInSession = false
private let defaults: UserDefaults private let initiallyLoadedCardKeys: Set<String>
var cardLayouts: [String: CanvasCardLayout] { didSet { save() } }
var zOrder: [String] { didSet { save() } }
init(defaults: UserDefaults = .standard) { self.defaults = defaults let stored = Self.load(from: defaults) cardLayouts = stored.cardLayouts zOrder = stored.zOrder initiallyLoadedCardKeys = Set(stored.cardLayouts.keys) }
// No actor-bound cleanup is needed. Avoid inferred isolated deinit, which can // hit the TaskLocal invalid-free bug in older system Swift runtimes (swiftlang/swift#85204). nonisolated deinit {}
func shouldAutoArrangeOnInitialEntry(for cardKeys: [String]) -> Bool { guard !cardKeys.isEmpty else { return false } return !cardKeys.contains { initiallyLoadedCardKeys.contains($0) } }
func setCardLayouts(_ layouts: [String: CanvasCardLayout], zOrder newZOrder: [String]? = nil) { cardLayouts = layouts zOrder = normalizedZOrder(newZOrder ?? zOrder, visibleKeys: Array(layouts.keys)) }
func ensureZOrder(for visibleKeys: [String]) { zOrder = normalizedZOrder(zOrder, visibleKeys: visibleKeys) }
func prune(to visibleKeys: Set<String>) { cardLayouts = cardLayouts.filter { visibleKeys.contains($0.key) } zOrder = zOrder.filter { visibleKeys.contains($0) } }
func moveToFront(_ cardKey: String) { guard cardLayouts[cardKey] != nil else { return } zOrder.removeAll { $0 == cardKey } zOrder.append(cardKey) }
func zIndex(for cardKey: String) -> Double { guard let index = zOrder.firstIndex(of: cardKey) else { return 0 } return Double(index) }
private static func load(from defaults: UserDefaults) -> CanvasLayoutStoragePayload { guard let data = defaults.data(forKey: storageKey) else { return CanvasLayoutStoragePayload(cardLayouts: [:], zOrder: []) }
if let payload = try? JSONDecoder().decode(CanvasLayoutStoragePayload.self, from: data) { return CanvasLayoutStoragePayload( cardLayouts: payload.cardLayouts, zOrder: normalizedZOrder(payload.zOrder, visibleKeys: Array(payload.cardLayouts.keys)) ) }
if let layouts = try? JSONDecoder().decode([String: CanvasCardLayout].self, from: data) { return CanvasLayoutStoragePayload(cardLayouts: layouts, zOrder: Array(layouts.keys).sorted()) }
return CanvasLayoutStoragePayload(cardLayouts: [:], zOrder: []) }
private func save() { let payload = CanvasLayoutStoragePayload( cardLayouts: cardLayouts, zOrder: Self.normalizedZOrder(zOrder, visibleKeys: Array(cardLayouts.keys)) ) if let data = try? JSONEncoder().encode(payload) { defaults.set(data, forKey: Self.storageKey) } }
private func normalizedZOrder(_ order: [String], visibleKeys: [String]) -> [String] { Self.normalizedZOrder(order, visibleKeys: visibleKeys) }
private static func normalizedZOrder(_ order: [String], visibleKeys: [String]) -> [String] { let visibleKeySet = Set(visibleKeys) var seen: Set<String> = [] var normalized: [String] = [] for key in order where visibleKeySet.contains(key) && seen.insert(key).inserted { normalized.append(key) } for key in visibleKeys.sorted() where seen.insert(key).inserted { normalized.append(key) } return normalized }}
private struct CanvasLayoutStoragePayload: Codable, Equatable { var cardLayouts: [String: CanvasCardLayout] var zOrder: [String]}