# API Reference Source: https://libtmux.git-pull.com/api/ (api)= (reference)= # API Reference libtmux's public API mirrors tmux's object hierarchy: {class}`~libtmux.Server` → {class}`~libtmux.Session` → {class}`~libtmux.Window` → {class}`~libtmux.Pane`. Attached terminals show up as {class}`~libtmux.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 {meth}`server.sessions.get() `, {meth}`session.windows.get() `. ::: :::{grid-item-card} Send commands or keys to a terminal? :link: libtmux.pane :link-type: doc Use {meth}`pane.send_keys() ` and {meth}`pane.enter() `. ::: :::{grid-item-card} Capture output from a pane? :link: libtmux.pane :link-type: doc Use {meth}`pane.capture_pane() `. ::: :::{grid-item-card} Write tests against tmux? :link: testing/index :link-type: doc Use the {doc}`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 {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' ``` Inspect field names on {class}`~libtmux.neo.Obj`: ```python >>> from libtmux.neo import Obj >>> sorted(Obj.__dataclass_fields__)[:3] ['active_window_index', 'alternate_saved_x', 'alternate_saved_y'] ``` ```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 [pytest] 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 ``` [pytest]: https://docs.pytest.org/en/stable/ --- # 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. In doctests, libtmux injects the same objects through `doctest_namespace`: ```python >>> created_session = server.new_session(session_name="my-session") >>> created_session is not None True >>> created_session.kill() >>> created_window = session.new_window(window_name="test") >>> created_window is not None True >>> created_window.kill() ``` ## 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 | Fixture | Use | |---------|-----| | {fixture}`server` | Bare isolated server | | {fixture}`session` | Ready-to-use isolated session | | {fixture}`home_path` / {fixture}`user_path` | Temporary home directories | | {fixture}`config_file` | Test `.tmux.conf` | | {fixture}`session_params` | Session creation override | | {fixture}`TestServer` | Factory for extra isolated servers | | {fixture}`control_mode` | Attached client factory | | {fixture}`clear_env` | Minimal test environment | --- ## Core Fixtures The primary injection points for libtmux tests. ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.server .. autofixture:: libtmux.pytest_plugin.session ``` ## 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 ``` ## Factories ```{eval-rst} .. autofixture:: libtmux.pytest_plugin.TestServer .. autofixture:: libtmux.pytest_plugin.control_mode ``` ## 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]: https://docs.pytest.org/en/stable/ --- # 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 ``` [pytest]: https://docs.pytest.org/en/stable/ --- # 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? If the fixture defaults do not fit your test suite, [start a discussion] with the use case before depending on undocumented behavior. [start a discussion]: 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 plugin is automatically detected by [pytest], and the fixtures are 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-driven tmux tests [pytest-tmux] also works through {ref}`pytest fixtures `, so the same fixture concepts apply. 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() -> dict[str, int]: ... return {"x": 800, "y": 600} ``` 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 >>> temp_server = Server() >>> temp_session = temp_server.new_session() >>> temp_server.is_alive() True >>> temp_server.kill() ``` You can also use it with custom configurations, similar to the {ref}`server fixture `: ```python >>> config_path = request.getfixturevalue("tmp_path") / "tmux.conf" >>> _ = config_path.write_text("set -g status off") >>> configured_server = Server(config_file=str(config_path)) >>> _ = configured_server.new_session() >>> configured_server.is_alive() True >>> configured_server.kill() ``` 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, ... ) -> None: ... monkeypatch.setenv("HOME", str(user_path)) ``` [pytest]: https://docs.pytest.org/en/stable/ [pytest-tmux]: https://pytest-tmux.readthedocs.io/ [tmuxp]: https://tmuxp.git-pull.com/ --- # 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. winlink The link between a {term}`session` and a {term}`window`: the triple ``(session, index, window)``. A window does not live *in* one session; a session holds *links* to windows, each at an index. ``link-window`` adds another link to the same window, so one window can be reachable from several sessions -- and even from one session at two indexes. This is what tmux enumerates: a row of ``list-windows`` or ``list-panes -a`` names a winlink, not a window. See {ref}`winlinks`. Target A target, cited in the manual as ``[-t target]`` can be a session, window or pane. TMUX Environment variable tmux exports into every {term}`Pane` it spawns. Holds ``socket_path,server_pid,session_id`` for the {term}`Server` the pane belongs to. The session id is spelled bare, e.g. ``47``, where libtmux spells the same session ``$47``. Written once, when the pane is spawned, and never revised — so its session id records where the process was *launched*, and goes stale if the pane's {term}`Window` later moves. libtmux takes only the socket path from it, and asks tmux for the rest. See {ref}`self-location`. TMUX_PANE Environment variable tmux exports into every {term}`Pane` it spawns. Holds that pane's ``pane_id``, e.g. ``%1``. Unlike ``TMUX`` it always names the pane the process is really in, so it is the id libtmux anchors on to answer where a process is running. Read back by libtmux in {ref}`self-location`. ``` --- # 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 Isolated tmux fixtures and test helpers. ::: :::{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 >>> demo_window = session.new_window(window_name="my-project") >>> demo_pane = demo_window.active_pane >>> demo_pane.send_keys("echo hello") >>> demo_window.kill() ``` ``` 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 | ## Know where you're running Sometimes you hold no handle at all, because your code is *running inside* a pane. You don't have to search the server for yourself — tmux writes `TMUX` and `TMUX_PANE` into every pane it spawns, and each level of the hierarchy reads them back: ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}") >>> monkeypatch.setenv("TMUX_PANE", pane.pane_id) >>> Pane.from_env().pane_id == pane.pane_id True >>> Session.from_env().session_name == session.session_name True ``` Inside a pane tmux has already set those two variables, so {meth}`Pane.from_env() ` takes no arguments; these docs are not running in a pane, so the example sets them first. Outside tmux there is no pane to return and {exc}`~libtmux.exc.NotInsideTmux` is raised instead. See {ref}`self-location`. ## Testing libtmux ships a [pytest plugin](api/testing/pytest-plugin/index.md) with isolated tmux fixtures: ```python >>> test_window = session.new_window(window_name="test") >>> test_pane = test_window.active_pane >>> test_pane.send_keys("echo hello") >>> test_window.window_name 'test' >>> test_window.kill() ``` ```{toctree} :hidden: quickstart topics/index api/index api/testing/index internals/index project/index history migration glossary MCP GitHub ``` --- # Internal Constants Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.constants/ # Internal Constants The {mod}`libtmux._internal.constants` module documents private constants used inside libtmux. :::{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 Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.dataclasses/ # Dataclass helpers The {mod}`libtmux._internal.dataclasses` module contains private dataclass utilities used by internal objects. ```{eval-rst} .. automodule:: libtmux._internal.dataclasses :members: :special-members: ``` --- # List querying Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.query_list/ # List querying The {mod}`libtmux._internal.query_list` module contains the private collection filtering implementation behind public list accessors. ```{eval-rst} .. automodule:: libtmux._internal.query_list :members: ``` --- # Internal Sparse Array Source: https://libtmux.git-pull.com/internals/api/libtmux._internal.sparse_array/ # Internal Sparse Array The {mod}`libtmux._internal.sparse_array` module contains the sparse-index mapping used by indexed hooks and options. :::{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 This page's content moved. Formatting, linting, typing, and import conventions are now in {doc}`Contributing `. Docstring conventions are in [`.github/WRITING.md`](https://github.com/tmux-python/libtmux/blob/master/.github/WRITING.md#docstrings). --- # 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 --- # Contributing Source: https://libtmux.git-pull.com/project/contributing/ (development)= # Contributing The contributor guide — environment setup, the gates, tests, documentation builds, releases, and the pull request process — now lives in [.github/CONTRIBUTING.md](https://github.com/tmux-python/libtmux/blob/master/.github/CONTRIBUTING.md). Prose conventions — README, `CHANGES`, commit messages, docstrings, and source comments — are in [.github/WRITING.md](https://github.com/tmux-python/libtmux/blob/master/.github/WRITING.md). --- # 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 ``` [Ruff]: https://docs.astral.sh/ruff/ [mypy]: https://mypy-lang.org/ [NumPy]: https://numpydoc.readthedocs.io/en/latest/format.html --- # 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 | |--------|-------------| | {class}`~libtmux.Server` | `from libtmux.server import Server` | | {class}`~libtmux.Session` | `from libtmux.session import Session` | | {class}`~libtmux.Window` | `from libtmux.window import Window` | | {class}`~libtmux.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) ``` [PyPI]: https://pypi.org/project/libtmux/ --- # 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 notifications 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/ [PyPI]: https://pypi.org/project/libtmux/ [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}`~libtmux.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 {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.Pane` objects. ## 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}`~libtmux.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}`~libtmux.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}`~libtmux.Session` If you have multiple tmux sessions open, all methods in {class}`~libtmux.Server` are available. We can list sessions with {attr}`Server.sessions `: ```python >>> server.sessions [Session($1 ...), Session($0 ...)] ``` This returns a list of {class}`~libtmux.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 {attr}`Server.sessions ` and {attr}`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 ID {attr}`Server.sessions ` returned above. ```python >>> server.sessions.filter(session_id='$1')[0] Session($1 ...) ``` You may call {meth}`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 {meth}`filter() `, pass in attributes and return a list of matches. In this case, a {class}`~libtmux.Server` holds a collection of child {class}`~libtmux.Session` objects. {class}`~libtmux.Session` and {class}`~libtmux.Window` both utilize {meth}`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}`~libtmux.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}`~libtmux.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 call {meth}`Window.kill() ` on the {class}`~libtmux.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 {attr}`Session.windows ` and {meth}`Session.windows.filter() ` to list and sort through active {class}`~libtmux.Window` objects. ## Manipulating windows Now that we know how to create windows, let's use one. Let's use {attr}`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}`~libtmux.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. {attr}`session.active_window ` gave us the {class}`~libtmux.Window` of the current attached window. 2. `attach=False` assures the cursor didn't switch to the newly created pane. 3. Returned the created {class}`~libtmux.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}`~libtmux.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 {meth}`Pane.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 {meth}`pane.enter() ` after. ## Working with options libtmux provides a unified API for managing tmux options across {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.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 use tmux's internal IDs 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, {ref}`workspace-setup` uses these same internals to build 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`.) ::: [test suite]: https://github.com/tmux-python/libtmux/tree/master/tests --- # Architecture Source: https://libtmux.git-pull.com/topics/architecture/ (about)= # Architecture When you use libtmux, you work through a hierarchy of typed Python objects — {class}`~libtmux.server.Server`, {class}`~libtmux.session.Session`, {class}`~libtmux.window.Window`, and {class}`~libtmux.pane.Pane` — each a proxy for the tmux entity it represents. You navigate from one to the next (a server's sessions, a session's windows, a window's panes), and every method you call turns into a tmux command directed at that exact object. You don't need anything on this page to use the API; the objects and their methods work out of the box. This is reference material for when you're curious how libtmux tracks those objects, keeps their identities stable across refreshes, and lays out the code underneath. Skim the first section and stop whenever you've seen enough. Under the hood, libtmux is a [typed](https://docs.python.org/3/library/typing.html) abstraction layer built on two tmux primitives: targets (`-t`), which direct a command at an individual session, window, or pane, and `FORMATS`, the template variables tmux exposes to describe each object's properties. ## Object hierarchy libtmux mirrors tmux's object hierarchy as a typed Python ORM, so the parent-child relationships you know from tmux carry over directly into the objects you hold: ``` 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` | The Session, Window, Pane, and Client classes share a common dataclass base (`Obj`) defined in {mod}`libtmux.neo`, which fetches each object's fields from tmux; the parent and child links above are plain properties on each class. One object breaks the ownership chain: {class}`~libtmux.client.Client` is a *view*, not a child. Each attached terminal points at the Session/Window/Pane it is currently displaying, but is not owned by them — so its view can change the moment a user switches sessions. See {ref}`clients` for the view-vs-identity distinction. ## Internal identifiers When tmux state changes, libtmux needs a way to recognize that the same window is still the same window. tmux assigns each session, window, and pane a unique ID for exactly this, and libtmux stores it as a dataclass attribute on each object (`session_id`, `window_id`, `pane_id`) to track objects reliably across state refreshes rather than relying on names or indexes that shift around. | 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 These are the five classes you'll actually hold and call methods on. Each level wraps the tmux commands and format queries for its tier of the hierarchy: - {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 Every interaction follows the same round-trip: you act on a Python object, libtmux talks to tmux, and the result comes back as more typed objects. Reading state and changing it both cost a tmux call, which is why an object can go stale and why you refresh it rather than trust a cached value indefinitely. 1. User creates a {class}`~libtmux.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 The codebase splits along the same hierarchy: one module per tier, plus shared plumbing. The first block is what you import and use day to day; the second is lower-level and mostly of interest to contributors or deeper integrations. | 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 | The remaining modules are advanced — reach for them only when the core objects don't cover your case: | Module | Role | |--------|------| | {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 When you automate a terminal workflow, you are usually coordinating more than one process: you kick off work in one pane, watch another for a completion signal, and keep several tasks moving without blocking on any single one. libtmux's object API makes that coordination ordinary Python — you start commands with {meth}`~libtmux.Pane.send_keys`, read what came back with {meth}`~libtmux.Pane.capture_pane`, and fan work across panes with {meth}`~libtmux.Pane.split`. This guide collects the patterns that turn a loose pile of {meth}`send_keys() ` calls into automation you can trust: output monitoring, timeouts, retries, and multi-pane orchestration. Most scripts only need a couple of these patterns. Output monitoring and the context manager patterns cover the common case — send a command, wait for a marker, clean up after yourself — so start there. The later sections (state machines, task queues) are for the rarer cases where a single pane drives a longer sequence of steps; reach for them when you actually need them. These patterns lean on polling: you call {meth}`~libtmux.Pane.capture_pane` in a loop and `sleep` between reads. That is simpler than wiring up an event-driven system, and it costs you latency — each poll is a tmux round-trip, and a `sleep` between polls is dead time you pay whether the command finished or not. For most automation that trade is worth it. When milliseconds matter, look instead at tmux hooks or an external event-driven framework. Open two terminals: Terminal one: start tmux: ```console $ tmux ``` Terminal two, `python` or `ptpython` if you have it: ```console $ python ``` The examples below assume you already have `server` and `session` objects in scope. In this documentation they come from libtmux's {doc}`pytest fixtures `, which run the doctests against a live tmux server; in your own scripts you create them yourself (a {class}`~libtmux.Server` and a session from {meth}`~libtmux.Server.new_session`). Each example builds its own window or pane and tears it down at the end, so the snippets stand alone and don't depend on each other. ## Process control ### Starting long-running processes When you send a command to a pane with {meth}`~libtmux.Pane.send_keys`, it runs in the background — control returns to your script immediately, while the command keeps going in the pane. The pane object stays your handle on that running work. ```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 Because {meth}`send_keys() ` doesn't wait, you find out whether a command is still running the same way a person would: by reading what's on screen. Capture the pane and look for a marker your command prints when it reaches a known state. ```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 The workhorse of terminal automation is "run something, then block until a string shows up." You wrap {meth}`~libtmux.Pane.capture_pane` in a loop with a timeout, so a command that never finishes can't hang your script forever. The `poll_interval` is the latency/work trade in one knob: poll faster to react sooner, slower to spare tmux the round-trips. ```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 Waiting for success is only half the job — you also want to notice failure. The same capture-and-scan approach works for spotting error patterns, so you can bail out early instead of timing out on a command that already crashed. ```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 Sometimes you don't want the whole scrollback — you want just the lines a command produced. Bracket the interesting output with a marker you control, then return everything that follows it. This is how you pull a command's result out of a shared pane without dragging along the prompt and prior history. ```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 To run work in parallel, give each task its own pane. You split the window with {meth}`~libtmux.Pane.split`, choosing where the new pane lands with {class}`~libtmux.constants.PaneDirection`, then fire a command into each. Because {meth}`send_keys() ` returns immediately, the tasks run concurrently; you gather their results afterward by capturing every pane. ```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 A fixed `sleep` only works when you know how long the slowest task takes. When tasks finish at different times, watch them all at once and drop each pane from the watch-list as its marker appears — you return as soon as the last one completes, instead of always waiting for a worst-case timeout. ```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 Cleanup is the part of automation that's easy to forget — and forgetting leaves orphaned sessions and windows behind on the tmux server. A `with` block makes the cleanup automatic: the session lives for the body and is killed on the way out, even if an exception interrupts you. It costs a little to spin a session up and tear it down, but you get a guaranteed-clean slate that never leaks. ```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 When you only need a scratch space for one subtask, scope a window the same way. The window opens for the body of the block and is gone afterward, so a short-lived job never outlives its purpose. ```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 Any command you wait on can hang, so give every wait an upper bound. Pair the command with a completion marker and poll until either the marker shows up or the clock runs out — and when it runs out, raise, so a stuck command surfaces as an error you can catch instead of a script that quietly stalls. ```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 For flaky work that succeeds on a later attempt, retry until a success marker appears. Be honest about the cost: each retry runs the command again and waits the full `delay`, so a slow `delay` times `max_retries` is the worst case you're signing up for. Tune both for how expensive the command is and how patient you can be. ```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 The patterns so far drive one command at a time. The two below compose them into longer sequences that one pane runs end to end — reach for these when a task is genuinely a pipeline of steps, not a single call. ### Task queue processor A task queue runs a list of commands in order, waiting for each to finish before starting the next. You tag every task with an indexed marker so you know exactly which step you're waiting on, and you collect a pass/fail result per task. ```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 When the next step depends on the previous one finishing, model the work as a state machine: each state runs a command and waits for the transition marker that unlocks the next. A per-state timeout keeps a single stuck step from stalling the whole run, and the history tells you how far you got before it stopped. ```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 Timing is a guess; a marker is a fact. Instead of sleeping long enough and hoping a command finished, have it print an explicit marker and poll for that. Your automation then reacts to what actually happened rather than to a clock. ```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 Every window and session you create lives on the tmux server until something kills it. Tear down what you opened when you're done, so a long-running automation process doesn't accumulate orphaned objects. ```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 Better still, let a `with` block do the cleanup for you. It runs even when the body raises, which is exactly when manual cleanup tends to get skipped — so the resource is released whether the work succeeded or blew up. ```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. Most code reads a client's current attachment once and branches on it; the details about staleness and refresh below rarely matter in practice. ## View, not identity You rarely need this detail unless you're tracking a client across several user commands, but it's worth understanding why certain fields go stale. 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 | :::{seealso} **Why there is no `Client.from_env()`.** A pane can name the {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.Pane` it is running in, because tmux writes those ids into its environment. A client is the one thing it cannot name. Viewing is not owning: no client may be attached at all — a detached session, a CI job, a `send-keys` script — or several may be, each looking somewhere else. tmux exports no client id into a pane because there is no single right answer to export. See {ref}`self-location` for what a pane *can* resolve about itself. ::: ## Live attachment lookup When you want the *current* attachment — not the snapshot — use the three `attached_*` properties. Each calls {meth}`~libtmux.Client.refresh` to query the current state (one tmux round-trip) and then resolves the typed {class}`~libtmux.Session`, {class}`~libtmux.Window`, or {class}`~libtmux.Pane` it's viewing. This costs a little — you're asking for the live state — but you get the current view in return: ```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, then walks to the active window and its active pane. ```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 If you need to find or filter clients, you iterate over or query the server's client collection. {attr}`~libtmux.Server.clients` returns a {class}`~libtmux._internal.query_list.QueryList` of every client tmux reports through `list-clients`. Filter or {meth}`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 ``` For filtering clients, use `server.clients.filter(...)` or iterate over {attr}`~libtmux.Server.clients` directly; see {ref}`native-filtering` if you want tmux's native format-based filtering on sessions, windows, panes, and buffers. ## Missing live attachments When a client detaches or its view becomes stale, the `attached_*` properties return `None` so you can branch on truthiness without a `try`/`except` block. This happens in three cases: - 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` for you. ## See also - {doc}`/api/libtmux.client` — autodoc reference - {ref}`about` — where {class}`~libtmux.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 You configure libtmux through Python: there are no config files, and you set everything through method calls on {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.Pane` objects, with sensible defaults. If you're driving tmux through the standard object API, you're already configured correctly and can stop reading here. The rest of this page is for the rarer cases. It documents two lower layers you can reach for when the defaults aren't enough: the environment variables libtmux reads, and the format-string system libtmux uses internally to read tmux state. ## Environment variables You set almost nothing here. The two variables that matter most, tmux writes for you and libtmux only reads back, so a normal Python process driving tmux has nothing to arrange in this section. tmux exports both into every pane it spawns: | Variable | What tmux puts in it | |---|---| | `TMUX` | the server that pane belongs to, as `socket_path,server_pid,session_id` | | `TMUX_PANE` | the id of the pane itself, e.g. `%1` | Code running *inside* a pane — a script you started in a split, a hook, a test harness — reads them back to get a handle on itself, rather than searching the server for a pane it already is. That is the `from_env` family: {meth}`Server.from_env() `, {meth}`Session.from_env() `, {meth}`Window.from_env() `, and {meth}`Pane.from_env() `. Outside a pane neither variable is set, and all four raise {exc}`~libtmux.exc.NotInsideTmux`. You never write them yourself: {ref}`self-location` covers what each call does with them, why the session id in `TMUX` goes stale, and the `env` mapping you hand `from_env` in tests instead of touching the real environment. tmux reads `TMUX` too — it is how tmux notices you are already inside a session and guards against nesting one. {meth}`Server.new_session() ` unsets it for the length of that one call and restores it afterward, so creating a session from inside a pane works without you arranging anything. That leaves the two variables that *are* yours to set, and most people set neither. `TMUX_TMPDIR` is tmux's own — the directory it keeps sockets in. libtmux never reads it, but the tmux binary it shells out to does, so it shapes which server a bare {class}`~libtmux.Server` lands on; pass `socket_name` or `socket_path` when you would rather name the server outright. `LIBTMUX_TMUX_FORMAT_SEPARATOR` is the one variable libtmux itself defines: an advanced override for the separator (default `␞`) it uses internally to parse tmux's format output — you'd touch it only if that character ever collided with your own data. ## Format strings When you read a typed attribute like {attr}`~libtmux.Window.window_name` or {attr}`pane.pane_current_path `, libtmux is querying tmux behind the scenes through tmux's own format system. The format constants that drive those queries live in {mod}`libtmux.formats` and are used internally by every object type, so in normal use you never write format strings yourself — the typed attributes on each object hand you the values directly. For the rarer case where you want to know exactly which formats tmux exposes, see the [tmux man page](http://man.openbsd.org/OpenBSD-current/man1/tmux.1) for the full list. --- # Context managers Source: https://libtmux.git-pull.com/topics/context_managers/ (context_managers)= # Context managers When you create tmux objects through libtmux, they normally live until you explicitly kill them. A context manager hands that cleanup back to Python: you scope an object to a block, and libtmux kills the underlying tmux object the moment you leave it — whether you exit cleanly or an exception unwinds the stack. The {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.Pane` classes (all main tmux objects) support this. Most readers never reach for this. If you're building a long-running application, you typically let objects persist and tear them down yourself. The context-manager form earns its keep in test fixtures and short-lived scripts, where you want a tmux object to exist for exactly one block and then vanish. 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 You 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 You 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 You 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 You 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 For complex setups, you can nest contexts to build a whole tmux hierarchy at once and have every layer torn down for you: ```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 Reaching for a context manager buys you a few things. Resources clean themselves up the moment you leave the block, so you never manually call the {meth}`~libtmux.Server.kill`, {meth}`~libtmux.Session.kill`, {meth}`~libtmux.Window.kill`, or {meth}`~libtmux.Pane.kill` methods and the code stays uncluttered. Because cleanup runs on the way out of the block, it fires even when an exception unwinds the stack — so you don't leak a stray session or pane on the error path. And when you nest contexts, the objects tear down in hierarchical order, which keeps tmux's own bookkeeping consistent. ## When to use Use context managers when you're writing test fixtures, running short-lived sessions, or managing several tmux servers that each need to disappear cleanly. They also pay off in any script that might raise partway through, or when you're spinning up an isolated environment that has 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 This page explains the "why" behind libtmux's shape: the four core choices it makes about representing tmux to your Python code. You don't need any of it to get started — the defaults work out of the box, and most code never thinks about the rationale below. Read on when a choice starts to matter to you: why {attr}`session.windows ` is a live collection, why properties read cleanly off an object, and what to expect from a pre-1.0 API. ## Why ORM-style objects Most of your code just writes {attr}`session.windows ` and gets a live, filterable collection back — you rarely think about why it's shaped that way. This section is for when you're curious about the design. tmux organizes terminals in a strict hierarchy: {class}`~libtmux.Server` → {class}`~libtmux.Session` → {class}`~libtmux.Window` → {class}`~libtmux.Pane`. Each level owns the next. libtmux mirrors that hierarchy with Python objects that maintain the same parent-child relationships, so navigating tmux feels like navigating Python. What you get is a relational structure you can walk in either direction: {attr}`session.windows ` lists a session's windows, {attr}`pane.window ` points back up to the pane's parent. The alternative — a flat command-builder API (`tmux("new-session", "-s", "foo")`) — hands back raw strings and leaves you to track which windows belong to which session yourself. The trade-off is that an object is a snapshot. If tmux state changes out from under you — another client splits a window, a process exits — your object can go stale, and you reach for {meth}`~libtmux.Session.refresh` to re-read it. You trade that occasional refresh for an API that reads like the hierarchy it models. ## 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 queries through this system 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 The cost is a tmux round-trip on the live collections: reading a property like {attr}`session.windows ` runs a subprocess against the server each time you access it, not a cached value (the scalar fields like {attr}`session.session_name ` are different — read once when the object is built). What it buys is a value you can trust — pulled straight from tmux's own reporting, not reconstructed by guessing at the layout of display text. The format constants that make this work live in {mod}`libtmux.formats`. ## Why typed data rows Advanced — for contributors and lower-level query work. Most code uses the ORM objects above and never touches this layer directly. {mod}`libtmux.neo` provides a modern [dataclass](https://docs.python.org/3/library/dataclasses.html)-based interface alongside the legacy dict-style objects. The motivation: - **Type safety**: dataclass fields have declared types, enabling [mypy](https://mypy-lang.org/) 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 Coexistence is the honest trade-off: two interfaces are more surface area to learn than one. The payoff is that you can adopt the typed path incrementally, file by file, without a flag-day rewrite. ## 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 for code you write today: - **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. --- # Filtering collections Source: https://libtmux.git-pull.com/topics/filtering/ (querylist-filtering)= # Filtering collections Every collection libtmux hands you — {attr}`server.sessions `, {attr}`session.windows `, and {attr}`window.panes ` — is a {class}`~libtmux._internal.query_list.QueryList`, a list that knows how to filter itself. You narrow one by calling {meth}`~libtmux._internal.query_list.QueryList.filter` with keyword arguments, optionally suffixed with a lookup like `__contains`, `__startswith`, or `__regex`, and you get back another {class}`~libtmux._internal.query_list.QueryList` you can iterate or chain further. It's Django-style filtering applied to sessions, windows, and panes. Most readers never look beyond `.filter()`. It's the common path, it works out of the box on every collection, and the lookup suffixes and chaining cover almost every query you'll write. The tmux-native `.search_*()` methods at the end of this page are an optional escape hatch for large servers — you can skip them until you measure a reason to care. ## Basic filtering Every collection is already a {class}`~libtmux._internal.query_list.QueryList`, so you can inspect one before you narrow it. Here's the full set of sessions on your server: ```python >>> server.sessions # doctest: +ELLIPSIS [Session($... ...)] ``` ### Exact match When you pass a bare keyword like `session_name=...`, the default lookup is `exact` — so these two calls mean the same thing: ```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 Add a suffix to the keyword to match part of a value instead of the whole thing: ```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 Each suffix you append after the `__` selects one of these lookups. The `i`-prefixed variants ignore case: | 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 When you expect exactly one match and want the object itself rather than a list, reach for {meth}`~libtmux._internal.query_list.QueryList.get`: ```python >>> window = session.windows.get(window_id=session.active_window.window_id) >>> window # doctest: +ELLIPSIS Window(@... ..., Session($... ...)) ``` `get()` insists on exactly one result. If the query matches the wrong number of objects, it raises: - {exc}`~libtmux.exc.ObjectDoesNotExist` - no matching object found - {exc}`~libtmux.exc.MultipleObjectsReturned` - more than one object matches Both are {exc}`~libtmux.exc.LibTmuxException`s, so one `except` clause catches either, and both say what they went looking for: ```python >>> from libtmux import exc >>> try: ... session.windows.get(window_name="nonexistent") ... except exc.LibTmuxException as e: ... print(e) No objects found: window_name='nonexistent' ``` Pass a `default` to get a fallback value back instead of an exception: ```python >>> session.windows.get(window_name="nonexistent", default=None) is None True ``` A `default` stands in for an object that is *absent*, so it does not apply when a query is merely ambiguous — {exc}`~libtmux.exc.MultipleObjectsReturned` is raised whether or not you passed one. Handing back one of several equally valid matches is how you end up driving the wrong pane. The next section is about the one case where an ambiguous match is routine rather than a mistake. (winlinks)= ## When one window is in two sessions A server-wide collection — {attr}`server.windows ` and {attr}`server.panes ` — does not enumerate windows. It enumerates {term}`winlinks `: the `(session, index, window)` edges tmux actually stores. Nearly always there is exactly one edge per window and you never notice the difference. Sharing a window adds edges. `link-window` does it explicitly, and a grouped session (`tmux new-session -t existing`, the mechanism behind [tmuxp](https://tmuxp.git-pull.com/)'s session groups) does it for every window at once. The window is then genuinely reachable from each session that links it, and a server-wide listing reports it once per edge: ```python >>> home = server.new_session(session_name="home") >>> shared = home.new_window(window_name="shared", attach=False) >>> guest = server.new_session(session_name="guest") >>> _ = server.cmd( ... "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" ... ) >>> len(server.windows.filter(window_id=shared.window_id)) 2 ``` Two rows for one `window_id` is not a miscount — it is the shape of the data. The window is in both sessions, and a listing that collapsed the rows would be throwing away the very fact you need. The consequence is that a *point lookup* against a server-wide collection can be ambiguous, and says so rather than guessing: ```python >>> from libtmux import exc >>> home = server.new_session(session_name="home") >>> shared = home.new_window(window_name="shared", attach=False) >>> guest = server.new_session(session_name="guest") >>> _ = server.cmd( ... "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" ... ) >>> try: ... server.windows.get(window_id=shared.window_id) ... except exc.MultipleObjectsReturned as e: ... print(e) # doctest: +ELLIPSIS Multiple objects returned (2): window_id='@...' ``` ### Which sessions hold it? {attr}`Window.linked_sessions ` answers directly, listing each holding session once however many indexes it links the window at: ```python >>> home = server.new_session(session_name="home") >>> shared = home.new_window(window_name="shared", attach=False) >>> guest = server.new_session(session_name="guest") >>> _ = server.cmd( ... "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" ... ) >>> sorted(s.session_name for s in shared.linked_sessions) ['guest', 'home'] ``` ### Just fetch the object When you have an id and want the object, don't scan a listing for it — name it. {meth}`Pane.from_pane_id ` and {meth}`Window.from_window_id ` hand the id to tmux with a `-t` target, and tmux always resolves it to exactly one object — the same one it would act on if you typed the command yourself. They cannot be ambiguous, so they are the right tool for a lookup by id: ```python >>> from libtmux.window import Window >>> home = server.new_session(session_name="home") >>> shared = home.new_window(window_name="shared", attach=False) >>> guest = server.new_session(session_name="guest") >>> _ = server.cmd( ... "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" ... ) >>> Window.from_window_id(server, shared.window_id).window_id == shared.window_id True ``` Reserve the server-wide collections for what they are good at — sweeping the whole server — and reach for them with {meth}`~libtmux._internal.query_list.QueryList.filter`, which is happy to return two rows, rather than `get()`, which is not. ## Chaining filters You can stack conditions two ways, and both narrow with AND. Pass several keywords to a single `.filter()` call, or chain `.filter()` calls one after another: ```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 Reach for the `i`-prefixed variants when the casing of a name shouldn't matter: ```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 When a prefix or substring isn't expressive enough, the regex lookups match against a full pattern: ```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 When you already have a set of names in hand, `in` keeps the matches and `nin` (not in) drops them: ```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 You aren't limited to one window's panes. Every level of the hierarchy returns a {class}`~libtmux._internal.query_list.QueryList`, and the server-wide collections — {attr}`server.panes `, {attr}`server.windows `, and {attr}`server.sessions ` — flatten everything beneath them into a single list. That lets you query the whole server at once, which is handy when you want a pane by some attribute and don't care which session or window it lives in: ```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 A couple of patterns you'll reach for in practice. ### Find all editor windows Match several editor names at once with a single regex lookup: ```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 If you name windows by convention, a prefix match pulls the whole group, and `.get()` plucks one out by name: ```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)= ## Filtering before object creation Everything above runs in Python, *after* tmux has already returned every row. That's fine for the handful of sessions and windows most servers carry. But on a large server — hundreds or thousands of panes, where you want only a few — you pay to build objects you immediately discard. The `search_*()` methods push the filtering down to tmux itself: tmux applies a format expression and hands back only the matching rows, so libtmux builds objects for the matches alone. Every level of the hierarchy ships one: | 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 | 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. --- # Floating panes Source: https://libtmux.git-pull.com/topics/floating_panes/ (floating-panes)= # Floating panes You can create floating panes — non-modal panes that hover above the tiled layout like a popup, but with full escape-sequence support and all the regular pane operations (capture, send-keys, and so on). You create them with {meth}`Window.new_pane() ` or {meth}`Pane.new_pane() `, the same way you reach for {meth}`Window.split() ` to add a tiled pane. Most workflows never need one — tiled panes cover the everyday cases, and you can stop reading here unless you want a transient overlay (a quick log tail, a scratch shell, a status readout) sitting on top of your layout without rearranging it. Because a floating pane behaves like any other {class}`~libtmux.Pane`, everything you already do — capturing output, sending keys, querying state — works on it unchanged. ```{note} Floating panes require **tmux 3.7+** ({meth}`Window.new_pane() ` / {meth}`Pane.new_pane() ` raise {exc}`~libtmux.exc.LibTmuxException` on older tmux). ``` ## Creating a floating pane When you call {meth}`Window.new_pane() `, you get back the new {class}`~libtmux.Pane`, exactly as {meth}`Window.split() ` hands you a tiled one. You can confirm a pane is floating by reading its {attr}`pane_floating_flag `, which is `"1"` when it floats: ```python >>> from libtmux.common import has_gte_version >>> if has_gte_version("3.7"): ... floating = window.new_pane(width=20, height=5, shell="sleep 30") ... is_floating = floating.pane_floating_flag ... else: ... is_floating = "1" >>> is_floating '1' ``` ## Sizing and positioning You set the pane's **size** with `width` and `height` (tmux's `-x` / `-y`), and its **position** with `x` and `y` — cells measured from the top-left of the window (tmux's `-X` / `-Y`). tmux reports the placement back through the {attr}`pane_x ` / {attr}`pane_y ` fields: ```python >>> from libtmux.common import has_gte_version >>> if has_gte_version("3.7"): ... placed = window.new_pane(width=20, height=5, x=2, y=1, shell="sleep 30") ... position = (placed.pane_x, placed.pane_y) ... else: ... position = ("2", "1") >>> position ('2', '1') ``` ## Styling For the rarer cases where appearance matters, you can style a floating pane with the same overlay options tmux's `new-pane` accepts: `style` (the pane body), `active_border_style`, and `inactive_border_style`. Each takes a tmux style string, for example `style="bg=black"` or `active_border_style="fg=green"`. The defaults read fine on most terminals, so reach for these only when you want a float to stand out. ## Keeping a pane open By default a floating pane closes the moment its command exits — fine for a fire-and-forget command, but you lose whatever it printed. When you want the output to linger, pass `keep=True` to hold the pane open until you press a key (tmux's `-k`), or `message="..."` to hold it open showing a custom `remain-on-exit-format` line (tmux's `-m`). The cost is explicit and small: both flip the pane's `remain-on-exit` option to `key`, which buys you a pane that stays on screen and waits for you after the command finishes instead of vanishing: ```python >>> from libtmux.common import has_gte_version >>> if has_gte_version("3.7"): ... held = window.new_pane(width=20, height=5, shell="sleep 30", keep=True) ... remain = held.cmd("show-options", "-p", "-v", "remain-on-exit").stdout ... else: ... remain = ["key"] >>> remain ['key'] ``` ## Identifying floating panes When you need to tell floating panes from tiled ones in code, reach for the tmux 3.7 {attr}`pane_floating_flag ` field. Every {class}`~libtmux.Pane` carries it, so you can branch on it anywhere you hold a pane — including filtering a window's {attr}`~libtmux.Window.panes` down to just the floats: ```python >>> from libtmux.common import has_gte_version >>> if has_gte_version("3.7"): ... _ = window.new_pane(width=20, height=5, shell="sleep 30") ... floating = [p for p in window.panes if p.pane_floating_flag == "1"] ... found = len(floating) >= 1 ... else: ... found = True >>> found True ``` See {meth}`Pane.new_pane() ` for the full parameter reference and {ref}`format-tokens` for the floating-pane geometry fields ({attr}`pane_x `, {attr}`pane_y `, {attr}`pane_z `, {attr}`pane_floating_flag `). --- # Format-token fields Source: https://libtmux.git-pull.com/topics/format-tokens/ (format-tokens)= # Format-token fields When you work with a libtmux object — {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, {class}`~libtmux.Pane`, or {class}`~libtmux.Client` — you get a flat set of typed string attributes that report the object's current state straight from tmux, mirroring tmux's built-in [FORMATS](https://man.openbsd.org/tmux.1#FORMATS) tokens (`pane_id`, `window_zoomed_flag`, `session_name`, etc.). This is why a single {class}`~libtmux.Pane` can hand you {attr}`pane.pane_id `, {attr}`pane.window_id `, and {attr}`pane.session_id ` without you writing a raw tmux command. Most of the time you just read these attributes and move on. Not every field holds a value on every object, though: the object's type and your tmux version decide which fields are populated and which stay `None`. Which fields hold a value comes down to two gates: 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. You trade an occasional `None` check for attribute access that stays safe on every supported version. ## 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 | {attr}`~libtmux.Pane.pane_dead_signal`, {attr}`~libtmux.Pane.pane_dead_time` | | 3.7 | {attr}`~libtmux.Pane.bracket_paste_flag`, {attr}`~libtmux.Pane.pane_flags`, {attr}`~libtmux.Pane.pane_floating_flag`, {attr}`~libtmux.Pane.pane_pb_progress`, {attr}`~libtmux.Pane.pane_pb_state`, {attr}`~libtmux.Pane.pane_pipe_pid`, {attr}`~libtmux.Pane.pane_x`, {attr}`~libtmux.Pane.pane_y`, {attr}`~libtmux.Pane.pane_z`, {attr}`~libtmux.Pane.pane_zoomed_flag`, {attr}`~libtmux.Pane.synchronized_output_flag` | 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 Reach for {attr}`session.pane_id ` and you get a real pane id back, not an error. When tmux lists a parent object, it also reports fields from that parent's active child — so the pane fields on a session row 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`. So read {attr}`session.pane_id ` as "the active pane of the session's current window," not "the session's pane id." Treat it as the latter and the value will surprise you the moment the {attr}`~libtmux.Session.active_window` changes. ## Inspecting which fields apply For the rarer cases — contributors, or code that introspects libtmux's own queries — you can ask, for a given `list-*` subcommand and tmux version, which tokens libtmux will request. Use {func}`libtmux.neo.get_output_format`: ```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 You never call this directly, but it's worth knowing how the version gate gets its answer. 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. That lookup is memoized for the process lifetime, as is the raw-string {func}`libtmux.common.get_version_str`; the two cache independently, so if you're swapping the `tmux` binary mid-test, clear both with `libtmux.common.get_version.cache_clear()` and `get_version_str.cache_clear()`. 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 {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, {class}`~libtmux.Pane` hierarchy. ::: :::{grid-item-card} Locating Yourself :link: self_location :link-type: doc Code running inside a pane asking which pane, window, session, and server it is in. ::: :::{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} Floating Panes :link: floating_panes :link-type: doc Create and position floating (overlay) panes on tmux 3.7+. ::: :::{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 self_location filtering pane_interaction floating_panes 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 You shape how tmux sessions, windows, and panes behave by setting *options* — values like `automatic-rename` or the status-bar format — and by registering *hooks*, commands that tmux runs when an event fires, such as `session-renamed` or `after-split-window`. libtmux gives you one consistent Python API to read, set, and remove both, and it works the same way on every object in the hierarchy: {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, and {class}`~libtmux.Pane`. Most scripts run happily on tmux's defaults and never open this page — reaching for options and hooks is entirely optional. Read on only when you need to tweak how a session behaves or react to something that happens inside it. ## Options Options are the knobs that control tmux's behavior and appearance, from whether a window renames itself to how the status line looks. Whatever object you hold, you read and change its options through the same four methods on {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. The call returns the object you set it on, so the change is live the moment it returns — no {meth}`~libtmux.Window.refresh` needed: ```python >>> window.set_option('automatic-rename', False) # doctest: +ELLIPSIS Window(@... ...) >>> window.show_option('automatic-rename') False ``` ### Unsetting options Once you've overridden an option, you put it back the way tmux shipped it. 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 By default a call reads or writes the option for the object you're holding — a window's `set_option` touches that window. But tmux options live at distinct scopes (server, session, window, pane), and sometimes you want to reach a different level than the object in hand. Pass the `scope` parameter, drawn from {class}`~libtmux.constants.OptionScope`, to say which one: ```python >>> from libtmux.constants import OptionScope >>> # Get window-scoped options from a session >>> session.show_options(scope=OptionScope.Window) # doctest: +ELLIPSIS {...} ``` ### Global options Each scope also has a global layer — the fallback tmux uses when an object hasn't set its own value. Reach it with `global_=True` when you want the server-wide default rather than what one session or window happens to override: ```python >>> server.show_option('buffer-limit', global_=True) 50 ``` ## Hooks Hooks let you attach tmux commands to events, so something runs automatically whenever, say, a session is renamed or a window is split. You manage them through {class}`~libtmux.hooks.HooksMixin`, which mirrors the options API: set, show, and unset, on any object. ### Setting and getting hooks Use {meth}`~libtmux.hooks.HooksMixin.set_hook` to set a hook and {meth}`~libtmux.hooks.HooksMixin.show_hook` to read it back. The hook is registered with tmux the instant `set_hook` returns — there's no refresh step before it starts firing: ```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 {...} ``` A single hook reads back as a dict keyed by index rather than a bare string, because tmux stores hooks as arrays (more on that under indexed hooks, below). {meth}`show_hook() ` returns a {class}`~libtmux._internal.sparse_array.SparseArray`, a dict-like type whose keys are those array indices. ### Removing hooks Use {meth}`~libtmux.hooks.HooksMixin.unset_hook` to remove a hook: ```python >>> session.unset_hook('session-renamed') # doctest: +ELLIPSIS Session(...) ``` ### Indexed hooks A single event can fire more than one command. tmux models this by indexing each hook (`session-renamed[0]`, `session-renamed[1]`, …), so you register several commands against the same event and they all run: ```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] ``` This is why a hook comes back as a dict-like object: the index *is* part of the data, and those indices can be sparse. If you set index 0 and index 5 but nothing in between, tmux keeps the gap, and so does the {class}`~libtmux._internal.sparse_array.SparseArray` you get back — its keys stay exactly the indices tmux holds (0 and 5, with no 1–4), rather than collapsing into a contiguous list. ### Bulk hook operations When you're setting several indices at once, you don't have to call {meth}`~libtmux.hooks.HooksMixin.set_hook` per index. Use {meth}`~libtmux.hooks.HooksMixin.set_hooks` to set multiple indexed hooks in one call, passing the index-to-command mapping directly: ```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 Options and hooks need a reasonably recent tmux, and a few specific hooks arrived later still. The floor for everything on this page is tmux 3.2: | 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 A {class}`~libtmux.Pane` is a live terminal you drive from Python: you type into it, read back what it printed, resize it, and tear it down when you're finished. That makes the pane the unit you reach for when automating a shell, testing a CLI, or orchestrating a terminal workflow. Most of that work is two methods — {meth}`~libtmux.Pane.send_keys` to type and {meth}`~libtmux.Pane.capture_pane` to read the screen back. If those two cover you, you can stop after the first two sections; everything below is for the rarer cases — waiting on output, querying a pane's state, resizing, and cleanup. To follow along live, open two terminals. In the first, start tmux: ```console $ tmux ``` In the second, start `python` (or `ptpython`, if you have it): ```console $ python ``` ## Sending commands {meth}`~libtmux.Pane.send_keys` types text into the pane exactly as if you had typed it at the keyboard, and by default presses Enter so the shell runs it. That default is what you want most of the time: hand it a command string and it executes. The arguments below come into play only when you need to type without running, send characters tmux would otherwise interpret, or invoke send-keys purely for its flags. ### 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 Sometimes you want the text sitting at the prompt without running it — to stage a command, or to feed a keystroke a running program is waiting on. Pass `enter=False` to type without pressing Enter: ```python >>> pane.send_keys('echo "waiting"', enter=False) >>> # Text is typed but not executed >>> output = pane.capture_pane() >>> 'waiting' in '\\n'.join(output) True ``` When you're ready to run it, press Enter on its own 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 Both tmux and your shell interpret certain characters. Pass `literal=True` to send them through untouched, so a tab or escape arrives as the literal byte rather than a key tmux acts on: ```python >>> import time >>> pane.send_keys('echo "Tab:\\tNewline:\\n"', literal=True) >>> time.sleep(0.1) ``` ### Suppress shell history Pass `suppress_history=True` to prepend a space before the command. In a shell configured to ignore space-prefixed lines, that keeps the command out of your history — useful when the command carries a secret: ```python >>> import time >>> pane.send_keys('echo "secret command"', suppress_history=True) >>> time.sleep(0.1) ``` ### Flag-only invocation Sometimes you want send-keys only for its side effects — resetting the pane or repeating the last key — and have no text to type. Pass `cmd=None` to invoke it for the flags alone: ```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 {meth}`~libtmux.Pane.capture_pane` reads the pane's screen back to you as a list of lines — one string per row, top to bottom. With no arguments you get the visible screen, which is what most reads want. The parameters below extend that reach: into scrollback, keeping color, stitching wrapped lines, or preserving spacing. Reach for them only when a plain capture drops something you need. ### 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 By default you read the visible screen. Pass `start` and `end` to widen or narrow that window — negative numbers count back from the visible region, and `'-'` reaches the start of history or the current line: ```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 A plain capture strips color and formatting, handing you clean text. When you need the styling instead — to assert a prompt really printed in red — pass `escape_sequences=True` to keep the ANSI codes intact: ```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 A line longer than the pane wraps onto several rows, and a plain capture returns it as several strings. Pass `join_wrapped=True` to stitch those rows back into one logical line: ```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 The full set of capture flags, and the tmux flag each one maps to: | 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 For the rarer case where you need what tmux has read but not yet drawn, pass `pending=True`. It dumps 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 it 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 tmux runs commands asynchronously: {meth}`~libtmux.Pane.send_keys` returns the moment the keystrokes are sent, not when the command finishes. So when a later step depends on a command completing, you wait for proof in the output rather than guessing at a fixed delay. The honest cost is that this means polling — capturing the pane on a short interval until a marker you control appears. It's a busy wait, not an event, but it's reliable across shells and commands because you're checking the one thing that matters: what actually printed. ### 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 Wrapping that loop in a helper keeps the pattern out of the way of your actual logic: ```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 {meth}`~libtmux.Pane.display_message` asks tmux to evaluate a format string against the pane and hand back the result — its size, working directory, process id, and the rest of tmux's `#{pane_*}` variables. Pass `get_text=True` to get the answer as a list of strings. Each call is a round-trip to tmux, which is exactly what you want for state that moves as you watch it — dimensions during a resize, the working directory after a `cd` — since you get a fresh reading rather than a value cached when the object was built. ### 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 A few of the format variables you'll reach for most often: | 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 {meth}`~libtmux.Pane.resize` changes how much of the window a pane occupies. It covers three needs through one method: set an exact size, nudge a dimension by a relative amount, or toggle zoom to make the pane fill the window and back. The result is bounded by the window and the pane's neighbors — tmux grants the space it can, so treat a resize as a request, not a guarantee. ### Resize by specific dimensions ```python >>> # Make pane larger >>> pane.resize(height=20, width=80) # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ### Resize by adjustment To grow or shrink a pane relative to its current size, name a direction from {class}`~libtmux.constants.ResizeAdjustmentDirection` and how far to move: ```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 Zoom blows a pane up to fill the whole window so you can focus on it, then restores the layout on the next call. It's a toggle, so the same call both zooms and unzooms: ```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 {meth}`~libtmux.Pane.clear` wipes the pane's visible screen, leaving a clean prompt — it runs `reset` in the pane, so it restores terminal state, not just the screen: ```python >>> pane.clear() # doctest: +ELLIPSIS Pane(%... Window(@... ..., Session($... ...))) ``` ## Killing panes {meth}`~libtmux.Pane.kill` destroys a pane and the process running inside it. Once killed, the pane is gone from its window, and any {class}`~libtmux.Pane` object still pointing at it is stale — drop the reference rather than reusing it. ```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 Pass `all_except=True` to invert the target — kill every other pane in the window and keep this one. It's the quick way to collapse a window back to a single pane: ```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 These tie the pieces together into the patterns you'll actually reach for: running a command and collecting its output, and scanning output for trouble. Lift them as-is or adapt them — both lean only on the {meth}`~libtmux.Pane.send_keys` and {meth}`~libtmux.Pane.capture_pane` methods you've already met. ### 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 You can import anything from the `libtmux` namespace and build on it: those names are the public API — documented, and changed only through a deprecation process announced ahead of time. (libtmux is pre-1.0, so a minor version can still carry a breaking change; pin a version when you need to lock things down.) See the {doc}`public API reference ` for the stability policy. Anything with a leading underscore in its module path — `libtmux._internal.*`, `libtmux._vendor.*` — is implementation detail that can change without warning. If you only reach for the public API, that's the whole story, and you can stop reading here. ## The boundary The rule is mechanical: if you can import a name without a leading underscore anywhere in its module path, it's public. The table maps each import prefix to exactly what you can count on. | 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 | The authoritative list of what's stable lives in {doc}`Public API `. ## Why the split Staying on the public API buys you a predictable migration path: when a public name changes, it goes through a deprecation process first — a warning for at least one release, documented in the changelog — rather than vanishing without notice. (libtmux is pre-1.0, so a minor version can still carry a breaking change; pin a version when you need to lock things down.) Reaching into an internal module buys you none of that — a refactor of {mod}`~libtmux._internal.query_list` ships with no deprecation cycle, so an import that works today can break on the very next release. That freedom is the point: internal modules let the library iterate on implementation details without dragging downstream users through a migration for each one. The same line keeps the public surface intentionally small. Every public module is a commitment to maintain, so internal modules earn promotion only through proven stability and real user demand. ## What `_internal/` contains The `_internal/` package holds the machinery the public objects run on — implementation details you never need to understand to use libtmux: - {mod}`~libtmux._internal.query_list` — the filtering engine behind {meth}`.filter() ` and {meth}`.get() ` on collections - {mod}`~libtmux._internal.dataclasses` — base dataclass utilities used by the ORM objects - {mod}`~libtmux._internal.constants` — internal constants not meaningful to end users - {mod}`~libtmux._internal.sparse_array` — the sparse-index mapping behind indexed hooks and options These are documented in {ref}`internals` 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 bundled directly so libtmux can avoid adding dependencies. You're not meant to import from it; it isn't written by the libtmux authors and isn't part of the API. ## How internal APIs get promoted Most readers never need this section — it's for contributors and for anyone tempted to depend on an internal name. An API travels three stages on its way to the public contract: 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 ask for it. If you depend on an internal API, [file an issue](https://github.com/tmux-python/libtmux/issues) — that signal helps prioritize promotion. Once a name is public and later has to change, it doesn't vanish quietly; it moves through {doc}`a deprecation cycle ` first. For the platforms and tmux versions that stability is promised against, see {doc}`Compatibility `. --- # Locating yourself Source: https://libtmux.git-pull.com/topics/self_location/ (self-location)= # Locating yourself Most libtmux code starts from a handle you already hold — you make a {class}`~libtmux.Server`, you find a {class}`~libtmux.Session`, you walk down. Sometimes you hold nothing, because your code is *running inside* a pane: a script you launched in a split, a tmux hook, a test harness, an agent. Before it can do anything useful it has to answer one question — **where am I?** You don't have to search the server for yourself. tmux already told you. It writes two variables into every pane it spawns, and each level of the hierarchy reads them back: | | | |---|---| | {meth}`Server.from_env() ` | the tmux server you are running on | | {meth}`Session.from_env() ` | the session that holds you | | {meth}`Window.from_env() ` | the window that contains you | | {meth}`Pane.from_env() ` | the pane you are running in | If all you need is a handle on yourself, the first section is the whole story. The rest is for the rarer cases: outside tmux, background panes, and windows that live in more than one session. To follow along live, start tmux, then run `python` *inside* a pane — that is the situation this page is about: ```console $ tmux ``` ```console $ python ``` ## Ask where you are Inside a pane each call takes no arguments. It reads {data}`os.environ`, which is where tmux put the answer: ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}") >>> monkeypatch.setenv("TMUX_PANE", pane.pane_id) >>> Pane.from_env().pane_id == pane.pane_id True >>> Window.from_env().window_id == window.window_id True >>> Session.from_env().session_id == session.session_id True ``` That is the whole call — in a real pane tmux has already set those two variables for you, and there is nothing to arrange. These docs are not running in a pane, so the example sets them first. Once you hold any of the four you are back on the hierarchy, and everything in {ref}`traversal` applies. Each call also accepts an environment *mapping* in place of {data}`os.environ`. The examples below pass one explicitly, and it is the seam your own tests can use — see {ref}`self-location-testing`. ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> env = { ... "TMUX": f"{socket_path},1,{session.session_id}", ... "TMUX_PANE": pane.pane_id, ... } >>> Session.from_env(env).session_id == session.session_id True ``` ## When you are not in tmux There is no pane to return, and answering with somebody else's would be worse than not answering, so all four raise {exc}`~libtmux.exc.NotInsideTmux`. Catch it when your program is meant to run inside a pane *and* out: ```python >>> from libtmux import exc >>> try: ... here = Pane.from_env({}) ... except exc.NotInsideTmux: ... here = None >>> here is None True ``` ## The window that contains you, not the one in front A background pane is still somewhere. {meth}`Window.from_env() ` returns the window that *contains* you, which is not the window someone happens to be looking at: ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> worker_window = session.new_window(window_name="worker", attach=False) >>> worker = worker_window.active_pane >>> env = { ... "TMUX": f"{socket_path},1,{session.session_id}", ... "TMUX_PANE": worker.pane_id, ... } >>> session.active_window.window_id == worker_window.window_id False >>> Window.from_env(env).window_id == worker_window.window_id True ``` {attr}`session.active_window ` answers a different question — *what is focused*. ## The server answers, not the environment `TMUX` looks like it settles the session question on its own. Its three fields are `socket_path,server_pid,session_id`, and that last one is a session id. Don't reach for it. tmux writes these variables into a pane's environment *once*, when it spawns the pane, and never revises them — a running process's environment is not something tmux can rewrite. Move the pane's window to another session and the pane really is somewhere else, while `TMUX` still names the session it was born in. So `from_env` anchors on `TMUX_PANE`, the one id tmux keeps answering for live, and asks the server where that pane is *now*: ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> worker_window = session.new_window(window_name="worker", attach=False) >>> worker = worker_window.active_pane >>> env = { ... "TMUX": f"{socket_path},1,{session.session_id}", # names *this* session ... "TMUX_PANE": worker.pane_id, ... } >>> elsewhere = server.new_session(session_name="elsewhere") >>> _ = worker_window.move_window(session=elsewhere.session_id, no_select=True) >>> Session.from_env(env).session_name # where the pane is, not where TMUX says 'elsewhere' ``` The same staleness is why {attr}`pane.session ` resolves through {attr}`pane.window ` instead of reading the `session_id` it is already carrying: a {class}`~libtmux.Pane` you fetched earlier remembers the session it was in *then*. The extra round-trip is what keeps the answer current. If you read it in a loop, bind it to a variable once. ## When a window belongs to two sessions `link-window` puts a single window in several sessions at once, and then the pane genuinely belongs to all of them. Asked "which session am I in?", there is more than one true answer. libtmux does not invent a tie-break. tmux already has to settle this every time you type a command with a `-t` target, so libtmux asks it, and hands you back the session tmux itself would act on. Below, the pane's window is linked into a second session — `holders` shows it really is in both — and libtmux answers with the session tmux's own `display-message -t` names: ```python >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> home = server.new_session(session_name="aaa-home") >>> shared = home.new_window(window_name="shared", attach=False) >>> worker = shared.active_pane >>> env = { ... "TMUX": f"{socket_path},1,{home.session_id}", ... "TMUX_PANE": worker.pane_id, ... } >>> guest = server.new_session(session_name="zzz-guest") >>> _ = server.cmd( ... "link-window", "-s", shared.window_id, "-t", f"{guest.session_id}:" ... ) >>> holders = {p.session_name for p in server.panes.filter(pane_id=worker.pane_id)} >>> holders == {"aaa-home", "zzz-guest"} True >>> tmux_says = server.cmd( ... "display-message", "-p", "-t", worker.pane_id, "#{session_name}" ... ).stdout[0] >>> Session.from_env(env).session_name == tmux_says True ``` So the rule, in full: `TMUX_PANE` says which pane you are, and tmux says which session that pane is in — including when the answer is contested. The session id in `TMUX` is never read, not even as a tie-break. It records where the process was *spawned*, which is a different fact from where it is, and one that goes stale. ## There is no `Client.from_env()` A {class}`~libtmux.Client` is an attached terminal, and a pane is not owned by one. No client may be attached at all — a detached session, a CI job, a `send-keys` script all run with a perfectly good `TMUX_PANE` and nobody watching — or several may be, each with its own view. tmux exports no client id into a pane, so there is nothing to read back. See {ref}`clients` for the view-versus-identity model this follows from. (self-location-testing)= ## Testing code that locates itself Every `from_env` takes an optional `env` mapping. Passing one is how you test a function that locates itself without running your test suite inside a pane: ```python >>> def announce(env=None): ... """Report the session this code is running in.""" ... return Session.from_env(env).session_name >>> socket_path = server.cmd( ... "display-message", "-p", "-t", session.session_id, "#{socket_path}" ... ).stdout[0] >>> env = { ... "TMUX": f"{socket_path},1,{session.session_id}", ... "TMUX_PANE": pane.pane_id, ... } >>> announce(env) == session.session_name True ``` In production `announce()` takes no argument and reads the real environment. :::{seealso} - {ref}`traversal` — walking the hierarchy once you hold a handle - {ref}`clients` — why an attached terminal is a view, not an identity - {class}`~libtmux.Server`, {class}`~libtmux.Session`, {class}`~libtmux.Window`, {class}`~libtmux.Pane` for the full API ::: --- # Traversal Source: https://libtmux.git-pull.com/topics/traversal/ (traversal)= # Traversal When you navigate a tmux server with libtmux, you move through a hierarchy of related objects: a {class}`~libtmux.Server` holds {class}`~libtmux.Session` objects, each session holds {class}`~libtmux.Window` objects, and each window holds {class}`~libtmux.Pane` objects. Every object knows both its parents and its children, so you can traverse in either direction — reach for {attr}`session.windows ` to list the windows under a session, or {attr}`pane.session ` to jump from a pane back up to the session that contains it. Most of the time you call a handful of properties like {attr}`session.windows ` and {attr}`pane.session ` and never look further. This works out of the box, with no setup. The filtering and relationship checks later on the page are there for the rarer cases where you need to find a specific object by name or pattern, or confirm how two objects relate. Under the hood this all rides on libtmux's object abstraction of {term}`target`s (the `-t` argument) and the permanent internal IDs tmux assigns to each object, but you rarely have to think about that layer to move around. 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 The {class}`~libtmux.Server` sits at the top of the hierarchy. Start by viewing its 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($... ...)))] ``` Each of these properties queries tmux fresh every time you access it, so the result always reflects the server's current state. That freshness costs a tmux round-trip per access — worth it for correctness, but if you iterate over the same collection repeatedly, bind it to a variable once instead of re-reading the property inside a loop. ## Session level A {class}`~libtmux.Session` groups windows. Get the first one: ```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 A {class}`~libtmux.Window` groups panes. Get one and inspect its properties: ```python >>> window = session.windows[0] >>> window.window_index # doctest: +ELLIPSIS '...' ``` Traverse upward to 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 A {class}`~libtmux.Pane` is the leaf of the hierarchy. From a pane you can walk all the way back up to its window, session, and server: ```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 ``` ## Locating yourself Everything above starts from a handle you already hold. Sometimes you hold nothing, because your code is *running inside* a pane — and tmux has already told it where it is. {meth}`Pane.from_env() `, and its siblings on {class}`~libtmux.Server`, {class}`~libtmux.Session` and {class}`~libtmux.Window`, read that back, so you can pick up the hierarchy from wherever you happen to be running. See {ref}`self-location` for the whole story, including the window that *contains* you versus the one in front of you, and windows that live in more than one session at once. ## Filtering and finding objects Sometimes a property like {attr}`session.windows ` hands you more objects than you want, and you need the one — or the few — matching a name, an index, or a pattern. Every libtmux collection lets you narrow it down: call {meth}`~libtmux._internal.query_list.QueryList.filter` to keep the objects that match a condition, or {meth}`~libtmux._internal.query_list.QueryList.get` to pull out a single object (it raises if it finds zero or more than one). You match on the same attributes the objects already expose — `window_name`, `window_index`, `pane_id`, and so on. This is the opt-in part of the page. If the plain properties above already get you to the object you want, you can stop here. 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 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 Two questions come up often: does an object belong to a collection (membership), and do two handles point at the same tmux entity (identity)? Python's `in` operator answers the first — whether an object is part of a collection: ```python >>> window in session.windows True >>> pane in window.panes True >>> session in server.sessions True ``` Comparing IDs answers the second — here, whether the window you hold is the session's active window: ```python >>> window.window_id == session.active_window.window_id True ``` And whether the pane you hold is the window's active pane: ```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 A workspace is a single window carved into panes, each running its own program: an editor in one, a dev server in another, a log tail in a third. With libtmux you build that layout from Python instead of arranging it by hand — you open a window, split it into panes, arrange them with a layout, and send commands into each. You will reach for four methods more than any others: {meth}`~libtmux.Session.new_window` to open a window, {meth}`~libtmux.Window.split` to carve it into panes, {meth}`~libtmux.Window.select_layout` to arrange them, and {meth}`~libtmux.Pane.send_keys` to drive a command into one. The defaults are sensible, so most of what you build needs nothing more — the recipes near the end are ready-made patterns you can copy whole and adapt. To follow along you need two terminals: one running a live tmux server, one running a Python prompt to drive it. In the first terminal, start tmux: ```console $ tmux ``` In the second, start Python (`ptpython` if you have it): ```console $ python ``` ## Creating windows Every workspace begins with a window. {meth}`~libtmux.Session.new_window` opens one inside a session and hands you back a {class}`~libtmux.Window` you can split, rename, and fill with panes. ### Basic window creation Hand {meth}`~libtmux.Session.new_window` a name and you get a {class}`~libtmux.Window` back, added to the session's window list. By default the window is created in the background — it doesn't pull your focus from the window you're already on: ```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 Because `attach=False` is the default, the windows you build stay in the background while you assemble a workspace — focus never jumps to each one as it appears. Pass it explicitly when you want that intent on the page, and reach for `attach=True` only when a window should take focus as it's created: ```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 You can also choose what runs inside the window instead of the default shell — handy when a pane should boot straight into a REPL, a server, or a one-off script: ```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 A window with one pane is just a terminal. Splitting is what turns it into a workspace: {meth}`~libtmux.Window.split` (or the same method on a specific {meth}`~libtmux.Pane.split`) divides the available space and returns the new {class}`~libtmux.Pane`. Each split and {meth}`~libtmux.Window.resize` is a round-trip to the tmux server; the resize calls below buy a window large enough that the splits have room to land on a small terminal. ### Vertical split (top/bottom) Splitting top-and-bottom is the default — the new pane opens below the one you split: ```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) Pass a direction to split side-by-side instead. {class}`~libtmux.constants.PaneDirection` names where the new pane goes — here, to the right of the one you split: ```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 By default tmux halves the space. Ask for a specific share — a percentage or a cell count — when one pane should be smaller than the rest: ```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 Once a window holds several panes, a layout decides how they share the screen. {meth}`~libtmux.Window.select_layout` applies one of tmux's built-in arrangements so you don't have to size each pane by hand. ### 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 Pass a layout name and tmux re-tiles every pane in the window. You can switch layouts as often as you like — the panes and their contents stay put, only their geometry changes: ```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 A window's name is how you find it later, so give each one a label that says what it's for. {meth}`~libtmux.Window.rename_window` updates it in place: ```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 A {class}`~libtmux.Window` object reflects tmux's state at the moment you ask: its index in the session, its stable id, and the {class}`~libtmux.Session` it belongs to are all available as attributes. libtmux reads them once when it builds the object, so if something changes the window externally, call {meth}`~libtmux.Window.refresh` to re-fetch from tmux before you read them again: ```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 The methods above are enough to build any workspace. The recipes below stitch them into patterns worth keeping — lift one and adapt it, or read them as worked examples of how the pieces fit together. ### Recipe: create a development workspace A common shape: one large editing pane, a smaller terminal beneath it, and a log pane beside the terminal. This helper wires that up and returns the panes keyed by role, so the caller can drive each one by name: ```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 Need a uniform grid — four panes, nine, sixteen — for watching parallel jobs? Split a row across, repeat down the rows, then let the `tiled` {meth}`~libtmux.Window.select_layout` even everything out: ```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 Sending keys is how you put work into a pane. {meth}`~libtmux.Pane.send_keys` returns as soon as the keystrokes are delivered — the command itself runs asynchronously — so when you need to read its output back with {meth}`~libtmux.Pane.capture_pane`, give it a beat to finish first: ```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 When a window is only meant to live for the span of a task — a test run, a quick capture — let a `with` block own it. The window is created on entry and killed on exit, so you never leak a stray window even if something raises midway through: ```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 ::: ---