Skip to content

Tmux-family base: factor the shell-dialect wrapper out of new_window/new_parked_window into overridable hooks #46

Description

@dracic

Describe your idea

BaseTmuxBackend.new_window / new_parked_window (src/automator/adapters/tmux_base.py) hard-code the POSIX shell wrapper: sh -c, shlex quoting, $? exit capture, echo, read -r, and an sh return-trailer. Factor those shell-dialect fragments into small overridable hooks with POSIX defaults, so the tmux argv construction, the parked-window protocol, and the recipe shape (inner; exit-capture; banner; park; return-trailer) live in the base exactly once.

Why is this needed?

A native-Windows backend (the psmux tmux.exe drop-in, follow-up to #40) has no /bin/sh — its windows run under pwsh. With the wrapper hard-coded, a Windows subclass is forced to duplicate both entire methods just to swap the shell dialect, even though the tmux verbs and argv layout are identical (new-window -t =S: -n NAME -c CWD -P -F '#{window_id}' …, and the trailer's show-options/switch-client/detach-client hit tmux.exe unchanged). A prototype Windows backend did exactly that, and the two copies immediately became the biggest drift risk in the adapter layer: any later fix to the parked protocol in the base silently fails to reach the override.

How should it work?

Keep both contract methods in the base and route only the dialect through hooks (POSIX defaults shown; a Windows leaf overrides the strings, never a method body):

_EXIT_CAPTURE = "ec=$?"          # pwsh later: "$ec = $LASTEXITCODE"
_ECHO = "echo"                   # pwsh later: "Write-Host"
_PARK = "read -r"                # pwsh later: "Read-Host"

def _join_argv(self, argv): ...      # shlex.join    | pwsh: & 'a' 'b' with '' escaping
def _parked_trailer(self, opt): ...  # sh if/elif    | pwsh: if/elseif (verbs identical)
def _source_prefix(self): ...        # ""            | pwsh: env-hygiene prelude
def _shell_wrap(self, source): ...   # ["sh","-c",s] | pwsh: -NoProfile -EncodedCommand
def _window_launch(self, env, cmd):  # ["-e","K=V"…, cmd] | pwsh: wrapped cmd + prelude

new_parked_window composes _source_prefix() + f"{inner}; {exit}; {echo} \"[bmad-auto exited $ec — press enter]\"; {park}; {trailer}" and spawns *self._shell_wrap(source). The recipe line is genuinely dialect-neutral (; sequencing and "$ec" interpolation behave the same in sh and pwsh); only the trailer stays a whole-string hook because sh/pwsh control-flow syntax can't be tokenized.

Guarantees:

  • POSIX output stays byte-identical — locked by tests asserting the literal subprocess.run argv (including the full sh -c source) before/after.
  • tmux_backend.py (the POSIX leaf) overrides nothing new.
  • The pwsh side is deliberately not implemented here; this only opens the seam for the Windows backend to land against.

PR

I'm working on this — implementation with byte-identity + fake-dialect-leaf tests is ready on my fork; PR to follow.

Additional context

Companion to #40 (which widened the _run spawn seam for encoding/env the same way: default-preserving hooks instead of method duplication). Together they get a tmux-family Windows leaf to ~zero copied plumbing.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions