diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-23 13:59:59 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-23 13:59:59 +0300 |
| commit | 9c9c9dad93b16781ec2fc40bd7582db62240171c (patch) | |
| tree | cf85f3a53f8441eae36982d06244041cfe0ff6f3 /menubar/model.swift | |
| parent | c082f5044eedd2294d72f920d1018e40d73aab33 (diff) | |
restore, step 3: offer it in the panel, menu, and status line
when tmux stops (and its sessions are missing from the running server, or
no server runs), the daemon publishes the restore offer: offer.json for the
menu bar app and @dingger_restore for the status line, withdrawn once
restored or dismissed.
- panel: a first row "↺ Restore 9 sessions · 22 agents" with when it was
saved and how many agents were mid-turn; clicking restores, "Not now"
dismisses. an offer shows the panel like a waiting agent; off stays off.
- menu: the offer on top; restore all, one session, or not now.
- status line: a clickable blue ↺N before the clock.
- restore runs through the user's login shell, as a terminal starts tmux,
so restored panes get a real environment, not launchd's; --report writes
restore.log and notifies a summary. restore --dismiss.
- commands log only warnings by default; SIGPIPE ends them quietly.
e2e: the offer through the real menu bar binary, the summary notification,
the offer clearing, a restore with no tmux running, and not now staying
dismissed when a terminal starts tmux. the second test daemon is started
directly (it leaked through a function's $! before, and dozens of leaked
daemons fought over the test socket and loaded the machine); each run uses
its own socket and checks its daemons stop.
Diffstat (limited to 'menubar/model.swift')
| -rw-r--r-- | menubar/model.swift | 52 |
1 files changed, 52 insertions, 0 deletions
diff --git a/menubar/model.swift b/menubar/model.swift index e891697..da5ad15 100644 --- a/menubar/model.swift +++ b/menubar/model.swift @@ -165,6 +165,58 @@ func tmux(_ args: [String]) -> String? { let dinggerBinary = environment["DINGGER_BIN"] ?? (NSHomeDirectory() + "/.local/bin/dingger") +// MARK: restore offer + +/// tmux sessions the daemon offers to restore after tmux stopped +/// (`offer.json`, written by `dingger watch`). +struct RestoreOffer: Decodable, Equatable { + struct Session: Decodable, Equatable { + let name: String + let windows: Int + let panes: Int + let agents: Int + } + let saved_at: Int + let sessions: [Session] + let agents: Int + let interrupted: Int + + /// "Restore 9 sessions · 22 agents" + var title: String { + var text = "Restore \(sessions.count) \(sessions.count == 1 ? "session" : "sessions")" + if agents > 0 { text += " · \(agents) \(agents == 1 ? "agent" : "agents")" } + return text + } + + /// "saved 13:01 · 1 was mid-turn" + var detail: String { + let formatter = DateFormatter() + let saved = Date(timeIntervalSince1970: TimeInterval(saved_at) / 1000) + formatter.dateFormat = Calendar.current.isDateInToday(saved) ? "HH:mm" : "MMM d, HH:mm" + var parts = ["tmux stopped · saved \(formatter.string(from: saved))"] + if interrupted > 0 { parts.append("\(interrupted) \(interrupted == 1 ? "was" : "were") mid-turn") } + return parts.joined(separator: " · ") + } +} + +let stateDirectory = environment["DINGGER_STATE_DIR"] ?? (NSHomeDirectory() + "/.local/state/dingger") + +func fetchRestoreOffer() -> RestoreOffer? { + guard let data = FileManager.default.contents(atPath: stateDirectory + "/offer.json") else { return nil } + return try? JSONDecoder().decode(RestoreOffer.self, from: data) +} + +/// Run `dingger <args>` through the user's login shell, the way a terminal +/// starts tmux: restored panes inherit this environment (PATH, LANG), and +/// launchd's is bare. DINGGER_RESTORE_DIRECT runs it directly (tests). +func runDinggerInLoginShell(_ args: [String]) -> (ok: Bool, output: String) { + if environment["DINGGER_RESTORE_DIRECT"] != nil { + return runReporting(dinggerBinary, args) + } + let shell = getpwuid(getuid()).flatMap { String(validatingUTF8: $0.pointee.pw_shell) } ?? "/bin/zsh" + return runReporting(shell, ["--login", "-i", "-c", "exec \"$0\" \"$@\"", dinggerBinary] + args) +} + private let home = NSHomeDirectory() /// Every agent pane, in herdr's priority order, oldest waiting first. |
