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
9 changes: 6 additions & 3 deletions WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ the copied example. Preserve collected examples when changing formatting.
Repeat a complete capture example with its native tools and tmux on `PATH`:

```console
$ python3 scripts/check-capture-prose.py \
$ python3 scripts/check-example-prose.py \
--example capture \
--port kotlin \
--output-dir /tmp/capture-kotlin-proof
```
Expand All @@ -117,10 +118,12 @@ The output directory must be new. The runner extracts the displayed files,
checks their recorded hashes, executes the displayed setup, and saves logs
and a result. Dependency downloads and native builds are separate from the
routine site tests. A changed hash needs a new native run before review.
Use `--example attach` to check the existing-server programs in the attach guide.

Put explanatory comments on separate lines above the code they describe.
Wrap example comments at 80 columns, including indentation. Put long source
links and attribution in prose outside the code block.
Limit example comments to 100 columns, including indentation; prefer shorter
lines that fit the code panel. Put long source links and attribution in prose
outside the code block.

### Examples across ports

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Run a complete capture example exactly as displayed, including its setup.
"""Run a complete example exactly as displayed, including its setup.

Use --port to choose a language and --output-dir for a new evidence directory.
The selected language's native tools, Git, and tmux must already be on PATH.
Expand All @@ -18,12 +18,15 @@

def main():
repo = Path(__file__).resolve().parent.parent
manifest = json.loads((repo / 'site/test/fixtures/capture-examples.json').read_text())
examples = {item['port']: item for item in manifest['examples']}
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', required=True, choices=examples)
parser.add_argument('--example', choices=['capture', 'attach'], default='capture')
parser.add_argument('--port', required=True)
parser.add_argument('--output-dir', required=True, type=Path)
args = parser.parse_args()
manifest = json.loads((repo / f'site/test/fixtures/{args.example}-examples.json').read_text())
examples = {item['port']: item for item in manifest['examples']}
if args.port not in examples:
parser.error(f'Unknown port: {args.port}; choose from {", ".join(examples)}')
example = examples[args.port]
page = repo / 'site/src/content/docs' / (example['page'] + '.md')
content = page.read_text()
Expand Down Expand Up @@ -68,7 +71,7 @@ def main():
if result.returncode:
break
passed = all(row['exit'] == 0 for row in results)
passed = passed and 'libtmux capture ready' in log.read_text().splitlines()
passed = passed and example.get('expectedOutput', 'libtmux capture ready') in log.read_text().splitlines()
report = {'port': args.port, 'sourceRevision': example['sourceRevision'],
'pageSha256': hashlib.sha256(page.read_bytes()).hexdigest(),
'files': example['files'], 'runs': results, 'passed': passed,
Expand Down
244 changes: 110 additions & 134 deletions site/src/content/docs/guides/attaching-to-tmux.md
Original file line number Diff line number Diff line change
@@ -1,168 +1,144 @@
---
supportedPorts: [py, ts, rs, go, java, dotnet, cxx, swift]
supportedPorts: []
title: Attaching to tmux
description: What a plain constructor call actually connects to, and how to find a session that might already exist instead of always creating a new one.
description: Create or attach to a tmux session, select its socket, and keep terminal attachment separate from automation.
sidebar:
label: Attaching to tmux
group: Guides
order: 3
tableOfContents: true
---

Obtain a server and session handle to control tmux from your program. Your
process keeps its own stdin and stdout, and tmux continues running
independently. [Attach and send keys](/examples/attach-and-send-keys/)
demonstrates this workflow.

<!-- port:py -->
Attaching your terminal is a separate operation. Python's `Session.attach()`
runs `tmux attach-session` and hands the terminal to tmux.
[tmuxp](https://tmuxp.git-pull.com/) uses it after building a workspace. Check
your port's reference if your program needs to hand over the terminal.
<!-- /port -->

## Which socket a bare constructor reaches

Use an explicit socket when several tmux servers may be running. The examples
below show each constructor's defaults and environment-aware alternatives. To
locate the server from inside a pane, use the port's environment lookup API for
`TMUX` and `TMUX_PANE`.

```python
# Server() with no arguments talks to tmux's own default socket.
# Server(socket_name=...) or Server(socket_path=...) pick a different one.
server = libtmux.Server()

# from_env() is also on Session, Window, and Pane, for code running inside a
# pane that wants to ask "where am I" instead of being told.
server = libtmux.Server.from_env()
```
Attaching opens a tmux session in your terminal. Its shells and programs keep
running when you detach. A script can also query or control that session
without taking over a terminal.

```typescript
// Select the default tmux socket.
const server = new Server();
```
## Open a session in your terminal

```go
// Resolves the tmux binary once through the snapshotted PATH and freezes
// it. SocketName and SocketPath select a socket explicitly (SocketPath wins
// if both are set); a Server built this way does not drift if the
// environment changes later.
server, err := tmux.NewServer(tmux.ServerOptions{})
if err != nil {
return err
}
```
Run this from a terminal outside tmux. It creates `work` if needed and attaches
to it otherwise. `-L libtmux-demo` keeps this demonstration on its own named
server. `-f /dev/null` skips personal tmux configuration for this demonstration.

```rust
// Server::new() uses the process's own environment. from_env() reads the
// TMUX variable directly; find.rs tries the pane-local one first and falls
// back to a fresh connection.
let server = Server::from_env().or_else(|_| Server::new())?;
```console
$ tmux -L libtmux-demo -f /dev/null new-session -A -s work
```

```java
Server server = Server.open(
ServerConfig.builder().endpoint(ServerEndpoint.socketPath(socket)).build());
Detach with **Ctrl-b**, then **d**. You return to the original shell while
the tmux session keeps running. List that server's sessions:

// The pane-local read-back: takes nothing, returns empty outside a pane.
TmuxEnvironment.current();
```console
$ tmux -L libtmux-demo list-sessions
```

```csharp
// Resolves in a fixed order: an explicit ServerConnectionOptions, then
// LIBTMUX_SOCKET_PATH, then LIBTMUX_SOCKET_NAME (under TMUX_TMPDIR, or
// /tmp), then the socket named "default". A named option always wins over
// an environment variable.
Server server = await Server.ConnectAsync();

// The separate pane-local read-back; ConnectAsync never consults TMUX.
Server fromPane = Server.FromEnvironment();
```

```cpp
// Four named constructors instead of one flexible one: pick the one that
// names how you're reaching this tmux:
libtmux::Server::from_env(); // inside tmux
libtmux::Server::at_socket_name(name);
libtmux::Server::at_socket_path(path);
libtmux::Server::at_default(); // "my tmux", to a person
```
Attach to the existing session again. The leading `=` selects its exact name:

```swift
// Select the default tmux socket explicitly.
let server = try Server(socketName: "default")
```console
$ tmux -L libtmux-demo attach-session -t '=work'
```

[Socket and servers](/topics/socket-and-servers/) covers endpoint selection
and liveness. [Environment](/topics/environment/) covers pane-local lookup.
`attach-session` expects a session to exist. `new-session -A` is the command
to use when either creating or attaching is acceptable. From inside tmux,
`attach-session` switches the attached client to the target session.

## Finding a session instead of always creating one
After detaching, remove the demonstration session when you are finished:

A script that runs more than once usually wants "attach if a session by
this name already exists, create it otherwise," not a fresh session every
time.

```python
# default only stands in for *absence*: an ambiguous match still raises
# MultipleObjectsReturned even with a default supplied: handing back an
# arbitrary match from several is how a script ends up driving the wrong
# pane. See Filtering and queries.
session = server.sessions.get(session_name="demo", default=None)
if session is None:
session = server.new_session(session_name="demo")
```console
$ tmux -L libtmux-demo kill-session -t '=work'
```

```typescript
const session = snapshot.sessions.where({ name: "demo" }).oneOrUndefined();
<a id="which-socket-a-bare-constructor-reaches"></a>

## Choose the server socket

A socket identifies a tmux server. Use the same selection on every command:

- `-L name` selects a named socket in tmux's socket directory.
- `-S path` selects an explicit socket path and overrides `-L`.
- Without either flag, tmux uses the socket from `TMUX` when applicable,
otherwise its default socket.

Inside a pane, `TMUX` identifies its server and `TMUX_PANE` identifies the
pane. Prefer explicit socket selection in automation that may run both
inside and outside tmux.

## Query a session from a shell script

This complete program creates an isolated server, looks up `work`, and prints
its name. Each command stays in the calling shell; no terminal is attached.
It stops its own server on success or failure.

Save it as `connect.sh` and run `sh connect.sh`, or paste the whole block into
a POSIX shell. It requires tmux 3.2a or newer.

```sh title="connect.sh"
(
set -eu
directory=$(mktemp -d "${TMPDIR:-/tmp}/libtmux-attach.XXXXXX")
socket="$directory/tmux.sock"

cleanup() {
status=$?
trap - 0 HUP INT TERM
if [ -S "$socket" ] && ! tmux -S "$socket" kill-server; then
printf 'Cannot stop tmux; kept %s\n' "$directory" >&2
exit 1
fi
rm -rf "$directory" || exit 1
exit "$status"
}
trap cleanup 0
trap 'exit 1' HUP INT TERM

unset TMUX TMUX_PANE
tmux -S "$socket" -f /dev/null new-session -d -s work /bin/cat
tmux -S "$socket" has-session -t '=work'
tmux -S "$socket" list-sessions -F '#{session_name}'
)
```

```go
// Push the check into tmux itself with a typed filter, rather than reading
// everything back and filtering in the process.
live := tmux.TmuxFilter("#{==:#{session_name},demo}")
sessions, err := server.SearchSessions(ctx, &live)
if err != nil {
return err
}
fmt.Println("matching sessions:", len(sessions))
```
`-d` starts the session without attaching. `/bin/cat` keeps its pane open
without loading a shell configuration. The script addresses only its private
socket. If shutdown fails, it reports the error and keeps that socket's
directory for inspection.

```java
Session session = server.hasSession("work")
? server.sessions().stream()
.filter(candidate -> candidate.name().equals("work"))
.findFirst()
.orElseThrow()
: server.newSession("work");
```
<a id="finding-a-session-instead-of-always-creating-one"></a>

```swift
// hasSession answers the question directly as a Bool, no exception needed
// either way.
if try await server.hasSession("work") == false {
_ = try await server.newSession(named: "work", windowName: "start")
}
```
## Find a session before creating one

<!-- port:dotnet -->
`Server.HasSessionAsync(name)` checks for a session. Use `NewSessionRequest.ReplaceExisting`
with `Server.CreateSessionAsync` only when killing and recreating it is intended.
<!-- /port -->
Use `has-session -t '=name'` to check an exact session name. It exits
unsuccessfully when tmux cannot find the session or contact the server; keep
the diagnostic so you can distinguish those failures.

Finding an object and creating one are separate operations. Another client
can change tmux state between them. Handle the creation error if the name was
taken after the lookup.
A lookup does not reserve a name. Another client can create or remove a
session before your next command. Handle the result of `new-session` even
after checking. Use `new-session -A` for the interactive create-or-attach
workflow shown above.

[Filtering and querying](../querying-and-filtering/) covers absent and
ambiguous matches. [Attach and send keys](/examples/attach-and-send-keys/)
contains complete programs and their source details.
## Connect from a language library

Use the port menu to open this guide with a complete program, imports, build
files, and a private-server launcher. Each program connects to an existing
socket and leaves that server running:

[Python](/py/latest/guides/attaching-to-tmux/) ·
[TypeScript](/ts/latest/guides/attaching-to-tmux/) ·
[Go](/go/latest/guides/attaching-to-tmux/) ·
[Rust](/rs/latest/guides/attaching-to-tmux/) ·
[Java](/java/latest/guides/attaching-to-tmux/) ·
[Kotlin](/kotlin/latest/guides/attaching-to-tmux/) ·
[Scala](/scala/latest/guides/attaching-to-tmux/) ·
[.NET](/dotnet/latest/guides/attaching-to-tmux/) ·
[F#](/fsharp/latest/guides/attaching-to-tmux/) ·
[C++](/cxx/latest/guides/attaching-to-tmux/) ·
[Swift](/swift/latest/guides/attaching-to-tmux/) ·
[Ruby](/ruby/latest/guides/attaching-to-tmux/) ·
[Lua](/lua/latest/guides/attaching-to-tmux/)

## Where to go next

- [Sending keys](../sending-keys/) and [Capturing output](../capturing-output/)
pick up once you have a pane handle.
- [Attach and send keys](/examples/attach-and-send-keys/) has the full,
sourced code for the round trip this guide assumes.
- [Testing with libtmux](../testing-with-libtmux/) if the server you want to
attach to is one your own test suite should own and tear down.
- [Sending keys](../sending-keys/) explains input and command completion.
- [Capturing output](../capturing-output/) reads a pane's screen and history.
- [Socket and servers](/topics/socket-and-servers/) covers server selection.

The [tmux manual source](https://github.com/tmux/tmux/blob/94796f6b1182507efac8a272fc309a79e22e58a5/tmux.1)
describes `new-session`, `attach-session`, socket selection, and targeting.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ This complete program creates a private tmux server, captures its output, and
cleans up. Follow the [setup and run instructions](#setup-and-run) below. You need
tmux and a Unix environment; no existing tmux session is required.

`ScopedTmuxServer` is temporary example scaffolding. In an application, use a
[`Server`](../../reference/libtmux-server/) connected to the tmux server you manage.
Future versions of this example will use that regular server object directly.

## Read what's on screen

The program sends `printf` with a leading newline, then waits for the complete
Expand Down
Loading
Loading