Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menubar-swift: polygon icon — homogenize color across all edges, hue tracks awaiting ratio

Drops per-session identity hue. Geometry alone shows session count;
color is one shared health signal applied uniformly:

- hue slides green (0.35) → red (0.0) as awaiting/total climbs
- saturation rises with urgency
- brightness pulses harder the more urgent it is
- all-stale state still wins: rotation freezes, color goes uniform
gray, polygon blinks

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

+37 -54
+37 -54
slab/menubar-swift/Sources/SlabMenubar/IconRenderer.swift
··· 87 87 let visible = Array(sessions.prefix(maxSides)) 88 88 let n = visible.count 89 89 let overflow = sessions.count - n 90 - // When every session has ended (all stale), the polygon goes 91 - // homogeneous and blinks instead of carrying per-session colors. 92 - let allStale = !visible.isEmpty && visible.allSatisfy { $0.state == .stale } 93 90 94 91 let cx = pointsW / 2.0 95 92 let cy = pointsH / 2.0 ··· 98 95 let radius: CGFloat = 9.5 99 96 let lineWidth: CGFloat = 1.6 100 97 98 + // Color is homogenized across every edge — geometry shows count, 99 + // color shows aggregate health. 100 + let color = aggregateColor(state: state, phase: phase) 101 + 101 102 if n == 1 { 102 103 // Single horizontal line, rotated. 103 - let session = visible[0] 104 104 drawSegment( 105 105 center: NSPoint(x: cx, y: cy), 106 106 length: 2 * radius, 107 107 angle: rotation, 108 - color: colorFor(session: session, indexFromTop: 0, phase: phase, allStale: allStale), 108 + color: color, 109 109 lineWidth: lineWidth 110 110 ) 111 111 } else if n == 2 { ··· 117 117 let length: CGFloat = 17 118 118 let nx = -sin(rotation) 119 119 let ny = cos(rotation) 120 - for (i, session) in visible.enumerated() { 120 + for i in 0..<n { 121 121 let sign: CGFloat = (i == 0) ? 1 : -1 122 122 let center = NSPoint(x: cx + nx * half * sign, y: cy + ny * half * sign) 123 123 drawSegment( 124 124 center: center, 125 125 length: length, 126 126 angle: rotation, 127 - color: colorFor(session: session, indexFromTop: i, phase: phase, allStale: allStale), 127 + color: color, 128 128 lineWidth: lineWidth 129 129 ) 130 130 } 131 131 } else { 132 132 // Regular n-gon. Vertices offset by π/n so the midpoint of edge 133 133 // 0 sits at the top — ensures triangles point up, squares are 134 - // squares (not diamonds), etc. Edge i carries session[i]'s 135 - // color. 134 + // squares (not diamonds), etc. 136 135 var vertices: [NSPoint] = [] 137 136 vertices.reserveCapacity(n) 138 137 for k in 0..<n { ··· 141 140 + rotation 142 141 vertices.append(NSPoint(x: cx + radius * cos(theta), y: cy + radius * sin(theta))) 143 142 } 144 - for (i, session) in visible.enumerated() { 145 - let a = vertices[i] 146 - let b = vertices[(i + 1) % n] 147 - let path = NSBezierPath() 148 - path.move(to: a) 149 - path.line(to: b) 150 - path.lineWidth = lineWidth 151 - path.lineCapStyle = .round 152 - colorFor(session: session, indexFromTop: i, phase: phase, allStale: allStale).setStroke() 153 - path.stroke() 143 + color.setStroke() 144 + let path = NSBezierPath() 145 + path.lineWidth = lineWidth 146 + path.lineJoinStyle = .round 147 + path.move(to: vertices[0]) 148 + for k in 1..<n { 149 + path.line(to: vertices[k]) 154 150 } 151 + path.close() 152 + path.stroke() 155 153 } 156 154 157 155 if overflow > 0 { ··· 179 177 path.stroke() 180 178 } 181 179 182 - private static func colorFor(session: ClaudeSession, indexFromTop: Int, phase: CGFloat, allStale: Bool) -> NSColor { 183 - // Hue is anchored to the session's identity — same session keeps 184 - // the same color across refreshes and across state changes, so the 185 - // ring is readable: "the orange edge that was awaiting is now 186 - // working" instead of "everything just got reshuffled." 187 - let hue = stableHue(for: session.sessionId) 188 - switch session.state { 189 - case .awaiting: 190 - // Steady hue, pulsing brightness — reads as urgent without 191 - // losing identity. 192 - let pulse = 0.55 + 0.45 * (0.5 + 0.5 * cos(phase * .pi * 4)) 193 - return NSColor(deviceHue: hue, saturation: 0.95, brightness: pulse, alpha: 1.0) 194 - case .working: 195 - // Same hue, calm and steady. 196 - return NSColor(deviceHue: hue, saturation: 0.55, brightness: 0.85, alpha: 1.0) 197 - case .stale: 198 - if allStale { 199 - // Every session is over — homogenize color across all 200 - // edges and blink so the icon reads as "done, attention 201 - // optional" rather than just sitting there gray. 202 - let blink = 0.5 + 0.5 * cos(phase * .pi * 2) 203 - return NSColor(deviceWhite: 0.30 + 0.45 * blink, alpha: 1.0) 204 - } 205 - return NSColor(deviceWhite: 0.45, alpha: 1.0) 206 - } 207 - } 208 - 209 - /// Deterministic FNV-1a hash → hue in [0, 1). Stable across launches 210 - /// for a given session id. 211 - private static func stableHue(for sessionId: String) -> CGFloat { 212 - var h: UInt64 = 0xcbf29ce484222325 213 - for byte in sessionId.utf8 { 214 - h ^= UInt64(byte) 215 - h = h &* 0x100000001b3 180 + /// One color for the whole polygon, derived from aggregate state. Hue 181 + /// slides green → red as the awaiting ratio climbs (overall health), 182 + /// brightness pulses harder the more urgent it gets, and an all-stale 183 + /// shape blinks gray to signal "done, sessions still on disk." 184 + private static func aggregateColor(state: StateSnapshot, phase: CGFloat) -> NSColor { 185 + let sessions = state.claudeSessions 186 + let allStale = !sessions.isEmpty && sessions.allSatisfy { $0.state == .stale } 187 + if allStale { 188 + let blink = 0.5 + 0.5 * cos(phase * .pi * 2) 189 + return NSColor(deviceWhite: 0.30 + 0.45 * blink, alpha: 1.0) 216 190 } 217 - return CGFloat(h % 360) / 360.0 191 + let total = max(1, sessions.count) 192 + let urgency = CGFloat(state.awaitingCount) / CGFloat(total) 193 + // Health gradient: 0.35 (green) when calm, 0.0 (red) when fully awaiting. 194 + let hue = 0.35 * (1 - urgency) 195 + let saturation = 0.55 + 0.40 * urgency 196 + // Brightness pulses with depth proportional to urgency. At urgency=0 197 + // the shape is steady; at urgency=1 it pulses fully. 198 + let pulse = 0.5 + 0.5 * cos(phase * .pi * 4) 199 + let brightness = 0.85 - 0.35 * urgency * (1 - pulse) 200 + return NSColor(deviceHue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) 218 201 } 219 202 220 203 private static func fallbackImage() -> NSImage {