Background
The terminal-multiplexer backends share a base class, BaseTmuxBackend (src/automator/adapters/tmux_base.py), that funnels every tmux invocation through a single subprocess primitive:
def _run(self, argv: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
proc = subprocess.run(
["tmux", *argv], capture_output=True, text=True, timeout=TMUX_TIMEOUT_S
)
if check and proc.returncode != 0:
raise TmuxError(f"tmux {' '.join(argv[:2])} failed: {proc.stderr.strip()}")
return proc
The POSIX TmuxMultiplexer uses this as-is. The _run docstring already advertises the extension point:
Subclasses (e.g. a native-Windows psmux) override this to tweak the binary, decoding (encoding="utf-8"), or timeout — and nothing else.
Problem
A native-Windows backend (a psmux tmux.exe drop-in) needs two things the seam does not actually expose, despite the docstring:
- Output decoding. Windows defaults to cp1252, which mangles the UTF-8 that psmux emits for
-F format output and multibyte field separators — so every read must force encoding="utf-8". The base _run hard-codes text=True with no encoding, so there is no hook to set it.
- Per-call
env. Creating a session from inside an existing multiplexer pane requires spawning new-session with a scrubbed environment (e.g. dropping a nesting-guard variable) without mutating the parent process's env. The base _run passes no env, so there is no hook for this either.
Because neither is exposed, a subclass that needs either is forced to re-implement the whole subprocess.run(...) primitive — duplicating the capture_output/text/timeout setup, the check handling, and the TmuxError raise — and to widen the method signature ad-hoc. That is exactly the kind of copy-paste-with-drift the single-seam design was meant to prevent: any later change to timeout/error handling in the base silently fails to reach the override.
Proposed resolution
Make the seam carry the two knobs it already promises, with defaults that are byte-for-byte the current behavior:
-
Output encoding as a class attribute.
class BaseTmuxBackend(TerminalMultiplexer):
_ENCODING: str | None = None # system default (today's behavior); a Windows leaf sets "utf-8"
and thread it into the call: subprocess.run(..., text=True, encoding=self._ENCODING).
-
Optional per-call env.
def _run(self, argv, *, check=True, env: dict[str, str] | None = None):
proc = subprocess.run(
["tmux", *argv], capture_output=True, text=True,
encoding=self._ENCODING, env=env, timeout=TMUX_TIMEOUT_S,
)
...
Callers that need a scrubbed env (e.g. new_session) pass it explicitly; everyone else omits it.
A native-Windows subclass then overrides zero lines of spawn plumbing — it sets _ENCODING = "utf-8" and passes env= where needed, inheriting the base timeout/check/error logic unchanged.
Guarantees this does not change tmux (POSIX) behavior
_ENCODING defaults to None. subprocess.run(..., text=True, encoding=None) is identical to the current text=True call — None is already the default, so decoding still uses the locale's preferred encoding. No POSIX read path changes.
env defaults to None. subprocess.run(..., env=None) inherits the parent environment, exactly as today. No call passes env unless it explicitly needs to.
- Both new parameters are keyword-only with defaults, so no existing call site or signature changes.
_tmux and every current caller are untouched.
- The change is purely additive plumbing; the existing tmux test suite passes unmodified, plus a regression test asserting the POSIX backend still spawns with no
encoding and no env override.
Acceptance criteria
Notes
- Non-behavioral refactor / enablement — it unblocks a clean native-Windows backend and shrinks that backend's override surface to near zero.
- The encoding half is independently useful to anyone driving
tmux from a non-UTF-8 locale, where the current locale-default decode can corrupt -F output.
Background
The terminal-multiplexer backends share a base class,
BaseTmuxBackend(src/automator/adapters/tmux_base.py), that funnels everytmuxinvocation through a single subprocess primitive:The POSIX
TmuxMultiplexeruses this as-is. The_rundocstring already advertises the extension point:Problem
A native-Windows backend (a
psmuxtmux.exedrop-in) needs two things the seam does not actually expose, despite the docstring:-Fformat output and multibyte field separators — so every read must forceencoding="utf-8". The base_runhard-codestext=Truewith noencoding, so there is no hook to set it.env. Creating a session from inside an existing multiplexer pane requires spawningnew-sessionwith a scrubbed environment (e.g. dropping a nesting-guard variable) without mutating the parent process's env. The base_runpasses noenv, so there is no hook for this either.Because neither is exposed, a subclass that needs either is forced to re-implement the whole
subprocess.run(...)primitive — duplicating thecapture_output/text/timeoutsetup, thecheckhandling, and theTmuxErrorraise — and to widen the method signature ad-hoc. That is exactly the kind of copy-paste-with-drift the single-seam design was meant to prevent: any later change to timeout/error handling in the base silently fails to reach the override.Proposed resolution
Make the seam carry the two knobs it already promises, with defaults that are byte-for-byte the current behavior:
Output encoding as a class attribute.
and thread it into the call:
subprocess.run(..., text=True, encoding=self._ENCODING).Optional per-call
env.Callers that need a scrubbed env (e.g.
new_session) pass it explicitly; everyone else omits it.A native-Windows subclass then overrides zero lines of spawn plumbing — it sets
_ENCODING = "utf-8"and passesenv=where needed, inheriting the base timeout/check/error logic unchanged.Guarantees this does not change tmux (POSIX) behavior
_ENCODINGdefaults toNone.subprocess.run(..., text=True, encoding=None)is identical to the currenttext=Truecall —Noneis already the default, so decoding still uses the locale's preferred encoding. No POSIX read path changes.envdefaults toNone.subprocess.run(..., env=None)inherits the parent environment, exactly as today. No call passesenvunless it explicitly needs to._tmuxand every current caller are untouched.encodingand noenvoverride.Acceptance criteria
BaseTmuxBackend._runaccepts an output encoding (via a class attribute) and an optional per-callenv, both defaulting to today's behavior.TmuxMultiplexeris unchanged at runtime — noencoding, noenvpassed — and all existing tmux tests pass without modification.new-sessionwith a scrubbed env without re-implementing_run._ENCODING, and a call passing a customenvthat does not leak into the parent process.Notes
tmuxfrom a non-UTF-8 locale, where the current locale-default decode can corrupt-Foutput.