Skip to content

fix: guard pane_title with ':' prefix — empty titles collapse TSV fields and corrupt restored cwd - #583

Open
josh-stephens wants to merge 1 commit into
tmux-plugins:masterfrom
josh-stephens:master
Open

fix: guard pane_title with ':' prefix — empty titles collapse TSV fields and corrupt restored cwd#583
josh-stephens wants to merge 1 commit into
tmux-plugins:masterfrom
josh-stephens:master

Conversation

@josh-stephens

Copy link
Copy Markdown

The bug

dump_panes() (save.sh) and the pane-restore reader (restore.sh:178) both parse the tab-delimited pane record with while IFS=$d read …. Because tab is IFS-whitespace, bash collapses consecutive tabs, so a pane with an empty #{pane_title} loses that field entirely and every subsequent field shifts left by one:

  • :#{pane_current_path} lands in the title slot
  • #{pane_active} (0/1) lands in the dir slot

On restore, the dir parses as 1cd fails → the pane silently opens in the default cwd, and the path-string gets applied as the pane title (select-pane -T ':/home/user/project').

Any pane whose title is empty is affected on every save. Panes whose programs set a title (e.g. via OSC 2) escape, which makes the corruption look random. On a 91-pane server this silently mis-restored ~75 panes' working directories.

The fix

window_format() already guards :#{window_name} for exactly this reason; pane_title (added in 1ad109d) just missed the same guard. This PR emits :#{pane_title} and strips the first char where the title is applied on restore — identical to the existing window_name/dir convention. Two-line change, no format-version bump needed beyond the same convention already in the file.

🤖 Generated with Claude Code

dump_panes() and the restore-side reader both parse the tab-delimited
pane line with 'IFS=$d read'. Tab is IFS-whitespace, so an EMPTY
pane_title field is collapsed and every subsequent field shifts left:
the dir lands in the title slot and pane_active lands in the dir slot.
Restore then reads a bogus dir ('1') and silently falls back to the
default cwd for every pane without a title, and paints ':/path' strings
onto pane titles.

window_format already guards window_name with the ':' prefix for
exactly this reason; pane_title (added in 1ad109d) missed the same
guard. Emit ':#{pane_title}' and strip the first char where the title
is applied on restore, matching the window_name/dir convention.
@afrijaldz

Copy link
Copy Markdown

Confirming this from a real reboot, and adding one consequence I have not seen mentioned in the other reports of this bug (#520, #564, #581): the shift also swallows pane_full_command, so @resurrect-processes silently stops working for exactly the panes that lose their cwd.

Env: tmux 3.4, tmux-resurrect at cff343c (current master), tmux-continuum with @resurrect-capture-pane-contents on. Paths below are redacted, field structure is untouched.

The mechanism

dump_panes and restore_pane both parse the TSV with while IFS=$'\t' read .... Tab is an IFS whitespace character, so the shell folds runs of tabs into a single delimiter and an empty field disappears rather than being read as empty:

$ printf 'pane\tsess\t1\t0\t:\t0\t\t:/home/user/dotfiles\t0\tzsh\t2196\t500\n' |
    while IFS=$'\t' read lt s w wa wf pi pane_title dir pane_active pane_command pane_pid hs; do
      echo "pane_title=[$pane_title] dir=[$dir] pane_active=[$pane_active] pane_command=[$pane_command]"
    done
pane_title=[:/home/user/dotfiles] dir=[0] pane_active=[zsh] pane_command=[2196]

dir gets pane_active, remove_first_char turns "0" into "", and new_pane is called with an empty directory.

What that looks like in a real save file

This is the last autosave before the machine rebooted. Two shapes coexist in one file — panes with a title parse correctly, panes without one are shifted a column to the left:

NF=11 f7=[host]                f8=[:/home/user/projects/rust]  f9=[0]     f10=[zsh]   f11=[:]
NF=11 f7=[:/home/user/projects/rust]  f8=[:/home/user]         f9=[1]     f10=[zsh]   f11=[:]
NF=11 f7=[:/home/user/projects/beta]  f8=[1]                   f9=[claude] f10=[2341] f11=[:]

The third line is the interesting one. f8 holds pane_active, f10 holds a live PID that leaked through from #{pane_pid}, and f11pane_full_command — is empty even though f9 shows the pane was running claude, which is in my @resurrect-processes. On restore that pane came back as a bare shell in $HOME: wrong directory and no process. Windows, panes, layout and captured scrollback all restored perfectly, which is what made this so confusing to track down — everything looked fine except that every affected pane had forgotten where it was.

Why empty titles are common now

Default pane_title is the hostname, so this looks rare at first. But any program that sets its own title via OSC and clears it on exit leaves the title empty. That is how it happened here: the affected panes had all been running a TUI that sets a per-pane title. Those panes are precisely the long-lived working panes you most want restored, which is why the failure feels arbitrary: on my machine the same three panes broke every reboot and the rest never did.

After applying the change

I ran this with the save.sh hunk exactly as in this PR; my restore.sh side calls remove_first_char into a variable a few lines earlier instead of inline, which is functionally the same. Same session, columns line up for every pane including the deliberately-emptied one, and pane_full_command survives:

NF=11 f7=[:host]           f8=[:/home/user/projects/rust]  f9=[1] f10=[zsh]    f11=[:]
NF=11 f7=[:<tui title>]    f8=[:/home/user/projects/beta]  f9=[1] f10=[claude] f11=[:claude]
NF=11 f7=[:]               f8=[:/home/user]                f9=[1] f10=[zsh]    f11=[:]

Feeding those lines back through restore_pane's exact read line yields the right dir for all of them, empty title included.

Two notes on the diff itself

The : guard here is the same convention the format already uses for window_flags and pane_current_path, which I think makes it preferable to a sentinel like _ (#564) — a placeholder becomes indistinguishable from a pane whose title genuinely is _.

One migration wrinkle worth a line in the PR body: save files written before the patch store the title without the guard, so the first restore after upgrading will strip the leading character from a non-empty title. It is cosmetic, it self-corrects on the next save, and titles are usually overwritten by the running program anyway — but it will surprise someone who restores from an old file and sees ost instead of host.

Happy to test any revision of this against my setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants