aboutsummaryrefslogtreecommitdiff
path: root/menubar/main.swift
diff options
context:
space:
mode:
Diffstat (limited to 'menubar/main.swift')
-rw-r--r--menubar/main.swift90
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)
}