Skip to content

Commit ae892c0

Browse files
authored
feat(tmux) Client, typed fields, native filter (#672)
libtmux 0.57.0 broadens tmux coverage. It introduces Client as a first-class typed object, threads tmux's native -f filter through the typed listing methods so callers can push predicates into the tmux server, adds typed access to many more format tokens with scope-and-version-gated -F templates, and propagates subcommand context through LibTmuxException so downstream tools can dispatch on which tmux command produced an error. What's new: - **Client object and Server.clients accessor.** New typed dataclass for attached terminals. client_name is the stable identifier; session_id / window_id / pane_id are attached-view snapshots hydrated via tmux's downward format_defaults cascade. attached_session / attached_window / attached_pane re-read list-clients before resolving; attached_pane is session-scope (cascade), not the client's CLIENT_ACTIVEPANE focus. - **Server.display_message and Window.display_message.** Server reads like #{version} and #{socket_path} work without a pane handle; window reads (#{window_zoomed_flag}, #{window_active_clients_list}) auto-bind to the window's id. All three display_message wrappers (including the existing Pane.display_message) surface tmux stderr via warnings.warn rather than dropping it silently. Callers that want to escalate can wrap in warnings.catch_warnings with filterwarnings("error"). - **Native filter on typed listing methods.** Server.search_panes / search_windows / search_sessions plus the Session and Window analogues take a filter= kwarg routed to tmux's -f flag. tmux evaluates the predicate and drops non-matching rows before any Python instance is constructed. Caveat: tmux silently expands a malformed predicate to empty, which the format engine treats as false — a typo looks identical to "no matches". - **Scope-and-version-aware -F templates.** The format string libtmux sends each list-* subcommand is now scope- and version-aware. A Session row hydrates active-window and active-pane fields via tmux's downward cascade; a Client row likewise hydrates the client's attached session, window, and active pane. Tokens introduced after tmux 3.2a are gated through FIELD_VERSION so the -F template stays safe on the project's minimum supported tmux. Unknown tokens stay None on the typed surface — no crash, no warning. - **Typed Pane / Window / Session / Client format-token fields for the 3.2a set.** Pane state (pane_dead, pane_in_mode, pane_marked, pane_synchronized, pane_path, pane_pipe …), window state (window_zoomed_flag, window_silence_flag, window_flags …), session state (session_marked …), and client view (client_session, client_readonly, client_termtype …). - **Server.run_shell(cwd=, show_stderr=).** New kwargs map to tmux's -c (3.4+) and -E (3.6+) flags. Older tmux warns and ignores the kwarg instead of erroring. - **Pane.send_keys(cmd=None, …) flag-only invocation.** Pass cmd=None together with reset=True or repeat=N to invoke tmux's flag-only send-keys -R / send-keys -N <n> form without any trailing key argument. - **Server.list_buffers(format_string=, filter=).** Project a chosen -F template or push a buffer-name match expression through tmux's format engine. - **Pane.capture_pane(pending=True).** Return bytes tmux has read from the pane but not yet committed to the terminal — useful for diagnosing programs whose output stalls mid-sequence. Fixes: - Pane.reset() now clears pane scrollback. The history clear silently no-op'd in 0.56.0, leaving the scrollback intact (#650). - Server.sessions / Server.clients / Server.search_sessions now distinguish a fresh server (no daemon yet) from a real tmux failure (permission denied, malformed daemon response). The former still returns an empty QueryList; the latter raises LibTmuxException instead of being swallowed. Breaking changes: - **LibTmuxException string form gains a subcommand prefix.** str(exc) now begins with the originating tmux subcommand name followed by ": ". An error from Session.last_window() used to render as "can't find window" and now renders as "last-window: can't find window". The wrapped-stderr type also changed: exc.args[0] is now str (joined with "\n" when tmux emitted multiple stderr lines); previously it was list[str]. Substring matches and unanchored re.search patterns continue to work unchanged. - **Server.sessions, Server.clients, and Server.search_sessions raise on tmux errors.** Previously, a tmux command failure was swallowed by a bare except Exception: pass, returning an empty QueryList indistinguishable from "no sessions/clients" or "filter matched nothing". The wrappers now let LibTmuxException propagate; only "no server running" / "error connecting to" on a fresh server remain empty-result cases. Deferred to follow-up shipments: - Typed Obj fields for tokens tmux added in 3.4 / 3.5 / 3.6 and the forward-looking set from tmux master. Held until tmux 3.7 reaches a tagged release. - Per-client active-pane resolution via tmux's CLIENT_ACTIVEPANE flag. Client.attached_pane currently follows the cascade (session's current window's active pane); the two diverge once a client has used select-pane -P to set its own active pane. - A predictable error contract for display_message (version-gated raise, per-call raise_on_stderr kwarg, or kept-permanent warn). Fixes #650. Refs #670 (umbrella). Companion: tmux-python/libtmux-mcp#48.
2 parents 4d5f7f7 + 61688af commit ae892c0

31 files changed

Lines changed: 4260 additions & 253 deletions

CHANGES

Lines changed: 219 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,222 @@ $ uvx --from 'libtmux' --prerelease allow python
4545
_Notes on the upcoming release will go here._
4646
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
4747

48+
libtmux 0.57.0 broadens tmux support around attached clients, tmux-native
49+
filtering, and format-token fields. {class}`~libtmux.Client` gives callers a
50+
typed object for attached terminals, `search_*()` methods let tmux return only
51+
matching sessions, windows, and panes, and more tmux format tokens are exposed
52+
as typed attributes. {exc}`~libtmux.exc.LibTmuxException` now records which
53+
tmux subcommand failed, making command errors easier to handle downstream.
54+
55+
### Breaking changes
56+
57+
#### `LibTmuxException` string form gains a subcommand prefix (#672)
58+
59+
When {exc}`~libtmux.exc.LibTmuxException` is raised from a libtmux method,
60+
``str(exc)`` now begins with the originating tmux subcommand name followed
61+
by ``": "``. For example, an error from
62+
{meth}`~libtmux.Session.last_window` used to render as ``"can't find
63+
window"`` and now renders as ``"last-window: can't find window"``.
64+
65+
``exc.args[0]`` still carries the original tmux error text, and the new
66+
{attr}`~libtmux.exc.LibTmuxException.subcommand` attribute exposes
67+
the tmux subcommand name as a separate field.
68+
{func}`~libtmux.common.raise_if_stderr` is the shared helper that
69+
populates both.
70+
71+
The error-payload *type* changed: ``exc.args[0]`` is now ``str``,
72+
joined with ``"\n"`` when tmux emitted multiple stderr lines.
73+
Previously it was ``list[str]``.
74+
75+
This is a serialization-format change: code that pattern-matches on
76+
``str(exc)`` exactly, anchors a regex with ``^`` against the old
77+
shape, or indexes ``exc.args[0]`` element-by-element will no longer
78+
match.
79+
80+
```python
81+
# Before
82+
try:
83+
session.last_window()
84+
except LibTmuxException as exc:
85+
if str(exc) == "can't find window":
86+
...
87+
88+
# After — dispatch on the typed attribute
89+
try:
90+
session.last_window()
91+
except LibTmuxException as exc:
92+
if exc.subcommand == "last-window":
93+
...
94+
95+
# Or — match against the original tmux error text without the prefix
96+
try:
97+
session.last_window()
98+
except LibTmuxException as exc:
99+
if exc.args and exc.args[0] == "can't find window":
100+
...
101+
```
102+
103+
Substring matches (``"can't find" in str(exc)``) and unanchored
104+
``re.search`` patterns continue to work unchanged.
105+
106+
#### `Server.sessions`, `Server.clients`, and `Server.search_sessions` raise on tmux errors (#672)
107+
108+
Previously, a tmux command failure under
109+
{attr}`~libtmux.Server.sessions`, {attr}`~libtmux.Server.clients`, or
110+
{meth}`~libtmux.Server.search_sessions` could return an empty
111+
{class}`~libtmux._internal.query_list.QueryList` indistinguishable
112+
from "no sessions / no clients attached" or "filter matched nothing".
113+
Those accessors now let {exc}`~libtmux.exc.LibTmuxException` propagate
114+
for real tmux failures.
115+
116+
Genuine empty results — a server with no attached clients, or a
117+
filter that matched zero sessions — still return an empty
118+
``QueryList``. A missing or not-yet-started tmux server is also still
119+
treated as an empty result, preserving the historic contract that a fresh
120+
{class}`~libtmux.Server` can be safely introspected before its daemon is
121+
up. Other tmux errors, such as socket permission failures or unsupported
122+
flags, now surface.
123+
124+
```python
125+
# Before — silent on tmux failure
126+
if not server.clients:
127+
log("no clients") # also runs when tmux itself crashed
128+
129+
# After — distinguish the two cases
130+
try:
131+
clients = server.clients
132+
except LibTmuxException as exc:
133+
log(f"list-clients failed: {exc}")
134+
else:
135+
if not clients:
136+
log("no clients")
137+
```
138+
139+
### What's new
140+
141+
#### `Client` object and `Server.clients` accessor (#672)
142+
143+
New {class}`~libtmux.Client` and {attr}`~libtmux.Server.clients` support tmux's
144+
attached-client model directly. Reads like ``client.client_readonly`` and
145+
``client.client_session`` work on the client object without dropping down to
146+
{meth}`~libtmux.Server.cmd`.
147+
148+
Note that ``client.session_id`` / ``client.window_id`` / ``client.pane_id``
149+
reflect the client's attached view when the object was read —
150+
{meth}`~libtmux.Client.refresh` re-reads them after the client switches focus.
151+
``client.client_name`` is the client's stable identifier.
152+
153+
For typed access to the live attachment, use
154+
{attr}`~libtmux.Client.attached_session`,
155+
{attr}`~libtmux.Client.attached_window`, and
156+
{attr}`~libtmux.Client.attached_pane`. Each property re-reads the
157+
client from ``list-clients`` before resolving; if tmux no longer
158+
reports that ``client_name``, such as after the client detaches, the
159+
property returns ``None``. Otherwise, the returned
160+
{class}`~libtmux.Session` / {class}`~libtmux.Window` /
161+
{class}`~libtmux.Pane` reflects where the client is attached *now*,
162+
not where it was when the {class}`~libtmux.Client` was constructed.
163+
Direct {meth}`~libtmux.Client.refresh` and
164+
{meth}`~libtmux.Client.from_client_name` calls still surface missing
165+
client lookup errors.
166+
167+
{attr}`~libtmux.Client.attached_pane` follows the attached session's current
168+
window. That can differ from the per-client active pane set by
169+
``select-pane -P``; see {attr}`~libtmux.Client.attached_pane` for the exact
170+
behavior.
171+
172+
#### `Server.display_message` and `Window.display_message` (#672)
173+
174+
{meth}`~libtmux.Server.display_message` and
175+
{meth}`~libtmux.Window.display_message` join the existing
176+
{meth}`~libtmux.Pane.display_message`. Server reads like
177+
``#{version}`` and ``#{socket_path}`` work without a pane handle;
178+
window reads (``#{window_zoomed_flag}``, ``#{window_active_clients_list}``)
179+
auto-bind to the window's id.
180+
181+
All three wrappers surface tmux stderr via :func:`warnings.warn`
182+
rather than dropping it silently. tmux uses stderr for both genuine
183+
errors and informational messages on some versions, so the wrappers
184+
warn rather than raise; callers that want to escalate can wrap the
185+
call in :func:`warnings.catch_warnings` with
186+
``filterwarnings("error")``. See {doc}`migration` for the escalation
187+
pattern.
188+
189+
#### tmux-native filtering with `search_*()` (#672)
190+
191+
{meth}`~libtmux.Server.search_panes`,
192+
{meth}`~libtmux.Server.search_windows`,
193+
{meth}`~libtmux.Server.search_sessions`, and the Session/Window
194+
analogues take a ``filter=`` kwarg routed to tmux's ``-f`` flag. tmux
195+
evaluates the expression and returns only matching objects.
196+
197+
Caveat: tmux silently expands a malformed filter expression to empty, which
198+
it treats as false — a typo looks identical to "no matches". Verify filter
199+
syntax against the FORMATS section of ``tmux(1)``.
200+
201+
#### `Pane.send_keys(cmd=None, …)` flag-only invocation (#672)
202+
203+
{meth}`~libtmux.Pane.send_keys` accepts ``cmd=None`` together with
204+
``reset=True`` or ``repeat=N`` to invoke tmux's flag-only
205+
``send-keys -R`` / ``send-keys -N <n>`` form without any trailing
206+
key argument.
207+
208+
#### `Server.list_buffers(format_string=, filter=)` (#672)
209+
210+
{meth}`~libtmux.Server.list_buffers` gains ``format_string`` (``-F``)
211+
and ``filter`` (``-f``) kwargs. Callers can ask tmux to return selected
212+
fields (e.g. ``"#{buffer_name}"``) or only buffers matching an expression —
213+
same bad-filter caveat as the ``search_*`` methods.
214+
215+
#### `Server.run_shell(cwd=, show_stderr=)` (#672)
216+
217+
{meth}`~libtmux.Server.run_shell` gains ``cwd`` (``-c``) to set the
218+
shell command's working directory and ``show_stderr`` (``-E``) to
219+
merge the command's stderr into the captured output. Both kwargs are
220+
version-gated; older tmux warns and ignores the flag instead of
221+
erroring.
222+
223+
#### `Pane.capture_pane(pending=True)` (#672)
224+
225+
{meth}`~libtmux.Pane.capture_pane` gains a ``pending`` kwarg that
226+
returns bytes tmux has read from the pane but not yet committed to
227+
the terminal — useful for diagnosing programs whose output stalls
228+
mid-sequence.
229+
230+
#### More format-token fields on tmux objects (#672)
231+
232+
libtmux now asks each ``list-*`` subcommand for the format tokens that make
233+
sense for that object and tmux version. Tokens the running tmux does not
234+
support stay ``None`` instead of making the listing fail.
235+
236+
{class}`~libtmux.Pane`, {class}`~libtmux.Window`,
237+
{class}`~libtmux.Session`, and {class}`~libtmux.Client` expose more typed
238+
attributes for pane state (``pane_dead``, ``pane_in_mode``, ``pane_marked``,
239+
``pane_synchronized``, ``pane_path``, ``pane_pipe``), window state
240+
(``window_zoomed_flag``, ``window_silence_flag``, ``window_flags``), session
241+
state (``session_marked``), and client state (``client_session``,
242+
``client_readonly``, ``client_termtype``). Some fields describe the active
243+
child object tmux reports with the row: for example, ``session.pane_id`` is
244+
the active pane in the session's current window, not a separate "session
245+
pane." See {ref}`format-tokens` for details.
246+
247+
### Fixes
248+
249+
- {meth}`~libtmux.Pane.reset` now clears pane scrollback. In 0.56.0
250+
the history clear silently no-op'd, leaving the scrollback intact
251+
(#650).
252+
253+
### Documentation
254+
255+
- New API page: {doc}`api/libtmux.client`.
256+
- New {ref}`format-tokens` topic explains why some fields describe an active
257+
child object, such as ``session.pane_id`` reflecting the active pane in the
258+
session's current window (#672).
259+
48260
## libtmux 0.56.0 (2026-05-10)
49261

50262
libtmux 0.56.0 is the tmux command-parity release. It adds more than
51-
50 wrappers across {class}`~libtmux.Server`, {class}`~libtmux.Session`,
263+
50 commands across {class}`~libtmux.Server`, {class}`~libtmux.Session`,
52264
{class}`~libtmux.Window`, and {class}`~libtmux.Pane`, filling in many
53265
commands that previously required raw {meth}`~libtmux.Server.cmd` calls.
54266
It also adds attached-client test support so interactive tmux commands can be
@@ -58,7 +270,7 @@ covered in headless test suites.
58270

59271
#### Interactive tmux commands are now scriptable (#653)
60272

61-
libtmux now exposes Python wrappers for tmux commands that normally depend on
273+
libtmux now exposes Python commands for tmux that normally depend on
62274
an attached client: {meth}`~libtmux.Pane.display_popup`,
63275
{meth}`~libtmux.Server.display_menu`,
64276
{meth}`~libtmux.Server.command_prompt`,
@@ -80,7 +292,7 @@ The `detach-client` API is split by the same scopes tmux actually honors:
80292
`tmux detach-client -a [-t <keep>]`. This keeps each method to one tmux flag
81293
group and one subprocess call.
82294

83-
#### tmux buffer I/O has first-class wrappers (#653)
295+
#### tmux buffer I/O has first-class support (#653)
84296

85297
Named tmux buffers can now be used from libtmux without hand-built commands.
86298
{meth}`~libtmux.Server.set_buffer`,
@@ -94,7 +306,7 @@ inter-process handoff workflows.
94306

95307
#### Server commands cover key bindings, clients, shell execution, and access (#653)
96308

97-
{class}`~libtmux.Server` gains wrappers for key-binding inspection and mutation
309+
{class}`~libtmux.Server` gains support for key-binding inspection and mutation
98310
({meth}`~libtmux.Server.bind_key`,
99311
{meth}`~libtmux.Server.unbind_key`,
100312
{meth}`~libtmux.Server.list_keys`,
@@ -137,7 +349,7 @@ Navigation helpers fill in the surrounding topology:
137349
{meth}`~libtmux.Session.next_window`, and
138350
{meth}`~libtmux.Session.previous_window`.
139351

140-
#### Existing wrappers expose more tmux flags (#653)
352+
#### Improvements (#653)
141353

142354
Several established methods now surface tmux flags that were previously only
143355
available by dropping to raw commands. Highlights include
@@ -196,7 +408,7 @@ old hardcoded socket paths. Fixes #664.
196408

197409
#### tmux 3.7 is within the known-version range (#653)
198410

199-
{data}`~libtmux.common.TMUX_MAX_VERSION` is now `"3.7"`, enabling wrappers and
411+
{data}`~libtmux.common.TMUX_MAX_VERSION` is now `"3.7"`, enabling support and
200412
tests for version-gated tmux 3.7 flags such as
201413
{meth}`~libtmux.Server.command_prompt` `bspace_exit` and
202414
{meth}`~libtmux.Server.show_messages` `terminals` / `jobs`. Installations on
@@ -343,7 +555,7 @@ surface from Make to Just.
343555

344556
### Fixes
345557

346-
#### `Server.new_session()` no longer races its own hydration query (#625)
558+
#### `Server.new_session()` no longer races its own initialization query (#625)
347559

348560
{meth}`~libtmux.Server.new_session` now parses all required session fields from
349561
the `tmux new-session -P` output instead of creating the session and then

0 commit comments

Comments
 (0)