No description
Find a file
Vincent Batts 2383bf199f Initial gosh-tmux plugin reporting the tmux pane you were looking at
tmux keeps no history of what was on screen, so `sample` (run each minute
from a systemd user timer, installed by `install-timer`) records, for every
attached client, the pane in front of it: its shell pid, directory and
foreground command name, and how long the client has been idle. collect
turns that log into events.

An event's path is the pane's directory, so gosh puts the time in the git
project that contains it, and its session is the pane's shell pid, the
number gosh shows beside that shell's commands, so a pane and its commands
line up in `gosh timeline`. A stretch ends on a pane or directory change,
2 minutes without input, or a missing sample; long stretches report a
heartbeat every 5 minutes and refs depend only on a stretch's start, so
collecting repeatedly never duplicates.

tmux is asked for UTF-8 output (-u) and records end with a marker, since
it escapes control characters and a directory name can hold a newline.
Pane titles and command arguments are never recorded. The README documents
the limits: reading without input counts as away, samples are a minute
apart, only attached clients are seen, and the timer has not been run for
long.
2026-10-17 09:38:19 -04:00
.gitignore Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
go.mod Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
LICENSE Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
main.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
main_test.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
panes.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
panes_test.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
README.md Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
sample.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00
sample_test.go Initial gosh-tmux plugin reporting the tmux pane you were looking at 2026-10-17 09:38:19 -04:00

gosh-tmux

A gosh plugin that reports which tmux pane you were looking at: in which directory, running what, and for how long, including the time between commands that shell history can't see.

gosh's shell history records a command when you run it. It doesn't know about an editor open in a pane for an hour, which pane of several you were actually watching, or a build you waited on. tmux knows all of that but keeps no history of it, so gosh-tmux sample asks it once a minute, from a systemd user timer, and appends the answer to ~/.local/state/gosh/tmux-panes.log, next to gosh's own cwd.log. gosh collect runs gosh-tmux collect, which turns that log into events.

kind one event per
pane time you started looking at a pane in a directory (detail: session:window (command))
active 5 minutes you stayed on it, plus one where the stretch ended

gosh infers time from the spacing of events, so a long stretch reports itself every 5 minutes and an hour in vim counts as an hour, not 5 minutes.

Install

go install git.batts.cloud/vbatts/gosh-tmux@latest
gosh-tmux install-timer                       # writes the user units; enables nothing
systemctl --user daemon-reload
systemctl --user enable --now gosh-tmux-sample.timer
gosh-tmux doctor                              # is it sampling?
gosh plugin add tmux
gosh collect

The timer runs gosh-tmux sample every minute. With no tmux server running it just records that it ran, so a gap in the log always means the sampler wasn't running, never "nothing happened". To watch a tmux server on a non-default socket, edit the unit's ExecStart to add -L <name> or -S <path>.

How it lines up with your shell history

Each event's path is the pane's directory, so gosh puts it in the git project that contains it, and its session is the pane's shell pid: the pid is #{pane_pid}, the same number gosh's prompt hook logs for that shell's commands. gosh timeline shows it beside both, so a pane's time and its commands line up:

$ gosh timeline -prefix ~/src/gosh
09:22:26  @tmux pane 0:claude (claude)    (~/src/gosh) [36949]
09:27:26  @tmux active 0:claude (claude)    (~/src/gosh) [36949]
09:27:46  $ go test ./...    (~/src/gosh) [36949]
09:32:46  $ git status    (~/src/gosh) [36949]

This is exact for the pane's own shell. A shell you start inside the pane (bash, su, a nested tmux) has a different pid, so its commands carry a different number.

How time is worked out

A client counts while it is attached, has had input in the last 2 minutes, and its pane and directory stay the same:

  • Switching pane, or cd-ing in the pane, ends the stretch and starts another. A command changing (shell to vim and back) does not; it only changes the detail.
  • Idle. If the client has been silent for 2 minutes, the stretch ends where its input stopped.
  • Missing samples. A client missing from one sample (detached, or the sampler didn't run) ends the stretch, so a suspended laptop or a detached session isn't counted.
  • Still going. A stretch that is still going reports its start and heartbeats but no end yet, and every event's ref depends only on when the stretch began, so collecting repeatedly never duplicates anything.
  • Two clients attached to the same pane at once are two overlapping stretches; gosh joins them in time.

Privacy

The log holds timestamps, your session and window names, the pane's id, shell pid and directory, and the foreground command's name. It never holds command arguments, or pane titles, which programs set to anything (ssh puts user@host: path there). It's a plain file in your state directory, mode 0600. Directories are already in your shell history.

Limitations

  • Reading counts as away. tmux only knows when a client last had input, so a pane you watch without touching, such as a long build or an agent working, stops counting after 2 minutes. Typing, scrolling or clicking in tmux resets it. This was seen on the first real run: a client that had been watching an agent for 220 seconds contributed nothing. It is deliberate rather than guessing that silence means attention. Two ways to loosen it were considered and not taken:

    • A longer idle limit (say 10 minutes) also counts the time after you walk away, up to that limit, at the end of every stretch.
    • Counting output (#{window_activity}) catches builds you are waiting on, but a tail -f left running after you walk away would count for as long as it prints.

    gosh-gnome sees window focus and any input, including the mouse, and answers "was the terminal in front of you" better. The two overlap, so use -only / -exclude. Neither plugin can see the other's data, so nothing combines them: a pane that tmux calls idle is not rescued by gosh-gnome saying the terminal had focus.

  • Samples are a minute apart, so a stretch's start and end are accurate to about a minute (gosh adds its usual 5 minutes of lead-in to every session on top).

  • Only attached clients are seen. Detached sessions, even with something running, are not reported.

  • The pid join is exact only for the pane's own shell (#{pane_pid}). A shell started inside the pane, such as bash, su or a nested tmux, has another pid, and its commands won't carry the pane's number. gosh does not do the joining; it only shows the two numbers next to each other.

  • pane_current_path is the foreground process's directory. In a pane running ssh, that is the local directory ssh was started from, and the detail says ssh.

  • The log is never rotated and collect reads all of it each run (about 330 KB a day per attached client). Truncating it is safe: gosh already has the events.

  • tmux's output depends on its locale: without -u, non-ASCII characters in names and paths turn into _. The sampler passes -u and the unit sets LANG=C.UTF-8. A directory name containing a control character (a newline, say) is skipped rather than reported wrong.

Status

The parsing, the stretch logic and collect are covered by tests (go test ./...), and sample was run against a real tmux 3.5a server and its output collected by gosh. What has not been done: running it from the systemd timer for any length of time (the unit files are written and install-timer is tested, but nothing here has enabled them), or against tmux other than 3.5a. The formats it asks for (client_activity, pane_pid, pane_current_path, pane_current_command) are long-standing, but doctor and a day of real data are how you find out. Its last 24h line is a quick check that the numbers look like your day.

Protocol

gosh-tmux describe
gosh-tmux collect --since <unix seconds> [--log <file>]
gosh-tmux sample [-L <socket name>] [-S <socket path>] [--log <file>]
gosh-tmux doctor [--log <file>]
gosh-tmux install-timer [--dir <systemd user dir>]

The plugin declares protocol 1: paths are plain directories, so it works with any gosh. Output is JSON lines; see gosh's docs/PLUGINS.md. Only the standard library is used.

License

MIT, see LICENSE.