diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-19 19:30:03 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-19 19:30:03 +0300 |
| commit | fed066bc3908d18ede5490130975203a56c48fc2 (patch) | |
| tree | 17ecccec1a1b5669e1868a0e169f263bdee53c73 /menubar | |
| parent | 7081a103443bafa28d902529d502a89c2dccb79a (diff) | |
menu bar: herdr-style colored state glyphs; tmux segment before the clock
the design panel's monochrome template rendering made every row look
alike; restore herdr's agent-panel visual language - green ready, yellow
working, red blocked, filled when unseen in the queue. the tmux segment
moves left of the clock so it is visible at a glance; the status item
logs what it observes for diagnosis.
Diffstat (limited to 'menubar')
| -rw-r--r-- | menubar/main.swift | 62 |
1 files changed, 41 insertions, 21 deletions
diff --git a/menubar/main.swift b/menubar/main.swift index b5a3059..6c6c9a3 100644 --- a/menubar/main.swift +++ b/menubar/main.swift @@ -1,10 +1,9 @@ // dingger menu bar: live agent states from tmux pane user options // (@agent / @agent_state), written by the dingger daemon. // -// Design (per design panel): one question answered — "how many agents are -// waiting on me?" The status item is a bell plus the ready-queue depth; -// shape carries state (filled = needs you, outline = seen/calm, dotted = -// working); color is reserved for the tmux segment's blocked accent. +// Visual language is herdr's agent panel: green = ready (finished, waiting +// for you), yellow = working, red = blocked; filled glyph = in the ready +// queue (unseen), outline = already seen. Priority order = queue order. import AppKit @@ -15,18 +14,37 @@ struct PaneRow { let state: String } -func symbol(_ name: String) -> NSImage? { - let image = NSImage(systemSymbolName: name, accessibilityDescription: nil) - return image?.withSymbolConfiguration(.init(pointSize: 13, weight: .regular)) +let red = NSColor.systemRed +let yellow = NSColor.systemYellow +let green = NSColor.systemGreen +let dim = NSColor.systemGray + +/// Tint a symbol image (non-template, so it keeps its color in menus). +func tinted(_ image: NSImage?, _ color: NSColor) -> NSImage? { + guard let image = image else { return nil } + let img = NSImage(size: image.size, flipped: false) { rect in + image.draw(in: rect) + color.set() + rect.fill(using: .sourceAtop) + return true + } + img.isTemplate = false + return img +} + +func symbol(_ name: String, _ color: NSColor? = nil) -> NSImage? { + let image = NSImage(systemSymbolName: name, accessibilityDescription: nil)? + .withSymbolConfiguration(.init(pointSize: 13, weight: .regular)) + return color.map { tinted(image, $0) } ?? image } -func stateSymbol(_ state: String, queued: Bool) -> NSImage? { +func rowSymbol(_ state: String, queued: Bool) -> NSImage? { switch (state, queued) { - case ("blocked", true): return symbol("exclamationmark.circle.fill") - case ("idle", true): return symbol("circle.fill") - case ("working", _): return symbol("circle.dotted") ?? symbol("hourglass") - case ("blocked", _): return symbol("exclamationmark.circle") - default: return symbol("circle") + case ("blocked", true): return symbol("exclamationmark.circle.fill", red) + case ("idle", true): return symbol("checkmark.circle.fill", green) + case ("working", _): return symbol("circle.dotted", yellow) + case ("blocked", _): return symbol("exclamationmark.circle", dim) + default: return symbol("checkmark.circle", dim) } } @@ -38,7 +56,7 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { func applicationDidFinishLaunching(_: Notification) { NSApp.setActivationPolicy(.accessory) // no Dock icon - item.button?.image = symbol("bell") + item.button?.image = symbol("bell", dim) item.button?.font = NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular) item.menu = menu menu.delegate = self @@ -102,21 +120,23 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { private func refreshTitle() { panes = fetchPanes() queue = fetchQueue() + FileHandle.standardError.write(Data( + "\(Date()) menubar: panes=\(panes.count) queue=\(queue.count)\n".utf8)) let queued = panes.filter { row in queue.contains(row.paneId) } let blocked = queued.filter { $0.state == "blocked" }.count let working = panes.filter { $0.state == "working" }.count - let (name, count): (String, String) + let (name, color, count): (String, NSColor, String) if blocked > 0 { - (name, count) = ("bell.fill", String(queued.count)) + (name, color, count) = ("bell.fill", red, String(queued.count)) } else if !queued.isEmpty { - (name, count) = ("bell", String(queued.count)) + (name, color, count) = ("bell.fill", green, String(queued.count)) } else if working > 0 { - (name, count) = ("bell", "") + (name, color, count) = ("bell", yellow, "") } else { - (name, count) = ("bell", "") + (name, color, count) = ("bell", dim, "") } - item.button?.image = symbol(name) + item.button?.image = symbol(name, color) item.button?.title = count var tip: [String] = [] @@ -169,7 +189,7 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { let row = NSMenuItem(title: "\(pane.agent)\t\(pane.target)", action: #selector(paneClicked(_:)), keyEquivalent: "") row.target = self - row.image = stateSymbol(pane.state, queued: queued) + row.image = rowSymbol(pane.state, queued: queued) row.attributedTitle = attributedRow(agent: pane.agent, target: pane.target) row.toolTip = "\(pane.state) — \(pane.target)" row.representedObject = pane.paneId |
