Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menubar: theme by status sets full palette (text + bold + cursor), not just bg

Per-state coordinated palette so each window shifts as one tone instead of
new bg fighting Pro's default light text. ANSI table colors stay off
(would require profile swap → font/window flicker); per-tab knobs
(background / normal text / bold text / cursor) cover what Claude renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+110 -26
+110 -26
slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift
··· 349 349 /// pass (state flipped, subject moved, or new session appeared). 350 350 private func applyTerminalDecor() { 351 351 guard state.themeByStatus else { return } 352 - // {R, G, B} in AppleScript color space (each component 0–65535). 353 - // Picked to read at a glance on dark monospace text without 354 - // requiring a profile swap. We avoid `set current settings` because 355 - // applying a profile resets the tab's font + the window's pixel 356 - // size to the profile default, causing a brief reframe-flicker 357 - // even when we save/restore around it. 358 - struct Assignment { let tty: String; let bg: (Int, Int, Int)?; let title: String; let fontSize: Int? } 352 + // Each state ships a coordinated palette (bg + normal text + bold 353 + // text + cursor) so the whole window shifts as one tone — not a 354 + // new background fighting Pro's default light text. ANSI table 355 + // colors live on the profile (`settings set`) and applying a 356 + // profile resets font + window size, so we touch only the per-tab 357 + // knobs AppleScript exposes directly. RGB triples are AppleScript 358 + // colorspace 0–65535. 359 + typealias RGB = (Int, Int, Int) 360 + struct Palette { let bg: RGB?; let text: RGB?; let bold: RGB?; let cursor: RGB? } 361 + struct Assignment { let tty: String; let palette: Palette; let title: String; let fontSize: Int? } 359 362 var changes: [Assignment] = [] 360 363 var seen = Set<String>() 361 364 // Typographic gradient by state — green smallest (calm), red largest ··· 374 377 let darkAppearance = Self.isDarkAppearance() 375 378 for s in state.claudeSessions where !s.tty.isEmpty { 376 379 seen.insert(s.sessionId) 377 - let bg: (Int, Int, Int)? 380 + let palette: Palette 378 381 let glyph: String 379 382 let fontSize: Int? 380 383 switch s.state { 381 384 // Blank = pure macOS appearance (white in light mode, black in 382 385 // dark mode) so a fresh window reads as a blank page until the 383 - // first prompt fires. No glyph, no title chrome — wholly clean. 386 + // first prompt fires. Text/cursor flip with appearance so 387 + // default ANSI output stays legible on either page color. 384 388 case .blank: 385 - bg = darkAppearance ? (0, 0, 0) : (65535, 65535, 65535) 389 + if darkAppearance { 390 + palette = Palette( 391 + bg: (0, 0, 0), 392 + text: (60000, 60000, 60000), 393 + bold: (65535, 65535, 65535), 394 + cursor: (50000, 50000, 50000)) 395 + } else { 396 + palette = Palette( 397 + bg: (65535, 65535, 65535), 398 + text: (8000, 8000, 8000), 399 + bold: (0, 0, 0), 400 + cursor: (20000, 20000, 20000)) 401 + } 386 402 glyph = "" 387 403 fontSize = sizeFor(1.00) 388 - // Working = green (active/healthy), complete = calm slate 389 - // (turn done, idle), awaiting = amber (needs you to continue), 390 - // stale = deep red (process dead, escalate). RGBs are 0–65535 391 - // AppleScript colorspace, dark enough that the profile's default 392 - // light text still reads on top. 393 - case .working: bg = (1500, 14000, 4000); glyph = "● working"; fontSize = sizeFor(1.00) // green — smallest 394 - case .complete: bg = (5000, 7000, 12000); glyph = "✓ complete"; fontSize = sizeFor(1.10) // slate — between green and orange 395 - case .awaiting: bg = (32000, 18000, 1500); glyph = "◉ awaiting"; fontSize = sizeFor(1.25) // orange — focus pop 396 - case .stale: bg = (30000, 2500, 4000); glyph = "○ stale"; fontSize = sizeFor(1.45) // red — largest, escalate 404 + // Working = green (active/healthy): pale mint text on dark forest. 405 + case .working: 406 + palette = Palette( 407 + bg: (1500, 14000, 4000), 408 + text: (42000, 60000, 46000), 409 + bold: (55000, 65535, 58000), 410 + cursor: (22000, 55000, 32000)) 411 + glyph = "● working" 412 + fontSize = sizeFor(1.00) 413 + // Complete = slate (turn done, calm "look when ready"): 414 + // pale lavender text on deep slate. 415 + case .complete: 416 + palette = Palette( 417 + bg: (5000, 7000, 12000), 418 + text: (46000, 50000, 60000), 419 + bold: (58000, 60000, 65535), 420 + cursor: (30000, 40000, 55000)) 421 + glyph = "✓ complete" 422 + fontSize = sizeFor(1.10) 423 + // Awaiting = warm amber (needs input, focus pop): cream text on 424 + // amber so the warm tone reads coherently rather than fighting 425 + // the default light gray. 426 + case .awaiting: 427 + palette = Palette( 428 + bg: (32000, 18000, 1500), 429 + text: (65535, 58000, 38000), 430 + bold: (65535, 65535, 50000), 431 + cursor: (65535, 45000, 8000)) 432 + glyph = "◉ awaiting" 433 + fontSize = sizeFor(1.25) 434 + // Stale = deep red (process dead, escalate): pale rose text on 435 + // deep red — readable but unmistakably alert. 436 + case .stale: 437 + palette = Palette( 438 + bg: (30000, 2500, 4000), 439 + text: (65535, 42000, 42000), 440 + bold: (65535, 55000, 55000), 441 + cursor: (65535, 18000, 18000)) 442 + glyph = "○ stale" 443 + fontSize = sizeFor(1.45) 397 444 } 398 445 // Blank windows get an empty custom title so Terminal shows just 399 446 // its default tty/process line — no "● working · …" badge while 400 447 // the page is meant to look blank. 401 448 let title = (s.state == .blank) ? "" : "\(glyph) · \(s.titleString)" 402 - let bgKey = bg.map { "\($0.0),\($0.1),\($0.2)" } ?? "-" 403 - let key = "\(bgKey)|\(title)|\(fontSize.map(String.init) ?? "-")" 449 + func keyOf(_ c: RGB?) -> String { c.map { "\($0.0),\($0.1),\($0.2)" } ?? "-" } 450 + let key = [ 451 + keyOf(palette.bg), 452 + keyOf(palette.text), 453 + keyOf(palette.bold), 454 + keyOf(palette.cursor), 455 + title, 456 + fontSize.map(String.init) ?? "-", 457 + ].joined(separator: "|") 404 458 if lastTerminalDecor[s.sessionId] == key { continue } 405 459 lastTerminalDecor[s.sessionId] = key 406 - changes.append(Assignment(tty: s.tty, bg: bg, title: title, fontSize: fontSize)) 460 + changes.append(Assignment(tty: s.tty, palette: palette, title: title, fontSize: fontSize)) 407 461 } 408 462 // Reap entries for sessions that disappeared since last tick — they 409 463 // either died or got reaped by the janitor; either way our memo is ··· 414 468 if changes.isEmpty { return } 415 469 416 470 // One osascript pass that walks every window×tab once and applies 417 - // every change in this batch. We only touch `background color` and 418 - // `custom title` — neither resets font or window size, so there's 471 + // every change in this batch. We only touch the per-tab color knobs 472 + // and `custom title` — none reset font or window size, so there's 419 473 // nothing to save/restore and nothing to flicker. 420 474 var lines = [ 421 475 "tell application \"Terminal\"", ··· 424 478 " try", 425 479 " set ttyName to tty of t", 426 480 ] 481 + func rgbStr(_ c: RGB) -> String { "{\(c.0), \(c.1), \(c.2)}" } 427 482 for a in changes { 428 483 let escTty = a.tty.replacingOccurrences(of: "\"", with: "\\\"") 429 484 let escTitle = a.title 430 485 .replacingOccurrences(of: "\\", with: "\\\\") 431 486 .replacingOccurrences(of: "\"", with: "\\\"") 432 487 lines.append(" if ttyName ends with \"\(escTty)\" then") 433 - if let bg = a.bg { 434 - lines.append(" set background color of t to {\(bg.0), \(bg.1), \(bg.2)}") 488 + if let bg = a.palette.bg { 489 + lines.append(" set background color of t to \(rgbStr(bg))") 490 + } 491 + if let text = a.palette.text { 492 + lines.append(" set normal text color of t to \(rgbStr(text))") 493 + } 494 + if let bold = a.palette.bold { 495 + lines.append(" set bold text color of t to \(rgbStr(bold))") 496 + } 497 + if let cursor = a.palette.cursor { 498 + lines.append(" set cursor color of t to \(rgbStr(cursor))") 435 499 } 436 500 lines.append(" set custom title of t to \"\(escTitle)\"") 437 501 // Font sets snap Terminal to the row/col grid, so save the window's ··· 652 716 let appearance = NSApp.effectiveAppearance 653 717 let match = appearance.bestMatch(from: [.darkAqua, .aqua]) 654 718 return match == .darkAqua 719 + } 720 + 721 + // MARK: - Background overlay (experimental) 722 + 723 + /// Toggle a translucent tint overlay over the focused Terminal window. 724 + /// Proof-of-concept: AppleScript can't set Terminal's own bg image or 725 + /// transparency, so Slab paints over the window via a borderless NSWindow 726 + /// tracked to its bounds. Click again on the same window to turn off. 727 + @objc func toggleBackgroundOverlay() { 728 + let dark = Self.isDarkAppearance() 729 + // Cool indigo tint dark mode, warm peach in light mode — visible on 730 + // both default profiles without obliterating the text underneath. 731 + let tint = dark 732 + ? NSColor(red: 0.30, green: 0.45, blue: 0.95, alpha: 1.0) 733 + : NSColor(red: 1.00, green: 0.80, blue: 0.50, alpha: 1.0) 734 + BackgroundOverlayController.shared.toggleFrontTerminal(tint: tint, alpha: 0.35) 735 + } 736 + 737 + @objc func clearBackgroundOverlays() { 738 + BackgroundOverlayController.shared.clearAll() 655 739 } 656 740 657 741 private func notify(title: String, subtitle: String?, body: String) {