Skip to content

Force UTF-8 locale to keep tab-separated parsing intact - #568

Open
vivek8031 wants to merge 1 commit into
tmux-plugins:masterfrom
vivek8031:fix/utf8-locale-for-tab-separators
Open

Force UTF-8 locale to keep tab-separated parsing intact#568
vivek8031 wants to merge 1 commit into
tmux-plugins:masterfrom
vivek8031:fix/utf8-locale-for-tab-separators

Conversation

@vivek8031

Copy link
Copy Markdown

Problem

When any script in this repo (save.sh, restore.sh, the helpers they share) runs in an environment with no LC_ALL or LANG set, tmux 3.x's -F format substitution renders tab characters (0x09) as underscores (0x5f). Because this script uses IFS=$'\t' read everywhere (pane_format, window_format, state_format, …), every line collapses into a single field, the parser bails out, and save_all writes a near-empty resurrect file containing only the trailing state line — about 8 bytes.

Common environments that hit this on macOS:

  • launchd LaunchAgents (TMPDIR/PATH are set but LC_ALL/LANG are not)
  • minimal cron environments
  • a few CI runners

The result is silent: save.sh exits 0, last symlink updates to the broken file, and the next auto-restore brings back nothing.

Reproduction (macOS, tmux 3.6a)

$ unset LC_ALL LANG LC_CTYPE
$ tmux list-panes -a -F "pane\t#{session_name}\t#{pane_pid}" | head -1 | xxd
00000000: 7061 6e65 5f34 5f31 3335 3934 0a   pane_4_13594.   ← underscores

$ export LC_ALL=C.UTF-8
$ tmux list-panes -a -F "pane\t#{session_name}\t#{pane_pid}" | head -1 | xxd
00000000: 7061 6e65 0934 0931 3335 3934 0a   pane.4.13594.   ← tabs

This pairs nastily with tmux-continuum: a LaunchAgent / status-bar driven continuum_save overwrites the last symlink with the 8-byte file right before auto-restore runs on next boot. Discussed at length in tmux-continuum issues #90 / #94 and at https://joeywrites.dev/posts/fixing-broken-tmux-resurrect-save/, where the documented workaround is to ln -sf a known-good save back into place by hand.

Fix

In scripts/helpers.sh (sourced by every script in the repo), set LC_ALL=C.UTF-8 if and only if the user has no locale configured at all:

if [ -z "${LC_ALL:-}${LANG:-}" ]; then
    export LC_ALL=\"C.UTF-8\"
fi

C.UTF-8 is available on glibc (≥ 2.13) and macOS (Big Sur+), and is a sensible neutral default. Existing LC_ALL or LANG are left alone, so users who have explicitly chosen a locale see no change.

Verification

# unpatched, no locale: 8-byte file, parsing broken
$ unset LC_ALL LANG LC_CTYPE
$ bash /path/to/installed/plugin/scripts/save.sh quiet
$ wc -l ~/.local/share/tmux/resurrect/last
       1
$ ls -la ~/.local/share/tmux/resurrect/last
… 8 …

# patched, no locale: full 2 KB save with all sessions
$ unset LC_ALL LANG LC_CTYPE
$ bash /path/to/fork/scripts/save.sh quiet
$ wc -l ~/.local/share/tmux/resurrect/last
      23
$ awk '$1==\"pane\"{print $2}' ~/.local/share/tmux/resurrect/last | sort -u
4
demo
main
property-mastering-engine
$ head -1 ~/.local/share/tmux/resurrect/last | xxd | head -1
00000000: 7061 6e65 0934 0931 0931 093a 2a09 …   pane.4.1.1.:*. …   ← tabs

Compatibility

  • No new tmux options, no behaviour change for users with LC_ALL or LANG already set.
  • Falls back to a locale that exists on every platform this plugin supports.
  • Single 9-line addition in helpers.sh.

…ntact

When this script runs in an environment with empty LC_ALL/LANG (notably
macOS launchd jobs and minimal cron environments) tmux 3.x's `-F`
format substitution renders tab characters (0x09) as underscores
(0x5f). Every save.sh / restore.sh code path uses `IFS=$'\t' read` to
parse this output, so the parser silently sees a single field per line
and produces near-empty resurrect files (only the trailing `state`
line survives, ~8 bytes).

Reproduce on macOS:

  $ unset LC_ALL LANG LC_CTYPE
  $ tmux list-panes -a -F "pane\t#{session_name}\t#{pane_pid}" \
      | head -1 | xxd
  00000000: 7061 6e65 5f34 5f31 3335 3934 0a   pane_4_13594.   ← underscores
  $ export LC_ALL=C.UTF-8
  $ tmux list-panes -a -F "pane\t#{session_name}\t#{pane_pid}" \
      | head -1 | xxd
  00000000: 7061 6e65 0934 0931 3335 3934 0a   pane.4.13594.   ← tabs

This was particularly nasty when used in tandem with tmux-continuum: a
LaunchAgent-driven `continuum_save` could overwrite the `last` symlink
with the broken 8-byte save right before auto-restore ran, so the next
boot restored nothing. (See tmux-continuum issues tmux-plugins#90 / tmux-plugins#94 and
https://joeywrites.dev/posts/fixing-broken-tmux-resurrect-save .)

Fix: in helpers.sh (sourced by every script in this repo), set
LC_ALL=C.UTF-8 if the user has no locale configured at all. Existing
LC_ALL or LANG values are left untouched.

Verified: the patched save.sh, run from a shell with all locale vars
unset, produces a 2 KB resurrect file containing real tabs (xxd 0x09)
and all live sessions, instead of the 8-byte `state__` line that the
unpatched script writes.
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.

1 participant