Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4007eb2
common(cmd) AsyncTmuxCmd
tony Nov 9, 2025
81b2061
Server,Session,Window,Pane: Add `.acmd`
tony Nov 9, 2025
3d7711e
AsyncTmuxCmd: Updates for TmuxCmd
tony Nov 9, 2025
56125d2
fix(AsyncTmuxCmd): Handle text decoding manually for async subprocess
tony Nov 9, 2025
138697e
tests(async) Basic example
tony Nov 9, 2025
658c51f
py(deps[dev]) Add `pytest-asyncio`
tony Nov 9, 2025
c1e3846
test(async): Add Server.acmd() tests
tony Nov 9, 2025
ca2e37b
test(async): Add Session.acmd() tests
tony Nov 9, 2025
fc13065
test(async): Add Window.acmd() and Pane.acmd() tests
tony Nov 9, 2025
3e85775
test(async): Add concurrent operations test
tony Nov 9, 2025
f18420f
test(async): Add error handling tests
tony Nov 9, 2025
6122923
test(async): Add integration test & use fixture cleanup
tony Nov 9, 2025
7ea7e8a
style(test_async): Use set literal instead of tuple
tony Nov 9, 2025
a8f49f5
refactor(tests): Reorganize async tests into tests/asyncio/
tony Nov 9, 2025
e8dda16
feat(server): Add async wrapper methods ahas_session() and anew_sessi…
tony Nov 9, 2025
db02b2c
test(server): Add comprehensive tests for async wrapper methods
tony Nov 9, 2025
ca8adea
feat(session): Add async wrapper methods anew_window() and arename_se…
tony Nov 9, 2025
2a12b7a
test(session): Add comprehensive tests for async wrapper methods
tony Nov 9, 2025
1a81cf2
feat(window): Add async wrapper method akill()
tony Nov 9, 2025
c9ae006
test(window): Add comprehensive tests for akill() async wrapper
tony Nov 9, 2025
1cfd2f0
docs(server): Add comprehensive docstrings for async methods
tony Nov 9, 2025
670872a
docs(server): Convert async examples to narrative code blocks
tony Nov 9, 2025
1b158db
docs(session,window): Add comprehensive docstrings for async methods
tony Nov 9, 2025
900e76e
docs: Add comprehensive async documentation
tony Nov 9, 2025
4f366a1
docs: Add bidirectional linking from sync to async methods
tony Nov 9, 2025
3344e2b
docs: Restore executable async doctests using asyncio.run() pattern
tony Nov 9, 2025
c4b6850
feat: Add critical async pane methods (asend_keys, acapture_pane, asp…
tony Nov 9, 2025
bff7c64
test: Add comprehensive unit tests for async pane methods (16 tests)
tony Nov 10, 2025
409cd45
test: Add integration tests for async pane workflows (3 tests)
tony Nov 10, 2025
88f5ce8
docs: Add bidirectional cross-references between sync and async pane …
tony Nov 10, 2025
920e7ea
docs: Additional asyncio docs
tony Nov 10, 2025
a618cf8
docs: tidy async docstrings and exports
tony Nov 9, 2025
fbe9d82
tests(asyncio): strengthen pane typing
tony Nov 9, 2025
c42401a
tests(asyncio): add dataclasses for session typing
tony Nov 9, 2025
4b56d65
tests(asyncio): harden server typing
tony Nov 9, 2025
004732d
tests(asyncio): structure integration helpers
tony Nov 9, 2025
cca62cb
chore(rebase): restore imports lost during rebase onto post-v0.56.0 m…
tony May 10, 2026
105b49e
chore(rebase): Restore has_lt_version import in pane.py
tony Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 196 additions & 229 deletions README.md

Large diffs are not rendered by default.

486 changes: 486 additions & 0 deletions docs/api/async.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Server <libtmux.server>
Session <libtmux.session>
Window <libtmux.window>
Pane <libtmux.pane>
Async <async>
Client <libtmux.client>
Common <libtmux.common>
Neo <libtmux.neo>
Expand Down
11 changes: 11 additions & 0 deletions docs/api/libtmux.server.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@

tmux initializes a server automatically on first running (e.g. executing `tmux`)

## Async Methods

Server provides async versions of key methods for use in async applications:

- {meth}`~Server.ahas_session` - Check if session exists asynchronously
- {meth}`~Server.anew_session` - Create new session asynchronously

See {ref}`async` for comprehensive async documentation.

## API Reference

```{eval-rst}
.. autoclass:: libtmux.Server
:members:
Expand Down
11 changes: 11 additions & 0 deletions docs/api/libtmux.session.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
- Contain {ref}`Windows` (which contain {ref}`Panes`)
- Identified by `$`, e.g. `$313`

## Async Methods

Session provides async versions of key methods for use in async applications:

- {meth}`~Session.anew_window` - Create new window asynchronously
- {meth}`~Session.arename_session` - Rename session asynchronously

See {ref}`async` for comprehensive async documentation.

## API Reference

```{eval-rst}
.. autoclass:: libtmux.Session
:members:
Expand Down
10 changes: 10 additions & 0 deletions docs/api/libtmux.window.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
- Contain {ref}`Panes`
- Identified by `@`, e.g. `@313`

## Async Methods

Window provides async versions of key methods for use in async applications:

- {meth}`~Window.akill` - Kill window asynchronously

See {ref}`async` for comprehensive async documentation.

## API Reference

```{module} libtmux
:no-index:
```
Expand Down
106 changes: 75 additions & 31 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ from inside a live tmux session.

## Requirements

- [tmux] 3.2a or newer
- [tmux]
- [pip] - for this handbook's examples

[tmux]: https://tmux.github.io/
Expand Down Expand Up @@ -45,15 +45,10 @@ before general availability.
- [pipx]\:

```console
$ pipx install \
--suffix=@next \
--pip-args '\--pre' \
--force \
'libtmux'
$ pipx install --suffix=@next 'libtmux' --pip-args '\--pre' --force
// Usage: libtmux@next [command]
```

Usage: `libtmux@next [command]`

- [uv tool install][uv-tools]\:

```console
Expand Down Expand Up @@ -83,10 +78,7 @@ via trunk (can break easily):
- [pipx]\:

```console
$ pipx install \
--suffix=@master \
--force \
'libtmux @ git+https://github.com/tmux-python/libtmux.git@master'
$ pipx install --suffix=@master 'libtmux @ git+https://github.com/tmux-python/libtmux.git@master' --force
```

- [uv]\:
Expand Down Expand Up @@ -139,7 +131,7 @@ $ ptpython
```

```{module} libtmux
:no-index:

```

First, we can grab a {class}`~libtmux.Server`.
Expand Down Expand Up @@ -471,38 +463,89 @@ 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() <libtmux.Pane.enter>` after.

## Working with options
## Async Support

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
For async applications, libtmux provides async versions of key methods using the 'a' prefix naming convention.

### Basic Async Usage

```python
>>> server.show_option('buffer-limit')
50
import asyncio
from libtmux import Server

>>> window.show_options() # doctest: +ELLIPSIS
{...}
```
async def main():
server = Server()

### Setting options
# Create session asynchronously
session = await server.anew_session(session_name="async_demo")

```python
>>> window.set_option('automatic-rename', False) # doctest: +ELLIPSIS
Window(@... ...)
# Create window asynchronously
window = await session.anew_window(window_name="async_window")

>>> window.show_option('automatic-rename')
False
# Check session exists
exists = await server.ahas_session("async_demo")
print(f"Session exists: {exists}") # True

>>> window.unset_option('automatic-rename') # doctest: +ELLIPSIS
Window(@... ...)
asyncio.run(main())
```

:::{seealso}
See {ref}`options-and-hooks` for more details on options and hooks.
:::
### Concurrent Operations

One of the key benefits of async methods is concurrent execution:

```python
import asyncio

async def setup_workspace():
server = Server()

# Create multiple sessions concurrently
frontend, backend, database = await asyncio.gather(
server.anew_session(
session_name="frontend",
start_directory="~/project/frontend"
),
server.anew_session(
session_name="backend",
start_directory="~/project/backend"
),
server.anew_session(
session_name="database",
start_directory="~/project/database"
),
)

# Set up windows in each session concurrently
await asyncio.gather(
frontend.anew_window(window_name="editor"),
frontend.anew_window(window_name="server"),
backend.anew_window(window_name="api"),
backend.anew_window(window_name="tests"),
)

return frontend, backend, database
```

### Available Async Methods

- {meth}`Server.ahas_session` - Check if session exists
- {meth}`Server.anew_session` - Create new session
- {meth}`Session.anew_window` - Create new window
- {meth}`Session.arename_session` - Rename session
- {meth}`Window.akill` - Kill window

### When to Use Async

Use async methods when:
- Your application uses asyncio
- You need to perform multiple tmux operations concurrently
- You're integrating with async frameworks (FastAPI, aiohttp, etc.)

For more details, see {ref}`async`.

## Final notes

Expand All @@ -521,3 +564,4 @@ and our [test suite] (see {ref}`development`.)
:::

[test suite]: https://github.com/tmux-python/libtmux/tree/master/tests
[ptpython]: https://github.com/prompt-toolkit/ptpython
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dev = [
"typing-extensions; python_version < '3.11'",
"gp-libs>=0.0.19",
"pytest",
"pytest-asyncio",
"pytest-rerunfailures",
"pytest-mock",
"pytest-watcher",
Expand All @@ -84,6 +85,7 @@ testing = [
"typing-extensions; python_version < '3.11'",
"gp-libs>=0.0.19",
"pytest",
"pytest-asyncio",
"pytest-rerunfailures",
"pytest-mock",
"pytest-watcher",
Expand Down
144 changes: 144 additions & 0 deletions src/libtmux/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import asyncio
import functools
import logging
import re
Expand Down Expand Up @@ -378,6 +379,149 @@ def __init__(self, *args: t.Any, tmux_bin: str | None = None) -> None:
)


class AsyncTmuxCmd:
"""
An asyncio-compatible class for running any tmux command via subprocess.

Attributes
----------
cmd : list[str]
The full command (including the "tmux" binary path).
stdout : list[str]
Lines of stdout output from tmux.
stderr : list[str]
Lines of stderr output from tmux.
returncode : int
The process return code.

Examples
--------
>>> import asyncio
>>>
>>> async def main():
... proc = await AsyncTmuxCmd.run('-V')
... if proc.stderr:
... raise exc.LibTmuxException(
... f"Error invoking tmux: {proc.stderr}"
... )
... print("tmux version:", proc.stdout)
...
>>> asyncio.run(main())
tmux version: [...]

This is equivalent to calling:

.. code-block:: console

$ tmux -V
"""

def __init__(
self,
cmd: list[str],
stdout: list[str],
stderr: list[str],
returncode: int,
) -> None:
"""
Store the results of a completed tmux subprocess run.

Parameters
----------
cmd : list[str]
The command used to invoke tmux.
stdout : list[str]
Captured lines from tmux stdout.
stderr : list[str]
Captured lines from tmux stderr.
returncode : int
Subprocess exit code.
"""
self.cmd: list[str] = cmd
self.stdout: list[str] = stdout
self.stderr: list[str] = stderr
self.returncode: int = returncode

@classmethod
async def run(cls, *args: t.Any) -> AsyncTmuxCmd:
"""
Execute a tmux command asynchronously and capture its output.

Parameters
----------
*args : str
Arguments to be passed after the "tmux" binary name.

Returns
-------
AsyncTmuxCmd
An instance containing the cmd, stdout, stderr, and returncode.

Raises
------
exc.TmuxCommandNotFound
If no "tmux" executable is found in the user's PATH.
exc.LibTmuxException
If there's any unexpected exception creating or communicating
with the tmux subprocess.
"""
tmux_bin: str | None = shutil.which("tmux")
if not tmux_bin:
msg = "tmux executable not found in PATH"
raise exc.TmuxCommandNotFound(
msg,
)

cmd: list[str] = [tmux_bin] + [str(c) for c in args]

try:
process: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout_bytes, stderr_bytes = await process.communicate()
returncode: int = (
process.returncode if process.returncode is not None else -1
)

except Exception as e:
logger.exception("Exception for %s", " ".join(cmd))
msg = f"Exception while running tmux command: {e}"
raise exc.LibTmuxException(
msg,
) from e

# Decode bytes to string with error handling
stdout = stdout_bytes.decode(errors="backslashreplace")
stderr = stderr_bytes.decode(errors="backslashreplace")

# Split on newlines and filter empty lines
stdout_split: list[str] = stdout.split("\n")
# remove trailing newlines from stdout
while stdout_split and stdout_split[-1] == "":
stdout_split.pop()

stderr_split = stderr.split("\n")
stderr_split = list(filter(None, stderr_split)) # filter empty values

# Workaround for tmux "has-session" command behavior
if "has-session" in cmd and stderr_split and not stdout_split:
# If `has-session` fails, it might output an error on stderr
# with nothing on stdout. We replicate the original logic here:
stdout_split = [stderr_split[0]]

logger.debug("stdout for %s: %s", " ".join(cmd), stdout_split)
logger.debug("stderr for %s: %s", " ".join(cmd), stderr_split)

return cls(
cmd=cmd,
stdout=stdout_split,
stderr=stderr_split,
returncode=returncode,
)


class _TmuxVersionUnavailable(Exception):
"""Internal signal: this tmux predates the ``-V`` flag (pre-1.7)."""

Expand Down
Loading
Loading