Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a33339f
Server(feat[display_message]): Add Server.display_message wrapper
tony May 16, 2026
511c06f
Window(feat[display_message]): Add Window.display_message wrapper
tony May 16, 2026
85cbd51
neo(feat[fields]): Add window_zoomed_flag typed field on Obj
tony May 16, 2026
f16dcc5
Pane(fix[reset]): Split into two cmd calls so clear-history runs (#650)
tony May 16, 2026
9c84e4b
Pane(feat[send_keys]): Support flag-only invocation (cmd=None)
tony May 16, 2026
f774776
Server(feat[list_buffers]): Add format_string= and filter= kwargs
tony May 16, 2026
e65a136
neo,server,session,window(feat[search]): Add search_*() methods with …
tony May 16, 2026
8ff33af
exc,common(feat[subcommand]): Add LibTmuxException.subcommand and rai…
tony May 16, 2026
06e2b8f
core(refactor[exc]): Migrate ~80 stderr raises to raise_if_stderr
tony May 16, 2026
b02808c
Server(feat[cmd]): Warn on legacy -t-in-args usage
tony May 16, 2026
b0b6511
neo(feat[fields]): Declare 36 pane/window/session format tokens on Obj
tony May 16, 2026
18fbab5
Client(feat): Add Client dataclass and Server.clients accessor
tony May 16, 2026
8738355
Server(feat[run_shell]): Add cwd= and show_stderr= kwargs
tony May 16, 2026
9c6a3b3
Pane(feat[capture_pane]): Add pending= kwarg
tony May 16, 2026
1bac04a
neo(feat[fields]): Declare 8 format-token fields from tmux master
tony May 16, 2026
0fd67c1
docs(CHANGES): Add 0.57.0 release entry
tony May 16, 2026
9ca7ecf
session(docs[cmd]): Correct 0.34 versionchanged precedence rationale
tony May 16, 2026
2b7cbb6
neo(fix[fields]): Drop 8 tmux-master tokens that crash tmux 3.2a server
tony May 16, 2026
36d964a
neo(fix[fields]): Drop 8 tokens missing from tmux 3.2a format_table
tony May 16, 2026
4ae8330
neo,client(fix[fields]): Drop 11 new client_* tokens that crash tmux …
tony May 16, 2026
a2c09f0
tests(display_message): Skip control-mode dispatch tests on tmux 3.2a
tony May 16, 2026
fb874e1
Revert "Server(feat[cmd]): Warn on legacy -t-in-args usage"
tony May 16, 2026
c0ef6a1
Revert "session(docs[cmd]): Correct 0.34 versionchanged precedence ra…
tony May 16, 2026
0d1c385
docs(CHANGES): Drop -t-in-args deprecation note
tony May 16, 2026
c3f1aae
neo(feat[scope-aware]): Make get_output_format scope+version aware
tony May 16, 2026
dd528c9
neo,client(feat[fields]): Re-expose 11 client_* fields via scope gating
tony May 16, 2026
399c1c2
neo(feat[fields]): Re-expose 8 version-gated tokens added after tmux …
tony May 16, 2026
0c3b7c2
neo(feat[fields]): Declare 8 forward-looking tokens for tmux 3.7+
tony May 16, 2026
a1e305d
docs(CHANGES): Document scope+version gated typed format-token fields
tony May 16, 2026
e714867
display_message(fix): Raise LibTmuxException on tmux stderr
tony May 16, 2026
968baac
Pane(docs[capture_pane]): Correct -P/pending semantics
tony May 16, 2026
c0646aa
Server(docs[run_shell]): Note tmux chdir fallback for cwd=
tony May 16, 2026
ea4aca8
tests(display_message): Skip no-text test on tmux 3.2a control-mode c…
tony May 16, 2026
04fcec6
neo(fix[scope-gate]): Widen SCOPES_BY_LIST_CMD to admit downward cascade
tony May 16, 2026
5b09a30
common(perf[get_version]): Memoize via @functools.cache to eliminate …
tony May 16, 2026
63c1238
neo(fix[scope-gate]): Re-admit cursor_x/y/flag/character as pane-scope
tony May 16, 2026
b33ddc8
neo(fix[scope-gate]): Re-admit mouse_*_flag and scroll_region_* as pa…
tony May 17, 2026
81a021e
neo(fix[scope-gate]): Reclassify 12 universal-mislabeled tokens to tr…
tony May 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,118 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

libtmux 0.57.0 closes 14 wrapper gaps surfaced by downstream adoption of
0.56's typed API. The release introduces {class}`~libtmux.Client` as a
first-class object, broadens {meth}`~libtmux.Pane.send_keys` to support
tmux's flag-only invocations, exposes tmux's C-side ``-f`` filter on
typed listing methods, threads originating subcommand context through
{exc}`~libtmux.exc.LibTmuxException`, and adds typed access to ~45
additional format tokens across {class}`~libtmux.Pane`,
{class}`~libtmux.Window`, and {class}`~libtmux.Session`.

### What's new

#### `Client` object and `Server.clients` accessor (#670)

New {class}`~libtmux.Client` dataclass and
{attr}`~libtmux.Server.clients` property expose tmux's twelve
``client_*`` format tokens with the same typed-ORM ergonomics already in
place for Session/Window/Pane. Reads like ``client.client_readonly``,
``client.client_session``, and ``client.client_termtype`` work directly
on attached clients instead of forcing callers down to
{meth}`~libtmux.Server.cmd`.

#### `Server.display_message` and `Window.display_message` (#670)

{meth}`~libtmux.Server.display_message` and
{meth}`~libtmux.Window.display_message` join the existing
{meth}`~libtmux.Pane.display_message`. Server reads (``#{version}``,
``#{socket_path}``) work without a pane handle; window reads
(``#{window_zoomed_flag}``, ``#{window_active_clients_list}``) auto-bind
to the window's id.

#### C-side filter on typed listing methods (#670)

{meth}`~libtmux.Server.search_panes`,
{meth}`~libtmux.Server.search_windows`,
{meth}`~libtmux.Server.search_sessions`, and the Session/Window
analogues take a ``filter=`` kwarg routed to tmux's ``-f`` flag. tmux
evaluates the predicate and drops non-matching objects before any
Python instance is constructed.

#### `Pane.send_keys(cmd=None, …)` flag-only invocation (#670)

{meth}`~libtmux.Pane.send_keys` accepts ``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=)` (#670)

{meth}`~libtmux.Server.list_buffers` gains ``format_string`` (``-F``)
and ``filter`` (``-f``) kwargs. Callers can project a chosen template
(e.g. ``"#{buffer_name}"``) or push a buffer-name match expression into
tmux's format engine.

#### `Server.run_shell(cwd=, show_stderr=)` (#670)

{meth}`~libtmux.Server.run_shell` gains ``cwd`` (``-c``) to set the
shell command's working directory and ``show_stderr`` (``-E``) to merge
the command's stderr into the captured output.

#### `Pane.capture_pane(pending=True)` (#670)

{meth}`~libtmux.Pane.capture_pane` gains a ``pending`` kwarg. When set,
the wrapper emits tmux's ``-P`` to return the bytes tmux has read from
the pane but not yet committed to the terminal — output that begins an
incomplete escape sequence and is still pending the parser's ground
state. Useful for diagnosing programs whose output stalls mid-sequence.

#### Subcommand-tagged exceptions (#670)

{exc}`~libtmux.exc.LibTmuxException` takes an optional ``subcommand``
attribute. When set, ``str(exc)`` prefixes the originating tmux command
name (e.g. ``"last-window: no such window"``), giving downstream
consumers a stable way to dispatch on which tmux command produced the
error. {func}`~libtmux.common.raise_if_stderr` is the helper every
wrapper uses to populate it.

#### Typed format-token fields with scope and version gating (#670)

{class}`~libtmux.Pane`, {class}`~libtmux.Window`, {class}`~libtmux.Session`,
and {class}`~libtmux.Client` now declare typed dataclass fields for the
scope-relevant tokens from tmux's ``format_table[]`` — covering 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``, ``session_active``,
``session_silence_flag`` …), and the client view (``client_session``,
``client_readonly``, ``client_termtype``, ``client_theme`` …).

The ``-F`` template libtmux sends to each ``list-*`` subcommand is now
**scope-aware** and **version-aware**: ``list-clients`` emits the
``client_*`` tokens but never ``pane_*`` ones; tokens introduced in
tmux 3.4 / 3.5 / 3.6 are suppressed on tmux 3.2a; eight forward-looking
tokens from tmux master (``pane_zoomed_flag``, ``pane_floating_flag``,
``pane_flags``, ``pane_pb_state``, ``pane_pb_progress``,
``pane_pipe_pid``, ``synchronized_output_flag``, ``bracket_paste_flag``)
are declared on the dataclass but only hydrate once tmux 3.7 ships.
Tokens the running tmux doesn't recognize stay ``None`` on the typed
surface — no crash, no warning.

### Fixes

- {meth}`~libtmux.Pane.reset` now clears pane scrollback. In 0.56.0 the
history clear silently no-op'd, leaving the scrollback intact (#650).
- {meth}`~libtmux.Server.display_message`,
{meth}`~libtmux.Window.display_message`, and
{meth}`~libtmux.Pane.display_message` raise
{exc}`~libtmux.exc.LibTmuxException` when tmux reports an error,
matching the rest of the typed wrappers (#670).

### Documentation

- New API page: {doc}`api/libtmux.client`.

## libtmux 0.56.0 (2026-05-10)

libtmux 0.56.0 is the tmux command-parity release. It adds more than
Expand Down
2 changes: 2 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from _pytest.doctest import DoctestItem

from libtmux._internal.control_mode import ControlMode
from libtmux.client import Client
from libtmux.pane import Pane
from libtmux.pytest_plugin import USING_ZSH
from libtmux.server import Server
Expand All @@ -42,6 +43,7 @@ def add_doctest_fixtures(
doctest_namespace["Session"] = Session
doctest_namespace["Window"] = Window
doctest_namespace["Pane"] = Pane
doctest_namespace["Client"] = Client
doctest_namespace["server"] = request.getfixturevalue("server")
doctest_namespace["Server"] = request.getfixturevalue("TestServer")
session: Session = request.getfixturevalue("session")
Expand Down
10 changes: 9 additions & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
# API Reference

libtmux's public API mirrors tmux's object hierarchy:
`Server` → `Session` → `Window` → `Pane`
`Server` → `Session` → `Window` → `Pane`. Attached terminals show up as
`Client` objects accessed off the server.

## What do you want to do?

Expand Down Expand Up @@ -67,6 +68,12 @@ Manages panes, layouts, and window operations.
Terminal instance. Send keys and capture output.
:::

:::{grid-item-card} Client
:link: libtmux.client
:link-type: doc
Attached terminal. Read read-only state, theme, termtype.
:::

::::

## Supporting Modules
Expand Down Expand Up @@ -160,6 +167,7 @@ Server <libtmux.server>
Session <libtmux.session>
Window <libtmux.window>
Pane <libtmux.pane>
Client <libtmux.client>
Common <libtmux.common>
Neo <libtmux.neo>
Options <libtmux.options>
Expand Down
16 changes: 16 additions & 0 deletions docs/api/libtmux.client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(clients)=

# Clients

- Attached terminals connected to a tmux server
- Each client has its own view of the active session, window, and pane
- Identified by ``client_name`` (the path or label tmux assigns at attach time)

```{eval-rst}
.. autoclass:: libtmux.Client
:members:
:inherited-members:
:private-members:
:show-inheritance:
:member-order: bysource
```
2 changes: 2 additions & 0 deletions src/libtmux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
__title__,
__version__,
)
from .client import Client
from .pane import Pane
from .server import Server
from .session import Session
Expand All @@ -22,6 +23,7 @@
logging.getLogger(__name__).addHandler(logging.NullHandler())

__all__ = (
"Client",
"Pane",
"Server",
"Session",
Expand Down
81 changes: 81 additions & 0 deletions src/libtmux/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Pythonization of the :term:`tmux(1)` client.

libtmux.client
~~~~~~~~~~~~~~

"""

from __future__ import annotations

import dataclasses
import logging
import typing as t

from libtmux.neo import Obj, fetch_obj

if t.TYPE_CHECKING:
from libtmux.server import Server


logger = logging.getLogger(__name__)


@dataclasses.dataclass()
class Client(Obj):
""":term:`tmux(1)` :term:`Client` [client_manual]_.

A tmux client is an attached terminal. The same tmux server can have
multiple clients attached simultaneously (e.g. ``$ tmux attach`` from
several terminals) and each receives its own view of the active
session, window, and pane.

Parameters
----------
server : :class:`Server`

Examples
--------
>>> with control_mode() as ctl:
... attached = [
... c
... for c in server.clients
... if c.client_name == ctl.client_name
... ]
>>> bool(attached)
True

>>> with control_mode() as ctl:
... client = server.clients.get(client_name=ctl.client_name)
... client.client_readonly in {"0", "1"}
True

References
----------
.. [client_manual] tmux client. openbsd manpage for TMUX(1).
"tmux supports multiple attached clients. Each client has its
own keymap, view of the session, and message log."

https://man.openbsd.org/tmux.1#DESCRIPTION. Accessed 2026.
"""

server: Server

def refresh(self) -> None:
"""Refresh client attributes from tmux."""
assert isinstance(self.client_name, str)
return super()._refresh(
obj_key="client_name",
obj_id=self.client_name,
list_cmd="list-clients",
)

@classmethod
def from_client_name(cls, server: Server, client_name: str) -> Client:
"""Create Client from an existing client_name."""
client = fetch_obj(
obj_key="client_name",
obj_id=client_name,
list_cmd="list-clients",
server=server,
)
return cls(server=server, **client)
46 changes: 46 additions & 0 deletions src/libtmux/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import functools
import logging
import re
import shlex
Expand Down Expand Up @@ -241,6 +242,41 @@ def getenv(self, name: str) -> str | bool | None:
return opts_dict.get(name)


def raise_if_stderr(proc: tmux_cmd, subcommand: str) -> None:
"""Raise :exc:`LibTmuxException` tagged with the tmux subcommand on stderr.

Centralizes the ``if proc.stderr: raise exc.LibTmuxException(proc.stderr)``
pattern scattered across the wrappers. Tags the exception with the
originating tmux subcommand so downstream consumers (e.g. libtmux-mcp's
``handle_tool_errors``) keep the "which tmux command failed" context.

Parameters
----------
proc : :class:`tmux_cmd`
Result of a :meth:`Server.cmd` / :meth:`Session.cmd` / etc. call.
subcommand : str
The tmux subcommand the wrapper invoked, e.g. ``"last-window"``,
``"swap-pane"``. Surfaces in ``str(exc)`` as a ``"<subcommand>: …"``
prefix.

Raises
------
:exc:`LibTmuxException`
When ``proc.stderr`` is non-empty.

Examples
--------
>>> from libtmux.common import raise_if_stderr
>>> from libtmux import exc
>>> proc = session.cmd("display-message", "-p", "#{session_id}")
>>> raise_if_stderr(proc, "display-message") # no stderr → no raise

.. versionadded:: 0.57
"""
if proc.stderr:
raise exc.LibTmuxException(proc.stderr, subcommand=subcommand)


class tmux_cmd:
"""Run any :term:`tmux(1)` command through :py:mod:`subprocess`.

Expand Down Expand Up @@ -338,6 +374,7 @@ def __init__(self, *args: t.Any, tmux_bin: str | None = None) -> None:
)


@functools.cache
def get_version(tmux_bin: str | None = None) -> LooseVersion:
"""Return tmux version.

Expand All @@ -358,6 +395,15 @@ def get_version(tmux_bin: str | None = None) -> LooseVersion:
:class:`distutils.version.LooseVersion`
tmux version according to *tmux_bin* if provided, otherwise the
system tmux from :func:`shutil.which`

Notes
-----
Memoized via :func:`functools.cache`, keyed on the *tmux_bin* argument
(``None`` is a distinct key from any explicit path). The cache is sticky
across ``PATH`` changes and on-disk binary swaps when *tmux_bin* is
``None`` or the same path string — call ``get_version.cache_clear()`` to
invalidate. Tests that monkey-patch :class:`tmux_cmd` should call
``cache_clear()`` before asserting parsed-version behavior.
"""
proc = tmux_cmd("-V", tmux_bin=tmux_bin)
if proc.stderr:
Expand Down
29 changes: 28 additions & 1 deletion src/libtmux/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,34 @@


class LibTmuxException(Exception):
"""Base Exception for libtmux Errors."""
"""Base Exception for libtmux Errors.

Parameters
----------
*args : object
Forwarded to :class:`Exception`.
subcommand : str, optional
The tmux subcommand that produced this error (e.g. ``"last-window"``).
When set, :meth:`__str__` formats as ``"<subcommand>: <stderr>"`` so
downstream consumers see which tmux command failed.

.. versionadded:: 0.57
"""

def __init__(
self,
*args: object,
subcommand: str | None = None,
) -> None:
super().__init__(*args)
self.subcommand = subcommand

def __str__(self) -> str:
"""Render with optional ``"<subcommand>: …"`` prefix."""
base = super().__str__()
if self.subcommand is None:
return base
return f"{self.subcommand}: {base}"


class DeprecatedError(LibTmuxException):
Expand Down
Loading
Loading