pkg/tmux
Tmux session and window management primitives. Wraps the tmux CLI directly via os/exec (with bitfield/script reserved for piped fzf flows).
If SCRIPTS_TMUX_SOCKET is set and non-empty, every tmux invocation targets that socket (-S <socket>), e.g. for talking to a tmux server forwarded over ssh -R.
Session API
Switch(name string) error — switches to name, creating the session if it doesn't exist. Inside tmux uses switch-client; outside it uses attach.
SwitchExisting(name string) error — switches without creating; used by prev/next rotation through history.
NewSession(name string) error — creates a detached session. Local: -c <name> (working directory set to the session name). Qualified HOST:dir: starts at the local home with the ssh pane command and records the remote markers.
KillSession(name string) error — terminates the session.
HasSession(name string) error — returns nil if the session exists, error otherwise.
SessionExists(name string) (bool, error) — same check, surfaced as a boolean.
ListSessions() ([]string, error) — lists session names, swallowing the "no server running" error as an empty list.
Attach(name string) error / SwitchClient(name string) error — direct primitives behind Switch.
DisplaySessions() (string, error) — fzf picker over sessions with pane-capture preview.
GetCurrentSession() (string, error) — current session name (uses $TMUX_PANE / display-message).
ParseTarget(arg string) (host, session string) — splits the HOST:session argument form; HistoryName(host, session string) string joins it for storage.
RemoteInfo(name string) (host, dir string, ok bool) — reads the session's SCRIPTS_REMOTE_HOST / SCRIPTS_REMOTE_DIR markers, mapping a live session name back to its qualified HOST:dir form.
Harness API
ListHarnessPanes() ([]HarnessPane, error) — all panes running a harness (claude, codex, opencode), detected via @harness / @harness_state pane options (set by the shell hooks) with fallback inference from title, command, start command, and window name. A bare major.minor.patch foreground command means claude (it rewrites its process name to its version). A live foreground command naming a harness always overrides conflicting stored options (pane reused by another harness), resetting state to title-derived. Drops stale entries where the foreground is back to a shell with no harness title marker, or where the title marker names a different harness than the stored one.
IsStaleHarness(pane) bool — true when the harness exited: foreground is an interactive shell or empty with no harness title marker, or the title marker names a different harness than the recorded one.
DisplayHarnessPanes(panes) (HarnessPane, error) — fzf picker with pane-capture preview; returns the selected pane.
JumpToHarnessPane(pane) error — jumps to the pane across sessions via stable IDs (switch-client + select-window + select-pane), avoiding session-name escaping issues.
FormatHarnessPane(pane) string — tab-separated display line (pane_id, status, session, window:pane, title) for fzf with hidden ID column.
HarnessSymbol(state) string — maps hook state (working, waiting-input, waiting-permission, done) to ● ? ! ✓.
Notify API
Notify(verb, harness, reason string) error — implements scripts tmux notify {working|waiting|done|clear}: sets/clears the pane title and @harness/@harness_state/@harness_updated pane options, and for waiting/done sends an external notification via NOTIFY_SCRIPT (default $HOME/.local/bin/tmux-notifications.sh) when the pane isn't the active one. All tmux failures are swallowed (matches the old hook script); the only error is an unknown verb. The target pane is SCRIPTS_TMUX_PANE if set, else $TMUX_PANE; if neither is set, it's a no-op.
Window API
ListWindows() ([]string, error)
NewWindow(name, command, directory string) error
KillWindow(windowID string) error
SelectWindow(name string) error
Session Name Canonicalisation
Sessions are named after directory paths. Dots in directory names conflict with tmux's target-pattern syntax (session:window.pane), so CanonicalSessionName() replaces . with _ before any tmux call. Callers pass real paths; the canonicalisation happens inside the package.
Remote Targets
- A remote session is addressed as a qualified
HOST:session argument (e.g. web1:/a/b). Host names are ~/.ssh/config aliases typed by the user; there are no new config keys.
- It is a LOCAL tmux session whose panes run
ssh — remote machines need only ssh, no tmux. The pane command reverse-forwards the local tmux server socket (ssh -t -R remote_sock:local_sock host ...) and exports SCRIPTS_TMUX_SOCKET=remote_sock in the remote shell before cd dir && "$SHELL" -l, so a scripts binary on the remote drives the local tmux server; the socket is cleaned up with rm -f on shell exit.
- tmux silently maps
: to _ in session names at creation, so CanonicalSessionName() mirrors that (: and . → _) and runs before every tmux -t/-s call: a HOST:dir argument targets the stored HOST_dir session.
NewSession also sets the session-scoped default-command to the same ssh pane command, so Ctrl-b c, Ctrl-b %, and Ctrl-b " open new ssh panes in that session.
SCRIPTS_REMOTE_HOST / SCRIPTS_REMOTE_DIR session environment markers record the target; RemoteInfo reads them back to map a live session name to HOST:session.
- The pane command also exports
SCRIPTS_TMUX_PANE=$TMUX_PANE remotely (expanded locally, before the ssh hop), so scripts tmux notify run on the remote host still targets the local pane, without touching the remote's own TMUX_PANE.
sync maps live sessions through those markers before writing history; --reverse compares names canonically and recreates missing sessions from their qualified history entries.
- Every tmux call stays local; no remote tmux is involved. A remote directory picker is not implemented yet.
Switch-then-Persist Pattern
History (~/.scripts.yaml) is updated only after a successful switch. If tmux returns an error, the file is left alone — preventing a broken session from polluting recent history.
Error Handling
runTmux / runTmuxOutput capture combined output and join non-empty stderr into the returned error with fmt.Errorf("%w: %s", err, message).
isExitCode / isNoServerRunning classify expected error shapes (no server running ⇒ empty session list).