# API Reference Source: https://libtmux.git-pull.com/api/ (api)= (reference)= # API Reference libtmux's public API mirrors tmux's object hierarchy: `Server` → `Session` → `Window` → `Pane`. Attached terminals show up as `Client` objects accessed off the server. ## What do you want to do? ::::{grid} 1 2 2 2 :gutter: 2 :::{grid-item-card} Find a session, window, or pane? :link: libtmux.server :link-type: doc Use `server.sessions.get()`, `session.windows.get()`. ::: :::{grid-item-card} Send commands or keys to a terminal? :link: libtmux.pane :link-type: doc Use `pane.send_keys()` and `pane.enter()`. ::: :::{grid-item-card} Capture output from a pane? :link: libtmux.pane :link-type: doc Use `pane.capture_pane()`. ::: :::{grid-item-card} Write tests against tmux? :link: testing/index :link-type: doc Use the pytest plugin and test helpers. ::: :::: ## Core Objects ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Server :link: libtmux.server :link-type: doc Entry point. Manages sessions and executes raw tmux commands. ::: :::{grid-item-card} Session :link: libtmux.session :link-type: doc Manages windows within a tmux session. ::: :::{grid-item-card} Window :link: libtmux.window :link-type: doc Manages panes, layouts, and window operations. ::: :::{grid-item-card} Pane :link: libtmux.pane :link-type: doc 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 ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} Common :link: libtmux.common :link-type: doc Base classes and command execution. ::: :::{grid-item-card} Neo :link: libtmux.neo :link-type: doc Dataclass-based query interface. ::: :::{grid-item-card} Options :link: libtmux.options :link-type: doc tmux option get/set. ::: :::{grid-item-card} Hooks :link: libtmux.hooks :link-type: doc tmux hook management. ::: :::{grid-item-card} Constants :link: libtmux.constants :link-type: doc Format strings and constants. ::: :::{grid-item-card} Exceptions :link: libtmux.exc :link-type: doc Exception hierarchy. ::: :::: ## Testing ::::{grid} 1 1 1 1 :gutter: 2 :::{grid-item-card} Testing Utilities :link: testing/index :link-type: doc pytest plugin, fixtures, and test helpers for testing code that uses libtmux. ::: :::: ## API Policy and Guarantees These documents define the project's promises about the public API. ::::{grid} 1 2 3 3 :gutter: 2 :::{grid-item-card} Public API :link: ../project/public-api :link-type: doc What is and is not considered stable public API. ::: :::{grid-item-card} Compatibility :link: ../project/compatibility :link-type: doc Supported versions of Python and tmux. ::: :::{grid-item-card} Deprecations :link: ../project/deprecations :link-type: doc Active deprecations and migration guidance. ::: :::: ```{toctree} :hidden: :maxdepth: 1 Server Session Window Pane Client Common Neo Options Hooks Constants Exceptions ``` --- # Clients Source: https://libtmux.git-pull.com/api/libtmux.client/ (api-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 ``` --- # Utilities Source: https://libtmux.git-pull.com/api/libtmux.common/ # Utilities ```{eval-rst} .. automodule:: libtmux.common :members: ``` --- # Constants Source: https://libtmux.git-pull.com/api/libtmux.constants/ # Constants ```{eval-rst} .. automodule:: libtmux.constants :members: ``` --- # Exceptions Source: https://libtmux.git-pull.com/api/libtmux.exc/ # Exceptions ```{eval-rst} .. automodule:: libtmux.exc :members: ``` --- # Hooks Source: https://libtmux.git-pull.com/api/libtmux.hooks/ # Hooks ```{eval-rst} .. automodule:: libtmux.hooks :members: ``` --- # Properties Source: https://libtmux.git-pull.com/api/libtmux.neo/ (properties)= # Properties Get access to the data attributes behind tmux sessions, windows and panes. This is done through accessing the [formats][formats] available in `list-sessions`, `list-windows` and `list-panes`. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two: `python` or `ptpython` if you have it: ```console $ python ``` Import libtmux: ```python >>> import libtmux ``` Attach default tmux {class}`~libtmux.Server` to `t`: ```python >>> import libtmux >>> t = libtmux.Server() >>> t Server(socket_path=/tmp/tmux-.../default) ``` ## Session Get the {class}`~libtmux.Session` object: ```python >>> session = server.sessions[0] >>> session Session($1 libtmux_...) ``` Quick access to basic attributes: ```python >>> session.session_name 'libtmux_...' >>> session.session_id '$1' ``` To see all attributes for a session: ```python from libtmux.neo import Obj >>> sorted(list(Obj.__dataclass_fields__.keys())) ['session_attached', 'session_created', ...] ``` ```python >>> session.session_windows '...' ``` ## Windows The same concepts apply for {class}`~libtmux.Window`: ```python >>> window = session.active_window >>> window Window(@1 ...:..., Session($1 ...)) ``` Basics: ```python >>> window.window_name '...' >>> window.window_id '@1' >>> window.window_height '...' >>> window.window_width '...' ``` Use attribute access for details not accessible via properties: ```python >>> window.window_panes '1' ``` ## Panes Get the {class}`~libtmux.Pane`: ```python >>> pane = window.active_pane >>> pane Pane(%1 Window(@1 ...:..., Session($1 libtmux_...))) ``` Basics: ```python >>> pane.pane_current_command '...' >>> type(pane.pane_current_command) >>> pane.pane_height '...' >>> pane.pane_width '...' >>> pane.pane_index '0' ``` [formats]: http://man.openbsd.org/OpenBSD-5.9/man1/tmux.1#FORMATS --- # Options Source: https://libtmux.git-pull.com/api/libtmux.options/ # Options ```{eval-rst} .. automodule:: libtmux.options :members: ``` --- # Panes Source: https://libtmux.git-pull.com/api/libtmux.pane/ (panes)= # Panes - Contain [pseudoterminal]s ([pty(4)][pty(4)]) - Exist inside {ref}`Windows` - Identified by `%`, e.g. `%313` [pseudoterminal]: https://en.wikipedia.org/wiki/Pseudoterminal [pty(4)]: https://www.freebsd.org/cgi/man.cgi?query=pty&sektion=4 ```{eval-rst} .. autoclass:: libtmux.Pane :members: :inherited-members: :private-members: :show-inheritance: :member-order: bysource ``` --- # Servers Source: https://libtmux.git-pull.com/api/libtmux.server/ (servers)= # Servers - Identified by _socket path_ and _socket name_ - May have >1 servers running of tmux at the same time. - Contain {ref}`Sessions` (which contain {ref}`Windows`, which contain {ref}`Panes`) tmux initializes a server automatically on first running (e.g. executing `tmux`) ```{eval-rst} .. autoclass:: libtmux.Server :members: :inherited-members: :private-members: :show-inheritance: :member-order: bysource ``` --- # Sessions Source: https://libtmux.git-pull.com/api/libtmux.session/ (sessions)= # Sessions - Exist inside {ref}`Servers` - Contain {ref}`Windows` (which contain {ref}`Panes`) - Identified by `$`, e.g. `$313` ```{eval-rst} .. autoclass:: libtmux.Session :members: :inherited-members: :private-members: :show-inheritance: :member-order: bysource ``` --- # Windows Source: https://libtmux.git-pull.com/api/libtmux.window/ (windows)= # Windows - Exist inside {ref}`Sessions` - Contain {ref}`Panes` - Identified by `@`, e.g. `@313` ```{module} libtmux :no-index: ``` ```{eval-rst} .. autoclass:: Window :members: :inherited-members: :private-members: :show-inheritance: :member-order: bysource ``` --- # Testing Utilities Source: https://libtmux.git-pull.com/api/testing/ (testing)= # Testing Utilities Tools for testing code that uses libtmux. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} pytest Plugin :link: pytest-plugin/index :link-type: doc Fixtures for isolated tmux servers, sessions, windows, and panes in automated tests. ::: :::{grid-item-card} Test Helpers :link: test-helpers/index :link-type: doc Utilities for test setup: constants, environment mocking, retry logic, temporary resources. ::: :::: ```{toctree} :hidden: :maxdepth: 2 pytest-plugin/index test-helpers/index ``` --- # Fixtures Source: https://libtmux.git-pull.com/api/testing/pytest-plugin/fixtures/ (pytest_plugin_fixtures)= # Fixtures ## Quick Start Add a fixture name as a test parameter — pytest creates and injects it automatically. You never call fixtures yourself. ```python def test_basic(server: Server) -> None: session = server.new_session(session_name="my-session") assert session is not None def test_with_session(session: Session) -> None: window = session.new_window(window_name="test") assert window is not None ``` ## Which Fixture Do I Need? - Use {fixture}`session` when you want a ready-to-use tmux session. - Use {fixture}`server` when you want a bare server and will create sessions yourself. - Use {fixture}`TestServer` when you need multiple isolated servers in one test. - Override {fixture}`session_params` when you need custom session creation. - Override {fixture}`home_user_name` when you need a custom test user. - Request {fixture}`clear_env` when testing tmux behavior with a minimal environment. ## Fixture Summary ```{autofixture-index} libtmux.pytest_plugin ``` --- ## Core Fixtures The primary injection points for libtmux tests. ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.server .. rubric:: Example .. code-block:: python def test_server_sessions(server: Server) -> None: session = server.new_session(session_name="work") assert session.session_name == "work" .. autofixture:: libtmux.pytest_plugin.session .. rubric:: Example .. code-block:: python def test_session_windows(session: Session) -> None: window = session.new_window(window_name="editor") assert window.window_name == "editor" ``` ## Environment Fixtures Session-scoped fixtures that create an isolated filesystem environment. Shared across all tests in a session — created once, reused everywhere. ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.home_path .. autofixture:: libtmux.pytest_plugin.user_path .. autofixture:: libtmux.pytest_plugin.config_file .. autofixture:: libtmux.pytest_plugin.zshrc ``` ## Override Hooks Override these in your project's `conftest.py` to customise the test environment. ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.home_user_name :kind: override_hook .. autofixture:: libtmux.pytest_plugin.session_params :kind: override_hook .. rubric:: Example .. code-block:: python # conftest.py @pytest.fixture def session_params() -> dict: return {"x": 800, "y": 600} ``` ## Factories ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.TestServer .. autofixture:: libtmux.pytest_plugin.control_mode .. rubric:: Example .. code-block:: python def test_display_popup(control_mode) -> None: with control_mode() as ctl: # commands needing an attached client now work assert ctl.client_name != "" ``` ## Low-Level / Rarely Needed ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.clear_env ``` --- ## Configuration These `conf.py` values control how fixture documentation is rendered: ```{eval-rst} .. confval:: pytest_fixture_hidden_dependencies Fixture names to suppress from "Depends on" lists. Default: common pytest builtins (:external+pytest:std:fixture:`pytestconfig`, :external+pytest:std:fixture:`capfd`, :external+pytest:std:fixture:`capsysbinary`, :external+pytest:std:fixture:`capfdbinary`, :external+pytest:std:fixture:`recwarn`, :external+pytest:std:fixture:`tmpdir`, :external+pytest:std:fixture:`pytester`, :external+pytest:std:fixture:`testdir`, :external+pytest:std:fixture:`record_property`, ``record_xml_attribute``, :external+pytest:std:fixture:`record_testsuite_property`, :external+pytest:std:fixture:`cache`). .. confval:: pytest_fixture_builtin_links URL mapping for builtin fixture external links in "Depends on" blocks. Default: links to pytest docs for :external+pytest:std:fixture:`tmp_path_factory`, :external+pytest:std:fixture:`tmp_path`, :external+pytest:std:fixture:`monkeypatch`, :external+pytest:std:fixture:`request`, :external+pytest:std:fixture:`capsys`, :external+pytest:std:fixture:`caplog`. .. confval:: pytest_external_fixture_links URL mapping for external fixture cross-references. Default: ``{}``. ``` --- ```{note} All fixtures above are also auto-discoverable via: .. autofixtures:: libtmux.pytest_plugin :order: source Use ``autofixtures::`` in your own plugin docs to document all fixtures from a module without listing each one manually. ``` --- # pytest Plugin Source: https://libtmux.git-pull.com/api/testing/pytest-plugin/ (pytest_plugin)= # pytest Plugin libtmux's pytest plugin provides fixtures for isolated tmux servers, sessions, windows, and panes in automated tests. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Usage Guide :link: usage :link-type: doc Setup, configuration, custom session parameters, temporary servers. ::: :::{grid-item-card} Fixture Reference :link: fixtures :link-type: doc Complete autodoc for all fixtures and plugin API. ::: :::: ```{toctree} :hidden: :maxdepth: 1 usage fixtures ``` --- # Usage Guide Source: https://libtmux.git-pull.com/api/testing/pytest-plugin/usage/ (pytest_plugin_usage)= # Usage Guide libtmux provides pytest fixtures for tmux. The plugin automatically manages setup and teardown of an independent tmux server. ```{seealso} Using the pytest plugin? Do you want more flexibility? Correctness? Power? Defaults changed? [Connect with us] on the tracker, we want to know your case, we won't stabilize APIs until we're sure everything is by the book. [connect with us]: https://github.com/tmux-python/libtmux/discussions ``` ## Usage Install `libtmux` via the python package manager of your choosing, e.g. ```console $ pip install libtmux ``` The pytest plugin will be automatically detected via pytest, and the fixtures will be added. ### Real world usage View libtmux's own [tests/](https://github.com/tmux-python/libtmux/tree/master/tests) as well as tmuxp's [tests/](https://github.com/tmux-python/tmuxp/tree/master/tests). libtmux's tests `autouse` the {ref}`recommended-fixtures` above to ensure stable test execution, assertions and object lookups in the test grid. ## pytest-tmux `pytest-tmux` works through providing {ref}`pytest fixtures ` - so read up on those! The plugin's fixtures guarantee a fresh, headless {command}`tmux(1)` server, session, window, or pane is passed into your test. (recommended-fixtures)= ## Recommended fixtures These fixtures are automatically used when the plugin is enabled and `pytest` is run. - Creating temporary, test directories for: - `/home/` ({fixture}`home_path`) - `/home/${user}` ({fixture}`user_path`) - Default `.tmux.conf` configuration with these settings ({fixture}`config_file`): - `base-index -g 1` These are set to ensure panes and windows can be reliably referenced and asserted. (setting_a_tmux_configuration)= ## Setting a tmux configuration If you would like {fixture}`session ` to automatically use a configuration, you have a few options: - Pass a `config_file` into {class}`~libtmux.Server` - Set the `HOME` directory to a local or temporary pytest path with a configuration file You could also read the code and override {fixture}`server ` in your own doctest. (custom_session_params)= ### Custom session parameters You can override {fixture}`session_params` to customize the `session` fixture. The dictionary will directly pass into {meth}`Server.new_session` keyword arguments. ```python import pytest @pytest.fixture def session_params(): return { 'x': 800, 'y': 600 } def test_something(session): assert session ``` The above will assure the libtmux session launches with `-x 800 -y 600`. (temp_server)= ### Creating temporary servers If you need multiple independent tmux servers in your tests, the {fixture}`TestServer ` provides a factory that creates servers with unique socket names. Each server is automatically cleaned up when the test completes. ```python def test_something(TestServer): Server = TestServer() # Get unique partial'd Server server = Server() # Create server instance session = server.new_session() assert server.is_alive() ``` You can also use it with custom configurations, similar to the {ref}`server fixture `: ```python def test_with_config(TestServer, tmp_path): config_file = tmp_path / "tmux.conf" config_file.write_text("set -g status off") Server = TestServer() server = Server(config_file=str(config_file)) ``` This is particularly useful when testing interactions between multiple tmux servers or when you need to verify behavior across server restarts. (set_home)= ### Setting a temporary home directory ```python import pathlib import pytest @pytest.fixture(autouse=True, scope="function") def set_home( monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, ): monkeypatch.setenv("HOME", str(user_path)) ``` --- # Constants Source: https://libtmux.git-pull.com/api/testing/test-helpers/constants/ (test_helpers_constants)= # Constants Test-related constants used across libtmux test helpers. ```{eval-rst} .. automodule:: libtmux.test.constants :members: :undoc-members: :show-inheritance: :member-order: bysource ``` --- # Environment Source: https://libtmux.git-pull.com/api/testing/test-helpers/environment/ (test_helpers_environment)= # Environment Environment variable mocking utilities for tests. ```{eval-rst} .. automodule:: libtmux.test.environment :members: :undoc-members: :show-inheritance: :member-order: bysource ``` --- # Test Helpers Source: https://libtmux.git-pull.com/api/testing/test-helpers/ (test_helpers)= # Test Helpers Utilities for writing reliable tests against libtmux and downstream code that uses tmux. ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} Constants :link: constants :link-type: doc Predefined test constants. ::: :::{grid-item-card} Environment :link: environment :link-type: doc Environment variable mocking. ::: :::{grid-item-card} Random :link: random :link-type: doc Randomized name generators. ::: :::{grid-item-card} Retry :link: retry :link-type: doc Retry logic for async/tmux operations. ::: :::{grid-item-card} Temporary :link: temporary :link-type: doc Context managers for ephemeral tmux resources. ::: :::: ```{toctree} :hidden: :maxdepth: 1 constants environment random retry temporary ``` --- # Random Source: https://libtmux.git-pull.com/api/testing/test-helpers/random/ (test_helpers_random)= # Random Random string generation utilities for test names. ```{eval-rst} .. automodule:: libtmux.test.random :members: :undoc-members: :show-inheritance: :member-order: bysource ``` --- # Retry Utilities Source: https://libtmux.git-pull.com/api/testing/test-helpers/retry/ (test_helpers_retry)= # Retry Utilities Retry helper functions for libtmux test utilities. These utilities help manage testing operations that may require multiple attempts before succeeding. ## Basic Retry Functionality ```{eval-rst} .. automodule:: libtmux.test.retry :members: :undoc-members: :show-inheritance: :member-order: bysource ``` --- # Temporary Objects Source: https://libtmux.git-pull.com/api/testing/test-helpers/temporary/ (test_helpers_temporary_objects)= # Temporary Objects Context managers for temporary tmux objects (sessions, windows). ```{eval-rst} .. automodule:: libtmux.test.temporary :members: :undoc-members: :show-inheritance: :member-order: bysource ``` --- # Glossary Source: https://libtmux.git-pull.com/glossary/ (glossary)= # Glossary ```{glossary} tmuxp A tool to manage workspaces with tmux. A pythonic abstraction of tmux. tmux tmux(1) The tmux binary. Used internally to distinguish tmuxp is only a layer on top of tmux. kaptan configuration management library, see [kaptan on github](https://github.com/emre/kaptan). Server Tmux runs in the background of your system as a process. The server holds multiple {term}`Session`. By default, tmux automatically starts the server the first time ``$ tmux`` is run. A server contains {term}`session`'s. tmux starts the server automatically if it's not running. Advanced cases: multiple can be run by specifying ``[-L socket-name]`` and ``[-S socket-path]``. Client Attaches to a tmux {term}`server`. When you use tmux through CLI, you are using tmux as a client. Session Inside a tmux {term}`server`. The session has 1 or more {term}`Window`. The bottom bar in tmux show a list of windows. Normally they can be navigated with ``Ctrl-a [0-9]``, ``Ctrl-a n`` and ``Ctrl-a p``. Sessions can have a ``session_name``. Uniquely identified by ``session_id``. Window Entity of a {term}`session`. Can have 1 or more {term}`pane`. Panes can be organized with a layouts. Windows can have names. Pane Linked to a {term}`Window`. a pseudoterminal. Target A target, cited in the manual as ``[-t target]`` can be a session, window or pane. ``` --- # Changelog Source: https://libtmux.git-pull.com/history/ (changes)= (changelog)= (history)= ```{currentmodule} libtmux ``` ```{include} ../CHANGES ``` --- # libtmux Source: https://libtmux.git-pull.com/ (index)= # libtmux Typed Python API for [tmux](https://github.com/tmux/tmux). Control servers, sessions, windows, and panes as Python objects. ::::{grid} 1 1 3 3 :gutter: 2 2 3 3 :::{grid-item-card} Quickstart :link: quickstart :link-type: doc Install and make your first API call in 5 minutes. ::: :::{grid-item-card} Topics :link: topics/index :link-type: doc Architecture, traversal, filtering, and automation patterns. ::: :::{grid-item-card} API Reference :link: api/index :link-type: doc Every public class, function, and exception. ::: :::{grid-item-card} Testing :link: api/testing/index :link-type: doc pytest plugin and test helpers for isolated tmux environments. ::: :::{grid-item-card} Contributing :link: project/index :link-type: doc Development setup, code style, release process. ::: :::: ## Install ```console $ pip install libtmux ``` ```console $ uv add libtmux ``` Tip: libtmux is pre-1.0. Pin to a range: `libtmux>=0.55,<0.56` See [Quickstart](quickstart.md) for all methods and first steps. ## At a glance ```python import libtmux server = libtmux.Server() session = server.sessions.get(session_name="my-project") window = session.active_window pane = window.split() pane.send_keys("echo hello") ``` ``` Server → Session → Window → Pane ``` Every level of the [tmux hierarchy](topics/architecture.md) is a typed Python object with traversal, filtering, and command execution. | Object | What it wraps | |--------|---------------| | {class}`~libtmux.server.Server` | tmux server / socket | | {class}`~libtmux.session.Session` | tmux session | | {class}`~libtmux.window.Window` | tmux window | | {class}`~libtmux.pane.Pane` | tmux pane | ## Testing libtmux ships a [pytest plugin](api/testing/pytest-plugin/index.md) with isolated tmux fixtures: ```python def test_my_tool(session): window = session.new_window(window_name="test") pane = window.active_pane pane.send_keys("echo hello") assert window.window_name == "test" ``` ```{toctree} :hidden: quickstart topics/index api/index api/testing/index internals/index project/index history migration glossary MCP GitHub ``` --- # Internal Constants - libtmux._internal.constants Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.constants/ # Internal Constants - `libtmux._internal.constants` :::{warning} Be careful with these! These constants are private, internal as they're **not** covered by version policies. They can break or be removed between minor versions! If you need a data structure here made public or stabilized please [file an issue](https://github.com/tmux-python/libtmux/issues). ::: ```{eval-rst} .. automodule:: libtmux._internal.constants :members: :undoc-members: :inherited-members: :show-inheritance: ``` --- # Dataclass helpers - libtmux._internal.dataclasses Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.dataclasses/ # Dataclass helpers - `libtmux._internal.dataclasses` ```{eval-rst} .. automodule:: libtmux._internal.dataclasses :members: :special-members: ``` --- # List querying - libtmux._internal.query_list Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.query_list/ # List querying - `libtmux._internal.query_list` ```{eval-rst} .. automodule:: libtmux._internal.query_list :members: ``` --- # Internal Sparse Array - libtmux._internal.sparse_array Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.sparse_array/ # Internal Sparse Array - `libtmux._internal.sparse_array` :::{warning} Be careful with these! Internal APIs are **not** covered by version policies. They can break or be removed between minor versions! If you need an internal API stabilized please [file an issue](https://github.com/tmux-python/libtmux/issues). ::: ```{eval-rst} .. automodule:: libtmux._internal.sparse_array :members: :undoc-members: :show-inheritance: ``` --- # Internals Source: https://libtmux.git-pull.com/internals/ (internals)= # Internals :::{danger} **No stability guarantee.** Internal APIs are **not** covered by version policies. They can break or be removed between any minor versions without notice. If you need an internal API stabilized please [file an issue](https://github.com/tmux-python/libtmux/issues). ::: ::::{grid} 1 2 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Dataclass helpers :link: api/libtmux._internal.dataclasses :link-type: doc Typed dataclass utilities used across internal modules. ::: :::{grid-item-card} Query List :link: api/libtmux._internal.query_list :link-type: doc List filtering and attribute-based querying. ::: :::{grid-item-card} Constants :link: api/libtmux._internal.constants :link-type: doc Internal format strings and tmux constants. ::: :::{grid-item-card} Sparse Array :link: api/libtmux._internal.sparse_array :link-type: doc Sparse array data structure for tmux format parsing. ::: :::: ```{toctree} :hidden: :maxdepth: 1 api/libtmux._internal.dataclasses api/libtmux._internal.query_list api/libtmux._internal.constants api/libtmux._internal.sparse_array ``` ## Environmental variables (LIBTMUX_TMUX_FORMAT_SEPARATOR)= ### tmux format separator ```{versionadded} 0.11.0b0 ``` `LIBTMUX_TMUX_FORMAT_SEPARATOR` can be used to override the default string used to split `tmux(1)`'s formatting information. If you find any compatibility problems with the default, or better yet find a string copacetic many environments and tmux releases, note it at . --- # Migration notes Source: https://libtmux.git-pull.com/migration/ (migration)= ```{currentmodule} libtmux ``` ```{include} ../MIGRATION ``` --- # Code Style Source: https://libtmux.git-pull.com/project/code-style/ # Code Style ## Formatting libtmux uses [ruff](https://github.com/astral-sh/ruff) for both linting and formatting. ```console $ uv run ruff format . ``` ```console $ uv run ruff check . --fix --show-fixes ``` ## Type Checking Strict [mypy](https://mypy-lang.org/) is enforced across `src/` and `tests/`. ```console $ uv run mypy ``` ## Docstrings All public functions and methods use NumPy-style docstrings. See the [NumPy docstring guide](https://numpydoc.readthedocs.io/en/latest/format.html). ## Imports - Standard library: namespace imports (`import pathlib`, not `from pathlib import Path`) - Exception: `from dataclasses import dataclass, field` - Typing: `import typing as t`, access via `t.Optional`, `t.NamedTuple`, etc. - All files: `from __future__ import annotations` --- # Compatibility Source: https://libtmux.git-pull.com/project/compatibility/ # Compatibility ## Python - **Minimum**: Python 3.10 - **Tested**: Python 3.10, 3.11, 3.12, 3.13 - **Maximum**: Python < 4.0 ## tmux - **Minimum**: tmux 3.2a - **Tested**: latest stable tmux release - libtmux uses tmux's format system and control mode -- older tmux versions may lack required format variables ## Platforms | Platform | Status | |----------|--------| | Linux | Fully supported | | macOS | Fully supported | | WSL / WSL2 | Supported (tmux runs inside WSL) | | Windows (native) | Not supported (tmux does not run natively on Windows) | ## Known Limitations - tmux must be running and accessible via the default socket or a specified socket - Some operations require the tmux server to have at least one session - Format string availability depends on tmux version --- # Development Source: https://libtmux.git-pull.com/project/contributing/ # Development Install [git] and [uv] Clone: ```console $ git clone https://github.com/tmux-python/libtmux.git ``` ```console $ cd libtmux ``` Install packages: ```console $ uv sync --all-extras --dev ``` [installation documentation]: https://docs.astral.sh/uv/getting-started/installation/ [git]: https://git-scm.com/ [uv]: https://github.com/astral-sh/uv Makefile commands prefixed with `watch_` will watch files and rerun. ## Tests ```console $ uv run py.test ``` ### Helpers ```console $ make test ``` Rerun tests on file change: ```console $ make watch_test ``` (requires [entr(1)]) ### Pytest plugin :::{seealso} See {ref}`pytest_plugin`. ::: ## Documentation Default preview server: http://localhost:8023 [sphinx-autobuild] will automatically build the docs, watch for file changes and launch a server. From home directory: ```console $ make start_docs ``` From inside `docs/`: ```console $ make start ``` [sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild ### Manual documentation (the hard way) ```console $ cd docs/ && make html ``` to build. ```console $ make serve ``` to start http server. Helpers: Build docs: ```console $ make build_docs ``` Serve docs: ```console $ make serve_docs ``` Rebuild docs on file change: ```console $ make watch_docs ``` (requires [entr(1)]) Rebuild docs and run server via one terminal: ```console $ make dev_docs ``` (requires above, and {command}`make(1)` with `-J` support, e.g. GNU Make) ## Linting ### ruff The project uses [ruff] to handle formatting, sorting imports and linting. ````{tab} Command uv: ```console $ uv run ruff ``` If you setup manually: ```console $ ruff check . ``` ```` ````{tab} make ```console $ make ruff ``` ```` ````{tab} Watch ```console $ make watch_ruff ``` requires [entr(1)]. ```` ````{tab} Fix files uv: ```console $ uv run ruff check . --fix ``` If you setup manually: ```console $ ruff check . --fix ``` ```` #### ruff format [ruff format] is used for formatting. ````{tab} Command uv: ```console $ uv run ruff format . ``` If you setup manually: ```console $ ruff format . ``` ```` ````{tab} make ```console $ make ruff_format ``` ```` ### mypy [mypy] is used for static type checking. ````{tab} Command uv: ```console $ uv run mypy . ``` If you setup manually: ```console $ mypy . ``` ```` ````{tab} make ```console $ make mypy ``` ```` ````{tab} Watch ```console $ make watch_mypy ``` requires [entr(1)]. ```` ## Releasing Since this software is used by tens of thousands of users daily, we don't want to release breaking changes. Additionally this is packaged on large Linux/BSD distros, so we must be mindful of architectural changes. Choose what the next version is. Assuming it's version 0.9.0, it could be: - 0.9.0post0: postrelease, if there was a packaging issue - 0.9.1: bugfix / security / tweak - 0.10.0: breaking changes, new features Let's assume we pick 0.9.1 `CHANGES`: Assure any PRs merged since last release are mentioned. Give a thank you to the contributor. Set the header with the new version and the date. Leave the "current" header and _Insert changes/features/fixes for next release here_ at the top: ``` current ------- - *Insert changes/features/fixes for next release here* libtmux 0.9.1 (2020-10-12) -------------------------- - :issue:`1`: Fix bug ``` `libtmux/__init__.py` and `__about__.py` - Set version ```console $ git commit -m 'Tag v0.9.1' ``` ```console $ git tag v0.9.1 ``` After `git push` and `git push --tags`, CI will automatically build and deploy to PyPI. ### Releasing via GitHub Actions (manual) This isn't used yet since package maintainers may want setup.py in the source. See https://github.com/tmux-python/tmuxp/issues/625. As of v0.10, [uv] handles virtualenv creation, package requirements, versioning, building, and publishing. Therefore there is no setup.py or requirements files. Update `__version__` in `__about__.py` and `pyproject.toml`: ```console $ git commit -m 'build(libtmux): Tag v0.1.1' ``` ```console $ git tag v0.1.1 ``` ```console $ git push ``` ```console $ git push --tags ``` [twine]: https://twine.readthedocs.io/ [uv]: https://github.com/astral-sh/uv [entr(1)]: http://eradman.com/entrproject/ [ruff format]: https://docs.astral.sh/ruff/formatter/ [ruff]: https://ruff.rs [mypy]: http://mypy-lang.org/ --- # Deprecations Source: https://libtmux.git-pull.com/project/deprecations/ # Deprecations Active deprecations with timeline and migration paths. ## Active Deprecations No active deprecations at this time. See [history](../history.md) for past changes and the [migration guide](../migration.md) for upgrading between versions. ## Deprecation Policy See [Public API -- Deprecation Process](public-api.md#deprecation-process). --- # Project Source: https://libtmux.git-pull.com/project/ (project)= # Project Project guides, compatibility information, and API governance. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Contributing :link: contributing :link-type: doc Development setup, running tests, submitting PRs. ::: :::{grid-item-card} Code Style :link: code-style :link-type: doc Ruff, mypy, NumPy docstrings, import conventions. ::: :::{grid-item-card} Releasing :link: releasing :link-type: doc Release checklist and version policy. ::: :::: ## API Governance ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} Public API :link: public-api :link-type: doc What's public, stability policy, deprecation process. ::: :::{grid-item-card} Compatibility :link: compatibility :link-type: doc Python, tmux, and platform support. ::: :::{grid-item-card} Deprecations :link: deprecations :link-type: doc Active deprecations and migration guidance. ::: :::: ```{toctree} :hidden: contributing code-style releasing public-api compatibility deprecations ``` --- # Public API Source: https://libtmux.git-pull.com/project/public-api/ # Public API ## What Is Public Every module documented under [API Reference](index.md) is public API. This includes: ### Core Library | Module | Import Path | |--------|-------------| | Server | `from libtmux.server import Server` | | Session | `from libtmux.session import Session` | | Window | `from libtmux.window import Window` | | Pane | `from libtmux.pane import Pane` | | Common | `from libtmux.common import ...` | | Neo | `from libtmux.neo import ...` | | Options | `from libtmux.options import ...` | | Hooks | `from libtmux.hooks import ...` | | Constants | `from libtmux.constants import ...` | | Exceptions | `from libtmux.exc import ...` | ### Test Utilities | Module | Import Path | |--------|-------------| | Test helpers | `from libtmux.test import ...` | | Pytest plugin | `libtmux.pytest_plugin` (auto-loaded) | ## What Is Internal Modules under `libtmux._internal` and `libtmux._vendor` are **not public**. They may change or be removed without notice between any release. Do not import from: - `libtmux._internal.*` - `libtmux._vendor.*` ## Pre-1.0 Stability Policy libtmux is pre-1.0. This means: - **Minor versions** (0.x -> 0.y) may include breaking API changes - **Patch versions** (0.x.y -> 0.x.z) are bug fixes only - **Pin your dependency**: use `libtmux>=0.55,<0.56` or `libtmux~=0.55.0` Breaking changes are documented in the [changelog](../history.md) and the [deprecations](deprecations.md) page before removal. ## Deprecation Process Before removing or changing public API: 1. A deprecation warning is added for at least one minor release 2. The change is documented in [deprecations](deprecations.md) 3. Migration guidance is provided 4. The old API is removed in a subsequent minor release --- # Releasing Source: https://libtmux.git-pull.com/project/releasing/ # Releasing ## Version Policy libtmux is pre-1.0. Minor version bumps may include breaking API changes. Users should pin to `>=0.x,<0.y`. ## Release Process Releases are triggered by git tags and published to PyPI via OIDC trusted publishing. 1. Update `CHANGES` with the release notes 2. Bump version in `src/libtmux/__about__.py` 3. Commit: ```console $ git commit -m "libtmux " ``` 4. Tag: ```console $ git tag v ``` 5. Push: ```console $ git push && git push --tags ``` 6. CI builds and publishes to PyPI automatically via trusted publishing ## Changelog Format The `CHANGES` file uses this format: ```text libtmux () -------------------------- ### What's new - Description of feature (#issue) ### Bug fixes - Description of fix (#issue) ### Breaking changes - Description of break, migration path (#issue) ``` --- # Quickstart Source: https://libtmux.git-pull.com/quickstart/ (quickstart)= # Quickstart libtmux allows for developers and system administrators to control live tmux sessions using python code. In this example, we will launch a tmux session and control the windows from inside a live tmux session. (requirements)= ## Requirements - [tmux] 3.2a or newer - [pip] - for this handbook's examples [tmux]: https://tmux.github.io/ (installation)= ## Installation Next, ensure `libtmux` is installed: ```console $ pip install --user libtmux ``` (developmental-releases)= ### Developmental releases New versions of libtmux are published to PyPI as alpha, beta, or release candidates. In their versions you will see notification like `a1`, `b1`, and `rc1`, respectively. `1.10.0b4` would mean the 4th beta release of `1.10.0` before general availability. - [pip]\: ```console $ pip install --user --upgrade --pre libtmux ``` - [pipx]\: ```console $ pipx install \ --suffix=@next \ --pip-args '\--pre' \ --force \ 'libtmux' ``` Usage: `libtmux@next [command]` - [uv tool install][uv-tools]\: ```console $ uv tool install --prerelease=allow libtmux ``` - [uv]\: ```console $ uv add libtmux --prerelease allow ``` - [uvx]\: ```console $ uvx --from 'libtmux' --prerelease allow python ``` via trunk (can break easily): - [pip]\: ```console $ pip install --user -e git+https://github.com/tmux-python/libtmux.git#egg=libtmux ``` - [pipx]\: ```console $ pipx install \ --suffix=@master \ --force \ 'libtmux @ git+https://github.com/tmux-python/libtmux.git@master' ``` - [uv]\: ```console $ uv tool install libtmux --from git+https://github.com/tmux-python/libtmux.git ``` [pip]: https://pip.pypa.io/en/stable/ [pipx]: https://pypa.github.io/pipx/docs/ [uv]: https://docs.astral.sh/uv/ [uv-tools]: https://docs.astral.sh/uv/concepts/tools/ [uvx]: https://docs.astral.sh/uv/guides/tools/ [ptpython]: https://github.com/prompt-toolkit/ptpython ## Start a tmux session Now, let's open a tmux session. ```console $ tmux new-session -n bar -s foo ``` This tutorial will be using the session and window name in the example. Window name `-n`: `bar` Session name `-s`: `foo` ## Control tmux via python :::{seealso} {ref}`api` ::: ```console $ python ``` For commandline completion, you can also use [ptpython]. ```console $ pip install --user ptpython ``` ```console $ ptpython ``` ```{module} libtmux :no-index: ``` First, we can grab a {class}`Server`. ```python >>> import libtmux >>> server = libtmux.Server() >>> server Server(socket_path=/tmp/tmux-.../default) ``` :::{tip} You can also use [tmuxp]'s [`tmuxp shell`] to drop straight into your current tmux server / session / window pane. [tmuxp]: https://tmuxp.git-pull.com/ [`tmuxp shell`]: https://tmuxp.git-pull.com/cli/shell.html ::: :::{note} You can specify a `socket_name`, `socket_path` and `config_file` in your server object. `libtmux.Server(socket_name='mysocket')` is equivalent to `$ tmux -L mysocket`. ::: `server` is now a living object bound to the tmux server's Sessions, Windows and Panes. ## Raw, contextual commands New session: ```python >>> server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0] '$2' ``` ```python >>> session.cmd('new-window', '-P').stdout[0] 'libtmux...:2.0' ``` From raw command output, to a rich {class}`Window` object (in practice and as shown later, you'd use {meth}`Session.new_window()`): ```python >>> Window.from_window_id(window_id=session.cmd('new-window', '-P', '-F#{window_id}').stdout[0], server=session.server) Window(@2 2:..., Session($1 libtmux_...)) ``` Create a pane from a window: ```python >>> window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0] '%2' ``` Raw output directly to a {class}`Pane` (in practice, you'd use {meth}`Window.split()`): ```python >>> Pane.from_pane_id(pane_id=window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server) Pane(%... Window(@1 1:..., Session($1 libtmux_...))) ``` ## Find your {class}`Session` If you have multiple tmux sessions open, all methods in {class}`Server` are available. We can list sessions with {meth}`Server.sessions`: ```python >>> server.sessions [Session($1 ...), Session($0 ...)] ``` This returns a list of {class}`Session` objects you can grab. We can find our current session with: ```python >>> server.sessions[0] Session($1 ...) ``` However, this isn't guaranteed, libtmux works against current tmux information, the session's name could be changed, or another tmux session may be created, so {meth}`Server.sessions` and {meth}`Server.windows` exist as a lookup. ## Get session by ID tmux sessions use the `$[0-9]` convention as a way to identify sessions. `$1` is whatever the ID `sessions()` returned above. ```python >>> server.sessions.filter(session_id='$1')[0] Session($1 ...) ``` You may `session = server.get_by_id('$')` to use the session object. ## Get session by name / other properties ```python >>> server.sessions[0].rename_session('foo') Session($1 foo) >>> server.sessions.filter(session_name="foo")[0] Session($1 foo) >>> server.sessions.get(session_name="foo") Session($1 foo) ``` With `filter`, pass in attributes and return a list of matches. In this case, a {class}`Server` holds a collection of child {class}`Session`. {class}`Session` and {class}`Window` both utilize `filter` to sift through Windows and Panes, respectively. So you may now use: ```python >>> server.sessions[0].rename_session('foo') Session($1 foo) >>> session = server.sessions.get(session_name="foo") >>> session Session($1 foo) ``` to give us a `session` object to play with. ## Playing with our tmux session We now have access to `session` from above with all of the methods available in {class}`Session`. Let's make a {meth}`Session.new_window`, in the background: ```python >>> session.new_window(attach=False, window_name="ha in the bg") Window(@2 ...:ha in the bg, Session($1 ...)) ``` So a few things: 1. `attach=False` meant to create a new window, but not to switch to it. It is the same as `$ tmux new-window -d`. 2. `window_name` may be specified. 3. Returns the {class}`Window` object created. :::{note} Use the API reference {ref}`api` for more commands. ::: Let's delete that window ({meth}`Session.kill_window`). Method 1: Use passthrough to tmux's `target` system. ```python >>> session.kill_window(window.window_id) ``` The window in the bg disappeared. This was the equivalent of `$ tmux kill-window -t'ha in'` Internally, tmux uses `target`. Its specific behavior depends on what the target is, view the tmux manpage for more information: ``` This section contains a list of the commands supported by tmux. Most commands accept the optional -t argument with one of target-client, target-session, target-window, or target-pane. ``` In this case, you can also go back in time and recreate the window again. The CLI should have history, so navigate up with the arrow key. ```python >>> session.new_window(attach=False, window_name="ha in the bg") Window(@2 ...:ha in the bg, Session($1 ...)) ``` Try to kill the window by the matching id `@[0-9999]`. ```python >>> session.new_window(attach=False, window_name="ha in the bg") Window(@2 ...:ha in the bg, Session($1 ...)) >>> session.kill_window('ha in the bg') ``` In addition, you could also `.kill_window` direction from the {class}`Window` object: ```python >>> window = session.new_window(attach=False, window_name="check this out") >>> window Window(@2 2:check this out, Session($1 ...)) ``` And kill: ```python >>> window.kill() ``` Use {meth}`Session.windows` and {meth}`Session.windows.filter()` to list and sort through active {class}`Window`'s. ## Manipulating windows Now that we know how to create windows, let's use one. Let's use {meth}`Session.active_window()` to grab our current window. ```python >>> window = session.active_window ``` `window` now has access to all of the objects inside of {class}`Window`. Let's create a pane, {meth}`Window.split`: ```python >>> window.split(attach=False) Pane(%2 Window(@1 ...:..., Session($1 ...))) ``` Powered up. Let's have a break down: 1. `window = session.active_window()` gave us the {class}`Window` of the current attached to window. 2. `attach=False` assures the cursor didn't switch to the newly created pane. 3. Returned the created {class}`Pane`. Also, since you are aware of this power, let's commemorate the experience: ```python >>> window.rename_window('libtmuxower') Window(@1 ...:..., Session($1 ...)) ``` You should have noticed {meth}`Window.rename_window` renamed the window. ## Moving cursor across windows and panes You have two ways you can move your cursor to new sessions, windows and panes. For one, arguments such as `attach=False` can be omittted. ```python >>> pane = window.split() ``` This gives you the {class}`Pane` along with moving the cursor to a new window. You can also use the `.select_*` available on the object, in this case the pane has {meth}`Pane.select()`. ```python >>> pane = window.split(attach=False) ``` ```python >>> pane.select() Pane(%1 Window(@1 ...:..., Session($1 ...))) ``` ```{eval-rst} .. todo:: create a ``kill_pane()`` method. ``` ```{eval-rst} .. todo:: have a ``.kill()`` and ``.select()`` proxy for Server, Session, Window and Pane objects. ``` ## Sending commands to tmux panes remotely As long as you have the object, or are iterating through a list of them, you can use `.send_keys`. ```python >>> window = session.new_window(attach=False, window_name="test") >>> pane = window.split(attach=False) >>> pane.send_keys('echo hey', enter=False) ``` See the other window, notice that {meth}`Pane.send_keys` has "`echo hey`" written, _still in the prompt_. `enter=False` can be used to send keys without pressing return. In this case, you may leave it to the user to press return himself, or complete a command using {meth}`Pane.enter()`: ```python >>> pane.enter() Pane(%1 ...) ``` ### Avoid cluttering shell history `suppress_history=True` can send commands to pane windows and sessions **without** them being visible in the history. ```python >>> pane.send_keys('echo Howdy', enter=True, suppress_history=True) ``` In this case, {meth}`Pane.send_keys` has " `echo Howdy`" written, automatically sent, the leading space character prevents adding it to the user's shell history. Omitting `enter=false` means the default behavior (sending the command) is done, without needing to use `pane.enter()` after. ## Working with options libtmux provides a unified API for managing tmux options across Server, Session, Window, and Pane objects. ### Getting options ```python >>> server.show_option('buffer-limit') 50 >>> window.show_options() # doctest: +ELLIPSIS {...} ``` ### Setting options ```python >>> window.set_option('automatic-rename', False) # doctest: +ELLIPSIS Window(@... ...) >>> window.show_option('automatic-rename') False >>> window.unset_option('automatic-rename') # doctest: +ELLIPSIS Window(@... ...) ``` :::{seealso} See {ref}`options-and-hooks` for more details on options and hooks. ::: ## Final notes These objects created use tmux's internal usage of ID's to make servers, sessions, windows and panes accessible at the object level. You don't have to see the tmux session to be able to orchestrate it. After all, {class}`WorkspaceBuilder` uses these same internals to build your sessions in the background. :) :::{seealso} If you want to dig deeper, check out {ref}`API`, the code for and our [test suite] (see {ref}`development`.) ::: [workspacebuilder.py]: https://github.com/tmux-python/libtmux/blob/master/libtmux/workspacebuilder.py [test suite]: https://github.com/tmux-python/libtmux/tree/master/tests --- # Architecture Source: https://libtmux.git-pull.com/topics/architecture/ (about)= # Architecture libtmux is a [typed](https://docs.python.org/3/library/typing.html) abstraction layer for tmux. It builds upon tmux's concept of targets (`-t`) to direct commands against individual sessions, windows, and panes, and `FORMATS` — template variables tmux exposes to describe object properties. ## Object Hierarchy libtmux mirrors tmux's object hierarchy as a typed Python ORM: ``` Server ├── Session │ └── Window │ └── Pane └── Client (attached view) ``` | Object | Child | Parent | |--------|-------|--------| | {class}`~libtmux.server.Server` | {class}`~libtmux.session.Session`, {class}`~libtmux.client.Client` | None | | {class}`~libtmux.session.Session` | {class}`~libtmux.window.Window` | {class}`~libtmux.server.Server` | | {class}`~libtmux.window.Window` | {class}`~libtmux.pane.Pane` | {class}`~libtmux.session.Session` | | {class}`~libtmux.pane.Pane` | None | {class}`~libtmux.window.Window` | | {class}`~libtmux.client.Client` | None | {class}`~libtmux.server.Server` | {class}`~libtmux.common.TmuxRelationalObject` acts as the base container connecting these relationships. {class}`~libtmux.Client` is a *view*, not part of the ownership chain: each attached terminal points at a Session/Window/Pane it is currently displaying, but is not owned by them. See {ref}`clients` for the view- vs-identity distinction. ## Internal Identifiers tmux assigns unique IDs to sessions, windows, and panes. libtmux uses these — via {class}`~libtmux.common.TmuxMappingObject` — to track objects reliably across state refreshes. | Object | Prefix | Example | |--------|--------|---------| | {class}`~libtmux.server.Server` | N/A | Uses `socket-name` / `socket-path` | | {class}`~libtmux.session.Session` | `$` | `$13` | | {class}`~libtmux.window.Window` | `@` | `@3243` | | {class}`~libtmux.pane.Pane` | `%` | `%5433` | ## Core Objects Each level wraps tmux commands and format queries: - {class}`~libtmux.server.Server` — entry point, manages sessions, executes raw tmux commands - {class}`~libtmux.session.Session` — manages windows within a session - {class}`~libtmux.window.Window` — manages panes, handles layouts - {class}`~libtmux.pane.Pane` — terminal instance, sends keys and captures output - {class}`~libtmux.client.Client` — attached terminal viewing a session, window, and pane ## Data Flow 1. User creates a `Server` (connects to a running tmux server) 2. Queries use tmux format strings ({mod}`libtmux.constants`) to fetch state 3. Results are parsed into typed Python objects 4. Mutations dispatch tmux commands via the `cmd()` method 5. Objects refresh state from tmux on demand ## Module Map | Module | Role | |--------|------| | {mod}`libtmux.server` | Server connection and session management | | {mod}`libtmux.session` | Session operations | | {mod}`libtmux.window` | Window operations and pane management | | {mod}`libtmux.pane` | Pane I/O and capture | | {mod}`libtmux.client` | Attached-client view and live-attachment lookup | | {mod}`libtmux.common` | Base classes, command execution | | {mod}`libtmux.neo` | Modern dataclass-based query interface | | {mod}`libtmux.constants` | Format string constants | | {mod}`libtmux.options` | tmux option get/set | | {mod}`libtmux.hooks` | tmux hook management | | {mod}`libtmux.exc` | Exception hierarchy | ## Naming Conventions tmux commands use dashes (`new-window`). libtmux replaces these with underscores (`new_window`) to follow Python naming conventions. ## References - [tmux man page](https://man.openbsd.org/tmux.1) - [tmux source code](https://github.com/tmux/tmux) --- # Automation Patterns Source: https://libtmux.git-pull.com/topics/automation_patterns/ (automation-patterns)= # Automation Patterns libtmux is ideal for automating terminal workflows, orchestrating multiple processes, and building agentic systems that interact with terminal applications. This guide covers practical patterns for automation use cases. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` ## Process Control ### Starting long-running processes ```python >>> import time >>> proc_window = session.new_window(window_name='process', attach=False) >>> proc_pane = proc_window.active_pane >>> # Start a background process >>> proc_pane.send_keys('sleep 2 && echo "Process complete"') >>> # Process is running >>> time.sleep(0.1) >>> proc_window.window_name 'process' >>> # Clean up >>> proc_window.kill() ``` ### Checking process status ```python >>> import time >>> status_window = session.new_window(window_name='status-check', attach=False) >>> status_pane = status_window.active_pane >>> def is_process_running(pane, marker='RUNNING'): ... """Check if a marker indicates process is still running.""" ... output = pane.capture_pane() ... return marker in '\\n'.join(output) >>> # Start and mark a process >>> status_pane.send_keys('echo "RUNNING"; sleep 0.3; echo "DONE"') >>> time.sleep(0.1) >>> # Check while running >>> 'RUNNING' in '\\n'.join(status_pane.capture_pane()) True >>> # Wait for completion >>> time.sleep(0.5) >>> 'DONE' in '\\n'.join(status_pane.capture_pane()) True >>> # Clean up >>> status_window.kill() ``` ## Output Monitoring ### Waiting for specific output ```python >>> import time >>> monitor_window = session.new_window(window_name='monitor', attach=False) >>> monitor_pane = monitor_window.active_pane >>> def wait_for_output(pane, text, timeout=5.0, poll_interval=0.1): ... """Wait for specific text to appear in pane output.""" ... start = time.time() ... while time.time() - start < timeout: ... output = '\\n'.join(pane.capture_pane()) ... if text in output: ... return True ... time.sleep(poll_interval) ... return False >>> monitor_pane.send_keys('sleep 0.2; echo "READY"') >>> wait_for_output(monitor_pane, 'READY', timeout=2.0) True >>> # Clean up >>> monitor_window.kill() ``` ### Detecting errors in output ```python >>> import time >>> error_window = session.new_window(window_name='error-check', attach=False) >>> error_pane = error_window.active_pane >>> def check_for_errors(pane, patterns=None): ... """Check pane output for error patterns.""" ... if patterns is None: ... patterns = ['Error:', 'error:', 'ERROR', 'FAILED', 'Exception'] ... output = '\\n'.join(pane.capture_pane()) ... for pattern in patterns: ... if pattern in output: ... return pattern ... return None >>> # Test with successful output >>> error_pane.send_keys('echo "Success!"') >>> time.sleep(0.1) >>> check_for_errors(error_pane) is None True >>> # Clean up >>> error_window.kill() ``` ### Capturing output between markers ```python >>> import time >>> capture_window = session.new_window(window_name='capture', attach=False) >>> capture_pane = capture_window.active_pane >>> def capture_after_marker(pane, marker, timeout=5.0): ... """Capture output after a marker appears.""" ... start_time = time.time() ... while time.time() - start_time < timeout: ... lines = pane.capture_pane() ... output = '\\n'.join(lines) ... if marker in output: ... # Return all lines after the marker ... found = False ... result = [] ... for line in lines: ... if marker in line: ... found = True ... continue ... if found: ... result.append(line) ... return result ... time.sleep(0.1) ... return None >>> # Test marker capture >>> capture_pane.send_keys('echo "MARKER"; echo "captured data"') >>> time.sleep(0.3) >>> result = capture_after_marker(capture_pane, 'MARKER', timeout=2.0) >>> any('captured' in line for line in (result or [])) True >>> # Clean up >>> capture_window.kill() ``` ## Multi-Pane Orchestration ### Running parallel tasks ```python >>> import time >>> from libtmux.constants import PaneDirection >>> parallel_window = session.new_window(window_name='parallel', attach=False) >>> parallel_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> pane1 = parallel_window.active_pane >>> pane2 = pane1.split(direction=PaneDirection.Right) >>> pane3 = pane1.split(direction=PaneDirection.Below) >>> # Start tasks in parallel >>> tasks = [ ... (pane1, 'echo "Task 1"; sleep 0.2; echo "DONE1"'), ... (pane2, 'echo "Task 2"; sleep 0.1; echo "DONE2"'), ... (pane3, 'echo "Task 3"; sleep 0.3; echo "DONE3"'), ... ] >>> for pane, cmd in tasks: ... pane.send_keys(cmd) >>> # Wait for all tasks >>> time.sleep(0.5) >>> # Verify all completed >>> all('DONE' in '\\n'.join(p.capture_pane()) for p, _ in tasks) True >>> # Clean up >>> parallel_window.kill() ``` ### Monitoring multiple panes for completion ```python >>> import time >>> from libtmux.constants import PaneDirection >>> multi_window = session.new_window(window_name='multi-monitor', attach=False) >>> multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> panes = [multi_window.active_pane] >>> panes.append(panes[0].split(direction=PaneDirection.Right)) >>> panes.append(panes[0].split(direction=PaneDirection.Below)) >>> def wait_all_complete(panes, marker='COMPLETE', timeout=10.0): ... """Wait for all panes to show completion marker.""" ... start = time.time() ... remaining = set(range(len(panes))) ... while remaining and time.time() - start < timeout: ... for i in list(remaining): ... if marker in '\\n'.join(panes[i].capture_pane()): ... remaining.remove(i) ... time.sleep(0.1) ... return len(remaining) == 0 >>> # Start tasks with different durations >>> for i, pane in enumerate(panes): ... pane.send_keys(f'sleep 0.{i+1}; echo "COMPLETE"') >>> # Wait for all >>> wait_all_complete(panes, 'COMPLETE', timeout=2.0) True >>> # Clean up >>> multi_window.kill() ``` ## Context Manager Patterns ### Temporary session for isolated work ```python >>> # Create isolated session for a task >>> with server.new_session(session_name='temp-work') as temp_session: ... window = temp_session.new_window(window_name='task') ... pane = window.active_pane ... pane.send_keys('echo "Isolated work"') ... # Session exists during work ... temp_session in server.sessions True >>> # Session automatically killed after context >>> temp_session not in server.sessions True ``` ### Temporary window for subtask ```python >>> import time >>> with session.new_window(window_name='subtask') as sub_window: ... pane = sub_window.active_pane ... pane.send_keys('echo "Subtask running"') ... time.sleep(0.1) ... 'Subtask' in '\\n'.join(pane.capture_pane()) True >>> # Window cleaned up automatically >>> sub_window not in session.windows True ``` ## Timeout Handling ### Command with timeout ```python >>> import time >>> timeout_window = session.new_window(window_name='timeout-demo', attach=False) >>> timeout_pane = timeout_window.active_pane >>> class CommandTimeout(Exception): ... """Raised when a command times out.""" ... pass >>> def run_with_timeout(pane, command, marker='__DONE__', timeout=5.0): ... """Run command and wait for completion with timeout.""" ... pane.send_keys(f'{command}; echo {marker}') ... start = time.time() ... while time.time() - start < timeout: ... output = '\\n'.join(pane.capture_pane()) ... if marker in output: ... return output ... time.sleep(0.1) ... raise CommandTimeout(f'Command timed out after {timeout}s') >>> # Test successful command >>> result = run_with_timeout(timeout_pane, 'echo "fast"', timeout=2.0) >>> 'fast' in result True >>> # Clean up >>> timeout_window.kill() ``` ### Retry pattern ```python >>> import time >>> retry_window = session.new_window(window_name='retry-demo', attach=False) >>> retry_pane = retry_window.active_pane >>> def retry_until_success(pane, command, success_marker, max_retries=3, delay=0.5): ... """Retry command until success marker appears.""" ... for attempt in range(max_retries): ... pane.send_keys(command) ... time.sleep(delay) ... output = '\\n'.join(pane.capture_pane()) ... if success_marker in output: ... return True, attempt + 1 ... return False, max_retries >>> # Test retry >>> success, attempts = retry_until_success( ... retry_pane, 'echo "OK"', 'OK', max_retries=3, delay=0.2 ... ) >>> success True >>> attempts 1 >>> # Clean up >>> retry_window.kill() ``` ## Agentic Workflow Patterns ### Task queue processor ```python >>> import time >>> queue_window = session.new_window(window_name='queue', attach=False) >>> queue_pane = queue_window.active_pane >>> def process_task_queue(pane, tasks, completion_marker='TASK_DONE'): ... """Process a queue of tasks sequentially.""" ... results = [] ... for i, task in enumerate(tasks): ... pane.send_keys(f'{task}; echo "{completion_marker}_{i}"') ... # Wait for this task to complete ... start = time.time() ... while time.time() - start < 5.0: ... output = '\\n'.join(pane.capture_pane()) ... if f'{completion_marker}_{i}' in output: ... results.append((i, True)) ... break ... time.sleep(0.1) ... else: ... results.append((i, False)) ... return results >>> tasks = ['echo "Step 1"', 'echo "Step 2"', 'echo "Step 3"'] >>> results = process_task_queue(queue_pane, tasks) >>> all(success for _, success in results) True >>> # Clean up >>> queue_window.kill() ``` ### State machine runner ```python >>> import time >>> state_window = session.new_window(window_name='state-machine', attach=False) >>> state_pane = state_window.active_pane >>> def run_state_machine(pane, states, timeout_per_state=2.0): ... """Run through a series of states with transitions.""" ... current_state = 0 ... history = [] ... ... while current_state < len(states): ... state_name, command, next_marker = states[current_state] ... pane.send_keys(command) ... ... start = time.time() ... while time.time() - start < timeout_per_state: ... output = '\\n'.join(pane.capture_pane()) ... if next_marker in output: ... history.append(state_name) ... current_state += 1 ... break ... time.sleep(0.1) ... else: ... return history, False # Timeout ... ... return history, True >>> states = [ ... ('init', 'echo "INIT_DONE"', 'INIT_DONE'), ... ('process', 'echo "PROCESS_DONE"', 'PROCESS_DONE'), ... ('cleanup', 'echo "CLEANUP_DONE"', 'CLEANUP_DONE'), ... ] >>> history, success = run_state_machine(state_pane, states) >>> success True >>> len(history) 3 >>> # Clean up >>> state_window.kill() ``` ## Best Practices ### 1. Always use markers for completion detection Instead of relying on timing, use explicit markers: ```python >>> bp_window = session.new_window(window_name='best-practice', attach=False) >>> bp_pane = bp_window.active_pane >>> # Good: Use completion marker >>> bp_pane.send_keys('long_command; echo "__DONE__"') >>> # Then poll for marker >>> import time >>> time.sleep(0.2) >>> '__DONE__' in '\\n'.join(bp_pane.capture_pane()) True >>> bp_window.kill() ``` ### 2. Clean up resources Always clean up windows and sessions when done: ```python >>> cleanup_window = session.new_window(window_name='cleanup-demo', attach=False) >>> cleanup_window # doctest: +ELLIPSIS Window(@... ...) >>> # Do work... >>> # Always clean up >>> cleanup_window.kill() >>> cleanup_window not in session.windows True ``` ### 3. Use context managers for automatic cleanup ```python >>> # Context managers ensure cleanup even on exceptions >>> with session.new_window(window_name='safe-work') as safe_window: ... pane = safe_window.active_pane ... # Work happens here ... pass # Even if exception occurs, window is cleaned up ``` :::{seealso} - {ref}`pane-interaction` for basic pane operations - {ref}`workspace-setup` for creating workspace layouts - {ref}`context-managers` for resource management patterns - {class}`~libtmux.Pane` for all pane methods ::: --- # Clients Source: https://libtmux.git-pull.com/topics/clients/ (clients)= # Clients A tmux {term}`Client` is an attached terminal — the side of the tmux connection a user sees. The same tmux server can host many clients at once (one per `$ tmux attach` from different terminals), and each client has its own view of the active session, window, and pane. {class}`~libtmux.Client` is the libtmux object for that attached terminal. It sits outside the {class}`~libtmux.server.Server` → {class}`~libtmux.session.Session` → {class}`~libtmux.window.Window` → {class}`~libtmux.pane.Pane` ownership hierarchy: a client *points at* a Session/Window/Pane it is currently viewing, but is not owned by them. ## View, not identity The fields that look like foreign keys — `client_session`, `session_id`, `window_id`, and `pane_id` — are snapshots of where the client was attached when libtmux read it. They go stale the instant the user runs `switch-client`, `select-window`, or `select-pane`. The client's *identity* is `client_name` (the tty path on Unix), which is stable for the lifetime of the attachment. | Field | What it is | Stable? | |-------|------------|---------| | `client_name` | tty path tmux assigned at attach time | Yes — identity | | `session_id` / `window_id` / `pane_id` | the client's *attached view* when read | No — snapshot | | `client_session` | session name of the same attached view | No — snapshot | | `client_pid` / `client_tty` / `client_user` | terminal-level facts | Yes — identity-adjacent | ## Live attachment with `attached_*` When you want the *current* attachment — not the snapshot — use the three live properties. Each calls {meth}`~libtmux.Client.refresh` to re-read the client from `list-clients`, then resolves the typed Session/Window/Pane: ```python >>> with control_mode() as ctl: ... client = server.clients.get(client_name=ctl.client_name) ... attached = client.attached_session >>> attached is not None True ``` {attr}`~libtmux.Client.attached_window` follows the client's attached session to its {attr}`~libtmux.session.Session.active_window`, and {attr}`~libtmux.Client.attached_pane` follows that window to its {attr}`~libtmux.window.Window.active_pane`. The three properties chain, so reading {attr}`~libtmux.Client.attached_pane` does one `list-clients` refresh plus two object lookups. ```python >>> with control_mode() as ctl: ... client = server.clients.get(client_name=ctl.client_name) ... pane = client.attached_pane >>> pane is None or pane.pane_id.startswith('%') True ``` ## Iterating attached clients {attr}`~libtmux.Server.clients` returns a {class}`~libtmux._internal.query_list.QueryList` of every client tmux reports through `list-clients`. Filter or `get()` it the same way as {attr}`~libtmux.Server.sessions`: ```python >>> with control_mode() as ctl: ... attached = [ ... c ... for c in server.clients ... if c.client_name == ctl.client_name ... ] >>> bool(attached) True ``` There is no `search_clients()` method yet. Use `server.clients.filter(...)` for client filtering; see {ref}`native-filtering` for tmux-native filtering on sessions, windows, panes, and buffers. ## When `attached_*` returns `None` The properties return `None` when: - the snapshot `session_id` is empty (e.g. the client is at the tmux command prompt rather than viewing a session), - the snapshot `session_id` no longer names a live session (the session was killed between the client read and access), or - the client has detached and `list-clients` no longer reports it. Calling {meth}`~libtmux.Client.refresh` directly still raises {exc}`~libtmux.exc.TmuxObjectDoesNotExist` on a detached client; the `attached_*` properties catch that case and return `None` so callers can branch on truthiness without a `try`/`except`. ## See also - {doc}`/api/libtmux.client` — autodoc reference - {ref}`about` — where `Client` fits in the overall object model - {ref}`native-filtering` — tmux-native filtering for sessions, windows, panes, and buffers --- # Configuration Source: https://libtmux.git-pull.com/topics/configuration/ # Configuration ## Environment Variables libtmux itself does not read environment variables for configuration. All configuration is done programmatically through the Python API. The tmux server libtmux connects to may be influenced by standard tmux environment variables (`TMUX`, `TMUX_TMPDIR`). ## Format Strings libtmux uses tmux's format system to query state. Format constants are defined in {mod}`libtmux.formats` and used internally by all object types. See the [tmux man page](http://man.openbsd.org/OpenBSD-current/man1/tmux.1) for the full list of available formats. --- # Context Managers Source: https://libtmux.git-pull.com/topics/context_managers/ (context_managers)= # Context Managers libtmux provides context managers for all main tmux objects to ensure proper cleanup of resources. This is done through Python's `with` statement, which automatically handles cleanup when you're done with the tmux objects. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` Import `libtmux`: ```python import libtmux ``` ## Server Context Manager Create a temporary server that will be killed when you're done: ```python >>> with Server() as server: ... session = server.new_session() ... print(server.is_alive()) True >>> print(server.is_alive()) # Server is killed after exiting context False ``` ## Session Context Manager Create a temporary session that will be killed when you're done: ```python >>> server = Server() >>> with server.new_session() as session: ... print(session in server.sessions) ... window = session.new_window() True >>> print(session in server.sessions) # Session is killed after exiting context False ``` ## Window Context Manager Create a temporary window that will be killed when you're done: ```python >>> server = Server() >>> session = server.new_session() >>> with session.new_window() as window: ... print(window in session.windows) ... pane = window.split() True >>> print(window in session.windows) # Window is killed after exiting context False ``` ## Pane Context Manager Create a temporary pane that will be killed when you're done: ```python >>> server = Server() >>> session = server.new_session() >>> window = session.new_window() >>> with window.split() as pane: ... print(pane in window.panes) ... pane.send_keys('echo "Hello"') True >>> print(pane in window.panes) # Pane is killed after exiting context False ``` ## Nested Context Managers Context managers can be nested to create a clean hierarchy of tmux objects that are automatically cleaned up: ```python >>> with Server() as server: ... with server.new_session() as session: ... with session.new_window() as window: ... with window.split() as pane: ... pane.send_keys('echo "Hello"') ... # Do work with the pane ... # Everything is cleaned up automatically when exiting contexts ``` This ensures that: 1. The pane is killed when exiting its context 2. The window is killed when exiting its context 3. The session is killed when exiting its context 4. The server is killed when exiting its context The cleanup happens in reverse order (pane → window → session → server), ensuring proper resource management. ## Benefits Using context managers provides several advantages: 1. **Automatic Cleanup**: Resources are automatically cleaned up when you're done with them 2. **Clean Code**: No need to manually call `kill()` methods 3. **Exception Safety**: Resources are cleaned up even if an exception occurs 4. **Hierarchical Cleanup**: Nested contexts ensure proper cleanup order 5. **Resource Management**: Prevents resource leaks by ensuring tmux objects are properly destroyed ## When to Use Context managers are particularly useful when: 1. Creating temporary tmux objects for testing 2. Running short-lived tmux sessions 3. Managing multiple tmux servers 4. Ensuring cleanup in scripts that may raise exceptions 5. Creating isolated environments that need to be cleaned up afterward [target]: http://man.openbsd.org/OpenBSD-5.9/man1/tmux.1#COMMANDS --- # Design Decisions Source: https://libtmux.git-pull.com/topics/design-decisions/ # Design Decisions ## Why ORM-Style Objects tmux organizes terminals in a strict hierarchy: Server → Session → Window → Pane. Each level owns the next. libtmux mirrors this with Python objects that maintain the same parent-child relationships. The alternative — a flat command-builder API (`tmux("new-session", "-s", "foo")`) — loses the relational structure. You'd have to track which windows belong to which session manually. The ORM approach lets you write `session.windows` and get a live, filterable collection. ## Why Format Strings tmux exposes object properties through its format system (`-F` flags). For example, `tmux list-sessions -F '#{session_id}:#{session_name}'` returns structured data. libtmux uses this instead of parsing human-readable `tmux ls` output because: - **Stability**: format variables are part of tmux's documented interface - **Precision**: no regex fragility from parsing prose output - **Completeness**: formats expose properties (like `session_id`) that don't appear in default output Format constants are defined in {mod}`libtmux.formats`. ## Why Dataclasses in `neo.py` {mod}`libtmux.neo` provides a modern dataclass-based interface alongside the legacy dict-style objects. The motivation: - **Type safety**: dataclass fields have declared types, enabling mypy checks and IDE completion - **Predictability**: attribute access (`session.session_name`) instead of dict access (`session["session_name"]`) - **Migration path**: the two interfaces coexist, allowing gradual adoption without breaking existing code ## Pre-1.0 API Evolution libtmux is pre-1.0. This is a deliberate choice — the API is still maturing. What this means in practice: - **Minor versions** (0.x → 0.y) may include breaking changes - **Patch versions** (0.x.y → 0.x.z) are bug fixes only - **Pin your dependency**: use `libtmux>=0.55,<0.56` or `libtmux~=0.55.0` Breaking changes always get: 1. A deprecation warning for at least one minor release 2. Documentation in the [changelog](../history.md) and [deprecations](../project/deprecations.md) 3. Migration guidance See [Public API](../project/public-api.md) for the stability contract. --- # QueryList Filtering Source: https://libtmux.git-pull.com/topics/filtering/ (querylist-filtering)= # QueryList Filtering libtmux uses `QueryList` to enable Django-style filtering on tmux objects. Every collection (`server.sessions`, `session.windows`, `window.panes`) returns a `QueryList`, letting you filter sessions, windows, and panes with a fluent, chainable API. ## Basic Filtering The `filter()` method accepts keyword arguments with optional lookup suffixes: ```python >>> server.sessions # doctest: +ELLIPSIS [Session($... ...)] ``` ### Exact Match The default lookup is `exact`: ```python >>> # These are equivalent >>> server.sessions.filter(session_name=session.session_name) # doctest: +ELLIPSIS [Session($... ...)] >>> server.sessions.filter(session_name__exact=session.session_name) # doctest: +ELLIPSIS [Session($... ...)] ``` ### Contains and Startswith Use suffixes for partial matching: ```python >>> # Create windows for this example >>> w1 = session.new_window(window_name="api-server") >>> w2 = session.new_window(window_name="api-worker") >>> w3 = session.new_window(window_name="web-frontend") >>> # Windows containing 'api' >>> api_windows = session.windows.filter(window_name__contains='api') >>> len(api_windows) >= 2 True >>> # Windows starting with 'web' >>> web_windows = session.windows.filter(window_name__startswith='web') >>> len(web_windows) >= 1 True >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ## Available Lookups | Lookup | Description | |--------|-------------| | `exact` | Exact match (default) | | `iexact` | Case-insensitive exact match | | `contains` | Substring match | | `icontains` | Case-insensitive substring | | `startswith` | Prefix match | | `istartswith` | Case-insensitive prefix | | `endswith` | Suffix match | | `iendswith` | Case-insensitive suffix | | `in` | Value in list | | `nin` | Value not in list | | `regex` | Regular expression match | | `iregex` | Case-insensitive regex | ## Getting a Single Item Use `get()` to retrieve exactly one matching item: ```python >>> window = session.windows.get(window_id=session.active_window.window_id) >>> window # doctest: +ELLIPSIS Window(@... ..., Session($... ...)) ``` If no match or multiple matches are found, `get()` raises an exception: - `ObjectDoesNotExist` - no matching object found - `MultipleObjectsReturned` - more than one object matches You can provide a default value to avoid the exception: ```python >>> session.windows.get(window_name="nonexistent", default=None) is None True ``` ## Chaining Filters Filters can be chained for complex queries: ```python >>> # Create windows for this example >>> w1 = session.new_window(window_name="feature-login") >>> w2 = session.new_window(window_name="feature-signup") >>> w3 = session.new_window(window_name="bugfix-typo") >>> # Multiple conditions in one filter (AND) >>> session.windows.filter( ... window_name__startswith='feature', ... window_name__endswith='signup' ... ) # doctest: +ELLIPSIS [Window(@... ...:feature-signup, Session($... ...))] >>> # Chained filters (also AND) >>> session.windows.filter( ... window_name__contains='feature' ... ).filter( ... window_name__contains='login' ... ) # doctest: +ELLIPSIS [Window(@... ...:feature-login, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ## Case-Insensitive Filtering Use `i` prefix variants for case-insensitive matching: ```python >>> # Create windows with mixed case >>> w1 = session.new_window(window_name="MyApp-Server") >>> w2 = session.new_window(window_name="myapp-worker") >>> # Case-insensitive contains >>> myapp_windows = session.windows.filter(window_name__icontains='MYAPP') >>> len(myapp_windows) >= 2 True >>> # Case-insensitive startswith >>> session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS [Window(@... ...:MyApp-Server, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() ``` ## Regex Filtering For complex patterns, use regex lookups: ```python >>> # Create windows with version-like names >>> w1 = session.new_window(window_name="app-v1.0") >>> w2 = session.new_window(window_name="app-v2.0") >>> w3 = session.new_window(window_name="app-beta") >>> # Match version pattern >>> versioned = session.windows.filter(window_name__regex=r'v\d+\.\d+$') >>> len(versioned) >= 2 True >>> # Case-insensitive regex >>> session.windows.filter(window_name__iregex=r'BETA') # doctest: +ELLIPSIS [Window(@... ...:app-beta, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ## Filtering by List Membership Use `in` and `nin` (not in) for list-based filtering: ```python >>> # Create test windows >>> w1 = session.new_window(window_name="dev") >>> w2 = session.new_window(window_name="staging") >>> w3 = session.new_window(window_name="prod") >>> # Filter windows in a list of names >>> target_envs = ["dev", "prod"] >>> session.windows.filter(window_name__in=target_envs) # doctest: +ELLIPSIS [Window(@... ...:dev, Session($... ...)), Window(@... ...:prod, Session($... ...))] >>> # Filter windows NOT in a list >>> non_prod = session.windows.filter(window_name__nin=["prod"]) >>> any(w.window_name == "prod" for w in non_prod) False >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ## Filtering Across the Hierarchy Filter at any level of the tmux hierarchy: ```python >>> # All panes across all windows in the server >>> server.panes # doctest: +ELLIPSIS [Pane(%... Window(@... ..., Session($... ...)))] >>> # Filter panes by their window's name >>> pane = session.active_pane >>> pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Real-World Examples ### Find all editor windows ```python >>> # Create sample windows >>> w1 = session.new_window(window_name="vim-main") >>> w2 = session.new_window(window_name="nvim-config") >>> w3 = session.new_window(window_name="shell") >>> # Find vim/nvim windows >>> editors = session.windows.filter(window_name__iregex=r'n?vim') >>> len(editors) >= 2 True >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ### Find windows by naming convention ```python >>> # Create windows following a naming convention >>> w1 = session.new_window(window_name="project:frontend") >>> w2 = session.new_window(window_name="project:backend") >>> w3 = session.new_window(window_name="logs") >>> # Find all project windows >>> project_windows = session.windows.filter(window_name__startswith='project:') >>> len(project_windows) >= 2 True >>> # Get specific project window >>> backend = session.windows.get(window_name='project:backend') >>> backend.window_name 'project:backend' >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` (native-filtering)= ## tmux-native Filtering with `search_*()` `QueryList.filter()` runs in Python *after* tmux has returned every row. For large servers, or when you only need a handful of matches, ask tmux to apply the filter before libtmux builds objects. Every level of the hierarchy ships a `search_*()` method that passes a format expression to tmux: | Caller | Method | Underlying tmux | |--------|--------|-----------------| | {class}`~libtmux.Server` | {meth}`~libtmux.Server.search_sessions` | `tmux list-sessions -f ` | | {class}`~libtmux.Server` | {meth}`~libtmux.Server.search_windows` | `tmux list-windows -a -f ` | | {class}`~libtmux.Server` | {meth}`~libtmux.Server.search_panes` | `tmux list-panes -a -f ` | | {class}`~libtmux.Session` | {meth}`~libtmux.Session.search_windows` | `tmux list-windows -t $sess -f ` | | {class}`~libtmux.Session` | {meth}`~libtmux.Session.search_panes` | `tmux list-panes -s -t $sess -f ` | | {class}`~libtmux.Window` | {meth}`~libtmux.Window.search_panes` | `tmux list-panes -t @win -f ` | The {meth}`~libtmux.Server.list_buffers` method also accepts a `filter=` kwarg with the same semantics. There is no `search_clients()` method; filter clients via the {attr}`~libtmux.Server.clients` accessor and Python-side {meth}`~libtmux._internal.query_list.QueryList.filter`. Filtering clients in Python is usually enough because a server's client count is bounded by attached terminals, not by session/window/pane fan-out. ### Python-side vs. tmux-native | | `.filter()` | `.search_*()` | |-|-------------|---------------| | Where | Python (after fetch) | tmux server (before fetch) | | Filter language | libtmux's lookup operators (`__contains`, `__regex`, etc.) | tmux's [FORMATS](https://man.openbsd.org/tmux.1#FORMATS) grammar | | Round trips | one (full list, then filter in memory) | one (tmux returns only matches) | | Best for | rich Python checks, set membership, post-fetch composition | exact/glob matches over many rows | | Stability | every libtmux version supports it | requires tmux ≥ 3.2 (≥ 3.4 for `list-clients -f`) | Both are valid; pick based on data volume and the filter language you want. ### Filter syntax tmux's filter language is the same one used in `-F` templates. Three shapes cover most use cases: ```python >>> # Match by glob >>> s_alpha = server.new_session(session_name='alpha-1') >>> s_beta = server.new_session(session_name='beta-1') >>> alphas = server.search_sessions(filter='#{m:alpha-*,#{session_name}}') >>> [s.session_name for s in alphas] ['alpha-1'] >>> # Match by equality >>> exact = server.search_sessions( ... filter='#{==:#{session_name},alpha-1}' ... ) >>> [s.session_name for s in exact] ['alpha-1'] >>> # Clean up >>> s_alpha.kill() >>> s_beta.kill() ``` `#{e:...}` evaluates an arithmetic expression; `#{?cond,a,b}` is the conditional form. See `man tmux` for the full grammar. ### The silent zero-match trap A malformed filter expression is the single biggest footgun. tmux expands an unclosed `#{...}` or an unknown format token to an empty string, which the filter engine evaluates as "false" — every row is filtered out and **no stderr is emitted**. A bad filter is indistinguishable from a filter that genuinely matched nothing. If `search_*()` returns empty unexpectedly: 1. Replace the filter with `#{m:*,#{session_name}}` (or the equivalent for windows/panes). If that returns rows, the issue is filter syntax, not data. 2. Expand the expression standalone via {meth}`~libtmux.Server.display_message` to see what tmux actually produced: ```python >>> result = server.display_message( ... '#{m:alpha-*,alpha-1}', get_text=True ... ) >>> result[0] '1' ``` A non-`1`, non-empty result tells you the expression is parsing as text, not as a boolean. 3. Cross-check the token name against the FORMATS section of `tmux(1)` and against the version gate (see {ref}`format-tokens`). ### When to prefer which Use `search_*()` when: - you have hundreds or thousands of windows/panes and only want a few, - your filter is a glob (`m:`) or equality check (`==:`), - you're already in tmux-format thinking (writing `#{...}` for a status-line template, for example). Use `.filter()` when: - your filter needs Python types you can't express in tmux format (set membership, complex regex, computed values from outside tmux), - you're chaining multiple filters and prefer composing in Python, - you want predictable, version-independent semantics. ## API Reference See {class}`~libtmux._internal.query_list.QueryList` for the complete QueryList API, and each `search_*()` method for the tmux-native filter contract. --- # Format-Token Fields Source: https://libtmux.git-pull.com/topics/format-tokens/ (format-tokens)= # Format-Token Fields Every libtmux object — {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, {class}`~libtmux.Pane`, {class}`~libtmux.Client` — exposes a flat set of typed string attributes named after tmux's [FORMATS](https://man.openbsd.org/tmux.1#FORMATS) tokens (`pane_id`, `window_zoomed_flag`, `client_theme`, etc.). This is why a single {class}`~libtmux.Pane` can expose `pane.pane_id`, `pane.window_id`, and `pane.session_id`. Two gates decide which fields actually hold a value on a given object: 1. **Scope** — which kind of tmux object can provide the token. A `pane_*` token needs pane context, a `session_*` token needs session context, and so on. 2. **Version** — which tmux release first registered the token in `format.c`'s static table. If either gate excludes a token, libtmux leaves the field at `None` rather than risking a server-side fault on an older tmux. ## Why a field is `None` A typed field is `None` for one of three reasons: - **Not yet introduced.** Older tmux doesn't know the token at all. {attr}`~libtmux.Pane.pane_dead_signal` is `None` on tmux 3.2a because the token landed in 3.3. - **Wrong scope for this object.** A {class}`~libtmux.Client` row can report client tokens plus the client's current session/window/pane. `buffer_*` tokens never apply to client rows. - **Live-only token.** Some tokens (`mouse_*`, `cursor_*`, `selection_*`) only resolve inside a live event context (key binding, copy-mode, popup) — never in a `list-*` snapshot. libtmux excludes them from every `-F` template. The version map for post-3.2a tokens is small and stable. The following are the tokens libtmux currently gates: | Added in | Tokens | |----------|--------| | 3.3 | `pane_dead_signal`, `pane_dead_time` | Everything not listed above is safe on every supported tmux (≥ 3.2a). Fields for newer tmux tokens will be added as each supported version is validated. ## Active Child Fields When tmux lists a parent object, it can also report fields from that parent's active child. That's why pane fields have meaningful values on a session row: they describe the active pane in the session's current window. ```python >>> session = server.new_session() >>> session.pane_id == session.active_window.active_pane.pane_id True >>> session.window_id == session.active_window.window_id True ``` The relationship is **one-way**. A {class}`~libtmux.Pane` carries `window_*` and `session_*` fields for its parents, but a {class}`~libtmux.Session` does not carry `client_*` fields because tmux cannot infer one attached client from a session row. The `client_*` tokens only appear on {class}`~libtmux.Client` rows returned by {attr}`~libtmux.Server.clients`. If you treat `session.pane_id` as "the session's pane id" (rather than "the active pane of the session's current window") you will be surprised when the active window changes. ## Inspecting which fields apply Use {func}`libtmux.neo.get_output_format` to ask, for a given `list-*` subcommand and tmux version, which tokens libtmux will request: ```python >>> from libtmux.neo import get_output_format >>> fields, _ = get_output_format("list-sessions", "3.6a") >>> 'session_id' in fields True >>> 'pane_id' in fields # active pane for the listed session True >>> 'client_name' in fields # client fields require list-clients False ``` For `list-clients`, the gate widens to include `client_*` plus every attached session/window/pane token: ```python >>> from libtmux.neo import get_output_format >>> fields, _ = get_output_format("list-clients", "3.6a") >>> all(t in fields for t in ("client_name", "session_id", "pane_id")) True ``` The result is cached per `(list_cmd, tmux_version)` pair. ## Tmux version detection libtmux detects the live tmux version via {func}`libtmux.common.get_version` and passes it through to `get_output_format` whenever it builds a `-F` template. The result is cached for the process lifetime; if you're swapping the `tmux` binary mid-test, call `libtmux.common.get_version.cache_clear()` to invalidate. The {ref}`project` page tracks the project's minimum tmux version (currently 3.2a); see {doc}`/project/compatibility` for the full matrix. ## See also - {doc}`/api/libtmux.neo` — API reference for format-field helpers - {func}`libtmux.neo.get_output_format` — the scope and version filter - {ref}`clients` — attached-client fields and live attachment lookups - {doc}`/project/compatibility` — supported tmux versions --- # Topics Source: https://libtmux.git-pull.com/topics/ # Topics Explore libtmux's core functionalities and underlying principles at a high level, while providing essential context and detailed explanations to help you understand its design and usage. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Architecture :link: architecture :link-type: doc Module hierarchy, data flow, and internal identifiers. ::: :::{grid-item-card} Traversal :link: traversal :link-type: doc Navigate the Server, Session, Window, Pane hierarchy. ::: :::{grid-item-card} Filtering :link: filtering :link-type: doc Query and filter collections by attributes. ::: :::{grid-item-card} Pane Interaction :link: pane_interaction :link-type: doc Send keys, capture output, and interact with panes. ::: :::{grid-item-card} Workspace Setup :link: workspace_setup :link-type: doc Create sessions, windows, and panes programmatically. ::: :::{grid-item-card} Automation Patterns :link: automation_patterns :link-type: doc Common patterns for scripting and automation. ::: :::{grid-item-card} Context Managers :link: context_managers :link-type: doc Automatic cleanup with temporary sessions and windows. ::: :::{grid-item-card} Options & Hooks :link: options_and_hooks :link-type: doc Get and set tmux options and hooks. ::: :::{grid-item-card} Clients :link: clients :link-type: doc Attached terminals, live-attachment lookup, and the view-vs-identity model. ::: :::{grid-item-card} Format-Token Fields :link: format-tokens :link-type: doc Scope- and version-gated typed fields on every libtmux object. ::: :::: ```{toctree} :hidden: architecture configuration design-decisions public-vs-internal traversal filtering pane_interaction workspace_setup automation_patterns context_managers options_and_hooks clients format-tokens ``` --- # Options and Hooks Source: https://libtmux.git-pull.com/topics/options_and_hooks/ (options-and-hooks)= # Options and Hooks libtmux provides a unified API for managing tmux options and hooks across all object types (Server, Session, Window, Pane). ## Options tmux options control the behavior and appearance of sessions, windows, and panes. libtmux provides a consistent interface through {class}`~libtmux.options.OptionsMixin`. ### Getting options Use {meth}`~libtmux.options.OptionsMixin.show_options` to get all options: ```python >>> session.show_options() # doctest: +ELLIPSIS {...} ``` Use {meth}`~libtmux.options.OptionsMixin.show_option` to get a single option: ```python >>> server.show_option('buffer-limit') 50 ``` ### Setting options Use {meth}`~libtmux.options.OptionsMixin.set_option` to set an option: ```python >>> window.set_option('automatic-rename', False) # doctest: +ELLIPSIS Window(@... ...) >>> window.show_option('automatic-rename') False ``` ### Unsetting options Use {meth}`~libtmux.options.OptionsMixin.unset_option` to revert an option to its default: ```python >>> window.unset_option('automatic-rename') # doctest: +ELLIPSIS Window(@... ...) ``` ### Option scopes tmux options exist at different scopes. Use the `scope` parameter to specify: ```python >>> from libtmux.constants import OptionScope >>> # Get window-scoped options from a session >>> session.show_options(scope=OptionScope.Window) # doctest: +ELLIPSIS {...} ``` ### Global options Use `global_=True` to work with global options: ```python >>> server.show_option('buffer-limit', global_=True) 50 ``` ## Hooks tmux hooks allow you to run commands when specific events occur. libtmux provides hook management through {class}`~libtmux.hooks.HooksMixin`. ### Setting and getting hooks Use {meth}`~libtmux.hooks.HooksMixin.set_hook` to set a hook and {meth}`~libtmux.hooks.HooksMixin.show_hook` to get its value: ```python >>> session.set_hook('session-renamed', 'display-message "Session renamed"') # doctest: +ELLIPSIS Session(...) >>> session.show_hook('session-renamed') # doctest: +ELLIPSIS {0: 'display-message "Session renamed"'} >>> session.show_hooks() # doctest: +ELLIPSIS {...} ``` Note that hooks are stored as indexed arrays in tmux, so `show_hook()` returns a {class}`~libtmux._internal.sparse_array.SparseArray` (dict-like) with index keys. ### Removing hooks Use {meth}`~libtmux.hooks.HooksMixin.unset_hook` to remove a hook: ```python >>> session.unset_hook('session-renamed') # doctest: +ELLIPSIS Session(...) ``` ### Indexed hooks tmux hooks support multiple values via indices (e.g., `session-renamed[0]`, `session-renamed[1]`). This allows multiple commands to run for the same event: ```python >>> session.set_hook('after-split-window[0]', 'display-message "Split 0"') # doctest: +ELLIPSIS Session(...) >>> session.set_hook('after-split-window[1]', 'display-message "Split 1"') # doctest: +ELLIPSIS Session(...) >>> hooks = session.show_hook('after-split-window') >>> sorted(hooks.keys()) [0, 1] ``` The return value is a {class}`~libtmux._internal.sparse_array.SparseArray`, which preserves sparse indices (e.g., indices 0 and 5 with no 1-4). ### Bulk hook operations Use {meth}`~libtmux.hooks.HooksMixin.set_hooks` to set multiple indexed hooks: ```python >>> session.set_hooks('window-linked', { ... 0: 'display-message "Window linked 0"', ... 1: 'display-message "Window linked 1"', ... }) # doctest: +ELLIPSIS Session(...) >>> # Clean up >>> session.unset_hook('after-split-window[0]') # doctest: +ELLIPSIS Session(...) >>> session.unset_hook('after-split-window[1]') # doctest: +ELLIPSIS Session(...) >>> session.unset_hook('window-linked[0]') # doctest: +ELLIPSIS Session(...) >>> session.unset_hook('window-linked[1]') # doctest: +ELLIPSIS Session(...) ``` ## tmux version compatibility | Feature | Minimum tmux | |---------|-------------| | All options/hooks features | 3.2+ | | Window/Pane hook scopes (`-w`, `-p`) | 3.2+ | | `client-active`, `window-resized` hooks | 3.3+ | | `pane-title-changed` hook | 3.5+ | :::{seealso} - {ref}`api` for the full API reference - {class}`~libtmux.options.OptionsMixin` for options methods - {class}`~libtmux.hooks.HooksMixin` for hooks methods - {class}`~libtmux._internal.sparse_array.SparseArray` for sparse array handling ::: --- # Pane Interaction Source: https://libtmux.git-pull.com/topics/pane_interaction/ (pane-interaction)= # Pane Interaction libtmux provides powerful methods for interacting with tmux panes programmatically. This is especially useful for automation, testing, and orchestrating terminal-based workflows. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` ## Sending Commands The {meth}`~libtmux.Pane.send_keys` method sends text to a pane, optionally pressing Enter to execute it. ### Basic command execution ```python >>> pane = window.split(shell='sh') >>> pane.send_keys('echo "Hello from libtmux"') >>> import time; time.sleep(0.1) # Allow command to execute >>> output = pane.capture_pane() >>> 'Hello from libtmux' in '\\n'.join(output) True ``` ### Send without pressing Enter Use `enter=False` to type text without executing: ```python >>> pane.send_keys('echo "waiting"', enter=False) >>> # Text is typed but not executed >>> output = pane.capture_pane() >>> 'waiting' in '\\n'.join(output) True ``` Press Enter separately with {meth}`~libtmux.Pane.enter`: ```python >>> import time >>> # First type something without pressing Enter >>> pane.send_keys('echo "execute me"', enter=False) >>> pane.enter() # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> time.sleep(0.2) >>> output = pane.capture_pane() >>> 'execute me' in '\\n'.join(output) True ``` ### Literal mode for special characters Use `literal=True` to send special characters without interpretation: ```python >>> import time >>> pane.send_keys('echo "Tab:\\tNewline:\\n"', literal=True) >>> time.sleep(0.1) ``` ### Suppress shell history Use `suppress_history=True` to prepend a space (prevents command from being saved in shell history): ```python >>> import time >>> pane.send_keys('echo "secret command"', suppress_history=True) >>> time.sleep(0.1) ``` ### Flag-only invocation When you want to invoke `send-keys` only for its flags — resetting the pane or repeating a key — pass `cmd=None`: ```python >>> # Repeat the last key 5 times (-N 5) >>> pane.send_keys(cmd=None, repeat=5) >>> # Reset the pane to default state (-R) >>> pane.send_keys(cmd=None, reset=True) ``` `cmd=None` requires at least one of `reset=True`, `repeat=N`, or `copy_mode_cmd=...`; calling it with no flag raises `ValueError` to prevent silent no-ops. ## Capturing Output The {meth}`~libtmux.Pane.capture_pane` method captures text from a pane's buffer. ### Basic capture ```python >>> import time >>> pane.send_keys('echo "Line 1"; echo "Line 2"; echo "Line 3"') >>> time.sleep(0.1) >>> output = pane.capture_pane() >>> isinstance(output, list) True >>> any('Line 2' in line for line in output) True ``` ### Capture with line ranges Capture specific line ranges using `start` and `end` parameters: ```python >>> # Capture last 5 lines of visible pane >>> recent = pane.capture_pane(start=-5, end='-') >>> isinstance(recent, list) True >>> # Capture from start of history to current >>> full_history = pane.capture_pane(start='-', end='-') >>> len(full_history) >= 0 True ``` ### Capture with ANSI escape sequences Capture colored output with escape sequences preserved using `escape_sequences=True`: ```python >>> import time >>> pane.send_keys('printf "\\033[31mRED\\033[0m \\033[32mGREEN\\033[0m"') >>> time.sleep(0.1) >>> # Capture with ANSI codes stripped (default) >>> output = pane.capture_pane() >>> 'RED' in '\\n'.join(output) True >>> # Capture with ANSI escape sequences preserved >>> colored_output = pane.capture_pane(escape_sequences=True) >>> isinstance(colored_output, list) True ``` ### Join wrapped lines Long lines that wrap in the terminal can be joined back together: ```python >>> import time >>> # Send a very long line that will wrap >>> pane.send_keys('echo "' + 'x' * 200 + '"') >>> time.sleep(0.1) >>> # Capture with wrapped lines joined >>> output = pane.capture_pane(join_wrapped=True) >>> isinstance(output, list) True ``` ### Preserve trailing spaces By default, trailing spaces are trimmed. Use `preserve_trailing=True` to keep them: ```python >>> import time >>> pane.send_keys('printf "text \\n"') # 3 trailing spaces >>> time.sleep(0.1) >>> # Capture with trailing spaces preserved >>> output = pane.capture_pane(preserve_trailing=True) >>> isinstance(output, list) True ``` ### Capture flags summary | Parameter | tmux Flag | Description | |-----------|-----------|-------------| | `escape_sequences` | `-e` | Include ANSI escape sequences (colors, attributes) | | `escape_non_printable` | `-C` | Escape non-printable chars as octal `\xxx` | | `join_wrapped` | `-J` | Join wrapped lines back together | | `preserve_trailing` | `-N` | Preserve trailing spaces at line ends | | `trim_trailing` | `-T` | Trim trailing empty positions (tmux 3.4+) | | `pending` | `-P` | Dump the unprocessed input buffer instead of the screen | :::{note} The `trim_trailing` parameter requires tmux 3.4+. If used with an older version, a warning is issued and the flag is ignored. ::: ### Capturing the pending input buffer Use `pending=True` to dump bytes tmux has buffered in its parser but not yet committed to the pane's terminal — input the tmux process read from the pane's PTY but hasn't fed through its escape-sequence parser into the visible screen. Use to inspect partial control sequences mid-write. ```python >>> pending = pane.capture_pane(pending=True) >>> isinstance(pending, list) True ``` `pending=True` is mutually exclusive with the line-range and screen-mode flags (`start`, `end`, `escape_sequences`, etc.) — tmux ignores them when `-P` is set. ## Waiting for Output A common pattern in automation is waiting for a command to complete. ### Polling for completion marker ```python >>> import time >>> pane.send_keys('sleep 0.2; echo "TASK_COMPLETE"') >>> # Poll for completion >>> for _ in range(30): ... output = pane.capture_pane() ... if 'TASK_COMPLETE' in '\\n'.join(output): ... break ... time.sleep(0.1) >>> 'TASK_COMPLETE' in '\\n'.join(output) True ``` ### Helper function for waiting ```python >>> import time >>> def wait_for_text(pane, text, timeout=5.0): ... """Wait for text to appear in pane output.""" ... start = time.time() ... while time.time() - start < timeout: ... output = pane.capture_pane() ... if text in '\\n'.join(output): ... return True ... time.sleep(0.1) ... return False >>> pane.send_keys('echo "READY"') >>> wait_for_text(pane, 'READY', timeout=2.0) True ``` ## Querying Pane State The {meth}`~libtmux.Pane.display_message` method queries tmux format variables. ### Get pane dimensions ```python >>> width = pane.display_message('#{pane_width}', get_text=True) >>> isinstance(width, list) and len(width) > 0 True >>> height = pane.display_message('#{pane_height}', get_text=True) >>> isinstance(height, list) and len(height) > 0 True ``` ### Get pane information ```python >>> # Current working directory >>> cwd = pane.display_message('#{pane_current_path}', get_text=True) >>> isinstance(cwd, list) True >>> # Pane ID >>> pane_id = pane.display_message('#{pane_id}', get_text=True) >>> pane_id[0].startswith('%') True ``` ### Common format variables | Variable | Description | |----------|-------------| | `#{pane_width}` | Pane width in characters | | `#{pane_height}` | Pane height in characters | | `#{pane_current_path}` | Current working directory | | `#{pane_pid}` | PID of the pane's shell | | `#{pane_id}` | Unique pane ID (e.g., `%0`) | | `#{pane_index}` | Pane index in window | ## Resizing Panes The {meth}`~libtmux.Pane.resize` method adjusts pane dimensions. ### Resize by specific dimensions ```python >>> # Make pane larger >>> pane.resize(height=20, width=80) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ### Resize by adjustment ```python >>> from libtmux.constants import ResizeAdjustmentDirection >>> # Increase height by 5 rows >>> pane.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=5) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> # Decrease width by 10 columns >>> pane.resize(adjustment_direction=ResizeAdjustmentDirection.Left, adjustment=10) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ### Zoom toggle ```python >>> # Zoom pane to fill window >>> pane.resize(zoom=True) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> # Unzoom >>> pane.resize(zoom=True) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Clearing the Pane The {meth}`~libtmux.Pane.clear` method clears the pane's screen: ```python >>> pane.clear() # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Killing Panes The {meth}`~libtmux.Pane.kill` method destroys a pane: ```python >>> # Create a temporary pane >>> temp_pane = pane.split() >>> temp_pane in window.panes True >>> # Kill it >>> temp_pane.kill() >>> temp_pane not in window.panes True ``` ### Kill all except current ```python >>> # Setup: create multiple panes >>> pane.window.resize(height=60, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> keep_pane = pane.split() >>> extra1 = pane.split() >>> extra2 = pane.split() >>> # Kill all except keep_pane >>> keep_pane.kill(all_except=True) >>> keep_pane in window.panes True >>> extra1 not in window.panes True >>> extra2 not in window.panes True >>> # Cleanup >>> keep_pane.kill() ``` ## Practical Recipes ### Recipe: Run command and capture output ```python >>> import time >>> def run_and_capture(pane, command, marker='__DONE__', timeout=5.0): ... """Run a command and return its output.""" ... pane.send_keys(f'{command}; echo {marker}') ... start = time.time() ... while time.time() - start < timeout: ... output = pane.capture_pane() ... output_str = '\\n'.join(output) ... if marker in output_str: ... return output # Return all captured output ... time.sleep(0.1) ... raise TimeoutError(f'Command did not complete within {timeout}s') >>> result = run_and_capture(pane, 'echo "captured text"', timeout=2.0) >>> 'captured text' in '\\n'.join(result) True ``` ### Recipe: Check for error patterns ```python >>> import time >>> def check_for_errors(pane, error_patterns=None): ... """Check pane output for error patterns.""" ... if error_patterns is None: ... error_patterns = ['error:', 'Error:', 'ERROR', 'failed', 'FAILED'] ... output = '\\n'.join(pane.capture_pane()) ... for pattern in error_patterns: ... if pattern in output: ... return True ... return False >>> pane.send_keys('echo "All good"') >>> time.sleep(0.1) >>> check_for_errors(pane) False ``` :::{seealso} - {ref}`api` for the full API reference - {class}`~libtmux.Pane` for all pane methods - {ref}`automation-patterns` for advanced orchestration patterns ::: --- # Public vs Internal API Source: https://libtmux.git-pull.com/topics/public-vs-internal/ # Public vs Internal API ## The Boundary libtmux draws a clear line between public and internal code: | Import path | Status | Stability | |-------------|--------|-----------| | `libtmux.*` | Public | Covered by [deprecation policy](../project/public-api.md) | | `libtmux._internal.*` | Internal | No guarantee — may break between any release | | `libtmux._vendor.*` | Vendored | Not part of the API at all | If you can import it without a leading underscore in the module path, it's public. ## Why the Split Internal modules exist so the library can iterate freely on implementation details without breaking downstream users. A refactor of `libtmux._internal.query_list` doesn't require a deprecation cycle — it's explicitly not part of the contract. This separation also keeps the public API surface intentionally small. Every public module is a commitment to maintain. Internal modules earn promotion through proven stability and user demand. ## What `_internal/` Contains The `_internal` package holds implementation details that support the public API: - **`query_list`** — the filtering engine behind `.filter()` and `.get()` on collections - **`dataclasses`** — base dataclass utilities used by the ORM objects - **`constants`** — internal constants not meaningful to end users - **`types`** — type aliases used across the codebase These are documented in [Internals](../internals/index.md) for contributors, but downstream projects should not import from them. ## What `_vendor/` Contains The `_vendor` package holds vendored third-party code — copies of external libraries included directly to avoid adding dependencies. This code is not written by the libtmux authors and is not part of the API. ## How Internal APIs Get Promoted 1. **Internal**: lives in `_internal/`, no stability promise 2. **Experimental**: documented, usable, but explicitly marked as subject to change 3. **Public**: moved to a top-level module, covered by the deprecation policy Promotion happens when an internal API proves stable across multiple releases and users request it. If you depend on an internal API, [file an issue](https://github.com/tmux-python/libtmux/issues) — that signal helps prioritize promotion. ## Reference - [Public API](../project/public-api.md) — the authoritative list of what's stable - [Compatibility](../project/compatibility.md) — platform and version support - [Deprecations](../project/deprecations.md) — what's changing --- # Traversal Source: https://libtmux.git-pull.com/topics/traversal/ (traversal)= # Traversal libtmux provides convenient access to move around the hierarchy of sessions, windows and panes in tmux. This is done by libtmux's object abstraction of {term}`target`s (the `-t` argument) and the permanent internal ID's tmux gives to objects. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` ## Setup First, create a test session: ```python >>> session = server.new_session() # Create a test session using existing server ``` ## Server Level View the server's representation: ```python >>> server # doctest: +ELLIPSIS Server(socket_name=...) ``` Get all sessions in the server: ```python >>> server.sessions # doctest: +ELLIPSIS [Session($... ...)] ``` Get all windows across all sessions: ```python >>> server.windows # doctest: +ELLIPSIS [Window(@... ..., Session($... ...))] ``` Get all panes across all windows: ```python >>> server.panes # doctest: +ELLIPSIS [Pane(%... Window(@... ..., Session($... ...)))] ``` ## Session Level Get first session: ```python >>> session = server.sessions[0] >>> session # doctest: +ELLIPSIS Session($... ...) ``` Get windows in a session: ```python >>> session.windows # doctest: +ELLIPSIS [Window(@... ..., Session($... ...))] ``` Get active window and pane: ```python >>> session.active_window # doctest: +ELLIPSIS Window(@... ..., Session($... ...)) >>> session.active_pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Window Level Get a window and inspect its properties: ```python >>> window = session.windows[0] >>> window.window_index # doctest: +ELLIPSIS '...' ``` Access the window's parent session: ```python >>> window.session # doctest: +ELLIPSIS Session($... ...) >>> window.session.session_id == session.session_id True ``` Get panes in a window: ```python >>> window.panes # doctest: +ELLIPSIS [Pane(%... Window(@... ..., Session($... ...)))] ``` Get active pane: ```python >>> window.active_pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Pane Level Get a pane and traverse upwards: ```python >>> pane = window.panes[0] >>> pane.window.window_id == window.window_id True >>> pane.session.session_id == session.session_id True >>> pane.server is server True ``` ## Filtering and Finding Objects libtmux collections support Django-style filtering with `filter()` and `get()`. For comprehensive coverage of all lookup operators, see {ref}`querylist-filtering`. For tmux-native filters that return only matching rows on large servers, see {ref}`native-filtering`. ### Basic Filtering Find windows by exact attribute match: ```python >>> session.windows.filter(window_index=window.window_index) # doctest: +ELLIPSIS [Window(@... ..., Session($... ...))] ``` Get a specific pane by ID: ```python >>> window.panes.get(pane_id=pane.pane_id) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ### Partial Matching Use lookup suffixes like `__contains`, `__startswith`, `__endswith`: ```python >>> # Create windows to demonstrate filtering >>> w1 = session.new_window(window_name="app-frontend") >>> w2 = session.new_window(window_name="app-backend") >>> w3 = session.new_window(window_name="logs") >>> # Find windows starting with 'app-' >>> session.windows.filter(window_name__startswith='app-') # doctest: +ELLIPSIS [Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))] >>> # Find windows containing 'end' >>> session.windows.filter(window_name__contains='end') # doctest: +ELLIPSIS [Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ### Case-Insensitive Matching Prefix any lookup with `i` for case-insensitive matching: ```python >>> # Create windows with mixed case >>> w1 = session.new_window(window_name="MyApp") >>> w2 = session.new_window(window_name="myapp-worker") >>> # Case-insensitive search >>> session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS [Window(@... ...:MyApp, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() ``` ### Regex Filtering For complex patterns, use `__regex` or `__iregex`: ```python >>> # Create versioned windows >>> w1 = session.new_window(window_name="release-v1.0") >>> w2 = session.new_window(window_name="release-v2.0") >>> w3 = session.new_window(window_name="dev") >>> # Match semantic version pattern >>> session.windows.filter(window_name__regex=r'v\d+\.\d+') # doctest: +ELLIPSIS [Window(@... ...:release-v1.0, Session($... ...)), Window(@... ...:release-v2.0, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ### Chaining Filters Multiple conditions can be combined: ```python >>> # Create windows for chaining example >>> w1 = session.new_window(window_name="api-prod") >>> w2 = session.new_window(window_name="api-staging") >>> w3 = session.new_window(window_name="web-prod") >>> # Multiple conditions in one call (AND) >>> session.windows.filter( ... window_name__startswith='api', ... window_name__endswith='prod' ... ) # doctest: +ELLIPSIS [Window(@... ...:api-prod, Session($... ...))] >>> # Chained calls (also AND) >>> session.windows.filter( ... window_name__contains='api' ... ).filter( ... window_name__contains='staging' ... ) # doctest: +ELLIPSIS [Window(@... ...:api-staging, Session($... ...))] >>> # Clean up >>> w1.kill() >>> w2.kill() >>> w3.kill() ``` ### Get with Default Avoid exceptions when an object might not exist: ```python >>> # Returns None instead of raising ObjectDoesNotExist >>> session.windows.get(window_name="nonexistent", default=None) is None True ``` ## Checking Relationships Check if objects are related: ```python >>> window in session.windows True >>> pane in window.panes True >>> session in server.sessions True ``` Check if a window is active: ```python >>> window.window_id == session.active_window.window_id True ``` Check if a pane is active: ```python >>> pane.pane_id == window.active_pane.pane_id True ``` [target]: http://man.openbsd.org/OpenBSD-5.9/man1/tmux.1#COMMANDS --- # Workspace Setup Source: https://libtmux.git-pull.com/topics/workspace_setup/ (workspace-setup)= # Workspace Setup libtmux makes it easy to create and configure multi-pane workspaces programmatically. This is useful for setting up development environments, running parallel tasks, and orchestrating terminal-based workflows. Open two terminals: Terminal one: start tmux in a separate terminal: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` ## Creating Windows The {meth}`~libtmux.Session.new_window` method creates new windows within a session. ### Basic window creation ```python >>> new_window = session.new_window(window_name='workspace') >>> new_window # doctest: +ELLIPSIS Window(@... ...:workspace, Session($... ...)) >>> # Window is part of the session >>> new_window in session.windows True ``` ### Create without attaching Use `attach=False` to create a window in the background: ```python >>> background_window = session.new_window( ... window_name='background-task', ... attach=False, ... ) >>> background_window # doctest: +ELLIPSIS Window(@... ...:background-task, Session($... ...)) >>> # Clean up >>> background_window.kill() ``` ### Create with specific shell ```python >>> shell_window = session.new_window( ... window_name='shell-test', ... attach=False, ... window_shell='sh -c "echo Hello; exec sh"', ... ) >>> shell_window # doctest: +ELLIPSIS Window(@... ...:shell-test, Session($... ...)) >>> # Clean up >>> shell_window.kill() ``` ## Splitting Panes The {meth}`~libtmux.Window.split` method divides windows into multiple panes. ### Vertical split (top/bottom) ```python >>> import time >>> from libtmux.constants import PaneDirection >>> # Create a window with enough space >>> v_split_window = session.new_window(window_name='v-split-demo', attach=False) >>> v_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> # Default split is vertical (creates pane below) >>> top_pane = v_split_window.active_pane >>> bottom_pane = v_split_window.split() >>> bottom_pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> len(v_split_window.panes) 2 >>> # Clean up >>> v_split_window.kill() ``` ### Horizontal split (left/right) ```python >>> from libtmux.constants import PaneDirection >>> # Create a fresh window for this demo >>> h_split_window = session.new_window(window_name='h-split', attach=False) >>> h_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> left_pane = h_split_window.active_pane >>> right_pane = left_pane.split(direction=PaneDirection.Right) >>> right_pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> len(h_split_window.panes) 2 >>> # Clean up >>> h_split_window.kill() ``` ### Split with specific size ```python >>> # Create a fresh window for size demo >>> size_window = session.new_window(window_name='size-demo', attach=False) >>> size_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> main_pane = size_window.active_pane >>> # Create pane with specific percentage >>> small_pane = main_pane.split(size='20%') >>> small_pane # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) >>> # Clean up >>> size_window.kill() ``` ## Layout Management The {meth}`~libtmux.Window.select_layout` method arranges panes using built-in layouts. ### Available layouts tmux provides five built-in layouts: | Layout | Description | |--------|-------------| | `even-horizontal` | Panes spread evenly left to right | | `even-vertical` | Panes spread evenly top to bottom | | `main-horizontal` | Large pane on top, others below | | `main-vertical` | Large pane on left, others on right | | `tiled` | Panes spread evenly in rows and columns | ### Applying layouts ```python >>> # Create window with multiple panes >>> layout_window = session.new_window(window_name='layout-demo', attach=False) >>> layout_window.resize(height=60, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> pane1 = layout_window.active_pane >>> pane2 = layout_window.split() >>> pane3 = layout_window.split() >>> pane4 = layout_window.split() >>> # Apply tiled layout >>> layout_window.select_layout('tiled') # doctest: +ELLIPSIS Window(@... ...) >>> # Apply even-horizontal layout >>> layout_window.select_layout('even-horizontal') # doctest: +ELLIPSIS Window(@... ...) >>> # Apply main-vertical layout >>> layout_window.select_layout('main-vertical') # doctest: +ELLIPSIS Window(@... ...) >>> # Clean up >>> layout_window.kill() ``` ## Renaming and Organizing ### Rename windows ```python >>> rename_window = session.new_window(window_name='old-name', attach=False) >>> rename_window.rename_window('new-name') # doctest: +ELLIPSIS Window(@... ...:new-name, Session($... ...)) >>> rename_window.window_name 'new-name' >>> # Clean up >>> rename_window.kill() ``` ### Access window properties ```python >>> demo_window = session.new_window(window_name='props-demo', attach=False) >>> # Window index >>> demo_window.window_index # doctest: +ELLIPSIS '...' >>> # Window ID >>> demo_window.window_id # doctest: +ELLIPSIS '@...' >>> # Parent session >>> demo_window.session # doctest: +ELLIPSIS Session($... ...) >>> # Clean up >>> demo_window.kill() ``` ## Practical Recipes ### Recipe: Create a development workspace ```python >>> import time >>> from libtmux.constants import PaneDirection >>> def create_dev_workspace(session, name='dev'): ... """Create a typical development workspace layout.""" ... window = session.new_window(window_name=name, attach=False) ... window.resize(height=50, width=160) ... ... # Main editing pane (large, left side) ... main_pane = window.active_pane ... ... # Terminal pane (bottom) ... terminal_pane = main_pane.split(size='30%') ... ... # Logs pane (right side of terminal) ... log_pane = terminal_pane.split(direction=PaneDirection.Right) ... ... return { ... 'window': window, ... 'main': main_pane, ... 'terminal': terminal_pane, ... 'logs': log_pane, ... } >>> workspace = create_dev_workspace(session, 'my-project') >>> len(workspace['window'].panes) 3 >>> # Clean up >>> workspace['window'].kill() ``` ### Recipe: Create a grid of panes ```python >>> from libtmux.constants import PaneDirection >>> def create_pane_grid(session, rows=2, cols=2, name='grid'): ... """Create an NxM grid of panes.""" ... window = session.new_window(window_name=name, attach=False) ... window.resize(height=50, width=160) ... ... panes = [] ... base_pane = window.active_pane ... panes.append(base_pane) ... ... # Create first row of panes ... current = base_pane ... for _ in range(cols - 1): ... new_pane = current.split(direction=PaneDirection.Right) ... panes.append(new_pane) ... current = new_pane ... ... # Create additional rows ... for _ in range(rows - 1): ... row_start = panes[-cols] ... current = row_start ... for col in range(cols): ... new_pane = panes[-cols + col].split(direction=PaneDirection.Below) ... panes.append(new_pane) ... ... # Apply tiled layout for even distribution ... window.select_layout('tiled') ... return window, panes >>> grid_window, grid_panes = create_pane_grid(session, rows=2, cols=2, name='test-grid') >>> len(grid_panes) >= 4 True >>> # Clean up >>> grid_window.kill() ``` ### Recipe: Run commands in multiple panes ```python >>> import time >>> def run_in_panes(panes, commands): ... """Run different commands in each pane.""" ... for pane, cmd in zip(panes, commands): ... pane.send_keys(cmd) >>> multi_window = session.new_window(window_name='multi-cmd', attach=False) >>> multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS Window(@... ...) >>> pane_a = multi_window.active_pane >>> pane_b = multi_window.split() >>> pane_c = multi_window.split() >>> run_in_panes( ... [pane_a, pane_b, pane_c], ... ['echo "Task A"', 'echo "Task B"', 'echo "Task C"'], ... ) >>> # Give commands time to execute >>> time.sleep(0.2) >>> # Verify all commands ran >>> 'Task A' in '\\n'.join(pane_a.capture_pane()) True >>> # Clean up >>> multi_window.kill() ``` ## Window Context Managers Windows can be used as context managers for automatic cleanup: ```python >>> with session.new_window(window_name='temp-window') as temp_win: ... pane = temp_win.active_pane ... pane.send_keys('echo "temporary workspace"') ... temp_win in session.windows True >>> # Window is automatically killed after exiting context >>> temp_win not in session.windows True ``` :::{seealso} - {ref}`pane-interaction` for working with pane content - {ref}`automation-patterns` for advanced orchestration - {class}`~libtmux.Window` for all window methods - {class}`~libtmux.Session` for session management ::: ---