Skip to content

Commit 8194b8a

Browse files
committed
feat(libtmux/cmd) add rust backend shim behind env vars
why: allow libtmux tests to drive a rust-backed tmux_cmd without altering object APIs. what: - route tmux_cmd through vibe_tmux when LIBTMUX_BACKEND=rust - add LIBTMUX_RUST_CONNECTION_KIND for explicit bin/protocol selection - align raise_if_dead with rust backend exit semantics
1 parent b218951 commit 8194b8a

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

src/libtmux/_rust.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Rust-backed libtmux bindings.
2+
3+
This module is intentionally thin: it re-exports Rust types from the
4+
vibe-tmux extension so libtmux can opt into the Rust backend.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import importlib
10+
from typing import Any
11+
12+
_EXPORTS = ("Server",)
13+
_NATIVE: Any | None = None
14+
15+
16+
def _load_native() -> Any:
17+
global _NATIVE
18+
if _NATIVE is None:
19+
try:
20+
_NATIVE = importlib.import_module("vibe_tmux")
21+
except Exception as exc: # pragma: no cover - import path is env-dependent
22+
raise ImportError(
23+
"libtmux rust backend requires the vibe_tmux extension to be installed"
24+
) from exc
25+
return _NATIVE
26+
27+
28+
def __getattr__(name: str) -> Any:
29+
if name in _EXPORTS:
30+
native = _load_native()
31+
return getattr(native, name)
32+
raise AttributeError(name)
33+
34+
35+
def __dir__() -> list[str]:
36+
return sorted(list(globals().keys()) + list(_EXPORTS))
37+
38+
39+
__all__ = _EXPORTS

src/libtmux/common.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from __future__ import annotations
99

1010
import logging
11+
import os
1112
import re
13+
import shlex
1214
import shutil
1315
import subprocess
1416
import sys
@@ -34,6 +36,142 @@
3436
WindowOptionDict = dict[str, t.Any]
3537
PaneDict = dict[str, t.Any]
3638

39+
_RUST_BACKEND = os.getenv("LIBTMUX_BACKEND") == "rust"
40+
_RUST_SERVER_CACHE: dict[tuple[str | None, str | None, int | None], t.Any] = {}
41+
_RUST_SERVER_CONFIG: dict[tuple[str | None, str | None, int | None], set[str]] = {}
42+
43+
44+
def _parse_tmux_args(args: tuple[t.Any, ...]) -> tuple[
45+
str | None, str | None, str | None, int | None, list[str]
46+
]:
47+
socket_name: str | None = None
48+
socket_path: str | None = None
49+
config_file: str | None = None
50+
colors: int | None = None
51+
cmd_parts: list[str] = []
52+
parsing_globals = True
53+
54+
idx = 0
55+
while idx < len(args):
56+
arg = str(args[idx])
57+
if parsing_globals and arg == "--":
58+
parsing_globals = False
59+
idx += 1
60+
continue
61+
if parsing_globals and arg.startswith("-L") and len(arg) > 2:
62+
socket_name = arg[2:]
63+
idx += 1
64+
continue
65+
if parsing_globals and arg == "-L" and idx + 1 < len(args):
66+
socket_name = str(args[idx + 1])
67+
idx += 2
68+
continue
69+
if parsing_globals and arg.startswith("-S") and len(arg) > 2:
70+
socket_path = arg[2:]
71+
idx += 1
72+
continue
73+
if parsing_globals and arg == "-S" and idx + 1 < len(args):
74+
socket_path = str(args[idx + 1])
75+
idx += 2
76+
continue
77+
if parsing_globals and arg.startswith("-f") and len(arg) > 2:
78+
config_file = arg[2:]
79+
idx += 1
80+
continue
81+
if parsing_globals and arg == "-f" and idx + 1 < len(args):
82+
config_file = str(args[idx + 1])
83+
idx += 2
84+
continue
85+
if parsing_globals and arg == "-2":
86+
colors = 256
87+
idx += 1
88+
continue
89+
if parsing_globals and arg == "-8":
90+
colors = 88
91+
idx += 1
92+
continue
93+
if parsing_globals:
94+
parsing_globals = False
95+
cmd_parts.append(arg)
96+
idx += 1
97+
98+
return socket_name, socket_path, config_file, colors, cmd_parts
99+
100+
101+
def _rust_server(
102+
socket_name: str | None,
103+
socket_path: str | None,
104+
colors: int | None,
105+
config_file: str | None,
106+
) -> t.Any:
107+
key = (socket_name, socket_path, colors)
108+
server = _RUST_SERVER_CACHE.get(key)
109+
if server is None:
110+
from libtmux import _rust as rust_backend
111+
112+
connection_kind = os.getenv("LIBTMUX_RUST_CONNECTION_KIND")
113+
kwargs: dict[str, t.Any] = {}
114+
if connection_kind:
115+
kwargs["connection_kind"] = connection_kind
116+
server = rust_backend.Server(
117+
socket_path=socket_path,
118+
socket_name=socket_name,
119+
**kwargs,
120+
)
121+
_RUST_SERVER_CACHE[key] = server
122+
_RUST_SERVER_CONFIG[key] = set()
123+
124+
if config_file:
125+
loaded = _RUST_SERVER_CONFIG.setdefault(key, set())
126+
if config_file not in loaded:
127+
quoted = shlex.quote(config_file)
128+
server.cmd(f"source-file {quoted}")
129+
loaded.add(config_file)
130+
131+
return server
132+
133+
134+
def _rust_cmd_result(args: tuple[t.Any, ...]) -> tuple[list[str], list[str], int, list[str]]:
135+
socket_name, socket_path, config_file, colors, cmd_parts = _parse_tmux_args(args)
136+
cmd_list = [str(c) for c in args]
137+
if not cmd_parts:
138+
return [], [], 0, cmd_list
139+
if cmd_parts == ["-V"]:
140+
tmux_bin = shutil.which("tmux")
141+
if not tmux_bin:
142+
raise exc.TmuxCommandNotFound
143+
process = subprocess.Popen(
144+
[tmux_bin, "-V"],
145+
stdout=subprocess.PIPE,
146+
stderr=subprocess.PIPE,
147+
text=True,
148+
errors="backslashreplace",
149+
)
150+
stdout_raw, stderr_raw = process.communicate()
151+
stdout = stdout_raw.split("\n") if stdout_raw else []
152+
while stdout and stdout[-1] == "":
153+
stdout.pop()
154+
stderr = list(filter(None, stderr_raw.split("\n"))) if stderr_raw else []
155+
return stdout, stderr, process.returncode, cmd_list
156+
157+
connection_kind = os.getenv("LIBTMUX_RUST_CONNECTION_KIND")
158+
if connection_kind in {"bin", "tmux-bin"} and config_file:
159+
cmd_parts = ["-f", config_file, *cmd_parts]
160+
config_file = None
161+
162+
server = _rust_server(socket_name, socket_path, colors, config_file)
163+
164+
cmd = " ".join(shlex.quote(part) for part in cmd_parts)
165+
result = server.cmd(cmd)
166+
stdout = result.stdout.split("\n") if result.stdout else []
167+
while stdout and stdout[-1] == "":
168+
stdout.pop()
169+
stderr_raw = getattr(result, "exit_message", None) or ""
170+
stderr = list(filter(None, stderr_raw.split("\n"))) if stderr_raw else []
171+
if "has-session" in cmd_list and stderr and not stdout:
172+
stdout = [stderr[0]]
173+
return stdout, stderr, result.exit_code, cmd_list
174+
37175

38176
class CmdProtocol(t.Protocol):
39177
"""Command protocol for tmux command."""
@@ -249,6 +387,14 @@ class tmux_cmd:
249387
"""
250388

251389
def __init__(self, *args: t.Any) -> None:
390+
if _RUST_BACKEND:
391+
stdout, stderr, returncode, cmd = _rust_cmd_result(args)
392+
self.cmd = cmd
393+
self.returncode = returncode
394+
self.stdout = stdout
395+
self.stderr = stderr
396+
return
397+
252398
tmux_bin = shutil.which("tmux")
253399
if not tmux_bin:
254400
raise exc.TmuxCommandNotFound

src/libtmux/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ def raise_if_dead(self) -> None:
220220
... print(type(e))
221221
<class 'subprocess.CalledProcessError'>
222222
"""
223+
if os.getenv("LIBTMUX_BACKEND") == "rust":
224+
proc = self.cmd("list-sessions")
225+
if proc.returncode != 0:
226+
raise subprocess.CalledProcessError(proc.returncode, proc.cmd)
227+
return
223228
tmux_bin = shutil.which("tmux")
224229
if tmux_bin is None:
225230
raise exc.TmuxCommandNotFound

0 commit comments

Comments
 (0)