Skip to content

Commit 3e7c081

Browse files
committed
Engine(feat[control-mode]): add protocol parser and tmux_cmd adapter
1 parent 9659359 commit 3e7c081

File tree

7 files changed

+722
-228
lines changed

7 files changed

+722
-228
lines changed
Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,164 @@
1-
"""Base engine for libtmux."""
1+
"""Engine abstractions and shared types for libtmux."""
22

33
from __future__ import annotations
44

5+
import dataclasses
6+
import enum
57
import typing as t
68
from abc import ABC, abstractmethod
79

8-
if t.TYPE_CHECKING:
9-
from libtmux.common import tmux_cmd
10+
from libtmux.common import tmux_cmd
11+
12+
13+
class ExitStatus(enum.Enum):
14+
"""Exit status returned by tmux control mode commands."""
15+
16+
OK = 0
17+
ERROR = 1
18+
19+
20+
@dataclasses.dataclass
21+
class CommandResult:
22+
"""Canonical result shape produced by engines.
23+
24+
This is the internal representation used by engines. Public-facing APIs
25+
still return :class:`libtmux.common.tmux_cmd` for compatibility; see
26+
:func:`command_result_to_tmux_cmd`.
27+
"""
28+
29+
argv: list[str]
30+
stdout: list[str]
31+
stderr: list[str]
32+
exit_status: ExitStatus
33+
cmd_id: int | None = None
34+
start_time: float | None = None
35+
end_time: float | None = None
36+
tmux_time: int | None = None
37+
flags: int | None = None
38+
39+
@property
40+
def returncode(self) -> int:
41+
"""Return a POSIX-style return code matching tmux expectations."""
42+
return 0 if self.exit_status is ExitStatus.OK else 1
43+
44+
45+
class NotificationKind(enum.Enum):
46+
"""High-level categories for tmux control-mode notifications."""
47+
48+
PANE_OUTPUT = enum.auto()
49+
PANE_EXTENDED_OUTPUT = enum.auto()
50+
PANE_MODE_CHANGED = enum.auto()
51+
WINDOW_ADD = enum.auto()
52+
WINDOW_CLOSE = enum.auto()
53+
WINDOW_RENAMED = enum.auto()
54+
WINDOW_PANE_CHANGED = enum.auto()
55+
SESSION_CHANGED = enum.auto()
56+
SESSIONS_CHANGED = enum.auto()
57+
SESSION_WINDOW_CHANGED = enum.auto()
58+
PAUSE = enum.auto()
59+
CONTINUE = enum.auto()
60+
SUBSCRIPTION_CHANGED = enum.auto()
61+
EXIT = enum.auto()
62+
RAW = enum.auto()
63+
64+
65+
@dataclasses.dataclass
66+
class Notification:
67+
"""Parsed notification emitted by tmux control mode."""
68+
69+
kind: NotificationKind
70+
when: float
71+
raw: str
72+
data: dict[str, t.Any]
73+
74+
75+
@dataclasses.dataclass
76+
class EngineStats:
77+
"""Light-weight diagnostics about engine state."""
78+
79+
in_flight: int
80+
notif_queue_depth: int
81+
dropped_notifications: int
82+
restarts: int
83+
last_error: str | None
84+
last_activity: float | None
85+
86+
87+
def command_result_to_tmux_cmd(result: CommandResult) -> tmux_cmd:
88+
"""Adapt :class:`CommandResult` into the legacy ``tmux_cmd`` wrapper."""
89+
proc = tmux_cmd(
90+
cmd=result.argv,
91+
stdout=result.stdout,
92+
stderr=result.stderr,
93+
returncode=result.returncode,
94+
)
95+
# Preserve extra metadata for consumers that know about it.
96+
proc.exit_status = result.exit_status # type: ignore[attr-defined]
97+
proc.cmd_id = result.cmd_id # type: ignore[attr-defined]
98+
proc.tmux_time = result.tmux_time # type: ignore[attr-defined]
99+
proc.flags = result.flags # type: ignore[attr-defined]
100+
proc.start_time = result.start_time # type: ignore[attr-defined]
101+
proc.end_time = result.end_time # type: ignore[attr-defined]
102+
return proc
10103

11104

12105
class Engine(ABC):
13-
"""Abstract base class for tmux execution engines."""
106+
"""Abstract base class for tmux execution engines.
107+
108+
Engines produce :class:`CommandResult` internally but surface ``tmux_cmd``
109+
to the existing libtmux public surface. Subclasses should implement
110+
:meth:`run_result` and rely on the base :meth:`run` adapter unless they have
111+
a strong reason to override both.
112+
"""
14113

15-
@abstractmethod
16114
def run(
17115
self,
18116
cmd: str,
19117
cmd_args: t.Sequence[str | int] | None = None,
20118
server_args: t.Sequence[str | int] | None = None,
21119
timeout: float | None = None,
22120
) -> tmux_cmd:
23-
"""Run a tmux command and return the result."""
24-
...
121+
"""Run a tmux command and return a ``tmux_cmd`` wrapper."""
122+
return command_result_to_tmux_cmd(
123+
self.run_result(
124+
cmd=cmd,
125+
cmd_args=cmd_args,
126+
server_args=server_args,
127+
timeout=timeout,
128+
),
129+
)
130+
131+
@abstractmethod
132+
def run_result(
133+
self,
134+
cmd: str,
135+
cmd_args: t.Sequence[str | int] | None = None,
136+
server_args: t.Sequence[str | int] | None = None,
137+
timeout: float | None = None,
138+
) -> CommandResult:
139+
"""Run a tmux command and return a :class:`CommandResult`."""
140+
141+
def iter_notifications(
142+
self,
143+
*,
144+
timeout: float | None = None,
145+
) -> t.Iterator[Notification]: # pragma: no cover - default noop
146+
"""Yield control-mode notifications if supported by the engine."""
147+
if False: # keeps the function a generator for typing
148+
yield timeout
149+
return
150+
151+
def get_stats(self) -> EngineStats: # pragma: no cover - default noop
152+
"""Return engine diagnostic stats."""
153+
return EngineStats(
154+
in_flight=0,
155+
notif_queue_depth=0,
156+
dropped_notifications=0,
157+
restarts=0,
158+
last_error=None,
159+
last_activity=None,
160+
)
161+
162+
def close(self) -> None: # pragma: no cover - default noop
163+
"""Clean up any engine resources."""
164+
return None

0 commit comments

Comments
 (0)