1- """Core engine abstractions: requests, results, and the engine protocols.
2-
3- A :class:`CommandRequest` is a rendered tmux argv plus an optional binary path; a
4- :class:`CommandResult` is the structured outcome. :class:`TmuxEngine` and
5- :class:`AsyncTmuxEngine` are :class:`typing.Protocol` types, so any object with
6- the right methods is an engine -- including a live :class:`libtmux.Server` for
7- the classic case -- without inheriting a base class.
1+ """Engine abstractions the experimental layer adds on top of Core's seam.
2+
3+ :class:`~libtmux.engines.base.CommandRequest`,
4+ :class:`~libtmux.engines.base.CommandResult`,
5+ :class:`~libtmux.engines.base.TmuxEngine` and the separator values live in
6+ Core's :mod:`libtmux.engines` and are re-exported here, so an experimental
7+ engine and Core's object API exchange the same values -- an engine written
8+ against either import path plugs into :class:`libtmux.Server`.
9+
10+ What remains local is what only the experimental transports need: the argv
11+ encoders that split tmux's client-global options from its command argv,
12+ control-mode line rendering and unescaping, the serializable
13+ :class:`EngineSpec` selector, and the asynchronous
14+ :class:`AsyncTmuxEngine` protocol.
815"""
916
1017from __future__ import annotations
1522import typing as t
1623from dataclasses import dataclass
1724
25+ from libtmux .engines .base import (
26+ CommandRequest ,
27+ CommandResult ,
28+ CommandSeparator ,
29+ SupportsTmuxVersion ,
30+ TmuxEngine ,
31+ is_command_separator ,
32+ )
33+
1834if t .TYPE_CHECKING :
19- import pathlib
2035 from collections .abc import Sequence
2136
22- from typing_extensions import Self
2337
2438#: tmux escapes a byte in ``%output`` as a backslash plus three octal digits.
2539_CONTROL_OCTAL = re .compile (rb"\\([0-7]{3})" )
3246 {"2" , "8" , "C" , "D" , "d" , "h" , "l" , "N" , "q" , "u" , "U" , "v" , "V" },
3347)
3448
35-
36- class CommandSeparator (str ):
37- """A planner-authored command boundary, distinct from a literal ``";"``.
38-
39- Examples
40- --------
41- >>> CommandSeparator(";")
42- ';'
43- >>> CommandSeparator("kill-server")
44- Traceback (most recent call last):
45- ...
46- ValueError: a command separator must be exactly ';'
47- """
48-
49- def __new__ (cls , value : str ) -> Self :
50- """Construct the one legal structural token."""
51- if value != ";" :
52- msg = "a command separator must be exactly ';'"
53- raise ValueError (msg )
54- return super ().__new__ (cls , value )
55-
56-
57- def is_command_separator (token : str ) -> bool :
58- """Return whether *token* is an intentional tmux command boundary.
59-
60- Examples
61- --------
62- >>> is_command_separator(CommandSeparator(";"))
63- True
64- >>> is_command_separator(";")
65- False
66- """
67- return type (token ) is CommandSeparator and token == ";"
49+ __all__ = (
50+ "AsyncTmuxEngine" ,
51+ "CommandRequest" ,
52+ "CommandResult" ,
53+ "CommandSeparator" ,
54+ "DirectArgv" ,
55+ "EngineKind" ,
56+ "EngineSpec" ,
57+ "SupportsTmuxVersion" ,
58+ "TmuxEngine" ,
59+ "encode_direct_argv" ,
60+ "is_command_separator" ,
61+ "render_control_line" ,
62+ "split_direct_argv" ,
63+ "unescape_control_output" ,
64+ )
6865
6966
7067class DirectArgv (t .NamedTuple ):
@@ -244,92 +241,6 @@ def unescape_control_output(payload: str) -> bytes:
244241 return _CONTROL_OCTAL .sub (lambda m : bytes ((int (m .group (1 ), 8 ),)), raw )
245242
246243
247- @dataclass (frozen = True )
248- class CommandRequest :
249- """A rendered tmux command, ready for an engine to execute.
250-
251- Attributes
252- ----------
253- args : tuple[str, ...]
254- The tmux argv *after* the binary (e.g. ``("split-window", "-t", "%1")``).
255- tmux_bin : str or None
256- Override the tmux binary for this request; ``None`` lets the engine
257- decide.
258-
259- Examples
260- --------
261- >>> CommandRequest.from_args("split-window", "-t", "%1")
262- CommandRequest(args=('split-window', '-t', '%1'), tmux_bin=None)
263- >>> CommandRequest.from_args("kill-window", "-t", 2).args
264- ('kill-window', '-t', '2')
265- """
266-
267- args : tuple [str , ...]
268- tmux_bin : str | None = None
269-
270- def __post_init__ (self ) -> None :
271- r"""Reject arguments that cannot survive tmux's C-string transports.
272-
273- Examples
274- --------
275- >>> CommandRequest(args=("display-message", "a\0b"))
276- Traceback (most recent call last):
277- ...
278- ValueError: tmux command arguments cannot contain NUL
279- """
280- if any (
281- type (arg ) is CommandSeparator and not is_command_separator (arg )
282- for arg in self .args
283- ):
284- msg = "a command separator must be exactly ';'"
285- raise ValueError (msg )
286- normalized = tuple (
287- arg if is_command_separator (arg ) else str .__str__ (arg ) for arg in self .args
288- )
289- if any ("\0 " in arg for arg in normalized ):
290- msg = "tmux command arguments cannot contain NUL"
291- raise ValueError (msg )
292- object .__setattr__ (self , "args" , normalized )
293-
294- @classmethod
295- def from_args (
296- cls ,
297- * args : t .Any ,
298- tmux_bin : str | pathlib .Path | None = None ,
299- ) -> CommandRequest :
300- """Build a request from arbitrary tokens, stringifying each."""
301- return cls (
302- args = tuple (arg if isinstance (arg , str ) else str (arg ) for arg in args ),
303- tmux_bin = str (tmux_bin ) if tmux_bin is not None else None ,
304- )
305-
306-
307- @dataclass (frozen = True )
308- class CommandResult :
309- """The structured outcome of executing a :class:`CommandRequest`.
310-
311- A tmux-side failure (``%error`` / nonzero exit) is *data* here -- it sets
312- ``returncode`` and ``stderr`` rather than raising. Only engine-broken
313- conditions (missing binary, lost connection, protocol desync) raise.
314-
315- Attributes
316- ----------
317- cmd : tuple[str, ...]
318- The full argv that ran (including the tmux binary).
319- stdout : tuple[str, ...]
320- Captured standard-output lines.
321- stderr : tuple[str, ...]
322- Captured standard-error lines.
323- returncode : int
324- tmux exit code (``-1`` when unknown).
325- """
326-
327- cmd : tuple [str , ...]
328- stdout : tuple [str , ...] = ()
329- stderr : tuple [str , ...] = ()
330- returncode : int = 0
331-
332-
333244class EngineKind (str , enum .Enum ):
334245 """Named engine families."""
335246
@@ -394,26 +305,6 @@ def imsg(cls, *, protocol_version: int | None = None) -> EngineSpec:
394305 return cls (kind = EngineKind .IMSG , protocol_version = protocol_version )
395306
396307
397- @t .runtime_checkable
398- class TmuxEngine (t .Protocol ):
399- """A synchronous executor of tmux commands."""
400-
401- def run (self , request : CommandRequest ) -> CommandResult :
402- """Execute one tmux command and return its structured result."""
403- ...
404-
405- def run_batch (
406- self ,
407- requests : Sequence [CommandRequest ],
408- ) -> list [CommandResult ]:
409- """Execute requests in order, returning one result per request.
410-
411- Persistent-connection engines (control mode) override this to pipeline;
412- stateless engines implement it as a loop over :meth:`run`.
413- """
414- ...
415-
416-
417308@t .runtime_checkable
418309class AsyncTmuxEngine (t .Protocol ):
419310 """An asynchronous executor of tmux commands."""
@@ -428,21 +319,3 @@ async def run_batch(
428319 ) -> list [CommandResult ]:
429320 """Execute requests in order, returning one result per request."""
430321 ...
431-
432-
433- @t .runtime_checkable
434- class SupportsTmuxVersion (t .Protocol ):
435- """An engine that can report the tmux version it targets.
436-
437- Optional engine capability. The executors
438- (:func:`~libtmux.experimental.ops.execute.run` / ``arun`` and the
439- :class:`~libtmux.experimental.ops.plan.LazyPlan` drivers) call
440- :meth:`tmux_version` to resolve the version for version-gated rendering when
441- the caller passes none. Engines that cannot know their version -- in-memory
442- or fake engines -- simply do not implement it, and resolution falls back to
443- "assume latest".
444- """
445-
446- def tmux_version (self ) -> str | None :
447- """Return the engine's tmux version string, or ``None`` if unknown."""
448- ...
0 commit comments