|  | 
| 9 | 9 | 
 | 
| 10 | 10 | import logging | 
| 11 | 11 | import re | 
| 12 |  | -import shutil | 
| 13 |  | -import subprocess | 
| 14 | 12 | import sys | 
| 15 | 13 | import typing as t | 
| 16 | 14 | 
 | 
|  | 15 | +from libtmux.engines import CommandResult, SubprocessEngine, TmuxEngine | 
|  | 16 | + | 
| 17 | 17 | from . import exc | 
| 18 | 18 | from ._compat import LooseVersion | 
| 19 | 19 | 
 | 
| @@ -190,8 +190,23 @@ def getenv(self, name: str) -> str | bool | None: | 
| 190 | 190 |         return opts_dict.get(name) | 
| 191 | 191 | 
 | 
| 192 | 192 | 
 | 
|  | 193 | +_default_engine: TmuxEngine = SubprocessEngine() | 
|  | 194 | + | 
|  | 195 | + | 
|  | 196 | +def _apply_result(target: tmux_cmd | None, result: CommandResult) -> tmux_cmd: | 
|  | 197 | +    cmd_obj: tmux_cmd = object.__new__(tmux_cmd) if target is None else target | 
|  | 198 | + | 
|  | 199 | +    cmd_obj.cmd = list(result.cmd) | 
|  | 200 | +    cmd_obj.stdout = result.stdout | 
|  | 201 | +    cmd_obj.stderr = result.stderr | 
|  | 202 | +    cmd_obj.returncode = result.returncode | 
|  | 203 | +    cmd_obj.process = result.process | 
|  | 204 | + | 
|  | 205 | +    return cmd_obj | 
|  | 206 | + | 
|  | 207 | + | 
| 193 | 208 | class tmux_cmd: | 
| 194 |  | -    """Run any :term:`tmux(1)` command through :py:mod:`subprocess`. | 
|  | 209 | +    """Run any :term:`tmux(1)` command through the configured engine. | 
| 195 | 210 | 
 | 
| 196 | 211 |     Examples | 
| 197 | 212 |     -------- | 
| @@ -219,52 +234,50 @@ class tmux_cmd: | 
| 219 | 234 |         Renamed from ``tmux`` to ``tmux_cmd``. | 
| 220 | 235 |     """ | 
| 221 | 236 | 
 | 
|  | 237 | +    cmd: list[str] | 
|  | 238 | +    stdout: list[str] | 
|  | 239 | +    stderr: list[str] | 
|  | 240 | +    returncode: int | 
|  | 241 | +    process: object | None | 
|  | 242 | + | 
| 222 | 243 |     def __init__(self, *args: t.Any) -> None: | 
| 223 |  | -        tmux_bin = shutil.which("tmux") | 
| 224 |  | -        if not tmux_bin: | 
| 225 |  | -            raise exc.TmuxCommandNotFound | 
| 226 |  | - | 
| 227 |  | -        cmd = [tmux_bin] | 
| 228 |  | -        cmd += args  # add the command arguments to cmd | 
| 229 |  | -        cmd = [str(c) for c in cmd] | 
| 230 |  | - | 
| 231 |  | -        self.cmd = cmd | 
| 232 |  | - | 
| 233 |  | -        try: | 
| 234 |  | -            self.process = subprocess.Popen( | 
| 235 |  | -                cmd, | 
| 236 |  | -                stdout=subprocess.PIPE, | 
| 237 |  | -                stderr=subprocess.PIPE, | 
| 238 |  | -                text=True, | 
| 239 |  | -                errors="backslashreplace", | 
| 240 |  | -            ) | 
| 241 |  | -            stdout, stderr = self.process.communicate() | 
| 242 |  | -            returncode = self.process.returncode | 
| 243 |  | -        except Exception: | 
| 244 |  | -            logger.exception(f"Exception for {subprocess.list2cmdline(cmd)}") | 
| 245 |  | -            raise | 
| 246 |  | - | 
| 247 |  | -        self.returncode = returncode | 
| 248 |  | - | 
| 249 |  | -        stdout_split = stdout.split("\n") | 
| 250 |  | -        # remove trailing newlines from stdout | 
| 251 |  | -        while stdout_split and stdout_split[-1] == "": | 
| 252 |  | -            stdout_split.pop() | 
| 253 |  | - | 
| 254 |  | -        stderr_split = stderr.split("\n") | 
| 255 |  | -        self.stderr = list(filter(None, stderr_split))  # filter empty values | 
| 256 |  | - | 
| 257 |  | -        if "has-session" in cmd and len(self.stderr) and not stdout_split: | 
| 258 |  | -            self.stdout = [self.stderr[0]] | 
| 259 |  | -        else: | 
| 260 |  | -            self.stdout = stdout_split | 
| 261 |  | - | 
| 262 |  | -        logger.debug( | 
| 263 |  | -            "self.stdout for {cmd}: {stdout}".format( | 
| 264 |  | -                cmd=" ".join(cmd), | 
| 265 |  | -                stdout=self.stdout, | 
| 266 |  | -            ), | 
| 267 |  | -        ) | 
|  | 244 | +        result = _default_engine.run(*args) | 
|  | 245 | +        _apply_result(self, result) | 
|  | 246 | + | 
|  | 247 | +    @classmethod | 
|  | 248 | +    def from_result(cls, result: CommandResult) -> tmux_cmd: | 
|  | 249 | +        """Create a :class:`tmux_cmd` instance from a raw result.""" | 
|  | 250 | +        return _apply_result(None, result) | 
|  | 251 | + | 
|  | 252 | + | 
|  | 253 | +def get_default_engine() -> TmuxEngine: | 
|  | 254 | +    """Return the process-based default engine.""" | 
|  | 255 | +    return _default_engine | 
|  | 256 | + | 
|  | 257 | + | 
|  | 258 | +def set_default_engine(engine: TmuxEngine) -> None: | 
|  | 259 | +    """Override the process-based default engine. | 
|  | 260 | +
 | 
|  | 261 | +    Parameters | 
|  | 262 | +    ---------- | 
|  | 263 | +    engine : TmuxEngine | 
|  | 264 | +        Engine implementation that should back :class:`tmux_cmd` and helper functions. | 
|  | 265 | +    """ | 
|  | 266 | +    global _default_engine | 
|  | 267 | +    _default_engine = engine | 
|  | 268 | + | 
|  | 269 | + | 
|  | 270 | +def run_tmux_command(engine: TmuxEngine, *args: t.Any) -> tmux_cmd: | 
|  | 271 | +    """Execute a tmux command using the provided engine. | 
|  | 272 | +
 | 
|  | 273 | +    Falls back to the global default engine when ``engine`` matches the default, | 
|  | 274 | +    preserving legacy monkeypatching that targets :class:`tmux_cmd` directly. | 
|  | 275 | +    """ | 
|  | 276 | +    if engine is _default_engine: | 
|  | 277 | +        return tmux_cmd(*args) | 
|  | 278 | + | 
|  | 279 | +    result = engine.run(*args) | 
|  | 280 | +    return tmux_cmd.from_result(result) | 
| 268 | 281 | 
 | 
| 269 | 282 | 
 | 
| 270 | 283 | def get_version() -> LooseVersion: | 
|  | 
0 commit comments