Skip to content

Commit 721b622

Browse files
committed
feat(engine): Add tmux control mode command runner
Implement ControlModeCommandRunner as alternative to subprocess execution, enabling persistent connection to tmux server for improved performance. Key features: - Protocol-based design with CommandResult interface - Thread-safe command execution with Lock - Auto-consumption of initial session creation blocks - Argument filtering for control mode compatibility (-L, -S, -f, -F, -2, -8) - Context manager support for automatic cleanup Implementation: - ControlModeResult: Duck-types as tmux_cmd for backward compatibility - ProtocolParser: Parses %begin/%end/%error response blocks - ControlModeCommandRunner: Manages persistent tmux -C connection Updated all cmd() methods (Server, Session, Window, Pane) to return CommandResult protocol instead of concrete tmux_cmd type. Known limitation: Control mode doesn't support custom format strings (-F), so these flags are filtered out during command execution.
1 parent f870a88 commit 721b622

File tree

11 files changed

+456
-12
lines changed

11 files changed

+456
-12
lines changed

src/libtmux/_internal/command_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Protocol
77

88
if t.TYPE_CHECKING:
9-
from libtmux.common import tmux_cmd
9+
pass
1010

1111

1212
class CommandResult(Protocol):
@@ -64,7 +64,7 @@ class CommandRunner(Protocol):
6464
>>> assert hasattr(result, 'returncode')
6565
"""
6666

67-
def run(self, *args: str) -> tmux_cmd:
67+
def run(self, *args: str) -> CommandResult:
6868
"""Execute a tmux command.
6969
7070
Parameters

src/libtmux/_internal/engines/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5-
__all__ = ("SubprocessCommandRunner",)
5+
__all__ = ("ControlModeCommandRunner", "SubprocessCommandRunner")
66

7+
from libtmux._internal.engines.control_mode import ControlModeCommandRunner
78
from libtmux._internal.engines.subprocess_engine import SubprocessCommandRunner
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Control mode command execution engine."""
2+
3+
from __future__ import annotations
4+
5+
__all__ = ("ControlModeCommandRunner", "ControlModeResult")
6+
7+
from libtmux._internal.engines.control_mode.result import ControlModeResult
8+
from libtmux._internal.engines.control_mode.runner import ControlModeCommandRunner
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Control mode protocol parser."""
2+
3+
from __future__ import annotations
4+
5+
from typing import IO
6+
7+
from .result import ControlModeResult
8+
9+
10+
class ProtocolParser:
11+
r"""Parser for tmux control mode protocol.
12+
13+
Handles %begin/%end/%error blocks and notifications.
14+
15+
The tmux control mode protocol format:
16+
- Commands produce output blocks
17+
- Blocks start with %begin and end with %end or %error
18+
- Format: %begin timestamp cmd_num flags
19+
- Notifications (%session-changed, etc.) can appear between blocks
20+
21+
Examples
22+
--------
23+
>>> import io
24+
>>> stdout = io.StringIO(
25+
... "%begin 1234 1 0\n"
26+
... "session1\n"
27+
... "%end 1234 1 0\n"
28+
... )
29+
>>> parser = ProtocolParser(stdout)
30+
>>> result = parser.parse_response(["list-sessions"])
31+
>>> result.stdout
32+
['session1']
33+
>>> result.returncode
34+
0
35+
"""
36+
37+
def __init__(self, stdout: IO[str]) -> None:
38+
self.stdout = stdout
39+
self.notifications: list[str] = []
40+
41+
def parse_response(self, cmd: list[str]) -> ControlModeResult:
42+
"""Parse a single command response.
43+
44+
Parameters
45+
----------
46+
cmd : list[str]
47+
The command that was executed (for result.cmd)
48+
49+
Returns
50+
-------
51+
ControlModeResult
52+
Parsed result with stdout, stderr, returncode
53+
54+
Raises
55+
------
56+
ConnectionError
57+
If connection closes unexpectedly
58+
ProtocolError
59+
If protocol format is unexpected
60+
"""
61+
stdout_lines: list[str] = []
62+
stderr_lines: list[str] = []
63+
returncode = 0
64+
65+
# State machine
66+
in_response = False
67+
68+
while True:
69+
line = self.stdout.readline()
70+
if not line: # EOF
71+
msg = "Control mode connection closed unexpectedly"
72+
raise ConnectionError(msg)
73+
74+
line = line.rstrip("\n")
75+
76+
# Parse line type
77+
if line.startswith("%begin"):
78+
# %begin timestamp cmd_num flags
79+
in_response = True
80+
continue
81+
82+
elif line.startswith("%end"):
83+
# Success - response complete
84+
return ControlModeResult(stdout_lines, stderr_lines, returncode, cmd)
85+
86+
elif line.startswith("%error"):
87+
# Error - command failed
88+
returncode = 1
89+
# Note: error details are in stdout_lines already
90+
return ControlModeResult(stdout_lines, stderr_lines, returncode, cmd)
91+
92+
elif line.startswith("%"):
93+
# Notification - queue for future processing
94+
self.notifications.append(line)
95+
# Don't break - keep reading for our response
96+
continue
97+
98+
else:
99+
# Regular output line
100+
if in_response:
101+
stdout_lines.append(line)
102+
# else: orphaned line before %begin (should not happen in practice)
103+
104+
105+
class ProtocolError(Exception):
106+
"""Raised when control mode protocol is violated."""
107+
108+
pass
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Result type for control mode command execution."""
2+
3+
from __future__ import annotations
4+
5+
6+
class ControlModeResult:
7+
"""Result from control mode execution.
8+
9+
Duck-types as tmux_cmd for backward compatibility.
10+
Has identical interface: stdout, stderr, returncode, cmd.
11+
12+
Attributes
13+
----------
14+
stdout : list[str]
15+
Command standard output, split by lines
16+
stderr : list[str]
17+
Command standard error, split by lines
18+
returncode : int
19+
Command return code (0 = success, 1 = error)
20+
cmd : list[str]
21+
The command that was executed (for debugging)
22+
23+
Examples
24+
--------
25+
>>> result = ControlModeResult(
26+
... stdout=["session1", "session2"],
27+
... stderr=[],
28+
... returncode=0,
29+
... cmd=["tmux", "-C", "list-sessions"]
30+
... )
31+
>>> result.stdout
32+
['session1', 'session2']
33+
>>> result.returncode
34+
0
35+
>>> bool(result.stderr)
36+
False
37+
"""
38+
39+
__slots__ = ("cmd", "returncode", "stderr", "stdout")
40+
41+
def __init__(
42+
self,
43+
stdout: list[str],
44+
stderr: list[str],
45+
returncode: int,
46+
cmd: list[str],
47+
) -> None:
48+
self.stdout = stdout
49+
self.stderr = stderr
50+
self.returncode = returncode
51+
self.cmd = cmd
52+
53+
def __repr__(self) -> str:
54+
return (
55+
f"ControlModeResult(returncode={self.returncode}, "
56+
f"stdout_lines={len(self.stdout)}, stderr_lines={len(self.stderr)})"
57+
)

0 commit comments

Comments
 (0)