Skip to content

Multiplexer _run seam: let subclasses set output encoding and per-call env without reimplementing the spawn primitive #40

Description

@dracic

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:

  1. 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.
  2. 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:

  1. 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).

  2. 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

  • BaseTmuxBackend._run accepts an output encoding (via a class attribute) and an optional per-call env, both defaulting to today's behavior.
  • The POSIX TmuxMultiplexer is unchanged at runtime — no encoding, no env passed — and all existing tmux tests pass without modification.
  • A native-Windows / psmux subclass can force UTF-8 decoding and spawn new-session with a scrubbed env without re-implementing _run.
  • Tests cover: POSIX default path (no encoding/env), a subclass setting _ENCODING, and a call passing a custom env that does not leak into the parent process.

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions