Mission Control Turbo: macOS multitasking turbocharged
at main 391 lines 16 kB view raw
1#!/usr/bin/env swift 2// Layout test harness compiles standalone, no Xcode needed. 3// Usage: swift test_layout.swift 4 5import Foundation 6import CoreGraphics 7 8// Minimal model types 9 10struct WindowInfo { 11 let windowID: CGWindowID 12 let title: String 13 let appName: String 14 let appBundleID: String 15 let frame: CGRect 16 let pid: pid_t 17 let isOnScreen: Bool 18 var thumbnail: CGImage? = nil 19 var appIcon: Any? = nil // NSImage not available without AppKit in script mode 20 21 var id: CGWindowID { windowID } 22 var fingerprintKey: String { "\(appBundleID)|\(title)" } 23} 24 25struct WindowGroup: Identifiable { 26 let appBundleID: String 27 let appName: String 28 var windows: [WindowInfo] 29 var frontmostIndex: Int = Int.max 30 var id: String { appBundleID } 31 var appIcon: Any? { nil } 32} 33 34struct WindowSnapshot { 35 let groups: [WindowGroup] 36 let fingerprint: Set<String> 37 let totalWindowCount: Int 38 39 init(groups: [WindowGroup]) { 40 self.groups = groups 41 self.totalWindowCount = groups.reduce(0) { $0 + $1.windows.count } 42 var fp = Set<String>() 43 for group in groups { 44 for window in group.windows { 45 fp.insert(window.fingerprintKey) 46 } 47 } 48 self.fingerprint = fp 49 } 50} 51 52// Copy of LayoutGeometry 53 54enum LayoutGeometry { 55 static let margin: CGFloat = 24 56 static let groupHeaderHeight: CGFloat = 28 57 static let compactGroupHeaderHeight: CGFloat = 4 58 static let groupSpacing: CGFloat = 8 59 static let itemSpacing: CGFloat = 8 60 static let titleHeight: CGFloat = 22 61 static let minThumbnailWidth: CGFloat = 80 62 static let maxThumbnailWidth: CGFloat = 480 63 static let compactGroupThreshold = 6 64 65 static func effectiveHeaderHeight(groupCount: Int) -> CGFloat { 66 groupCount > compactGroupThreshold ? compactGroupHeaderHeight : groupHeaderHeight 67 } 68 69 static func uniformThumbnailSize( 70 groupWindowCounts: [Int], 71 availableWidth: CGFloat, 72 availableHeight: CGFloat 73 ) -> (size: CGSize, columns: Int) { 74 let groupCount = groupWindowCounts.count 75 guard groupCount > 0 else { return (CGSize(width: 200, height: 150), 1) } 76 77 let headerH = effectiveHeaderHeight(groupCount: groupCount) 78 let headerOverhead = CGFloat(groupCount) * (headerH + groupSpacing) 79 let contentHeight = availableHeight - headerOverhead 80 81 guard contentHeight > 0 else { 82 return (CGSize(width: minThumbnailWidth, height: minThumbnailWidth * 0.625 + titleHeight), 1) 83 } 84 85 func totalRows(cols: Int) -> Int { 86 groupWindowCounts.reduce(0) { $0 + Int(ceil(Double($1) / Double(cols))) } 87 } 88 89 var bestWidth: CGFloat = 0 90 var bestHeight: CGFloat = 0 91 var bestCols = 1 92 93 let maxCols = max(1, Int((availableWidth + itemSpacing) / (minThumbnailWidth + itemSpacing))) 94 95 for cols in 1...maxCols { 96 let thumbWidth = min(maxThumbnailWidth, (availableWidth - CGFloat(cols - 1) * itemSpacing) / CGFloat(cols)) 97 guard thumbWidth >= minThumbnailWidth else { continue } 98 let thumbHeight = thumbWidth * 0.625 + titleHeight 99 let rows = totalRows(cols: cols) 100 let neededHeight = CGFloat(rows) * thumbHeight + CGFloat(max(0, rows - 1)) * itemSpacing 101 102 if neededHeight <= contentHeight && thumbWidth > bestWidth { 103 bestWidth = thumbWidth 104 bestHeight = thumbHeight 105 bestCols = cols 106 } 107 } 108 109 if bestWidth == 0 { 110 var lo: CGFloat = 20 111 var hi = min(maxThumbnailWidth, availableWidth) 112 113 for _ in 0..<40 { 114 let mid = (lo + hi) / 2 115 let cols = max(1, Int((availableWidth + itemSpacing) / (mid + itemSpacing))) 116 let thumbHeight = mid * 0.625 + titleHeight 117 let rows = totalRows(cols: cols) 118 let neededHeight = CGFloat(rows) * thumbHeight + CGFloat(max(0, rows - 1)) * itemSpacing 119 120 if neededHeight <= contentHeight { 121 bestWidth = mid 122 bestHeight = thumbHeight 123 bestCols = cols 124 lo = mid 125 } else { 126 hi = mid 127 } 128 } 129 130 if bestWidth == 0 { 131 bestCols = maxCols 132 bestWidth = max(20, (availableWidth - CGFloat(bestCols - 1) * itemSpacing) / CGFloat(bestCols)) 133 bestHeight = bestWidth * 0.625 + titleHeight 134 } 135 } 136 137 return (CGSize(width: bestWidth, height: bestHeight), bestCols) 138 } 139} 140 141// Simplified layout engine (no stability/reuse) 142 143struct GroupLayout { 144 let columns: Int 145 let thumbnailSize: CGSize 146 let itemRange: Range<Int> 147} 148 149struct LayoutItem { 150 let title: String 151 let appName: String 152 let frame: CGRect 153 let groupIndex: Int 154} 155 156struct LayoutGroupHeader { 157 let appName: String 158 let frame: CGRect 159} 160 161struct LayoutResult { 162 let items: [LayoutItem] 163 let headers: [LayoutGroupHeader] 164 let groupLayouts: [GroupLayout] 165 let totalHeight: CGFloat 166 let isCompactHeaders: Bool 167} 168 169func computeLayout(snapshot: WindowSnapshot, screenBounds: CGRect) -> LayoutResult { 170 let availableWidth = screenBounds.width - LayoutGeometry.margin * 2 171 let availableHeight = screenBounds.height - LayoutGeometry.margin * 2 172 let groupCount = snapshot.groups.count 173 let isCompact = groupCount > LayoutGeometry.compactGroupThreshold 174 let headerH = LayoutGeometry.effectiveHeaderHeight(groupCount: groupCount) 175 176 let groupWindowCounts = snapshot.groups.map { $0.windows.count } 177 let (thumbSize, cols) = LayoutGeometry.uniformThumbnailSize( 178 groupWindowCounts: groupWindowCounts, 179 availableWidth: availableWidth, 180 availableHeight: availableHeight 181 ) 182 183 var items: [LayoutItem] = [] 184 var headers: [LayoutGroupHeader] = [] 185 var groupLayouts: [GroupLayout] = [] 186 187 var y = screenBounds.origin.y + LayoutGeometry.margin 188 189 for (groupIndex, group) in snapshot.groups.enumerated() { 190 let windowsInRow = min(cols, group.windows.count) 191 let gridWidth = CGFloat(windowsInRow) * thumbSize.width + CGFloat(max(0, windowsInRow - 1)) * LayoutGeometry.itemSpacing 192 let xOffset = screenBounds.origin.x + LayoutGeometry.margin + (availableWidth - gridWidth) / 2 193 194 let headerFrame = CGRect( 195 x: screenBounds.origin.x + LayoutGeometry.margin, 196 y: y, 197 width: availableWidth, 198 height: headerH 199 ) 200 headers.append(LayoutGroupHeader(appName: group.appName, frame: headerFrame)) 201 y += headerH 202 203 let itemStart = items.count 204 205 for (windowIndex, window) in group.windows.enumerated() { 206 let col = windowIndex % cols 207 let row = windowIndex / cols 208 209 let x = xOffset + CGFloat(col) * (thumbSize.width + LayoutGeometry.itemSpacing) 210 let itemY = y + CGFloat(row) * (thumbSize.height + LayoutGeometry.itemSpacing) 211 212 let frame = CGRect(x: x, y: itemY, width: thumbSize.width, height: thumbSize.height) 213 items.append(LayoutItem(title: window.title, appName: window.appName, frame: frame, groupIndex: groupIndex)) 214 } 215 216 let rowsInGroup = Int(ceil(Double(group.windows.count) / Double(cols))) 217 let groupContentHeight = CGFloat(rowsInGroup) * thumbSize.height + CGFloat(max(0, rowsInGroup - 1)) * LayoutGeometry.itemSpacing 218 219 groupLayouts.append(GroupLayout( 220 columns: cols, 221 thumbnailSize: thumbSize, 222 itemRange: itemStart..<items.count 223 )) 224 225 y += groupContentHeight + LayoutGeometry.groupSpacing 226 } 227 228 let totalHeight = y - screenBounds.origin.y 229 return LayoutResult(items: items, headers: headers, groupLayouts: groupLayouts, totalHeight: totalHeight, isCompactHeaders: isCompact) 230} 231 232// Test scenarios 233 234func makeWindow(id: Int, title: String, appName: String, bundleID: String) -> WindowInfo { 235 WindowInfo( 236 windowID: CGWindowID(id), 237 title: title, 238 appName: appName, 239 appBundleID: bundleID, 240 frame: CGRect(x: 0, y: 0, width: 800, height: 600), 241 pid: pid_t(id), 242 isOnScreen: true 243 ) 244} 245 246func groupWindows(_ windows: [WindowInfo]) -> [WindowGroup] { 247 var dict: [String: WindowGroup] = [:] 248 for (i, w) in windows.enumerated() { 249 if dict[w.appBundleID] == nil { 250 dict[w.appBundleID] = WindowGroup(appBundleID: w.appBundleID, appName: w.appName, windows: []) 251 } 252 dict[w.appBundleID]!.windows.append(w) 253 dict[w.appBundleID]!.frontmostIndex = min(dict[w.appBundleID]!.frontmostIndex, i) 254 } 255 return dict.values.sorted { $0.frontmostIndex < $1.frontmostIndex } 256} 257 258func printResult(_ result: LayoutResult, screenBounds: CGRect, label: String) { 259 print("═══════════════════════════════════════════════════════════════") 260 print("TEST: \(label)") 261 print("Screen: \(Int(screenBounds.width))×\(Int(screenBounds.height))") 262 print("Total height used: \(Int(result.totalHeight)) / \(Int(screenBounds.height))") 263 print("Compact headers: \(result.isCompactHeaders)") 264 print("") 265 266 var allFit = true 267 268 for (gi, gl) in result.groupLayouts.enumerated() { 269 let header = result.headers[gi] 270 print(" Group \(gi): \(header.appName)") 271 print(" Header at y=\(Int(header.frame.minY)), h=\(Int(header.frame.height))") 272 print(" Thumbnail: \(Int(gl.thumbnailSize.width))×\(Int(gl.thumbnailSize.height)), cols=\(gl.columns)") 273 274 for idx in gl.itemRange { 275 let item = result.items[idx] 276 let f = item.frame 277 let withinScreen = f.minX >= screenBounds.minX && f.minY >= screenBounds.minY && 278 f.maxX <= screenBounds.maxX && f.maxY <= screenBounds.maxY 279 let marker = withinScreen ? "" : "✗ OFF-SCREEN" 280 if !withinScreen { allFit = false } 281 print(" [\(idx)] \"\(item.title)\" @ (\(Int(f.minX)),\(Int(f.minY))) \(Int(f.width))×\(Int(f.height)) \(marker)") 282 } 283 } 284 285 // Check uniform sizing within each group 286 for (gi, gl) in result.groupLayouts.enumerated() { 287 let sizes = Set(gl.itemRange.map { idx -> String in 288 let f = result.items[idx].frame 289 return "\(Int(f.width))×\(Int(f.height))" 290 }) 291 if sizes.count > 1 { 292 print(" ⚠️ Group \(gi) has NON-UNIFORM sizes: \(sizes)") 293 } 294 } 295 296 // Check sizes are consistent across groups (ideally similar or justified) 297 let allSizes = result.groupLayouts.map { "\(Int($0.thumbnailSize.width))×\(Int($0.thumbnailSize.height))" } 298 let uniqueSizes = Set(allSizes) 299 if uniqueSizes.count > 1 { 300 print("\n ℹ️ Different thumbnail sizes across groups: \(uniqueSizes)") 301 let widths = result.groupLayouts.map { $0.thumbnailSize.width } 302 let ratio = (widths.max() ?? 1) / max(widths.min() ?? 1, 1) 303 if ratio > 3.0 { 304 print(" ⚠️ Size ratio \(String(format: "%.1f", ratio))x between largest and smallest — may look unbalanced") 305 } 306 } 307 308 print("\n All items on screen: \(allFit ? "✓ YES" : "✗ NO")") 309 print("") 310} 311 312// Run tests 313 314let screen1080 = CGRect(x: 0, y: 0, width: 1920, height: 1080) 315let screen1440 = CGRect(x: 0, y: 0, width: 2560, height: 1440) 316let screenLaptop = CGRect(x: 0, y: 0, width: 1470, height: 956) // 14" MacBook Pro effective 317 318// Test 1: Few windows, few apps 319do { 320 let windows = [ 321 makeWindow(id: 1, title: "Document", appName: "Safari", bundleID: "com.apple.Safari"), 322 makeWindow(id: 2, title: "Gmail", appName: "Safari", bundleID: "com.apple.Safari"), 323 makeWindow(id: 3, title: "Main.swift", appName: "Xcode", bundleID: "com.apple.dt.Xcode"), 324 ] 325 let groups = groupWindows(windows) 326 let snapshot = WindowSnapshot(groups: groups) 327 let result = computeLayout(snapshot: snapshot, screenBounds: screen1080) 328 printResult(result, screenBounds: screen1080, label: "3 windows, 2 apps — 1080p") 329} 330 331// Test 2: Many windows, many apps 332do { 333 var windows: [WindowInfo] = [] 334 var id = 1 335 for app in ["Safari", "Xcode", "Terminal", "Finder", "Slack", "Discord", "Notes", "Mail"] { 336 let bundleID = "com.test.\(app.lowercased())" 337 let count = app == "Safari" ? 5 : (app == "Xcode" ? 3 : (app == "Terminal" ? 4 : 1)) 338 for w in 0..<count { 339 windows.append(makeWindow(id: id, title: "\(app) Window \(w+1)", appName: app, bundleID: bundleID)) 340 id += 1 341 } 342 } 343 let groups = groupWindows(windows) 344 let snapshot = WindowSnapshot(groups: groups) 345 let result = computeLayout(snapshot: snapshot, screenBounds: screen1080) 346 printResult(result, screenBounds: screen1080, label: "\(windows.count) windows, 8 apps — 1080p") 347} 348 349// Test 3: Lots of windows on laptop screen 350do { 351 var windows: [WindowInfo] = [] 352 var id = 1 353 for app in ["Safari", "Chrome", "Xcode", "Terminal", "Finder", "Slack", "Discord", "Notes", "Mail", "Calendar"] { 354 let bundleID = "com.test.\(app.lowercased())" 355 let count = app == "Safari" ? 6 : (app == "Chrome" ? 4 : (app == "Xcode" ? 3 : 1)) 356 for w in 0..<count { 357 windows.append(makeWindow(id: id, title: "\(app) Window \(w+1)", appName: app, bundleID: bundleID)) 358 id += 1 359 } 360 } 361 let groups = groupWindows(windows) 362 let snapshot = WindowSnapshot(groups: groups) 363 let result = computeLayout(snapshot: snapshot, screenBounds: screenLaptop) 364 printResult(result, screenBounds: screenLaptop, label: "\(windows.count) windows, 10 apps — Laptop (1470×956)") 365} 366 367// Test 4: Single app, many windows 368do { 369 let windows = (1...12).map { i in 370 makeWindow(id: i, title: "Tab \(i)", appName: "Safari", bundleID: "com.apple.Safari") 371 } 372 let groups = groupWindows(windows) 373 let snapshot = WindowSnapshot(groups: groups) 374 let result = computeLayout(snapshot: snapshot, screenBounds: screen1080) 375 printResult(result, screenBounds: screen1080, label: "12 windows, 1 app — 1080p") 376} 377 378// Test 5: Each app has exactly 1 window 379do { 380 let apps = ["Safari", "Xcode", "Terminal", "Finder", "Slack", "Discord", "Notes", "Mail", "Calendar", "Reminders", "Music", "Photos"] 381 let windows = apps.enumerated().map { (i, app) in 382 makeWindow(id: i + 1, title: "Main", appName: app, bundleID: "com.test.\(app.lowercased())") 383 } 384 let groups = groupWindows(windows) 385 let snapshot = WindowSnapshot(groups: groups) 386 let result = computeLayout(snapshot: snapshot, screenBounds: screenLaptop) 387 printResult(result, screenBounds: screenLaptop, label: "12 windows, 12 apps (1 each) — Laptop") 388} 389 390print("═══════════════════════════════════════════════════════════════") 391print("DONE")