Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### Find where you are running (#714)

Code running *inside* tmux — a script in a split, a hook, a test harness, an
agent — can now ask libtmux where it is.
{meth}`Pane.from_env() <libtmux.Pane.from_env>` returns the pane the calling
process is running in, and {meth}`Server.from_env() <libtmux.Server.from_env>`,
{meth}`Session.from_env() <libtmux.Session.from_env>` and
{meth}`Window.from_env() <libtmux.Window.from_env>` do the same for the rest of
the hierarchy. Each takes an optional environment mapping, so code that locates
itself stays testable outside a pane.

The answer stays true as tmux moves things around: it survives a `move-window`,
and for a window linked into several sessions it names the session tmux itself
would act on. Outside tmux, or with an environment that does not parse, the
family raises {exc}`~libtmux.exc.NotInsideTmux` rather than guessing.

See {ref}`self-location` for the whole story — why the session id tmux exports
goes stale, the window that *contains* you versus the one in front of you, and
how to test code that locates itself.

### Fixes

#### Linked windows resolve to the session tmux would use (#713)
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ libtmux is a typed Python API over [tmux], the terminal multiplexer. Stop shelli

- Typed, object-oriented control of tmux state
- Query and [traverse](https://libtmux.git-pull.com/topics/traversal/) live sessions, windows, and panes
- [Locate yourself](https://libtmux.git-pull.com/topics/self_location/) from inside a pane with `from_env()`
- Raw escape hatch via `.cmd(...)` on any object
- Works with multiple tmux sockets and servers
- [Context managers](https://libtmux.git-pull.com/topics/context_managers/) for automatic cleanup
Expand Down Expand Up @@ -226,6 +227,27 @@ Window(@... ...:..., Session($... ...))
Session($... ...)
```

### Know where you're running

[**Learn more about Locating yourself**](https://libtmux.git-pull.com/topics/self_location/)

Code *running inside* a pane — a script in a split, a tmux hook, an agent — can ask where it is. tmux writes `TMUX` and `TMUX_PANE` into every pane it spawns, and `Server`, `Session`, `Window`, and `Pane` each read 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(%... ...)
>>> Session.from_env().session_name == session.session_name
True
```

In a real pane tmux has already set those two variables, so `from_env()` takes no arguments and there is nothing to arrange — this README is not running in a pane, so the example sets them first. Outside tmux there is no pane to return, and `from_env()` raises `NotInsideTmux`.

## Core concepts

| libtmux object | tmux concept | Notes |
Expand Down
31 changes: 22 additions & 9 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,30 @@ requires them to actually execute — `testpaths` includes `docs/`, so
pytest runs every one. Lead with a small, runnable example early rather
than after paragraphs of prose; libtmux is code-first.

- Use the `doctest_namespace` fixtures — `server`, `session`, `window`,
`pane` (and `Server` / `Session` / `Window` / `Pane` / `Client`) —
instead of building a server by hand.
- Use the `doctest_namespace` fixtures instead of building a server by
hand. `conftest.py` seeds the namespace with `server`, `session`,
`window`, `pane`, the `Server` / `Session` / `Window` / `Pane` /
`Client` classes, `ControlMode` and `control_mode`, `monkeypatch`,
and `request`.
- Fence a `>>>` session as a ```` ```python ```` block, and reach for
`# doctest: +ELLIPSIS` when output varies (ids like `@1`, `$2`,
socket names). Use a ```` ```console ```` block for shell commands at
a `$` prompt.
- The code blocks on a page share one doctest session, so a later
block can use a `pane` an earlier block created. That makes their
**order load-bearing**: never reorder, add, or drop a code block when
you reshape the prose around it.
- **Every code block on a page is an independent doctest.** Blocks do
not share a session: each one gets a fresh copy of the namespace *and*
a fresh tmux server. A later block cannot use a `pane` an earlier
block created — the name is simply not defined there. So **write every
block self-contained**. The fixtures above are re-seeded for each
block, which makes that cheap: no block needs an import or a setup
preamble to get a `server`, a `session`, or a `pane`. Order is
load-bearing for the reader's narrative, not for state — reorder, add,
or drop a block as the prose demands, and keep the story coherent.
- Two of those names are not quite what they look like. `Server` is a
`TestServer` partial, not the real class; write
`>>> from libtmux.server import Server` when an example needs the
class itself. And the fixture server is created with a socket *name*,
so `server.socket_path` is `None` — an example that needs the path
must ask tmux for it with `display-message -p '#{socket_path}'`.

## What stays precise

Expand Down Expand Up @@ -109,8 +122,8 @@ another link useful.
Do not rely on a later reference section to satisfy the first-mention rule. If
the first occurrence would be a heading, grid-card teaser, or introductory
sentence, link that occurrence or retitle the heading so the first prose mention
can carry the link. Leave command examples, code blocks, Mermaid node labels,
and literal configuration values as code; link the surrounding prose instead.
can carry the link. Leave command examples, code blocks, and literal
configuration values as code; link the surrounding prose instead.

## A page that does this

Expand Down
23 changes: 23 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,27 @@ Pane
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`.
```
25 changes: 25 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,31 @@ Python object with traversal, filtering, and command execution.
| {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()
<libtmux.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
Expand Down
11 changes: 11 additions & 0 deletions docs/topics/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ stable for the lifetime of the attachment.
| `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
Expand Down
56 changes: 42 additions & 14 deletions docs/topics/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,51 @@ 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 tmux
environment variables that decide which server you connect to, and the
format-string system libtmux uses internally to read tmux state.
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

libtmux reads almost nothing from the environment, so in normal use
there's nothing to set here. The one knob it does read is
`LIBTMUX_TMUX_FORMAT_SEPARATOR`, an advanced override for the separator
(default `␞`) libtmux uses internally to parse tmux's format output — you'd
touch it only if that character ever collided with your own data. What more
often matters is the tmux *server* you connect to: the standard tmux
variables `TMUX` (the address of an
existing server) and `TMUX_TMPDIR` (where tmux keeps its socket) shape
which server a fresh {class}`~libtmux.Server` finds. If you run several
servers, or point tmux at a custom socket directory, those two variables
decide which one you land on — otherwise you can ignore them.
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() <libtmux.Server.from_env>`,
{meth}`Session.from_env() <libtmux.Session.from_env>`,
{meth}`Window.from_env() <libtmux.Window.from_env>`, and
{meth}`Pane.from_env() <libtmux.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()
<libtmux.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

Expand Down
8 changes: 8 additions & 0 deletions docs/topics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ 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
Expand Down Expand Up @@ -82,6 +89,7 @@ configuration
design-decisions
public-vs-internal
traversal
self_location
filtering
pane_interaction
floating_panes
Expand Down
Loading