diff options
Diffstat (limited to 'menubar/main.swift')
| -rw-r--r-- | menubar/main.swift | 90 |
1 files changed, 58 insertions, 32 deletions
diff --git a/menubar/main.swift b/menubar/main.swift index 602b5cc..5fb0ab6 100644 --- a/menubar/main.swift +++ b/menubar/main.swift @@ -11,6 +11,7 @@ // // Debug flags (used by tests/e2e.sh): // --print print the title and rows the menu would show, then exit +// (rows: "<symbol> <status>\t<title>\t<subtitle>") // --click <pane> perform a row click for <pane>, then exit import AppKit @@ -70,9 +71,41 @@ enum Status: String { struct Row { let paneId: String let agent: String + /// What the agent works on (its session/task name), if it has one. + let name: String let status: Status - let target: String + let session: String + let window: String let since: Int + + var location: String { "\(session) · \(window)" } + var waiting: Bool { status == .blocked || status == .done } + + /// The row's title text: the agent's name, or its harness when unnamed. + var label: String { + guard !name.isEmpty else { return agent } + return name.count > 80 ? String(name.prefix(79)) + "…" : name + } + + /// Dim second line: where it runs, which harness, and how long it waits. + var subtitle: String { + var parts = [location, agent] + if waiting, since != Int.max { + parts.append("\(status.rawValue) \(elapsed(sinceMs: since))") + } + return parts.joined(separator: " · ") + } +} + +/// Compact age: "now", "4m", "2h", "3d". +func elapsed(sinceMs: Int) -> String { + let seconds = max(0, Int(Date().timeIntervalSince1970) - sinceMs / 1000) + switch seconds { + case ..<60: return "now" + case ..<3600: return "\(seconds / 60)m" + case ..<86400: return "\(seconds / 3600)h" + default: return "\(seconds / 86400)d" + } } // MARK: tmux and dingger access @@ -110,23 +143,23 @@ let dinggerBinary = environment["DINGGER_BIN"] ?? (NSHomeDirectory() + "/.local/ /// Every agent pane, in herdr's priority order, oldest waiting first. func fetchRows() -> [Row] { let format = [ - "#{pane_id}", "#{@agent}", "#{@agent_state}", "#{@dingger_since}", - "#{session_name}:#{window_name}", "#{window_panes}", "#{pane_index}", + "#{pane_id}", "#{@agent}", "#{@agent_state}", "#{@dingger_since}", "#{@agent_name}", + "#{session_name}", "#{window_name}", "#{window_panes}", "#{pane_index}", ].joined(separator: "\t") guard let out = tmux(["list-panes", "-a", "-F", format]) else { return [] } let rows: [Row] = out.split(separator: "\n").compactMap { line in let f = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init) - guard f.count >= 7, !f[1].isEmpty else { return nil } + guard f.count >= 9, !f[1].isEmpty else { return nil } let status = Status(rawValue: f[2]) ?? .unknown guard status != .unknown else { return nil } - let target = (Int(f[5]) ?? 1) > 1 ? "\(f[4]).\(f[6])" : f[4] - return Row(paneId: f[0], agent: f[1], status: status, target: target, - since: Int(f[3]) ?? Int.max) + let window = (Int(f[7]) ?? 1) > 1 ? "\(f[6]).\(f[8])" : f[6] + return Row(paneId: f[0], agent: f[1], name: f[4], status: status, + session: f[5], window: window, since: Int(f[3]) ?? Int.max) } return rows.sorted { if $0.status.priority != $1.status.priority { return $0.status.priority > $1.status.priority } if $0.since != $1.since { return $0.since < $1.since } - return $0.target < $1.target + return $0.location < $1.location } } @@ -165,21 +198,17 @@ func plainTitle(for rows: [Row]) -> String { title(for: rows).string } -func rowTitle(_ row: Row, tabStop: CGFloat) -> NSAttributedString { +/// Row title: the colored status symbol, then the agent's name. Unnamed +/// agents show their harness, dimmed, so it never competes with real names. +func rowTitle(_ row: Row) -> NSAttributedString { let font = NSFont.menuFont(ofSize: 0) - let mono = NSFont.monospacedSystemFont(ofSize: font.pointSize - 1, weight: .regular) - let paragraph = NSMutableParagraphStyle() - paragraph.tabStops = [NSTextTab(textAlignment: .right, location: tabStop)] + let mono = NSFont.monospacedSystemFont(ofSize: font.pointSize, weight: .medium) let text = NSMutableAttributedString() text.append(NSAttributedString( - string: "\(row.status.symbol) ", - attributes: [.font: mono, .foregroundColor: row.status.color, .paragraphStyle: paragraph])) - text.append(NSAttributedString( - string: row.agent, attributes: [.font: font, .paragraphStyle: paragraph])) + string: "\(row.status.symbol) ", attributes: [.font: mono, .foregroundColor: row.status.color])) + let labelColor = row.name.isEmpty ? NSColor.secondaryLabelColor : NSColor.labelColor text.append(NSAttributedString( - string: "\t\(row.target)", - attributes: [.font: mono, .foregroundColor: NSColor.secondaryLabelColor, - .paragraphStyle: paragraph])) + string: row.label, attributes: [.font: font, .foregroundColor: labelColor])) return text } @@ -221,19 +250,16 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { none.isEnabled = false menu.addItem(none) } - let font = NSFont.menuFont(ofSize: 0) - let mono = NSFont.monospacedSystemFont(ofSize: font.pointSize - 1, weight: .regular) - let widest = rows.map { row -> CGFloat in - ("\(row.status.symbol) " as NSString).size(withAttributes: [.font: mono]).width - + (row.agent as NSString).size(withAttributes: [.font: font]).width - + (row.target as NSString).size(withAttributes: [.font: mono]).width - }.max() ?? 0 - let tabStop = ceil(widest + 40) - for row in rows { - let entry = NSMenuItem(title: row.agent, action: #selector(rowClicked(_:)), keyEquivalent: "") + for (index, row) in rows.enumerated() { + // One separator between the agents waiting for you and the rest. + if index > 0, rows[index - 1].waiting, !row.waiting { + menu.addItem(.separator()) + } + let entry = NSMenuItem(title: row.label, action: #selector(rowClicked(_:)), keyEquivalent: "") entry.target = self - entry.attributedTitle = rowTitle(row, tabStop: tabStop) - entry.toolTip = "\(row.status.rawValue) · \(row.target)" + entry.attributedTitle = rowTitle(row) + entry.subtitle = row.subtitle + entry.toolTip = row.name.isEmpty ? nil : row.name entry.representedObject = row.paneId menu.addItem(entry) } @@ -256,7 +282,7 @@ if arguments.contains("--print") { let rows = fetchRows() print("title: \(plainTitle(for: rows))") for row in rows { - print("\(row.status.symbol) \(row.status.rawValue) \(row.agent) \(row.target)") + print("\(row.status.symbol) \(row.status.rawValue)\t\(row.label)\t\(row.subtitle)") } exit(0) } |
