diff options
Diffstat (limited to 'menubar/main.swift')
| -rw-r--r-- | menubar/main.swift | 98 |
1 files changed, 94 insertions, 4 deletions
diff --git a/menubar/main.swift b/menubar/main.swift index a00288c..0d367b8 100644 --- a/menubar/main.swift +++ b/menubar/main.swift @@ -25,6 +25,10 @@ // --snapshot-search <png> <query> [<down>] // render agent search offscreen, then exit // --click <pane> perform a row click for <pane>, then exit +// --restore [<session>...] +// restore saved tmux sessions as the panel and menu do +// (through the login shell), then exit +// --dismiss-offer dismiss the restore offer as "Not now" does, then exit // --selftest check the panel and search rules, then exit import AppKit @@ -33,6 +37,17 @@ func log(_ message: String) { FileHandle.standardError.write(Data("\(Date()) \(message)\n".utf8)) } +/// Restore saved tmux sessions (all when `sessions` is empty) through the +/// login shell, and log the report. Blocks until done. +@discardableResult +func performRestore(sessions: [String]) -> Bool { + let args = ["restore", "--report"] + sessions.flatMap { ["--session", $0] } + let result = runDinggerInLoginShell(args) + let lines = result.output.split(separator: "\n").filter { !$0.contains("can't change option: zle") } + log("restore: \(result.ok ? "done" : "failed")\n" + lines.map { " \($0)" }.joined(separator: "\n")) + return result.ok +} + final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { private let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) private let menu = NSMenu() @@ -49,6 +64,8 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { private var searchHotkeyRegistered = false private var prefersFull = UserDefaults.standard.bool(forKey: "panelFull") private var rows: [Row] = [] + private var restoreOffer: RestoreOffer? + private var restoring = false /// The panel settings; the first run after the on/off split migrates the /// old three-way mode. @@ -71,7 +88,9 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { item.menu = menu panel = PanelController( onClick: { [weak self] paneId in self?.jump(to: paneId) }, - onToggleFull: { [weak self] in self?.toggleFull() }) + onToggleFull: { [weak self] in self?.toggleFull() }, + onRestore: { [weak self] in self?.restore(sessions: []) }, + onDismissRestore: { [weak self] in self?.dismissRestore() }) searchWindow = SearchController(onJump: { [weak self] paneId in self?.jump(to: paneId) }) Settings.ensureFile() reloadSettings() @@ -124,6 +143,8 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { item.button?.setAccessibilityLabel("dingger, " + tipText) visibility.anyWaiting = rows.contains(where: \.waiting) + restoreOffer = fetchRestoreOffer() + visibility.restoreOffered = restoreOffer != nil applyPanel() searchWindow.update(rows: rows) } @@ -141,7 +162,8 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { private var panelContent: PanelContent { PanelContent(rows: rows, full: panelShowsFull(policy: visibility.policy, prefersFull: prefersFull), - canToggle: visibility.policy == .always) + canToggle: visibility.policy == .always, + offer: restoreOffer, restoring: restoring) } private func applyPanel() { @@ -163,6 +185,7 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { private var panelState: String { "\(visibility.enabled ? "on" : "off"), \(visibility.policy.rawValue), " + (visibility.anyWaiting ? "agents waiting" : "nothing waiting") + + (visibility.restoreOffered ? ", restore offered" : "") } /// The shortcut and the menu item: turn the panel on or off, persistently. @@ -188,6 +211,33 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { } } + /// Restore saved tmux sessions (all when `sessions` is empty). + func restore(sessions: [String], completion: (() -> Void)? = nil) { + guard !restoring else { return } + restoring = true + applyPanel() + log("restore: \(sessions.isEmpty ? "all sessions" : sessions.joined(separator: ", "))") + DispatchQueue.global().async { + performRestore(sessions: sessions) + DispatchQueue.main.async { + self.restoring = false + self.refresh() + completion?() + } + } + } + + /// "Not now": stop offering this saved layout. + @objc private func dismissRestore() { + let result = runReporting(dinggerBinary, ["restore", "--dismiss"]) + log("restore: dismissed (\(result.ok ? "ok" : result.output))") + refresh() + } + + @objc private func restoreFromMenu(_ sender: NSMenuItem) { + restore(sessions: (sender.representedObject as? String).map { [$0] } ?? []) + } + @objc private func selectPolicy(_ sender: NSMenuItem) { guard let raw = sender.representedObject as? String, let policy = PanelPolicy(rawValue: raw) else { return } visibility.policy = policy @@ -213,6 +263,37 @@ final class Menubar: NSObject, NSApplicationDelegate, NSMenuDelegate { func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() rows = fetchRows() + restoreOffer = fetchRestoreOffer() + if let offer = restoreOffer { + let item = NSMenuItem(title: restoring ? "Restoring tmux Sessions…" : "↺ \(offer.title)", + action: nil, keyEquivalent: "") + if !restoring { + let submenu = NSMenu() + let all = NSMenuItem(title: "Restore All", action: #selector(restoreFromMenu(_:)), keyEquivalent: "") + all.target = self + submenu.addItem(all) + submenu.addItem(.separator()) + for session in offer.sessions { + var detail = "\(session.windows) \(session.windows == 1 ? "window" : "windows")" + if session.agents > 0 { detail += ", \(session.agents) \(session.agents == 1 ? "agent" : "agents")" } + let one = NSMenuItem(title: session.name, action: #selector(restoreFromMenu(_:)), keyEquivalent: "") + one.subtitle = detail + one.target = self + one.representedObject = session.name + submenu.addItem(one) + } + submenu.addItem(.separator()) + let later = NSMenuItem(title: "Not Now", action: #selector(dismissRestore), keyEquivalent: "") + later.target = self + submenu.addItem(later) + item.submenu = submenu + } else { + item.isEnabled = false + } + item.subtitle = offer.detail + menu.addItem(item) + menu.addItem(.separator()) + } if rows.isEmpty { let none = NSMenuItem(title: "No agents", action: nil, keyEquivalent: "") none.isEnabled = false @@ -321,9 +402,18 @@ if arguments.contains("--print") { } if arguments.contains("--print-panel") { let full = arguments.contains("--full") - print(PanelContent(rows: fetchRows(), full: full, canToggle: full || arguments.contains("--toggle")).plainText) + print(PanelContent(rows: fetchRows(), full: full, canToggle: full || arguments.contains("--toggle"), + offer: fetchRestoreOffer()).plainText) exit(0) } +if let index = arguments.firstIndex(of: "--restore") { + exit(performRestore(sessions: Array(arguments[(index + 1)...])) ? 0 : 1) +} +if arguments.contains("--dismiss-offer") { + let result = runReporting(dinggerBinary, ["restore", "--dismiss"]) + print(result.output) + exit(result.ok ? 0 : 1) +} if let index = arguments.firstIndex(of: "--snapshot-panel"), index + 1 < arguments.count { _ = NSApplication.shared let controller = PanelController(persistFrame: false, onClick: { _ in }, onToggleFull: {}) @@ -332,7 +422,7 @@ if let index = arguments.firstIndex(of: "--snapshot-panel"), index + 1 < argumen 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)) + controller.render(PanelContent(rows: fetchRows(), full: full, canToggle: full, offer: fetchRestoreOffer())) exit(controller.snapshot(to: arguments[index + 1]) ? 0 : 1) } if let index = arguments.firstIndex(of: "--print-search") { |
