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