aboutsummaryrefslogtreecommitdiff
path: root/menubar/main.swift
diff options
context:
space:
mode:
Diffstat (limited to 'menubar/main.swift')
-rw-r--r--menubar/main.swift108
1 files changed, 85 insertions, 23 deletions
diff --git a/menubar/main.swift b/menubar/main.swift
index e7532f6..e9294e8 100644
--- a/menubar/main.swift
+++ b/menubar/main.swift
@@ -14,7 +14,11 @@
// 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>")
-// --print-panel print what the floating panel would show, then exit
+// --print-panel [--full] [--toggle]
+// print what the floating panel would show, then exit
+// --snapshot-panel <png> [--full]
+// render the panel offscreen (after a different render, to
+// catch stale content) over white and black, then exit
// --click <pane> perform a row click for <pane>, then exit
// --selftest check the panel visibility rules, then exit
@@ -34,12 +38,17 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
private var settingsModified: Date?
private var hotkey: Hotkey?
private var hotkeyRegistered = false
+ private var jumpHotkey: Hotkey?
+ private var jumpHotkeyRegistered = false
+ private var prefersFull = UserDefaults.standard.bool(forKey: "panelFull")
private var rows: [Row] = []
func applicationDidFinishLaunching(_: Notification) {
menu.delegate = self
item.menu = menu
- panel = PanelController { [weak self] paneId in self?.jump(to: paneId) }
+ panel = PanelController(
+ onClick: { [weak self] paneId in self?.jump(to: paneId) },
+ onToggleFull: { [weak self] in self?.toggleFull() })
Settings.ensureFile()
reloadSettings()
refresh()
@@ -52,16 +61,24 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
private func reloadSettings() {
settingsModified = Settings.modified()
settings = Settings.load()
- guard let parsed = Hotkey.parse(settings.hotkey) else {
- GlobalHotkey.unregister()
- hotkey = nil
- hotkeyRegistered = false
- log("settings: invalid hotkey \"\(settings.hotkey)\"")
- return
+ (hotkey, hotkeyRegistered) = registerShortcut(.panel, settings.hotkey, "panel") { [weak self] in
+ self?.togglePanel()
+ }
+ (jumpHotkey, jumpHotkeyRegistered) = registerShortcut(.jump, settings.jumpHotkey, "jump") {
+ [weak self] in self?.jumpToTopWaiting()
}
- hotkey = parsed
- hotkeyRegistered = GlobalHotkey.register(parsed) { [weak self] in self?.togglePanel() }
- log("settings: hotkey \(parsed.display) \(hotkeyRegistered ? "registered" : "in use by another app")")
+ }
+
+ private func registerShortcut(_ id: GlobalHotkey.Id, _ spec: String, _ name: String,
+ action: @escaping () -> Void) -> (Hotkey?, Bool) {
+ guard let parsed = Hotkey.parse(spec) else {
+ GlobalHotkey.unregister(id)
+ log("settings: invalid \(name) shortcut \"\(spec)\"")
+ return (nil, false)
+ }
+ let registered = GlobalHotkey.register(id, parsed, action: action)
+ log("settings: \(name) shortcut \(parsed.display) \(registered ? "registered" : "in use by another app")")
+ return (parsed, registered)
}
// MARK: refresh
@@ -83,9 +100,18 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
applyPanel()
}
+ private var panelContent: PanelContent {
+ PanelContent(rows: rows,
+ full: panelShowsFull(mode: visibility.mode, prefersFull: prefersFull),
+ canToggle: visibility.mode == .on)
+ }
+
private func applyPanel() {
if visibility.visible {
- panel.render(PanelContent(rows: rows))
+ if panel.render(panelContent) {
+ let content = panelContent
+ log("panel: render \(content.rows.filter(\.waiting).count) waiting, \(content.full ? "full" : "compact")")
+ }
if !panel.isVisible {
panel.show()
log("panel: shown (\(visibility.mode.rawValue), forced: \(String(describing: visibility.forced)))")
@@ -101,6 +127,21 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
applyPanel()
}
+ private func toggleFull() {
+ prefersFull.toggle()
+ UserDefaults.standard.set(prefersFull, forKey: "panelFull")
+ applyPanel()
+ }
+
+ /// The jump shortcut: same as clicking the tmux status line.
+ @objc private func jumpToTopWaiting() {
+ DispatchQueue.global().async {
+ let output = run(dinggerBinary, ["focus"]) ?? "failed"
+ log("jump: dingger focus \(output.isEmpty ? "ok" : output)")
+ DispatchQueue.main.async { self.refresh() }
+ }
+ }
+
@objc private func selectMode(_ sender: NSMenuItem) {
guard let raw = sender.representedObject as? String, let mode = PanelMode(rawValue: raw) else { return }
visibility.setMode(mode)
@@ -154,6 +195,16 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
}
menu.addItem(toggle)
+ let jumpItem = NSMenuItem(title: "Jump to Waiting Agent", action: #selector(jumpToTopWaiting),
+ keyEquivalent: "")
+ jumpItem.target = self
+ jumpItem.isEnabled = rows.contains(where: \.waiting)
+ if let jumpHotkey, jumpHotkeyRegistered {
+ jumpItem.keyEquivalent = jumpHotkey.key.count == 1 ? jumpHotkey.key : ""
+ jumpItem.keyEquivalentModifierMask = jumpHotkey.modifierFlags
+ }
+ menu.addItem(jumpItem)
+
let modeMenu = NSMenu()
for mode in PanelMode.allCases {
let option = NSMenuItem(title: mode.label, action: #selector(selectMode(_:)), keyEquivalent: "")
@@ -166,16 +217,14 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate {
modeItem.submenu = modeMenu
menu.addItem(modeItem)
- if let hotkey, !hotkeyRegistered {
- let clash = NSMenuItem(title: "Shortcut \(hotkey.display) is used by another app", action: nil,
- keyEquivalent: "")
- clash.isEnabled = false
- menu.addItem(clash)
- } else if hotkey == nil {
- let invalid = NSMenuItem(title: "Shortcut \"\(settings.hotkey)\" is not valid", action: nil,
- keyEquivalent: "")
- invalid.isEnabled = false
- menu.addItem(invalid)
+ for (shortcut, registered, spec) in [(hotkey, hotkeyRegistered, settings.hotkey),
+ (jumpHotkey, jumpHotkeyRegistered, settings.jumpHotkey)] {
+ guard !registered else { continue }
+ let problem = shortcut.map { "Shortcut \($0.display) is used by another app" }
+ ?? "Shortcut \"\(spec)\" is not valid"
+ let item = NSMenuItem(title: problem, action: nil, keyEquivalent: "")
+ item.isEnabled = false
+ menu.addItem(item)
}
let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
settingsItem.target = self
@@ -199,6 +248,7 @@ if arguments.contains("--selftest") {
if Hotkey.parse("e") != nil { failures.append("hotkey: a key without modifiers is rejected") }
if Hotkey.parse("ctrl+opt+bogus") != nil { failures.append("hotkey: unknown keys are rejected") }
if Hotkey.parse("cmd+shift+f12")?.display != "⇧⌘F12" { failures.append("hotkey: parse cmd+shift+f12") }
+ if Hotkey.parse(Settings().jumpHotkey)?.display != "⌃⌥⌘U" { failures.append("hotkey: default jump is ⌃⌥⌘U") }
failures.forEach { print("FAIL \($0)") }
print(failures.isEmpty ? "selftest ok" : "selftest: \(failures.count) failed")
exit(failures.isEmpty ? 0 : 1)
@@ -212,9 +262,21 @@ if arguments.contains("--print") {
exit(0)
}
if arguments.contains("--print-panel") {
- print(PanelContent(rows: fetchRows()).plainText)
+ let full = arguments.contains("--full")
+ print(PanelContent(rows: fetchRows(), full: full, canToggle: full || arguments.contains("--toggle")).plainText)
exit(0)
}
+if let index = arguments.firstIndex(of: "--snapshot-panel"), index + 1 < arguments.count {
+ _ = NSApplication.shared
+ let controller = PanelController(persistFrame: false, onClick: { _ in }, onToggleFull: {})
+ // First render something else, so stale views would show in the image.
+ controller.render(PanelContent(rows: [Row(
+ paneId: "%stale", agent: "pi", name: "STALE ROW FROM A PREVIOUS RENDER", status: .blocked,
+ session: "old", window: "old", since: 0)]))
+ let full = arguments.contains("--full")
+ controller.render(PanelContent(rows: fetchRows(), full: full, canToggle: full))
+ exit(controller.snapshot(to: arguments[index + 1]) ? 0 : 1)
+}
if let index = arguments.firstIndex(of: "--click"), index + 1 < arguments.count {
focus(arguments[index + 1])
exit(0)