Skip to content

Commit 619cfb6

Browse files
authored
Resolve the pane you are running inside: from_env() (#714)
Code running inside tmux -- a script in a split, a hook, a test harness, an agent -- could not ask libtmux where it was, and the obvious hand-roll is wrong. tmux exports TMUX and TMUX_PANE into every pane it spawns, but it writes them once, at spawn, and never revises them: the session id in TMUX records where the process was launched, not where it is. Move the pane's window to another session and that id is simply wrong. - **New**: Pane.from_env() returns the pane the calling process runs in, and Server.from_env(), Session.from_env() and Window.from_env() do the same for the rest of the hierarchy. Each takes an optional env mapping defaulting to os.environ, so code that locates itself stays testable outside a pane. - **Correct by construction**: from_env reads only the socket path out of TMUX, anchors on TMUX_PANE -- which stays true for the life of the pane -- and asks tmux for the rest. The stale session id is never consulted, not as a fallback and not as a tie-break. So the answer survives a move-window, and for a window linked into several sessions it is the session tmux itself would act on. Server.from_env() runs no tmux command at all. - **Refuses to guess**: outside a pane, or with a TMUX that does not parse, the family raises exc.NotInsideTmux. TMUX is split from the right, because the pid and session id are integers and a socket path may contain a comma; a TMUX_PANE without its % sigil is rejected rather than matched against session names, which would silently resolve to the wrong object. - **Internal**: Server.__init__ derived socket_path from TMUX_TMPDIR in a branch that could never execute -- it required socket_name to be both None and not "default". Removed; callers that need the real path ask tmux for #{socket_path}. Documentation: a new self-location topic page, linked from the topics index and from traversal, with front-page examples in the README and the docs index, and TMUX / TMUX_PANE in the glossary. Compatibility: purely additive. No existing behaviour changes. Refs #704. Supersedes #705.
2 parents 1102472 + a98a7fe commit 619cfb6

18 files changed

Lines changed: 1444 additions & 34 deletions

CHANGES

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ $ uvx --from 'libtmux' --prerelease allow python
4545
_Notes on the upcoming release will go here._
4646
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
4747

48+
### What's new
49+
50+
#### Find where you are running (#714)
51+
52+
Code running *inside* tmux — a script in a split, a hook, a test harness, an
53+
agent — can now ask libtmux where it is.
54+
{meth}`Pane.from_env() <libtmux.Pane.from_env>` returns the pane the calling
55+
process is running in, and {meth}`Server.from_env() <libtmux.Server.from_env>`,
56+
{meth}`Session.from_env() <libtmux.Session.from_env>` and
57+
{meth}`Window.from_env() <libtmux.Window.from_env>` do the same for the rest of
58+
the hierarchy. Each takes an optional environment mapping, so code that locates
59+
itself stays testable outside a pane.
60+
61+
The answer stays true as tmux moves things around: it survives a `move-window`,
62+
and for a window linked into several sessions it names the session tmux itself
63+
would act on. Outside tmux, or with an environment that does not parse, the
64+
family raises {exc}`~libtmux.exc.NotInsideTmux` rather than guessing.
65+
66+
See {ref}`self-location` for the whole story — why the session id tmux exports
67+
goes stale, the window that *contains* you versus the one in front of you, and
68+
how to test code that locates itself.
69+
4870
### Fixes
4971

5072
#### Linked windows resolve to the session tmux would use (#713)

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ libtmux is a typed Python API over [tmux], the terminal multiplexer. Stop shelli
2121

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

230+
### Know where you're running
231+
232+
[**Learn more about Locating yourself**](https://libtmux.git-pull.com/topics/self_location/)
233+
234+
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:
235+
236+
```python
237+
>>> socket_path = server.cmd(
238+
... "display-message", "-p", "-t", session.session_id, "#{socket_path}"
239+
... ).stdout[0]
240+
>>> monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")
241+
>>> monkeypatch.setenv("TMUX_PANE", pane.pane_id)
242+
243+
>>> Pane.from_env()
244+
Pane(%... ...)
245+
>>> Session.from_env().session_name == session.session_name
246+
True
247+
```
248+
249+
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`.
250+
229251
## Core concepts
230252

231253
| libtmux object | tmux concept | Notes |

docs/AGENTS.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,30 @@ requires them to actually execute — `testpaths` includes `docs/`, so
6363
pytest runs every one. Lead with a small, runnable example early rather
6464
than after paragraphs of prose; libtmux is code-first.
6565

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

7891
## What stays precise
7992

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

115128
## A page that does this
116129

docs/glossary.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,27 @@ Pane
6161
Target
6262
A target, cited in the manual as ``[-t target]`` can be a session,
6363
window or pane.
64+
65+
TMUX
66+
Environment variable tmux exports into every {term}`Pane` it spawns.
67+
68+
Holds ``socket_path,server_pid,session_id`` for the {term}`Server`
69+
the pane belongs to. The session id is spelled bare, e.g. ``47``, where
70+
libtmux spells the same session ``$47``.
71+
72+
Written once, when the pane is spawned, and never revised — so its
73+
session id records where the process was *launched*, and goes stale if
74+
the pane's {term}`Window` later moves.
75+
76+
libtmux takes only the socket path from it, and asks tmux for the rest.
77+
See {ref}`self-location`.
78+
79+
TMUX_PANE
80+
Environment variable tmux exports into every {term}`Pane` it spawns.
81+
82+
Holds that pane's ``pane_id``, e.g. ``%1``. Unlike ``TMUX`` it always
83+
names the pane the process is really in, so it is the id libtmux
84+
anchors on to answer where a process is running.
85+
86+
Read back by libtmux in {ref}`self-location`.
6487
```

docs/index.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,31 @@ Python object with traversal, filtering, and command execution.
7777
| {class}`~libtmux.window.Window` | tmux window |
7878
| {class}`~libtmux.pane.Pane` | tmux pane |
7979

80+
## Know where you're running
81+
82+
Sometimes you hold no handle at all, because your code is *running inside* a
83+
pane. You don't have to search the server for yourself — tmux writes `TMUX` and
84+
`TMUX_PANE` into every pane it spawns, and each level of the hierarchy reads
85+
them back:
86+
87+
```python
88+
>>> socket_path = server.cmd(
89+
... "display-message", "-p", "-t", session.session_id, "#{socket_path}"
90+
... ).stdout[0]
91+
>>> monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")
92+
>>> monkeypatch.setenv("TMUX_PANE", pane.pane_id)
93+
94+
>>> Pane.from_env().pane_id == pane.pane_id
95+
True
96+
>>> Session.from_env().session_name == session.session_name
97+
True
98+
```
99+
100+
Inside a pane tmux has already set those two variables, so {meth}`Pane.from_env()
101+
<libtmux.Pane.from_env>` takes no arguments; these docs are not running in a
102+
pane, so the example sets them first. Outside tmux there is no pane to return and
103+
{exc}`~libtmux.exc.NotInsideTmux` is raised instead. See {ref}`self-location`.
104+
80105
## Testing
81106

82107
libtmux ships a [pytest plugin](api/testing/pytest-plugin/index.md) with

docs/topics/clients.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ stable for the lifetime of the attachment.
3535
| `client_session` | session name of the same attached view | No — snapshot |
3636
| `client_pid` / `client_tty` / `client_user` | terminal-level facts | Yes — identity-adjacent |
3737

38+
:::{seealso}
39+
**Why there is no `Client.from_env()`.** A pane can name the
40+
{class}`~libtmux.Session`, {class}`~libtmux.Window`, and
41+
{class}`~libtmux.Pane` it is running in, because tmux writes those ids into
42+
its environment. A client is the one thing it cannot name. Viewing is not
43+
owning: no client may be attached at all — a detached session, a CI job, a
44+
`send-keys` script — or several may be, each looking somewhere else. tmux
45+
exports no client id into a pane because there is no single right answer to
46+
export. See {ref}`self-location` for what a pane *can* resolve about itself.
47+
:::
48+
3849
## Live attachment lookup
3950

4051
When you want the *current* attachment — not the snapshot — use the

docs/topics/configuration.md

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,51 @@ tmux through the standard object API, you're already configured correctly
88
and can stop reading here.
99

1010
The rest of this page is for the rarer cases. It documents two lower
11-
layers you can reach for when the defaults aren't enough: the tmux
12-
environment variables that decide which server you connect to, and the
13-
format-string system libtmux uses internally to read tmux state.
11+
layers you can reach for when the defaults aren't enough: the
12+
environment variables libtmux reads, and the format-string system
13+
libtmux uses internally to read tmux state.
1414

1515
## Environment variables
1616

17-
libtmux reads almost nothing from the environment, so in normal use
18-
there's nothing to set here. The one knob it does read is
19-
`LIBTMUX_TMUX_FORMAT_SEPARATOR`, an advanced override for the separator
20-
(default ``) libtmux uses internally to parse tmux's format output — you'd
21-
touch it only if that character ever collided with your own data. What more
22-
often matters is the tmux *server* you connect to: the standard tmux
23-
variables `TMUX` (the address of an
24-
existing server) and `TMUX_TMPDIR` (where tmux keeps its socket) shape
25-
which server a fresh {class}`~libtmux.Server` finds. If you run several
26-
servers, or point tmux at a custom socket directory, those two variables
27-
decide which one you land on — otherwise you can ignore them.
17+
You set almost nothing here. The two variables that matter most, tmux
18+
writes for you and libtmux only reads back, so a normal Python process
19+
driving tmux has nothing to arrange in this section.
20+
21+
tmux exports both into every pane it spawns:
22+
23+
| Variable | What tmux puts in it |
24+
|---|---|
25+
| `TMUX` | the server that pane belongs to, as `socket_path,server_pid,session_id` |
26+
| `TMUX_PANE` | the id of the pane itself, e.g. `%1` |
27+
28+
Code running *inside* a pane — a script you started in a split, a hook, a
29+
test harness — reads them back to get a handle on itself, rather than
30+
searching the server for a pane it already is. That is the `from_env`
31+
family: {meth}`Server.from_env() <libtmux.Server.from_env>`,
32+
{meth}`Session.from_env() <libtmux.Session.from_env>`,
33+
{meth}`Window.from_env() <libtmux.Window.from_env>`, and
34+
{meth}`Pane.from_env() <libtmux.Pane.from_env>`. Outside a pane neither
35+
variable is set, and all four raise {exc}`~libtmux.exc.NotInsideTmux`.
36+
You never write them yourself: {ref}`self-location` covers what each call
37+
does with them, why the session id in `TMUX` goes stale, and the `env`
38+
mapping you hand `from_env` in tests instead of touching the real
39+
environment.
40+
41+
tmux reads `TMUX` too — it is how tmux notices you are already inside a
42+
session and guards against nesting one. {meth}`Server.new_session()
43+
<libtmux.Server.new_session>` unsets it for the length of that one call
44+
and restores it afterward, so creating a session from inside a pane works
45+
without you arranging anything.
46+
47+
That leaves the two variables that *are* yours to set, and most people
48+
set neither. `TMUX_TMPDIR` is tmux's own — the directory it keeps sockets
49+
in. libtmux never reads it, but the tmux binary it shells out to does, so
50+
it shapes which server a bare {class}`~libtmux.Server` lands on; pass
51+
`socket_name` or `socket_path` when you would rather name the server
52+
outright. `LIBTMUX_TMUX_FORMAT_SEPARATOR` is the one variable libtmux
53+
itself defines: an advanced override for the separator (default ``) it
54+
uses internally to parse tmux's format output — you'd touch it only if
55+
that character ever collided with your own data.
2856

2957
## Format strings
3058

docs/topics/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ Navigate the {class}`~libtmux.Server`, {class}`~libtmux.Session`,
1818
{class}`~libtmux.Window`, {class}`~libtmux.Pane` hierarchy.
1919
:::
2020

21+
:::{grid-item-card} Locating Yourself
22+
:link: self_location
23+
:link-type: doc
24+
Code running inside a pane asking which pane, window, session, and server
25+
it is in.
26+
:::
27+
2128
:::{grid-item-card} Filtering
2229
:link: filtering
2330
:link-type: doc
@@ -82,6 +89,7 @@ configuration
8289
design-decisions
8390
public-vs-internal
8491
traversal
92+
self_location
8593
filtering
8694
pane_interaction
8795
floating_panes

0 commit comments

Comments
 (0)