aboutsummaryrefslogtreecommitdiff
path: root/menubar/panel.swift
diff options
context:
space:
mode:
Diffstat (limited to 'menubar/panel.swift')
-rw-r--r--menubar/panel.swift160
1 files changed, 83 insertions, 77 deletions
diff --git a/menubar/panel.swift b/menubar/panel.swift
index e6ff569..2b3cad2 100644
--- a/menubar/panel.swift
+++ b/menubar/panel.swift
@@ -2,10 +2,10 @@
//
// - Never takes focus: a non-activating panel, so typing stays in the
// terminal. Visible on every Space and over full-screen apps.
-// - Modes: off, on ("Always"), auto ("When agents are waiting"). The global
-// shortcut shows or hides it on top of the mode.
+// - On or off (the global shortcut and the menu toggle it; off stays off),
+// and while on: shown always, or only when agents are waiting.
// - Layout: compact (waiting agents in full, working and idle as one summary
-// line); in "Always" mode a toggle switches to the full list.
+// line); when shown always, a toggle switches to the full list.
// - Always dark: herdr's mocha palette with fixed colors on a dark tint, so
// text reads the same over any desktop.
@@ -13,52 +13,58 @@ import AppKit
// MARK: visibility (pure, tested by --selftest)
-enum PanelMode: String, CaseIterable {
- case off, on, auto
+/// What the panel shows while it is on.
+enum PanelPolicy: String, CaseIterable {
+ case always, whenWaiting
var label: String {
switch self {
- case .off: return "Off"
- case .on: return "Always"
- case .auto: return "When agents are waiting"
+ case .always: return "Always"
+ case .whenWaiting: return "Only When Agents Are Waiting"
}
}
}
+/// Two persistent settings and one fact:
+/// - enabled: the panel is on (the shortcut and the menu toggle it);
+/// - policy: while on, show it always or only when agents are waiting;
+/// - waiting: whether any agent is blocked or done.
+/// Off stays off: nothing shows the panel until it is turned back on.
struct PanelVisibility: Equatable {
- var mode: PanelMode = .auto
- /// Set by the shortcut: true = shown, false = hidden, nil = follow mode.
- var forced: Bool?
- /// Pane ids of agents currently blocked or done.
- var waiting: Set<String> = []
-
- var visible: Bool {
- forced ?? (mode == .on || (mode == .auto && !waiting.isEmpty))
+ static let peekSeconds: TimeInterval = 3
+
+ var enabled = true
+ var policy: PanelPolicy = .whenWaiting
+ var anyWaiting = false
+ /// Turning the panel on with nothing to show shows it briefly, so the
+ /// shortcut visibly did something.
+ var peekUntil: Date?
+
+ func visible(at now: Date) -> Bool {
+ guard enabled else { return false }
+ if policy == .always || anyWaiting { return true }
+ return peekUntil.map { now < $0 } ?? false
}
- /// The shortcut: flip what is on screen right now.
- mutating func toggle() {
- forced = !visible
+ mutating func toggle(at now: Date) {
+ enabled.toggle()
+ peekUntil = enabled ? now.addingTimeInterval(Self.peekSeconds) : nil
}
- mutating func setMode(_ newMode: PanelMode) {
- mode = newMode
- forced = nil
- }
-
- /// A newly waiting agent cancels a manual hide (it needs you), except in
- /// mode off, where nothing shows unless asked for.
- mutating func update(waiting newWaiting: Set<String>) {
- if forced == false, !newWaiting.subtracting(waiting).isEmpty {
- forced = nil
+ /// Settings from before the on/off split: a three-way mode.
+ static func migrated(fromMode mode: String) -> PanelVisibility {
+ switch mode {
+ case "off": return PanelVisibility(enabled: false, policy: .whenWaiting)
+ case "on": return PanelVisibility(enabled: true, policy: .always)
+ default: return PanelVisibility(enabled: true, policy: .whenWaiting)
}
- waiting = newWaiting
}
}
-/// The full list is offered only in "Always" mode; otherwise compact.
-func panelShowsFull(mode: PanelMode, prefersFull: Bool) -> Bool {
- mode == .on && prefersFull
+/// The full list is offered only when the panel shows always; otherwise
+/// compact.
+func panelShowsFull(policy: PanelPolicy, prefersFull: Bool) -> Bool {
+ policy == .always && prefersFull
}
// MARK: content (pure, printed by --print-panel)
@@ -75,7 +81,7 @@ struct PanelContent {
let working: Int
let idle: Int
let full: Bool
- /// Whether the panel offers the compact/full toggle ("Always" mode).
+ /// Whether the panel offers the compact/full toggle (shown always).
let canToggle: Bool
init(rows all: [Row], full: Bool = false, canToggle: Bool = false) {
@@ -451,49 +457,49 @@ func panelSelftest() -> [String] {
if !condition { failures.append(message) }
}
- var auto = PanelVisibility(mode: .auto)
- check(!auto.visible, "auto: hidden with nothing waiting")
- auto.update(waiting: ["a"])
- check(auto.visible, "auto: shown when an agent waits")
- auto.toggle()
- check(!auto.visible, "auto: shortcut hides")
- auto.update(waiting: ["a"])
- check(!auto.visible, "auto: stays hidden for the same waiting agents")
- auto.update(waiting: ["a", "b"])
- check(auto.visible, "auto: a newly waiting agent shows it again")
- auto.update(waiting: [])
- check(!auto.visible, "auto: hides when nothing waits")
- auto.toggle()
- check(auto.visible, "auto: shortcut shows it with nothing waiting")
- auto.update(waiting: [])
- check(auto.visible, "auto: a manual show stays")
- auto.toggle()
- check(!auto.visible, "auto: shortcut hides again")
-
- var off = PanelVisibility(mode: .off)
- off.update(waiting: ["a"])
- check(!off.visible, "off: hidden even when an agent waits")
- off.toggle()
- check(off.visible, "off: shortcut shows")
- off.toggle()
- off.update(waiting: ["a", "b"])
- check(!off.visible, "off: a new waiting agent does not show it")
-
- var on = PanelVisibility(mode: .on)
- check(on.visible, "on: shown with nothing waiting")
- on.toggle()
- check(!on.visible, "on: shortcut hides")
- on.update(waiting: ["a"])
- check(on.visible, "on: a newly waiting agent shows it again")
-
- var changed = PanelVisibility(mode: .auto, forced: false, waiting: ["a"])
- changed.setMode(.on)
- check(changed.visible && changed.forced == nil, "mode change clears the shortcut override")
-
- check(!panelShowsFull(mode: .auto, prefersFull: true), "layout: compact when agents are waiting")
- check(!panelShowsFull(mode: .off, prefersFull: true), "layout: compact when shown by the shortcut in off")
- check(panelShowsFull(mode: .on, prefersFull: true), "layout: full in always when chosen")
- check(!panelShowsFull(mode: .on, prefersFull: false), "layout: compact in always by default")
+ let t0 = Date(timeIntervalSince1970: 1_000)
+ func at(_ seconds: TimeInterval) -> Date { t0.addingTimeInterval(seconds) }
+
+ var panel = PanelVisibility()
+ check(panel.enabled && panel.policy == .whenWaiting, "defaults: on, only when agents are waiting")
+ check(!panel.visible(at: t0), "waiting policy: hidden with nothing waiting")
+ panel.anyWaiting = true
+ check(panel.visible(at: t0), "waiting policy: shown when an agent waits")
+ panel.toggle(at: t0)
+ check(!panel.visible(at: t0), "off: the shortcut hides it")
+ panel.anyWaiting = false
+ panel.anyWaiting = true
+ check(!panel.visible(at: at(60)), "off: stays hidden when another agent starts waiting")
+ panel.toggle(at: at(60))
+ check(panel.visible(at: at(60)), "on again: shown, an agent is waiting")
+ panel.anyWaiting = false
+ check(!panel.visible(at: at(70)), "waiting policy: hides when nothing waits")
+
+ var peek = PanelVisibility(enabled: false, policy: .whenWaiting)
+ peek.toggle(at: t0)
+ check(peek.visible(at: at(1)), "turned on with nothing waiting: shown briefly")
+ check(!peek.visible(at: at(PanelVisibility.peekSeconds + 0.1)), "the brief show ends after the peek")
+ peek.toggle(at: at(10))
+ check(!peek.visible(at: at(10)) && peek.peekUntil == nil, "turned off: no peek")
+
+ var always = PanelVisibility(enabled: true, policy: .always)
+ check(always.visible(at: t0), "always: shown with nothing waiting")
+ always.toggle(at: t0)
+ always.anyWaiting = true
+ check(!always.visible(at: at(5)), "always, off: an agent waiting does not show it")
+ always.policy = .whenWaiting
+ check(!always.enabled, "changing what it shows keeps it off")
+
+ check(PanelVisibility.migrated(fromMode: "off") == PanelVisibility(enabled: false, policy: .whenWaiting),
+ "migration: off")
+ check(PanelVisibility.migrated(fromMode: "on") == PanelVisibility(enabled: true, policy: .always),
+ "migration: always")
+ check(PanelVisibility.migrated(fromMode: "auto") == PanelVisibility(enabled: true, policy: .whenWaiting),
+ "migration: when waiting")
+
+ check(!panelShowsFull(policy: .whenWaiting, prefersFull: true), "layout: compact when only waiting")
+ check(panelShowsFull(policy: .always, prefersFull: true), "layout: full in always when chosen")
+ check(!panelShowsFull(policy: .always, prefersFull: false), "layout: compact in always by default")
func row(_ id: String, _ status: Status) -> Row {
Row(paneId: id, agent: "pi", name: id, status: status, session: "s", window: id, since: 1)