from __future__ import annotations import os import subprocess from collections.abc import Sequence from dataclasses import dataclass class ControlModeError(RuntimeError): pass @dataclass class ControlModeClient: session: str tmux_command: Sequence[str] | None = None process: subprocess.Popen[str] | None = None def _command(self) -> tuple[str, ...]: socket_name = os.environ.get("TAMQ_TMUX_SOCKET") return tuple( self.tmux_command or (["tmux", "-L", socket_name] if socket_name else ["tmux"]) ) def session_exists(self, expected_pid: int | None = None) -> bool: result = subprocess.run( [*self._command(), "display-message", "-p", "-t", self.session, "#{pid}"], text=True, capture_output=True, check=False, ) if result.returncode != 0: return False return expected_pid is None or result.stdout.strip() == str(expected_pid) def start(self) -> None: if self.process is not None: return self.process = subprocess.Popen( [*self._command(), "-C", "attach-session", "-t", self.session], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, ) def command(self, value: str) -> None: if self.process is None or self.process.stdin is None: raise ControlModeError("control-mode client is not started") self.process.stdin.write(value.rstrip("\n") + "\n") self.process.stdin.flush() def inject(self, window: str, text: str) -> None: # send-keys -l preserves punctuation and whitespace in the message. self.command(f"send-keys -t {window} -l -- {shell_quote(text)}") def close(self) -> None: if self.process is not None: process = self.process process.terminate() try: process.wait(timeout=1) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=1) self.process = None def shell_quote(value: str) -> str: return "'" + value.replace("'", "'\\''") + "'"