Skip to content

Commit a8d81cd

Browse files
committed
ControlMode(core): Add engine stack and protocol bridge
why: introduce control-mode execution path with protocol parsing while keeping public cmd API compatible. what: - add Engine hooks (internal_session_names, exclude_internal_sessions) plus control/subprocess engines - parse control-mode stream via ControlProtocol and surface CommandResult metadata - retain tmux_cmd compatibility and control-aware capture_pane trimming/retry - extend exception types for control-mode timeouts/connection/protocol errors
1 parent 6b3315d commit a8d81cd

File tree

9 files changed

+1178
-25
lines changed

9 files changed

+1178
-25
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Engine abstractions and shared types for libtmux."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import enum
7+
import typing as t
8+
from abc import ABC, abstractmethod
9+
10+
from libtmux.common import tmux_cmd
11+
12+
if t.TYPE_CHECKING:
13+
from libtmux.session import Session
14+
15+
16+
class ExitStatus(enum.Enum):
17+
"""Exit status returned by tmux control mode commands."""
18+
19+
OK = 0
20+
ERROR = 1
21+
22+
23+
@dataclasses.dataclass
24+
class CommandResult:
25+
"""Canonical result shape produced by engines.
26+
27+
This is the internal representation used by engines. Public-facing APIs
28+
still return :class:`libtmux.common.tmux_cmd` for compatibility; see
29+
:func:`command_result_to_tmux_cmd`.
30+
"""
31+
32+
argv: list[str]
33+
stdout: list[str]
34+
stderr: list[str]
35+
exit_status: ExitStatus
36+
cmd_id: int | None = None
37+
start_time: float | None = None
38+
end_time: float | None = None
39+
tmux_time: int | None = None
40+
flags: int | None = None
41+
42+
@property
43+
def returncode(self) -> int:
44+
"""Return a POSIX-style return code matching tmux expectations."""
45+
return 0 if self.exit_status is ExitStatus.OK else 1
46+
47+
48+
class NotificationKind(enum.Enum):
49+
"""High-level categories for tmux control-mode notifications."""
50+
51+
PANE_OUTPUT = enum.auto()
52+
PANE_EXTENDED_OUTPUT = enum.auto()
53+
PANE_MODE_CHANGED = enum.auto()
54+
WINDOW_LAYOUT_CHANGED = enum.auto()
55+
WINDOW_ADD = enum.auto()
56+
WINDOW_CLOSE = enum.auto()
57+
UNLINKED_WINDOW_ADD = enum.auto()
58+
UNLINKED_WINDOW_CLOSE = enum.auto()
59+
UNLINKED_WINDOW_RENAMED = enum.auto()
60+
WINDOW_RENAMED = enum.auto()
61+
WINDOW_PANE_CHANGED = enum.auto()
62+
SESSION_CHANGED = enum.auto()
63+
CLIENT_SESSION_CHANGED = enum.auto()
64+
CLIENT_DETACHED = enum.auto()
65+
SESSION_RENAMED = enum.auto()
66+
SESSIONS_CHANGED = enum.auto()
67+
SESSION_WINDOW_CHANGED = enum.auto()
68+
PASTE_BUFFER_CHANGED = enum.auto()
69+
PASTE_BUFFER_DELETED = enum.auto()
70+
PAUSE = enum.auto()
71+
CONTINUE = enum.auto()
72+
SUBSCRIPTION_CHANGED = enum.auto()
73+
EXIT = enum.auto()
74+
RAW = enum.auto()
75+
76+
77+
@dataclasses.dataclass
78+
class Notification:
79+
"""Parsed notification emitted by tmux control mode."""
80+
81+
kind: NotificationKind
82+
when: float
83+
raw: str
84+
data: dict[str, t.Any]
85+
86+
87+
@dataclasses.dataclass
88+
class EngineStats:
89+
"""Light-weight diagnostics about engine state."""
90+
91+
in_flight: int
92+
notif_queue_depth: int
93+
dropped_notifications: int
94+
restarts: int
95+
last_error: str | None
96+
last_activity: float | None
97+
98+
99+
def command_result_to_tmux_cmd(result: CommandResult) -> tmux_cmd:
100+
"""Adapt :class:`CommandResult` into the legacy ``tmux_cmd`` wrapper."""
101+
proc = tmux_cmd(
102+
cmd=result.argv,
103+
stdout=result.stdout,
104+
stderr=result.stderr,
105+
returncode=result.returncode,
106+
)
107+
# Preserve extra metadata for consumers that know about it.
108+
proc.exit_status = result.exit_status # type: ignore[attr-defined]
109+
proc.cmd_id = result.cmd_id # type: ignore[attr-defined]
110+
proc.tmux_time = result.tmux_time # type: ignore[attr-defined]
111+
proc.flags = result.flags # type: ignore[attr-defined]
112+
proc.start_time = result.start_time # type: ignore[attr-defined]
113+
proc.end_time = result.end_time # type: ignore[attr-defined]
114+
return proc
115+
116+
117+
class Engine(ABC):
118+
"""Abstract base class for tmux execution engines.
119+
120+
Engines produce :class:`CommandResult` internally but surface ``tmux_cmd``
121+
to the existing libtmux public surface. Subclasses should implement
122+
:meth:`run_result` and rely on the base :meth:`run` adapter unless they have
123+
a strong reason to override both.
124+
"""
125+
126+
def run(
127+
self,
128+
cmd: str,
129+
cmd_args: t.Sequence[str | int] | None = None,
130+
server_args: t.Sequence[str | int] | None = None,
131+
timeout: float | None = None,
132+
) -> tmux_cmd:
133+
"""Run a tmux command and return a ``tmux_cmd`` wrapper."""
134+
return command_result_to_tmux_cmd(
135+
self.run_result(
136+
cmd=cmd,
137+
cmd_args=cmd_args,
138+
server_args=server_args,
139+
timeout=timeout,
140+
),
141+
)
142+
143+
@abstractmethod
144+
def run_result(
145+
self,
146+
cmd: str,
147+
cmd_args: t.Sequence[str | int] | None = None,
148+
server_args: t.Sequence[str | int] | None = None,
149+
timeout: float | None = None,
150+
) -> CommandResult:
151+
"""Run a tmux command and return a :class:`CommandResult`."""
152+
153+
def iter_notifications(
154+
self,
155+
*,
156+
timeout: float | None = None,
157+
) -> t.Iterator[Notification]: # pragma: no cover - default noop
158+
"""Yield control-mode notifications if supported by the engine."""
159+
if False: # keeps the function a generator for typing
160+
yield timeout
161+
return
162+
163+
# Optional hooks ---------------------------------------------------
164+
@property
165+
def internal_session_names(self) -> set[str]:
166+
"""Names of sessions reserved for engine internals."""
167+
return set()
168+
169+
def exclude_internal_sessions(
170+
self,
171+
sessions: list[Session],
172+
*,
173+
server_args: tuple[str | int, ...] | None = None,
174+
) -> list[Session]: # pragma: no cover - overridden by control mode
175+
"""Allow engines to hide internal/management sessions from user lists."""
176+
return sessions
177+
178+
def get_stats(self) -> EngineStats: # pragma: no cover - default noop
179+
"""Return engine diagnostic stats."""
180+
return EngineStats(
181+
in_flight=0,
182+
notif_queue_depth=0,
183+
dropped_notifications=0,
184+
restarts=0,
185+
last_error=None,
186+
last_activity=None,
187+
)
188+
189+
def close(self) -> None: # pragma: no cover - default noop
190+
"""Clean up any engine resources."""
191+
return None

0 commit comments

Comments
 (0)