# libtmux for Ruby > The Ruby port of libtmux (libtmux). Every code sample below is Ruby; the same pages exist for the other nine ports under their own prefix. - [Ruby API reference](https://libtmux.org/en/ruby/latest/reference/): every public symbol, generated from the source. Hosted on libtmux.org. --- # libtmux-async Source: https://libtmux.org/en/ruby/latest/guides/async/ > Source-owned Ruby guide at c6d9d2177182. Run libtmux operations inside an application-owned Async task. This gem provides a subprocess facade, ordered mapping, control replies and event subscriptions. Imports start no scheduler, tmux server or background task. Install the alpha with `gem install libtmux-async --pre`. The [complete program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/async_cancel.rb) owns an isolated server and its cleanup. This excerpt uses that server and creates the Async root: ```ruby Async do |parent| LibTmux::Async.open(parent: parent, server: server) do |scope| waiting = parent.async do scope.server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 0.5) rescue LibTmux::Cancelled => error error end scope.server.wait_for("ready", timeout: 0.5) Example.check(scope.diagnostics.fetch(:active_process_slots) == 1, "waiting client lost its slot") captures = scope.map(scope.server.list_panes.map(&:ref), concurrency: 2) do |ref| scope.server.pane(ref).capture end Example.check(captures.all?(&:success?), "sibling captures stalled") waiting.cancel failure = waiting.wait Example.check(failure.is_a?(LibTmux::Cancelled), "cancellation lost") Example.check(failure.delivery == :possibly_sent, "cancelled dispatch claimed no effects") Example.raises(Errno::ECHILD) { Process.waitpid(failure.pid, Process::WNOHANG) } Example.check(scope.server.diagnostics.fetch(:admitted_requests).zero?, "cancelled client remains admitted") end end.wait ``` Omitting `parent:` uses the existing current Async task. The source server must outlive the scope. References keep their source binding identity. Scope exit retires its clients and joins its owned tasks; it leaves the borrowed daemon alive. A scope rejects use from another thread, process or scheduler. Create a separate scope for each scheduler thread. Interactive terminal attachment stays on the blocking core facade and raises `UnsupportedFeatureError` on this facade. `scope.server.run` returns the same binary `CommandResult` as core. Its stdin, stdout and stderr are owned by scheduler tasks. A bounded native helper observes and reaps each child only after its final signalling handoff. Cancelling the calling task retires its client and raises `Cancelled` with `:not_sent` or `:possibly_sent` delivery. An already observed exit completes its bounded drain. Repeated cancellation does not restart cleanup deadlines or replace an earlier operation failure. Deadlines cannot undo tmux effects. `scope.map` returns a frozen Array in input order; independent requests may complete out of order. It caps retained items at 1024 and accounts payload bytes in strings, primitive values, `CommandResult`, Arrays and Hashes. Cycles and excessive nesting are refused. Application-defined results require an explicit `result_bytes:` callable returning a nonnegative Integer. Keep measured values unchanged until the map returns. These payload limits complement item counts; they are not exact Ruby heap measurements. | Scope limit | Default | | --- | --- | | Active subprocess clients | 4 | | Admitted subprocess requests, including unconsumed results | 32 | | Queued request payload | 4 MiB | | Retained process and map output payload | 8 MiB | | Per-command stdout / stderr | 1 MiB / 256 KiB | | Control connections | 4 | | Ordinary command deadline | 5 seconds | Control connections use their own request, reply and subscriber limits. Obtain one with `scope.server.open_control(session: ref)`. `exchange` returns guarded `GuardedReply` blocks with `:boundary_window` attribution. It makes no claim of final command completion. Outside-block events arrive through `events.next` or an explicit `subscribe`; consumer callbacks run outside the parser. Reliable subscriptions raise on overflow. Tail subscriptions report dropped ranges. Cancelling a dispatched, undrained exchange closes that connection. `pause_output(pane_id:)` and `resume_output(pane_id:)` retain guarded evidence and report possible output loss through gap events. A requested-action gap does not prove that the action took effect. Close a connection before passing it as `reconnect:` to `scope.server.open_control`; the replacement stays owned by that scope and reports a new generation plus a gap with unknown loss. Subscriptions expose their `generation`; prior subscriptions stay closed. Reconnect and resume never replay requests or missed output. The development bundle pins Async 2.46 and io-event 1.22. The [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml) exercises the selected Ruby/tmux versions on Linux and macOS and retains per-revision results. Package builds and tests do not publish this gem. --- # Concepts Source: https://libtmux.org/en/ruby/latest/concepts/ > tmux objects, command transports, queries, and workspaces across the libtmux language ports. libtmux lets you create sessions, arrange windows and panes, send commands, and read output from tmux. Start with the object hierarchy, then read about the transport, query, or workspace behavior your program needs. These pages explain shared concepts and differences between language ports: - **[Server, session, window, pane](server-session-window-pane/)**: tmux's object hierarchy and attached clients. - **[Control mode vs one-shot](transports/)**: subprocess commands, persistent connections, and batching. - **[Filtering and queries](queries/)**: find objects and handle absent or ambiguous matches. - **[Workspaces](workspaces/)**: build pane layouts from code or configuration files. Use the header's port links to open documentation for your language, including its API reference. --- # Server, session, window, pane Source: https://libtmux.org/en/ruby/latest/concepts/server-session-window-pane/ > The object hierarchy every libtmux port mirrors from tmux itself, and the client that sits outside it. libtmux models tmux's server, session, window, and pane objects: ``` Server ├── Session │ └── Window │ └── Pane └── Client (attached view) ``` A `Server` contains sessions. Each `Session` contains links to windows, and each `Window` contains panes. Commands run inside a `Pane`, where you send input and capture output. A window can be linked to more than one session. ## Stable identity, not name or index tmux assigns a unique ID to each session, window, and pane at creation. The ID remains stable for that object's lifetime even if its name or index changes: | Object | ID prefix | Example | |--------|-----------|---------| | Session | `$` | `$13` | | Window | `@` | `@3243` | | Pane | `%` | `%5433` | | Server | - | identified by socket name or path instead | Python handles use the object ID to refresh their fields. Immutable snapshots, such as those in TypeScript, Swift, and Go, use IDs to identify the same tmux object across reads. Walking the whole tree, in each port: ## Client: a view, not a child A `Client` represents a terminal attached to a session. Several clients can view the same server, and each can switch sessions or windows independently. Client fields describe the view at the time of the read. A [control-mode connection](../transports/) is also a client. It appears in `list-clients`, counts toward `session_attached`, and affects attachment-dependent behavior such as `destroy-unattached`. Closing the last attached client can therefore destroy a session configured with that option. ## What differs between ports Ports differ in how they read state and report failures: - **Refreshing state.** Python objects reflect their last read until you call `.refresh()`. TypeScript, Swift, and Go also provide snapshots whose relationships can be queried without another tmux command. - **Blocking and async calls.** Python and Java use blocking calls. Rust, TypeScript, .NET, and Swift provide async APIs. Go uses ordinary calls with contexts for cancellation and deadlines. - **Failure handling.** Python and .NET raise exceptions. C++ returns `expected`. Some commands have an expected negative answer, such as `has-session` when a session is absent; check the method's result contract before treating that answer as a failure. See [Control mode vs one-shot](../transports/) for command costs and connection behavior. --- # Control mode vs one-shot Source: https://libtmux.org/en/ruby/latest/concepts/transports/ > How a call in your program actually reaches the tmux server, and why a port might give you a choice. libtmux sends commands to tmux through subprocesses or persistent control-mode connections. Some ports also batch commands into one invocation: 1. **One-shot subprocess.** Each command spawns a fresh `tmux` process, which parses argv, executes the command, prints its output, and exits. This is the default in every port, and the *only* lane in Python: every `.cmd()` call underneath the object API is a `subprocess.Popen` around a `tmux` invocation. 2. **A persistent control-mode client.** `tmux -C attach-session` starts one long-lived tmux process that stays attached and speaks a line-oriented protocol over its stdout: commands go in, replies and asynchronous notifications (`%window-add`, `%output`, ...) come out, without starting a process per call. 3. **One invocation, several commands.** tmux accepts more than one command per invocation (`;`-joined, or one `-F`-tagged `list-*` per line). A port can fold several logical operations into a single process start without opening a control-mode connection at all. ## Where each port draws the line | Port | One-shot | Folded invocation | Persistent control client | |------|----------|--------------------|-----------------------------| | Python | every call | - | test-only (`ControlMode`, `libtmux._internal`) | | TypeScript | default | `pipeline()`, `batch()` | `connect()` / `watch()`: notifications only, commands stay per-process | | Go | `process` path | `Plan.Run` | `connection` (`Session.OpenControl`), `streaming` (`OpenNotifications`) | | Rust | `plan` feature, sequential | `plan`, folded | `control-mode` feature | | C# | "One-shot" mode | "Chained" mode (`server.Chain()`) | "Control" mode (`EnterControlModeAsync`) | | C++ | bounded subprocess (default) | `Chain` | `Server::control()` → `Connection` | | Java | every call | `Batch` | `ControlClient` (`attach`, `send`, `subscribeEvents`) | | Swift | default | - | `server.connect()` / `.watch()` (notifications; see below) | Choose based on whether you need command results, notifications, or a batch of changes. ## Notifications and commands are separable TypeScript's `connect()` adds an event observer while commands such as `session.newWindow(...)` and `pane.sendKeys(...)` continue to run as separate tmux processes. A dedicated process provides a completion boundary for output from alias-expanded or waiting commands. Go's `Session.OpenControl`, .NET's `EnterControlModeAsync`, Java's `ControlClient.send`, and Rust's `control-mode` feature can send commands through the persistent connection. Check your port's transport API before assuming that subscribing to events also changes how commands run. ## A control client is a real client A persistent control connection attaches a tmux client. It appears in `list-clients`, increments `session_attached`, and affects `destroy-unattached`, client hooks, and idle-client accounting. Each connection counts separately. Python's internal `ControlMode` test helper uses this behavior for commands that require an attached client, such as `display-popup` and `detach-client`. ## Why fold several commands into one invocation Creating an object can require a second command to read its resulting state. Batching reduces those repeated reads and process starts. TypeScript's `batch()` resolves planned mutations from one final snapshot. Go's `Plan`, .NET's `Chain`, and C++'s `Chain` also group operations without attaching a control client. ## What this costs in practice The Rust `matrix` example and .NET README compare process counts and timings. Their results describe specific workloads and environments: - **Rust's** `matrix` example runs the same create-and-query workload five ways. Blocking sequential and async sequential both cost 6 processes for 6 dispatches; folding the same 6 into async batches costs 3 processes; routing them over a control-mode connection costs exactly 1. - **C#'s** README reports the *marginal* cost of one more command in each mode, as medians against tmux 3.7b: roughly 2.3 ms for another one-shot process, roughly 0.2 ms for another command over an already-open control client, and roughly 0.02 ms for another command folded into one chained invocation. A persistent connection avoids starting a client for each command. A chain groups a known sequence into one invocation. For occasional commands, use the default subprocess transport; measure your workload before changing transports for performance. Use a notification stream when your program needs tmux events. ## Choosing a lane These examples use each port's subprocess API. See the port reference for batching and control-mode setup. To inspect tmux's control protocol, attach a control client: ```console $ tmux -C attach-session -t work ``` --- # Filtering and queries Source: https://libtmux.org/en/ruby/latest/concepts/queries/ > How you get from every session on the server to the one pane you mean, and what happens when zero or several match. Use a collection filter to find matching sessions, windows, or panes. Use an exactly-one lookup when your next operation requires a single target. - **Filtering returns a collection; exactly-one lookup checks the result count.** A `.filter()` or `.where()` call returns zero or more matches. Methods such as `.get()`, `.one()`, and `Selections.exactlyOne()` return one object or report a missing or ambiguous match. - **Choose where to filter.** Filter a snapshot in your program when you need several queries over the same data. A tmux format filter can reduce the rows returned by a live read. The cost depends on the data and queries you need. ## Python: `.filter()` and `.get()`, Django-style `server.sessions`, `session.windows`, and `window.panes` are `QueryList` collections. Call `.filter()` with field names and optional lookup suffixes: Lookups include `exact`, `contains`, `startswith`, `endswith`, `regex`, and their case-insensitive `i`-prefixed variants. Multiple keywords and chained `.filter()` calls combine with AND. `.get()` requires exactly one match. Its `default` argument handles an absent result; multiple matches still raise `MultipleObjectsReturned`. Server-wide collections (`server.windows`, `server.panes`) enumerate window links. A window linked to two sessions appears once per session, so a lookup can be ambiguous even when the window ID is unique. Use `Window.linked_sessions` to find its sessions. For a known ID, use `Pane.from_pane_id()` or `Window.from_window_id()` to resolve the object directly. For servers with hundreds or thousands of panes, `.filter()` still builds every object before you discard the ones that don't match. `search_sessions`, `search_windows`, and `search_panes` push a tmux `-f` filter expression down to the server instead, so libtmux builds objects only for the matches: Python-side lookups work with the library's supported tmux versions. The tmux filter grammar requires tmux 3.2 or newer. An unknown format token expands to an empty value, so a malformed filter can look like a valid filter with no matches. If `search_*()` unexpectedly returns no results, try `#{m:*,#{session_name}}` to check that the session data is available. ## TypeScript: criteria as data TypeScript's `Selection.where()` accepts structured, serializable criteria that can be stored in a configuration file or sent through MCP: `some`, `every`, and `none` test related objects. `{ mode: "insensitive" }` enables case-insensitive comparison. Use `.where()` for criteria that can be encoded with `encodeWhereDocument` and decoded with `decodeWhereDocument`; use `.filter()` for a predicate function. `.one()` throws `NoMatchError` or `MultipleMatchesError`. `.oneOrUndefined()` permits an absent result. ## Go, Rust, Java, C++: typed fields that fail queries at compile time These ports use typed fields to reject invalid comparisons at compile time: - **Go** offers both `tmux.PaneFilter{Active: tmux.Ptr(true), ...}` structs that push down into `SearchPanes` (one tmux command, only matches returned), and a `snapshot()` read followed by `tmuxq.Where(panes, predicate)` when you want several answers from one read. - **Rust** uses typed fields: `fields.pane_active.eq(true)` is valid, but `.gt(...)` on that boolean field is not. Expressions compose with `.and()`. With the `serde` feature, a query can be encoded as a versioned JSON document for configuration or MCP. - **Java** exposes each field as a typed accessor (`Pane_.index()`, `Session_.name()`) that plugs straight into an ordinary `Stream.filter()`; `Pane_.index().startsWith("2")` doesn't compile because the index is a number, not a string. `Selections.exactlyOne(...)` is the `.get()`-shaped call, throwing `NoMatchException` or `MultipleMatchesException`. - **C++** composes `FilterExpr` values with `&&`, `||`, and `!`, as in `pane::command.starts_with("nv") && pane::active`. Invalid field operations such as `pane::active.starts_with("x")` fail to compile. Examples of typed and local filters: ## The cardinality contract, side by side | Port | Collection filter | Exactly-one | Empty | Several | |------|--------------------|--------------|-------|---------| | Python | `.filter()` | `.get()` | `ObjectDoesNotExist` (or `default=`) | `MultipleObjectsReturned` | | TypeScript | `.where()` / `.filter()` | `.one()` | `NoMatchError` (or `.oneOrUndefined()`) | `MultipleMatchesError` | | Java | `Stream.filter()` | `Selections.exactlyOne()` | `NoMatchException` | `MultipleMatchesException` | See the Go, Rust, C++, .NET, and Swift references for their exactly-one result types and failure handling. --- # Workspaces Source: https://libtmux.org/en/ruby/latest/concepts/workspaces/ > Build pane layouts with the object API or a workspace configuration file. A workspace arranges windows and panes for a task, such as editing code, running a development server, and following logs. Build it with the object API when the layout depends on program logic. Use a declarative builder when you want to store the layout in a configuration file, such as [tmuxp](https://tmuxp.git-pull.com/) YAML or JSON. ## Building one imperatively Create a window, split it into panes, apply a layout, and send each pane its command: `Window.split()` or `Pane.split()` adds a pane. Direction and size control its placement. `select_layout()` rearranges the panes while their processes continue running. tmux provides `even-horizontal`, `even-vertical`, `main-horizontal`, `main-vertical`, and `tiled` layouts. Python's `attach=False` and C++'s detached creation keep new windows in the background. Check the creation defaults for your port if focus matters. Splits and resizes require tmux commands; [Control mode vs one-shot](../transports/) covers their transport costs and batching. ## Building one declaratively These packages read or build workspace configurations based on tmuxp: | Port | Package | Shape | |------|---------|-------| | Python | tmuxp itself | the format this whole idea is named after | | TypeScript | `@libtmux/workspace` | `applyWorkspace(server, { session_name, windows: [...] })` | | Go | `workspace` | tmuxp-shaped, per the port's own module layout | | Rust | `tmux-workspace` | tmuxp-shaped | | Java | `libtmux-workspace` | "enough of tmuxp's format to describe a workspace" | | C# | `LibTmux.Workspace` | reads tmuxp YAML directly | | Swift | `TmuxWorkspace` | Swift, JSON, or YAML (YAML needs the `YAMLWorkspaces` trait) | TypeScript's `applyWorkspace` applies a desired configuration. Applying the same configuration again reuses its existing objects: C++ provides a consumer example in `examples/workspace/` that reads tmuxp configuration. The workspace builder is part of that example, rather than a library package: ## Cleaning up For temporary workspaces, Python's `Window` and `Session` context managers kill their objects on block exit, including when the block raises: See [Context managers](/topics/context-managers/) for cleanup support in each port. Use explicit kill methods when the handle does not provide scope-based cleanup. --- # libtmux Source: https://libtmux.org/en/ruby/latest/guides/core/ > Source-owned Ruby guide at c6d9d2177182. Ruby tmux orchestration core. `Server.open` borrows an existing explicit endpoint. Closing it retires owned clients and preserves the daemon; `kill` explicitly terminates the daemon. `Server.start` creates a private owned foreground daemon whose lifetime ends with its server handle. Owned startup uses Linux and Darwin readiness backends. The [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml) retains exact per-revision platform and version results. Install the alpha with `gem install libtmux --pre`, then require `libtmux`. Imports do not start tmux, a scheduler or an MCP server. See the repository's contribution guide for local build and verification commands. Handles carry immutable refs bound to one open server binding. IDs and refs are local readers; `list_*`, `snapshot` and command methods perform explicit I/O. Global windows represent unique entities. `WindowLink` retains a session, index and window ID so repeated links stay distinct. Link selection, unlinking, movement, swapping and display check the complete link identity in the same tmux queue turn as their operation. Configured command aliases are avoided using unshadowed builtin spellings. Hook waits before dispatch cannot turn a stale link into its replacement. Concurrent rewriting of command aliases is outside this guarantee; applications must coordinate configuration changes. Typed arguments preserve literal semicolons and distinguish pane text from key names. Pane commands take executable argument arrays. Hook commands, display formats, pipe shell commands and `source_file` configuration are explicit executable inputs. `Server.run` remains the raw tmux escape hatch, including daemon aliases, separators and format semantics. Creation accepts `cwd:` and a String-to-String `environment:` map. Directories resolve from the Ruby caller and are checked before dispatch; concurrent filesystem changes can still trigger tmux's directory fallback. Creation returns tmux-assigned IDs after dispatch, without claiming program readiness or a successful program exit. `new_session(window_name:)` names the initial window; its index can be moved explicitly after creation. `new_window(index:)` refuses an occupied slot. `Pane#split` targets that exact pane, with `size:` as cells or a percentage string. Windows and splits retain focus unless `focus: true` is requested. Typed operations accept `timeout:` and `cancel:`. Composed link and copy operations share one deadline across their preflights and final dispatch. Copy-mode exit uses `cancel_mode: true`; `cancel:` accepts `LibTmux::Cancellation.new`. Call the token's `cancel` from another thread to wake a blocked request, join its caller, then `close` the token. See the [plain-Ruby cancellation recipe](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/cancel.rb) and [ownership contract](../ownership-errors/). `Options` retains raw bytes, inheritance and sparse array indexes; `OptionValue#as` requests a strict conversion. Hook values remain tmux command strings. The stable tmux option listing uses its escaped representation, including octal bytes; it does not split raw values on line breaks. Option names containing whitespace are currently rejected. Inherited hook listing is not implemented and raises explicitly. Indexed `get` acquires the array and selects locally, so a missing index raises `NoMatchError` while a present empty value remains present. Reading an array without an index raises `MultipleMatchesError` when it has several entries. Append follows tmux's lowest-free-index rule; indexed hooks execute in index order. Empty arrays remain distinct from empty String values. Binary buffers and command results preserve trailing newlines. Capture can join wrapped lines, include attribute escapes, escape nonprintable bytes, preserve trailing spaces and trim unused trailing cells. `mode_screen: true` reads the mode's backing screen (the copy-mode snapshot, without its UI), `alternate: true` reads tmux's saved screen and raises when absent (while an application occupies the alternate screen, this is the saved main screen), and `pending: true` reads an incomplete escape sequence. These three selectors are mutually exclusive. Mode-screen and trailing-cell flags are checked against advertised command usage; unsupported requests raise explicitly. Copy-mode flags are checked against the connected daemon's advertised command usage. Client discovery returns observations. `Server#attach` uses an explicit caller-owned TTY and terminal type, waits for its owned client to exit, and restores the terminal mode. `Server#switch_client(client:, session:)` switches an explicit current native client selector to an exact bound session, keeping the session environment. A missing selector fails without fallback. This operation does not turn a client observation into an incarnation-safe reference; a reconnect matching the selector is eligible at dispatch. Control connections expose bounded event subscriptions and raw guarded replies. `pause_output(pane_id:)` and `resume_output(pane_id:)` return `GuardedReply`; they do not establish that an action took effect. Their gap events identify `:pause_requested` or `:resume_requested` when no outside-block native notice was observed, including cancellation after possible dispatch. Native notices use `:pause` and `:resume`. Loss counts are unknown (`dropped_bytes: nil`); resume does not replay skipped output. Guarded notification-looking text stays in its reply body. Close the old control connection, then explicitly call `server.open_control(session: ref, reconnect: old_connection)` to reconnect within the same binding and session. The replacement has a new `generation` and retains `previous_generation`. Every new subscription begins with a `:reconnect` gap containing both generations and an unknown loss count. Old subscriptions stay closed, and requests are never replayed. Event sequences describe one connection's observations, not durable pane history. Typed command coverage includes hierarchy creation/listing; rename, split, resize, swap, join, break, respawn, and layout operations; link operations; options/hooks/environment, capture/send/paste/pipe/buffers, copy commands, display/source-file/wait-for. It does not establish complete flag parity or every compatibility cell; consult the workflow results. RBS validation checks declarations; installed signature consumers check selected real arguments, blocks and return values. [Executable recipes](../../examples/recipes/) run against installed artifacts; the documentation gate renders YARD and guides and checks local destinations and fragments. The [public method inventory](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/reference/api.md) links exported methods to source and behavioral contracts. These consumer checks do not establish whole-program static typing. --- # Examples Source: https://libtmux.org/en/ruby/latest/examples/ > Programs for sending input, capturing output, and building workspaces in each language. These examples show how to complete a tmux task in each language. Use the tabs to select your port. Each page includes the source file and the checks run by that port's test suite; see those details before adapting an excerpt into a standalone program. Examples marked with `file=` are read from the port source during the build. Other blocks are copied excerpts. The source details identify which mechanism each block uses. - **[Attach and send keys](attach-and-send-keys/)**: get a session, send a command, and read output. Uses the hierarchy described in [Server, session, window, pane](/concepts/server-session-window-pane/). - **[Capture pane output](capture-pane-output/)**: read back what a pane is showing, and wait for output to appear instead of guessing a delay. The companion to [Capturing output](/guides/capturing-output/). - **[Build a workspace from a file](workspace-from-file/)**: the tmuxp-shaped job of describing a multi-window session as data and building it in one call, in each port that has one. ## What "verified" means, per port Port repositories use the following checks for their source examples. This site reads or copies those examples; a successful site build alone does not execute them: | Port | Mechanism | What it checks | |------|-----------|-----------------| | Python | `pytest` `testpaths` includes `README.md` and `src/libtmux` | `>>>` doctest blocks run against a real, isolated tmux session on every test run | | TypeScript | `scripts/check-doc-runnable.ts` | A block tagged `` must appear, line for line, in that file, which the integration suite executes | | Go | `go generate ./tmux` (`internal/generate/docs`) | A `` region in `README.md` is rewritten from the matching `// docs:name` … `// docs:end` region in `examples/`; CI fails on drift | | Rust | `#![doc = include_str!("../README.md")]` | The entire README is a doc comment, so `cargo test --doc` compiles and runs every fenced Rust block in it | | Java | `docs-tests` (`./gradlew :docs-tests:test`) | Every Java fence in READMEs and guides is compiled against the real artifacts, then run against real tmux via `libtmux-junit5` | | .NET | `sync_snippets.py --check` + `ReadmeExampleTests` | A `` region is quoted from a tested `[Example]` method; every `csharp run` block is additionally compiled and executed | | C++ | `tools/docs/check_readme.py` | Each ` ```cpp ` block in `README.md` must appear verbatim as a `#region` in `examples/05-readme.cpp`, which CTest builds and runs | | Swift | `Scripts/check_examples.py` | Each ` ```swift ` block in `README.md` and product READMEs must appear in `Examples/Sources/`, which `swift test --package-path Examples` compiles through its public products | See [Testing with libtmux](/guides/testing-with-libtmux/) for the fixture each of those test suites runs against, and each example page below for the exact file a given snippet was quoted from. --- # Attach and send keys Source: https://libtmux.org/en/ruby/latest/examples/attach-and-send-keys/ > Get a session handle, send a command to a pane, and capture output. Get a session handle, send a command to a pane, and capture output. These examples use libtmux from your program; to attach your terminal interactively, see [Attaching to tmux](/guides/attaching-to-tmux/). Select your port below. The examples retain their source's setup, error handling, and cleanup, so the operations shown vary by port. [Where this comes from](#where-this-comes-from) identifies each source and its test coverage. ## Finding an existing session instead For a script that runs repeatedly, look up a session before creating it. [Attaching to tmux](/guides/attaching-to-tmux/#finding-a-session-instead-of-always-creating-one) shows that pattern, and [Filtering and querying, in practice](/guides/querying-and-filtering/) covers absent and ambiguous matches. ## Where this comes from ### Python **Source:** `src/libtmux/server.py`, `session.py`, `pane.py` docstrings **In this page:** hand-quoted, composed from three separate docstrings **Checked by:** `pytest` runs every `>>>` doctest (`testpaths` includes `src/libtmux`) against a real, isolated tmux session on every test run ### TypeScript **Source:** `examples/quickstart/quickstart.ts` **In this page:** read whole from the file **Checked by:** run against real tmux by `bun test examples`; its first half is also mirrored into README.md under a `` marker, checked line-for-line by `scripts/check-doc-runnable.ts` ### Rust **Source:** `crates/libtmux/examples/scratch.rs` **In this page:** read whole from the file **Checked by:** run to completion against a throwaway tmux by `scripts/run-examples.sh` (`just examples`), which CI runs and which also asserts the example leaves no session behind ### Go **Source:** `examples/quickstart/main.go` **In this page:** read whole from the file **Checked by:** the whole file runs against a real tmux server as `TestQuickstart`; the `docs:quickstart` region inside it is additionally mirrored into README.md by `go generate ./tmux`, and CI fails if the two drift ### Java **Source:** `examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java` **In this page:** read whole from the file **Checked by:** run against real tmux by the `examples` module's own `ExamplesRunTest` ### .NET **Source:** `examples/LibTmux.Examples/Snippets/OneShot.cs` **In this page:** read whole from the file **Checked by:** its `ConnectAndBuild` region is mirrored into README.md and checked by `sync_snippets.py --check`; the mirrored `csharp run` block is additionally compiled and run by `ReadmeExampleTests` ### C++ **Source:** `examples/05-readme.cpp`, the `connect` and `build` regions **In this page:** Copied excerpts from the `connect` and `build` regions. **Checked by:** quoted verbatim into README.md, checked for drift by `tools/docs/check_readme.py`, and the whole file is built and run by CTest ### Swift **Source:** `Examples/Sources/ExampleCode/Changing.swift` **In this page:** read whole from the file **Checked by:** matched against the README's "Change what is there" section by `Scripts/check_examples.py`; compiled and run through the package's public products by `swift test --package-path Examples` ### Source inclusion A `file="..."` fence reads the named source during the site build. Hand-quoted excerpts are copies; their source files and regions are listed above. --- # Capture pane output Source: https://libtmux.org/en/ruby/latest/examples/capture-pane-output/ > Capture a pane's screen and wait for expected output or a completion signal. Read a pane after [sending input](../attach-and-send-keys/). [Sending keys](/guides/sending-keys/#the-race-you-cant-see-from-the-call-site) explains why an immediate capture can miss output. These examples show screen capture and waiting; [Capturing output](/guides/capturing-output/) explains the choices. See [source details](#where-this-comes-from) for each example's source and validation. ## Read what's on screen The .NET example under "Wait for text instead of guessing a delay" uses `Pane.CaptureAsync` within a wait. Its separate Psmux transport also provides a capture API, shown in `examples/LibTmux.Examples/Snippets/Psmux.cs`. ## Wait for text instead of guessing a delay The Python example uses `wait_for`, tmux's signal channel. It waits for a signal from the command rather than matching pane text. `Session.OpenNotifications` receives tmux events as a stream. For tests that need to wait for screen text, use `tmuxtest.WaitForText`; see [Testing with libtmux](/guides/testing-with-libtmux/). Rust's `wait_for_text` looks before it sleeps, joins wrapped lines so a needle spanning a wrap still matches, and returns `PaneWait::Dead` rather than hanging forever if the pane's process ends first. Attach the `ControlClient` to receive `%output` notifications. An unattached client receives command replies only. `TmuxWait.UntilAsync` polls a read against a predicate rather than sleeping a fixed amount. C++ has no checked snippet that waits on pane *text*. `Server::wait_for(channel, timeout)`, in `include/libtmux/server.hpp`, uses tmux's own `wait-for` signal instead of scraping output, and its doc comment explains why that is the safer choice when the command you are waiting on can be made to announce itself: "a server that dies under a waiter makes tmux exit zero, which is indistinguishable from being signalled ... this reports that as a failure instead." `waitForOutput` takes patterns for both success and failure, so a process that fails fast is discovered immediately rather than by timing out. ## Where this comes from ### Python **Source:** `src/libtmux/pane.py` (`capture_pane`), `src/libtmux/server.py` (`wait_for`) docstrings **In this page:** hand-quoted **Checked by:** `pytest` runs every `>>>` doctest against a real, isolated tmux session on every test run ### TypeScript **Source:** `examples/capture/capture.ts` (read), `examples/agent/agent.ts` (wait) **In this page:** read whole from each file **Checked by:** both run against real tmux by `bun test examples`; `agent.ts` is additionally mirrored into README.md under a `` marker checked by `scripts/check-doc-runnable.ts` ### Go **Source:** `examples/quickstart/main.go` (read, already shown whole on the previous page), `examples/control-mode-subscribe/main.go` (wait) **In this page:** read: hand-quoted; wait: read whole from the file **Checked by:** both run against real tmux as `TestQuickstart` / `TestControlModeSubscribe`; the wait file's `docs:watching` region is additionally mirrored into README.md by `go generate ./tmux` ### Rust **Source:** `crates/libtmux/examples/scratch.rs`, already shown whole on the previous page **In this page:** hand-quoted excerpts of the same file **Checked by:** run to completion against a throwaway tmux by `scripts/run-examples.sh`, which CI runs ### Java **Source:** root `README.md` Quickstart (read), `examples/src/main/java/io/github/libtmux/examples/WatchPaneOutput.java` (wait) **In this page:** read: hand-quoted; wait: read whole from the file **Checked by:** every README fence is compiled and run against real tmux by `docs-tests`; `WatchPaneOutput` is additionally run by the `examples` module's `ExamplesRunTest` ### .NET **Source:** root `README.md`, "Running something, and reading it back" **In this page:** hand-quoted **Checked by:** one of the `csharp run` blocks compiled and run against real tmux by `ReadmeExampleTests` ### C++ **Source:** `examples/05-readme.cpp` `capture` region (read); `include/libtmux/server.hpp` doc comment (wait, no fence) **In this page:** hand-quoted **Checked by:** the `capture` region is quoted verbatim into README.md and checked by `tools/docs/check_readme.py`; the whole file is built and run by CTest ### Swift **Source:** `Examples/Sources/ExampleCode/Changing.swift` (read, already shown whole on the previous page), `Waiting.swift` (wait) **In this page:** read: hand-quoted excerpt; wait: read whole from the file **Checked by:** both matched against the README by `Scripts/check_examples.py` and run by `swift test --package-path Examples` ### Source inclusion Go, Rust, and Swift each reuse a file already shown in full on [Attach and send keys](../attach-and-send-keys/#where-this-comes-from): rather than dump the same file a second time, this page quotes just the relevant lines by hand, with a comment naming the source, and points back at the full listing there. --- # Build a workspace from a file Source: https://libtmux.org/en/ruby/latest/examples/workspace-from-file/ > The tmuxp-shaped job of describing a multi-window session as data and building it in one call, in each port that has a checked way to do it. [tmuxp](https://tmuxp.git-pull.com/) describes sessions, windows, panes, and shell commands in configuration files. Several libtmux ports provide builders for this format. [Source details](#where-this-comes-from) identify the example files and their checks. For Python, use [tmuxp](https://tmuxp.git-pull.com/), a separate application built on libtmux's `Server`, `Session`, `Window`, and `Pane` APIs. TypeScript's `applyWorkspace` reuses existing objects when the same configuration is applied again. Go's `Example()`, in `workspace/example_test.go`, is a Go `Example` function: `go test` runs it and checks its output against the `// Output:` comment at the end, so this is executed on every test run rather than merely present in a README. `Parse` rejects a field it doesn't recognize rather than dropping it silently, and reports every problem it finds at once with the line it's on. `Build` is not atomic: tmux has no transaction, so a failure partway through leaves whatever was already created in place, identified by the session `Build` still returns. Rust's `freeze(&session).await?` exports an existing session to the workspace format. It recovers windows, panes, and working directories, but cannot recover the shell command originally typed to start a process. Java's `read` and `parse` validate the configuration and return a `Workspace` value. Only `build` changes tmux state. The .NET builder can wait for shell readiness before sending commands; [Sending keys](/guides/sending-keys/#the-race-you-cant-see-from-the-call-site) explains the startup race. It polls `pane_current_command`, `cursor_x`, and `cursor_y` for up to ten seconds by default. `PaneReadiness.Auto` waits for zsh, `Always` waits for every pane running the session's default shell, and `Never` sends immediately. If `BuildAsync` fails partway through, `WorkspaceBuildException.PartialResult` identifies what was created. C++'s `examples/workspace/` implements a consumer of the core API with its own `workspace.hpp` and `tmuxp.hpp` types. Those types are part of the example, not the library package. See [the example's README](https://github.com/libtmux/libtmux-cxx/tree/main/examples/workspace) to adapt it. Swift's `WorkspaceBuilder.build` rejects an existing session with the requested name. `Workspace.decode(yaml:)` reads tmuxp YAML when the `YAMLWorkspaces` trait is enabled. `Workspace.decode(json:)` needs no additional trait. ## Where this comes from ### Python **Source:** Not listed. **In this page:** no fence; the README says tmuxp is a separate project by design **Checked by:** n/a ### TypeScript **Source:** `examples/workspace/workspace.ts` (`@libtmux/workspace`) **In this page:** read whole from the file **Checked by:** run against real tmux by `bun test examples/workspace` ### Go **Source:** `workspace/example_test.go` (`workspace.Parse` / `workspace.Build`) **In this page:** read whole from the file **Checked by:** `Example()` and its siblings run under `go test` and are checked against their own `// Output:` comments ### Rust **Source:** `crates/tmux-workspace/README.md`, "Build it" **In this page:** hand-quoted **Checked by:** the crate's own `crates/tmux-workspace/src/lib.rs` includes the README as a doc comment (`#![doc = include_str!("../README.md")]`), so `cargo test --doc` runs this exact block ### Java **Source:** `libtmux-workspace/README.md`, "What you get back" **In this page:** hand-quoted **Checked by:** every Java fence in the module's README is compiled and run against real tmux by `docs-tests` ### .NET **Source:** `src/LibTmux.Workspace/README.md` **In this page:** hand-quoted **Checked by:** one of the READMEs and docs `ReadmeExampleTests` compiles and runs against real tmux ### C++ **Source:** `examples/workspace/` (a consumer, not a library API) **In this page:** prose only **Checked by:** `examples/workspace/tests/` runs it against real tmux; `ctest -R consumer.workspace` selects it. It exercises the example's own types, not a published `libtmux` API ### Swift **Source:** `Examples/Sources/ExampleCode/Workspaces.swift` **In this page:** read whole from the file **Checked by:** matched against the README's "Workspaces, from a file or from Swift" section by `Scripts/check_examples.py`; compiled and run by `swift test --package-path Examples` ### Source inclusion Rust, Java, and .NET use copied excerpts from their README examples. The source details above identify those files and their checks. --- # Guides Source: https://libtmux.org/en/ruby/latest/guides/ > Task-oriented walkthroughs that sit between the concepts and each port's own API reference. Use these guides to connect to tmux, send input, capture output, query objects, and test your program. [Concepts](/concepts/) explains the object model and transport choices. - **[Getting started](getting-started/)**: install tmux, choose a port, and run an example. - **[Attaching to tmux](attaching-to-tmux/)**: select a socket and find or create a session. - **[Sending keys](sending-keys/)**: send literal text, named keys, and Enter. - **[Capturing output](capturing-output/)**: read the screen or scrollback and wait for a result. - **[Filtering and querying, in practice](querying-and-filtering/)**: apply the lookup contracts from [Filtering and queries](/concepts/queries/). - **[Testing with libtmux](testing-with-libtmux/)**: use isolated tmux servers and manage test cleanup. [Examples](/examples/) provides source-backed programs for the same tasks, with source and validation details on each page. --- # Getting started Source: https://libtmux.org/en/ruby/latest/guides/getting-started/ > Install tmux, pick a port, and run the smallest thing that proves your setup works. ## Install tmux The common tmux baseline documented here is 3.2a. Individual features can require a newer release; check your port's compatibility notes. Confirm your installed version: ```console $ tmux -V ``` If `tmux` is missing or older than 3.2a, install a supported version with your platform's package manager. libtmux uses an installed tmux executable. ## Pick a port Choose the port for your project's language: [Python](/py/), [TypeScript](/ts/), [Rust](/rs/), [Go](/go/), [Java and Kotlin](/java/), [.NET](/dotnet/), [C++](/cxx/), or [Swift](/swift/). [Server, session, window, pane](/concepts/server-session-window-pane/) explains the shared model, and [Control mode vs one-shot](/concepts/transports/) covers transport differences. For a prerelease package, pin an exact version and check its release notes before upgrading. API availability and defaults can differ between ports. ## Run the smallest thing that proves it works Start a tmux session to connect to: in one terminal: ```console $ tmux new-session -s foo -n bar ``` In a second terminal, install your port's package and run its example. The Python example uses the `foo` session above; the other examples create their own sessions. Installation commands appear in comments at the start of each block. [Attach and send keys](/examples/attach-and-send-keys/) provides the full examples, their source files, and their validation details. ## What just happened A server handle targets tmux without taking over your terminal. Creating a session starts the server if needed. Sending keys writes input to a pane; the method's Enter and literal-text options control how tmux interprets it. [Sending keys](../sending-keys/) explains those defaults. Capture methods read the pane's screen or a requested scrollback range. ## Where to go next - [Concepts](/concepts/) for the mental model behind what you just did: the object hierarchy, how commands actually reach tmux, and how filtering works once you have more than one session to choose from. - [Attaching to tmux](../attaching-to-tmux/), [Sending keys](../sending-keys/), and [Capturing output](../capturing-output/) go one level deeper into each half of the round trip you just ran. - [Attach and send keys](/examples/attach-and-send-keys/) for the fully checked version of every block above, and how each one is verified. - Your port's own API reference (via the port switcher) once you're ready to look up method details. --- # Attaching to tmux Source: https://libtmux.org/en/ruby/latest/guides/attaching-to-tmux/ > 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. 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. 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. ## 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`. Sources: Python's `Server.from_env()` is doctested in `src/libtmux/server.py` (`pyproject.toml` `testpaths`). .NET's resolution order and `FromEnvironment` are from `src/LibTmux/README.md` ("Where a bare connect lands"), one of the nine documents `ReadmeExampleTests` compiles and runs. Go's is `tmux/server_options.go`'s doc comments. Rust's fallback is `crates/libtmux/examples/find.rs`, run via `cargo run --example find`. C++'s four constructors are the README's own description of `Server`, at the top of "What is libtmux?". Swift's is `README.md`'s top-level quickstart, matched against `Examples/Sources/QuickStart/main.swift` by `Scripts/check_examples.py`. Java's is `README.md`, run by [`docs-tests`](../testing-with-libtmux/#java-docs-tests). ## Finding a session instead of always creating one 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. Sources: Go uses `examples/filter-query/main.go`, region `docs:query-in-tmux`. Java uses `examples/.../BuildAWorkspace.java`, run by the examples module's tests. For exactly-one lookup semantics, see [Filtering and querying, in practice](../querying-and-filtering/). Swift uses `Examples/Sources/ExampleCode/Querying.swift`, checked against the README by `Scripts/check_examples.py` and exercised by `Examples/Tests/ExampleTests/ModeTests.swift`. .NET exposes `Server.HasSessionAsync(name)` in `src/LibTmux/Server.Lifecycle.cs`. Its `CreateSessionAsync` supports `ReplaceExisting`, which kills and recreates the session. Rust and C++ provide query APIs for session lookup. See [Filtering and querying, in practice](../querying-and-filtering/) and the port references for the call signatures; this page has no tested excerpt for those combinations. ## 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 Source: https://libtmux.org/en/ruby/latest/guides/sending-keys/ > Literal text versus tmux key names, whether Enter is pressed for you, and why a command can outrun the shell about to run it. Send literal text to type characters into a pane, or send tmux key names such as `C-c`, `Enter`, and `Up` to press those keys. Check the method's literal-text and Enter defaults: typing the word `Enter` and pressing Enter are different operations. ## Literal text, key names, and whether Enter follows These examples show each port's text, named-key, and Enter behavior. [Attach and send keys](/examples/attach-and-send-keys/) provides the full source examples and validation details. Check each method's input contract before sending text that could be a key name. Some ports separate text and key-name methods; others use a literal-text flag. [Concepts](/concepts/) introduces the shared tmux model. ## The race you can't see from the call site Completing `send-keys` means tmux accepted the input. The shell may still be starting, and the command may still be running. The port examples provide different ways to wait: - **Rust** uses a `retry_until` loop in the README's query example to wait for the shell. - **Go** provides `tmuxtest.WaitForShellReady` for tests that need a ready shell. - **.NET** demonstrates waiting for command output in the README's "Running something, and reading it back" section. Wait for shell readiness before sending input when startup matters. Then wait for the command's expected output or a completion signal before reading its result. The next guide covers those waiting APIs. ## Where to go next - [Capturing output](../capturing-output/): reading back what you just sent, and waiting for it correctly instead of guessing a delay. - [Attach and send keys](/examples/attach-and-send-keys/): the full sourced round trip this guide picks apart piece by piece. --- # Capturing output Source: https://libtmux.org/en/ruby/latest/guides/capturing-output/ > Read a pane's screen or scrollback and wait for output or a completion signal. Capture a pane to read its visible screen or scrollback. After [Sending keys](../sending-keys/), wait for the expected output or a completion signal before reading the result. ## Visible pane vs. scrollback `tmux capture-pane` distinguishes the currently visible screen from the scrollback history above it, and every port exposes that split rather than flattening it: Java's `pane.capture()` and .NET's `pane.CaptureAsync()` return the visible pane as a list of lines; neither's own README shows a scrollback option as of this page, so check the port's reference before assuming one exists. Sources: TypeScript's is `examples/capture/capture.ts`, run by `bun test examples/capture`. Go's is `examples/quickstart/main.go`. Rust's is `crates/libtmux/README.md`'s capability table, doctested via `#![doc = include_str!("../README.md")]`. C++'s is `README.md`'s "Read a pane" section, quoted verbatim from `examples/05-readme.cpp`'s `capture` region and checked by `tools/docs/check_readme.py`. Swift's is `README.md`, "Change what is there." ## Wait for the expected text An immediate capture can race the shell, as [Sending keys](../sending-keys/#the-race-you-cant-see-from-the-call-site) explains. Wait for the expected text with a timeout so your program stops promptly when the output arrives and reports a failure if it never does: Python's pytest plugin supplies isolated test servers; see [Testing with libtmux](../testing-with-libtmux/). For Python and C++ signal-based waiting, use the `wait-for` APIs below. This page does not include a wait-for-text helper for those ports. Sources: Go's is `tmux/tmuxtest/screen.go`, quoted in `README.md`'s "Testing your own code" section. Rust's is `crates/libtmux/README.md`, doctested. TypeScript's is `examples/agent/agent.ts`, run by the integration suite and quoted in `packages/libtmux/README.md` (``). Java's is `examples/.../WatchPaneOutput.java`. .NET's is `README.md`, one of the `csharp run` blocks `ReadmeExampleTests` runs. Swift's is `Examples/Sources/ExampleCode/Waiting.swift`, matched against `` and the README by `Scripts/check_examples.py`; the same file's `server.capture(pane, since: mark)`, called in a loop with the cursor it returns, is the "watch as it prints" shape for output too large or too open-ended to wait on a single pattern. ## When the pane can announce itself: `wait-for`, not scraping If you control the command, have it signal completion with `tmux wait-for -S done`. Wait on the same channel to avoid matching screen text: The Python example is a doctest in `src/libtmux/server.py`. C++'s `Server::wait_for(channel, timeout)`, declared in `include/libtmux/server.hpp`, also detects a server that dies during the wait. Swift's `server.wait(for:)` example waits for a build to signal completion: Source: `Examples/Sources/ExampleCode/Waiting.swift`. Rust's `crates/libtmux/README.md` documents the same pattern under "tmux keeps a signal nobody is waiting on, so the job finishing first does not lose the race, and nothing polls," runnable as `examples/orchestrate.rs`. ## Where to go next - [Filtering and querying, in practice](../querying-and-filtering/): once you're reading more than one pane, finding the right one to capture. - [Testing with libtmux](../testing-with-libtmux/): the isolated-server fixtures that make waiting on real tmux practical inside a test suite. - [Capture pane output](/examples/capture-pane-output/): the full sourced code for the patterns above. --- # Filtering and querying, in practice Source: https://libtmux.org/en/ruby/latest/guides/querying-and-filtering/ > Filter tmux objects, require one match, and choose where a query runs. Find sessions, windows, or panes with collection filters and exactly-one lookups. [Filtering and queries](/concepts/queries/) explains the result-count contracts and the choice between local and tmux-side filtering. This guide adds examples for common queries. ## Filling in the rest of the cardinality table | Port | Collection filter | Exactly-one | Empty | Several | |------|--------------------|--------------|-------|---------| | Go | `tmuxq.Where(values, predicate)` | `tmuxq.ExactlyOne(values, predicate)` | `tmuxq.ErrNoMatch` | `tmuxq.ErrMultipleMatches` | | Rust | `.iter().matching(&expr)` | `.exactly_one()` | prints via the error's `Display` | same, one error type covers both | | C++ | pipe a range into [`libtmux::matching(expr)`](/cxx/latest/reference/libtmux-matching/) | `libtmux::exactly_one(range)` | `.error()` says which way it went wrong | same call, same error type | Go's `ExampleExactlyOne` in `tmuxq/example_test.go` checks the result with `go test` and `// Output:` assertions: Rust's is `examples/find.rs`, run via `cargo run --example find`: C++'s is quoted straight from `examples/05-readme.cpp`'s `cardinality` region into `README.md`, and `tools/docs/check_readme.py` fails the build if the two ever disagree: For .NET and Swift result-count handling, consult the port reference. The examples here demonstrate .NET's `IEnumerable.Matching(expression)` returning an `IReadOnlyList` and Swift's `hasSession(_:)` returning a `Bool`. The latter checks existence; see [Attaching to tmux](../attaching-to-tmux/#finding-a-session-instead-of-always-creating-one). ## Declarative filters that travel, beyond Python and TypeScript [Filtering and queries](/concepts/queries/) covers Python's `.filter()` lookups and TypeScript's `.where()` documents. Two more ports build the same "a query is data, not code" idea, verified against their own README: Sources: .NET's is `src/LibTmux/README.md`, "Filtering." Swift's is `Examples/Sources/ExampleCode/Filtering.swift`, matched against the README by `Scripts/check_examples.py`. ## Case-insensitive matching For case-insensitive matching in Java, .NET, Go, Rust, and C++, consult the port reference. Sources for the examples above: Python's lookup is covered in [Filtering and queries](../../concepts/queries/); TypeScript's is in `README.md`, "What querying looks like"; Swift's is in `Examples/Sources/ExampleCode/Filtering.swift`. ## Push the filter into tmux, or read once and filter locally Use a tmux-side filter to reduce the rows returned, or query a snapshot when you need several answers from one read. [Filtering and queries](/concepts/queries/) compares Python's `search_sessions()` with `.filter()`, and Go's `SearchPanes` with a snapshot plus `tmuxq.Where`. Check the required tmux version. Unknown format tokens expand to empty values, so validate an unexpectedly empty search before concluding that no objects match. ## Where to go next - [Attach and send keys](/examples/attach-and-send-keys/): its "Finding an existing session instead" section is this guide's recipes applied to one concrete lookup. - [Testing with libtmux](../testing-with-libtmux/): most of the fixtures there hand you a server with exactly one thing on it, which is precisely when an exactly-one query is the right tool instead of a filter you then index into. --- # Testing with libtmux Source: https://libtmux.org/en/ruby/latest/guides/testing-with-libtmux/ > Every port ships a way to give your own tests a real, disposable tmux server. What each one hands you, and what it guarantees about cleanup. Use a private tmux server to test code that creates sessions, sends input, or captures output. The fixtures below allocate a separate socket and manage normal test cleanup. Cleanup after abrupt process termination depends on the fixture; Java's stale-server cleanup is described below. Request the fixture to obtain its server. Python's `session` fixture depends on `server`, so requesting a session also creates an isolated server: That exact block is a doctest in `src/libtmux/pytest_plugin.py`, checked by running it as a nested pytest run and asserting it passes. `session_params` overrides how the fixture builds a session (window size, for instance) without forking it; a temporary `HOME` and tmux config keep window and pane indices stable across machines, so an assertion like `window_name == "test"` doesn't depend on whatever `.tmux.conf` the test runner happens to have. `tmuxtest.NewServer(ctx, t)` captures the environment and working directory, resolves the tmux executable, and creates a server on its own socket. Construction can return an error. Test cleanup kills the server, and wait failures include the last captured screen. Source: `README.md`, "Testing your own code," backed by `tmux/tmuxtest/`. Enable the `test-support` feature in a dev-dependency. The crate README uses these guards in doctests through `#![doc = include_str!("../README.md")]`. `libtmux::test::retry_until(deadline, condition)` polls an arbitrary async condition; `Pane::wait_for_text` waits specifically for pane text. See [Capturing output](../capturing-output/). `libtmux-junit5` supplies each test with a running `Server` containing a session named `libtmux`. Request `TmuxSocketPath` when your code takes a socket path. Fixtures live in JUnit's per-test extension store. A shutdown hook kills servers owned by that JVM, and startup cleanup removes servers left by JVMs that have exited. Source: `libtmux-junit5/README.md`. The port's `docs-tests` module compiles Java fences from READMEs and guides, then runs them against `libtmux-junit5` servers. A `` directive can instead require a named exception, a compile failure, or an explicit skip reason. Source: `docs-tests/README.md`. `LibTmux.Testing` ships as a separate package, under `src/LibTmux.Testing/`. `await using` disposes the scope and kills its server when the block exits. Use `TmuxWait.UntilAsync` to wait for expected state; see [Capturing output](../capturing-output/). Source: `README.md`, "Testing your own code," exercised by `ReadmeExampleTests`. The example comes from `README.md`, "Testing your own tmux tools," and is checked against the `fixture` region in `examples/05-readme.cpp` by `tools/docs/check_readme.py`. Enable the `testing` CMake component with `find_package(libtmux COMPONENTS testing)`. It creates a private socket and temporary directory, sets `TMUX_TMPDIR`, and removes `TMUX` and `TMUX_PANE` from the child environment. `SocketNamespace::consumer(...)` labels sockets with the consumer suite's name. `examples/tests/README.md` shows use from outside the library's build tree. `TmuxFixture` is a separate package product. It starts a server with a bootstrap session and limits concurrent fixtures to reduce process and pseudo-terminal exhaustion. `LIBTMUX_TMUX_BIN` selects the executable; otherwise it checks installed locations. Source: `Tests/TmuxFixture/README.md`. TypeScript's harness at `packages/libtmux/src/_internal/test/testkit.ts` is internal and unpublished. For external tests, create an isolated `Server` and manage its cleanup in your test framework. ## Where to go next - [Capturing output](../capturing-output/): the wait helpers most of these fixtures are meant to be used alongside, instead of a fixed `sleep` in a test. - [Attach and send keys](/examples/attach-and-send-keys/) and [Capture pane output](/examples/capture-pane-output/): the same operations these fixtures give you a server to run, shown as tested examples in their own right. --- # Executable recipes Source: https://libtmux.org/en/ruby/latest/examples/recipes/ > Source-owned Ruby guide at c6d9d2177182. Every linked program loads installed gems, starts an isolated server, asserts its result and verifies owned-daemon cleanup. The shared [support module](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/support.rb) supplies assertions and the cleanup wrapper. No program adds checkout paths to Ruby's load path. Run a program from the repository after installing its required local gems: ```console $ ruby examples/window_links.rb ``` The artifact suite copies programs outside the checkout and runs them against each gem's isolated dependency closure. [The manifest](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/manifest.json) owns file discovery and the source regions used below. `scripts/examples --check` rejects an unlisted program or a changed excerpt. ## Captured queries [Complete list/filter/exact-one program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/list_filter.rb). A fresh window does not alter an existing capture; local filtering still works after the binding closes. ```ruby session = server.new_session(name: "capture", command: ["/bin/cat"]) session.new_window(name: "second", command: ["/bin/cat"]) snapshot = server.snapshot panes = snapshot.panes first = panes.one(id: panes.first.id) session.new_window(name: "later", command: ["/bin/cat"]) Example.check(panes.size == 2, "captured membership changed") Example.check(panes.where(id: first.id).one.ref == first.ref, "wrong exact match") Example.raises(LibTmux::MultipleMatchesError) { panes.one } Example.check(panes.one_or_nil(id: "%4294967294").nil?, "missing pane was invented") ``` ## Layout and bytes [Complete layout/capture/send program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/layout_io.rb). A control output event establishes that the literal input has reached the pane before capture. Binary buffers round-trip without text decoding. ```ruby receipt = server.new_session(name: "layout", command: ["/bin/cat"], receipt: true) pane = receipt.pane second = pane.split(direction: :horizontal, size: "40%", command: ["/bin/cat"]) receipt.window.select_layout("tiled") Example.check(receipt.window.list_panes.map(&:id).sort == [pane.id, second.id].sort, "assigned pane IDs differ") server.open_control(session: receipt.entity.ref) do |control| control.exchange("display-message -p ready", timeout: 0.5) output = control.subscribe(pane_id: pane.id, max_bytes: 8192, max_events: 32) literal = "literal; #{'#{pane_id}'} $HOME" pane.send_text(literal) bytes = "".b bytes << output.next(timeout: 0.5).data until bytes.include?(literal) Example.check(pane.capture.stdout.include?(literal), "capture lost literal input") end payload = "NUL\0\xff\n".b server.write_buffer(name: "bytes", data: payload) Example.check(server.read_buffer("bytes").stdout == payload, "buffer bytes changed") ``` ## One window at three indexes [Complete window-link program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/window_links.rb). Window identity and selection context remain distinct. ```ruby session = server.new_session(name: "links", command: ["/bin/cat"]) window = session.list_windows.fetch(0) session.link_window(window.ref, index: 4) session.link_window(window.ref, index: 9) links = session.list_window_links Example.check(links.map(&:index) == [0, 4, 9], "link indexes differ") Example.check(links.map { |link| link.window.ref }.uniq == [window.ref], "window identity split") Example.check(links.map(&:ref).uniq.length == 3, "link contexts collapsed") links.find { |link| link.index == 9 }.select Example.check(session.display('#{window_index}').text == "9\n", "wrong current link") ``` ## Blocking request cancellation [Complete plain-Ruby cancellation program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/cancel.rb). A tmux event proves dispatch before another thread cancels the blocked client. The program joins the caller, checks client reaping and closes the token's owned pipe. ```ruby cancellation = LibTmux::Cancellation.new waiting = nil begin waiting = Thread.new do server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 0.5, cancel: cancellation) rescue LibTmux::Cancelled => error error end server.wait_for("ready", timeout: 0.5) cancellation.cancel failure = waiting.value Example.check(failure.is_a?(LibTmux::Cancelled), "cancellation lost") Example.check(failure.delivery == :possibly_sent, "dispatched wait claimed no effects") Example.raises(Errno::ECHILD) { Process.waitpid(failure.pid, Process::WNOHANG) } Example.check(server.diagnostics.fetch(:admitted_requests).zero?, "client remains admitted") ensure begin cancellation.cancel waiting&.join ensure cancellation.close end end ``` ## Async capture and cancellation [Complete Async program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/async_cancel.rb). A second task captures while another tmux client waits; cancellation retires that client's process. ```ruby Async do |parent| LibTmux::Async.open(parent: parent, server: server) do |scope| waiting = parent.async do scope.server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 0.5) rescue LibTmux::Cancelled => error error end scope.server.wait_for("ready", timeout: 0.5) Example.check(scope.diagnostics.fetch(:active_process_slots) == 1, "waiting client lost its slot") captures = scope.map(scope.server.list_panes.map(&:ref), concurrency: 2) do |ref| scope.server.pane(ref).capture end Example.check(captures.all?(&:success?), "sibling captures stalled") waiting.cancel failure = waiting.wait Example.check(failure.is_a?(LibTmux::Cancelled), "cancellation lost") Example.check(failure.delivery == :possibly_sent, "cancelled dispatch claimed no effects") Example.raises(Errno::ECHILD) { Process.waitpid(failure.pid, Process::WNOHANG) } Example.check(scope.server.diagnostics.fetch(:admitted_requests).zero?, "cancelled client remains admitted") end end.wait ``` ## Control overflow [Complete control program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/control_overflow.rb). A slow reliable subscriber fails explicitly; a tail subscriber reports a gap and replies continue to drain. ```ruby server.open_control(session: session.ref) do |control| control.exchange("display-message -p ready", timeout: 0.5) reliable = control.subscribe(max_events: 1, max_bytes: 1024) tail = control.subscribe(mode: :tail, max_events: 1, max_bytes: 1024) 3.times { |index| window.rename("event#{index}") } reply = control.exchange("display-message -p alive", timeout: 0.5) Example.check(reply.blocks.last.body == "alive\n", "slow reader blocked commands") Example.check(reply.attribution == :boundary_window, "reply overclaims attribution") Example.check(reliable.diagnostics.fetch(:overflowed), "overflow is missing from diagnostics") Example.check(control.diagnostics.fetch(:retained_reply_bytes).zero?, "consumed reply remains retained") reliable.next(timeout: 0.5) Example.raises(LibTmux::SubscriptionOverflow) { reliable.next(timeout: 0.5) } gap = tail.next(timeout: 0.5) Example.check(gap.kind == :gap && gap.dropped_bytes.positive?, "tail hid lost bytes") end ``` ## Failure inside a group [Complete group program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/failed_group.rb). The first mutation survives a later failure. A separate read establishes the missing later effect; the group result does not invent statuses for its members. ```ruby group = server.run_group([ ["set-option", "-g", "@before", "retained"], ["select-pane", "-t", "%4294967294"], ["set-option", "-g", "@after", "not-executed"] ]) Example.check(!group.success?, "failing group succeeded") Example.check(group.steps.all? { |step| step.fetch(:outcome) == :unknown }, "invented per-step status") Example.check(server.options(scope: :session).get("@before").raw == "retained", "earlier effect rolled back") Example.check(server.options(scope: :session).list.none? { |option| option.name == "@after" }, "later step executed") ``` ## MCP protocol [Complete MCP program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/mcp_protocol.rb). Real pipe frames exercise discovery, snapshot reads, default mutation denial and cancellation of an application-owned WAIT method. The custom WAIT method is a transport probe, not part of the advertised tmux tool catalog. ```ruby application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example") sdk = application.sdk_server input, client_input = IO.pipe client_output, output = IO.pipe transport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output) runner = parent.async { transport.run } ``` The installed executable also exercises explicitly enabled create, send, close and authored-run tools. Its complete program creates a zsh pane whose startup file explicitly sources the CLI's mode-0600 enrollment file, waits for the authenticated acknowledgement, and checks binary stderr and native exit status. It closes stdin and verifies enrollment cleanup and borrowed daemon survival. An unsupported advertised process backend exercises the structured refusal instead; that is not positive enrollment evidence. ```ruby executable = Gem.bin_path("libtmux-mcp", "libtmux-mcp") Open3.popen3(Gem.ruby, "-W:no-experimental", executable, "--socket", server.endpoint.socket_path, "--tmux", Example.executable, "--endpoint", "installed", "--enable-tool", "tmux_create", "--enable-tool", "tmux_send", "--enable-tool", "tmux_close", "--enable-tool", "tmux_run", *enrollment_arguments) do |input, output, errors, process| request = lambda do |id, method, params = {}| input.write(JSON.generate({jsonrpc: "2.0", id: id, method: method, params: params}) + "\n") Example.check(IO.select([output], nil, nil, id == 1 ? 1.0 : 0.5), "installed MCP did not return a frame") response = JSON.parse(output.gets) Example.check(response.fetch("id") == id, "MCP response identity changed") response.fetch("result") end request.call(1, "initialize", {protocolVersion: "2025-11-25", capabilities: {}, clientInfo: {name: "recipe", version: "1"}}) input.write(JSON.generate({jsonrpc: "2.0", method: "notifications/initialized"}) + "\n") names = request.call(2, "tools/list").fetch("tools").map { |tool| tool.fetch("name") } Example.check(names.sort == %w[tmux_capabilities tmux_close tmux_create tmux_run tmux_send tmux_snapshot], "tool policy differs") created = request.call(3, "tools/call", {name: "tmux_create", arguments: { kind: "session", name: "via-protocol", argv: ["/bin/cat"]}}).fetch("structuredContent") Example.check(created.fetch("ok"), "protocol creation failed") data = created.fetch("data") pane = data.fetch("created").find { |ref| ref.fetch("kind") == "pane" } sent = request.call(4, "tools/call", {name: "tmux_send", arguments: { target: pane, input: {type: "text", text: "literal;"}}}).fetch("structuredContent") Example.check(sent.fetch("data").fetch("completion") == "dispatch_only", "send claimed shell completion") target = pane if channel Example.check(File.stat(setup).mode & 0o777 == 0o600, "enrollment setup permissions differ") channel.puts(setup) Example.check(IO.select([channel], nil, nil, 0.5) && channel.gets == "ready\n", "shell enrollment was not acknowledged") target = pane.merge("id" => shell_pane.id) end script = 'printf "%s:%s" "$EXAMPLE_CONTEXT" "$TMUX_PANE"; printf "\\000\\377" >&2; exit 9' run = request.call(5, "tools/call", {name: "tmux_run", arguments: { target: target, script: script, stdout_limit: 128, stderr_limit: 2}}).fetch("structuredContent") if channel Example.check(run.fetch("ok"), "installed authored run failed") result = run.fetch("data") Example.check(result.fetch("stdout").fetch("data") == "installed:#{shell_pane.id}", "authored shell context differs") Example.check(result.fetch("stderr") == {"encoding" => "base64", "data" => "AP8=", "bytes" => 2, "truncated" => false}, "authored bytes differ") Example.check(result.fetch("completion") == {"state" => "exited", "exit_status" => 9, "signal" => nil}, "native completion differs") Example.check(result.fetch("authorization").fetch("state") == "authorized", "authorization receipt missing") else Example.check(run.dig("error", "code") == "unsupported" && run.dig("error", "delivery") == "not_sent", "unsupported enrollment did not refuse") end closed = request.call(6, "tools/call", {name: "tmux_close", arguments: {target: data.fetch("entity")}}) Example.check(closed.fetch("structuredContent").fetch("ok"), "protocol close failed") input.close Example.check(process.join(0.5), "MCP EOF did not retire its process") Example.check(process.value.success? && errors.read.empty?, "MCP executable failed") Example.check(!File.exist?(setup), "enrollment setup survived EOF") end ``` ## Workspace plan, load and failure [Complete workspace program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/workspace_apply.rb) consumes the [checked configuration](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/workspace.yaml), applies it, then verifies partial failure and guarded compensation when another layout exceeds available pane space. It also runs installed CLI validation, planning and loading. ```ruby workspace = LibTmux::Workspace.load(File.join(__dir__, "workspace.yaml")) plan = workspace.plan(snapshot: server.snapshot) Example.check(server.list_sessions.size == 1, "planning changed tmux") result = plan.apply(server: server) Example.check(result.success?, "workspace apply failed") Example.check(result.effects.any? { |effect| effect.outcome == :dispatch_only }, "shell dispatch overclaims completion") crowded = LibTmux::Workspace.parse(JSON.generate({ session_name: "crowded", windows: [{window_name: "small", panes: Array.new(40) { {} }}] }), format: :json, base_directory: __dir__) error = Example.raises(LibTmux::Workspace::ApplyError) do crowded.plan.apply(server: server, compensate: true) end Example.check(!error.result.created_refs.empty?, "failure lost partial creation ledger") Example.check(error.result.compensation == :completed, "owned compensation failed") Example.check(server.list_sessions.map(&:ref).include?(borrowed.ref), "borrowed session was removed") ``` --- # Execution modes Source: https://libtmux.org/en/ruby/latest/guides/execution-modes/ > Source-owned Ruby guide at c6d9d2177182. | Mode | Wait and ownership | Result evidence | | --- | --- | --- | | Core command | Calling thread blocks; server binding owns each client and its pipes | `CommandResult` has binary stdout/stderr and final client status | | Captured query | Snapshot acquisition performs explicit I/O; `Selection` filtering is local | Stable captured membership with acquisition interval and coverage | | Command group | One client submits an ordered, nontransactional group | Aggregate final status; individual steps remain `unknown` | | Core control | One owned reader thread drains the connection; each subscriber has limits | Guarded blocks between boundaries; hooks can share the interval | | Async | Caller supplies an Async parent task; pipe I/O yields on its scheduler | Same command evidence, with bounded request admission and cancellation | | MCP | Caller supplies transport streams, Async scope and tool policy | Versioned protocol envelopes and structured tool responses | | Workspace | Parsing and planning are inert; apply performs ordered commands | Created-reference ledger, observed effects and explicit uncertainty | Use `Server.open(socket_path: ...)` to borrow a daemon through a pinned binding. `Server.start` creates an owned daemon on a unique socket and closes it with the block. No API in these recipes chooses the default server. `CommandResult#success?` means the client exited successfully. Sending input does not establish shell completion. A `GuardedReply` has no `success?`: `%end` terminates a guard, while WAIT commands, aliases and hooks can change what finishes when. Its `boundary_window` attribution is deliberately weaker than per-command ownership. Concurrent control calls pipeline complete wire requests on one connection. The writer can submit a later request while an earlier reply waits; the reader assigns boundaries in the same order. Admission limits include all pending and completed but unconsumed requests. Cancellation after any request bytes are written closes the connection: other written requests have `possibly_sent` delivery, and requests with no written bytes have `not_sent`. No uncertain request is replayed. Sequential calls still wait for each reply. Reliable control subscriptions raise `SubscriptionOverflow` when they cannot retain the stream. Tail subscriptions emit a gap containing lost sequence and byte evidence. Neither mode blocks the command reader behind a slow consumer. Limits apply to connection-owned buffers; callers own the replies they retain after return. Captured queries evaluate the whole criteria tree before selecting rows, including inactive OR branches. Explain methods perform no I/O. Current source plans use explicit capture and local evaluation; unsupported required pushdown fails rather than silently changing semantics. See [the recipes](../../examples/recipes/) for event-based WAIT release, cancellation, overflow and group-failure examples. No throughput comparison is claimed. --- # libtmux for Ruby Source: https://libtmux.org/en/ruby/latest/guides/overview/ > Source-owned Ruby guide at c6d9d2177182. Create tmux sessions, split windows, send input, and capture pane output from Ruby. Read server state into a snapshot, then query it with `where`, `select`, and the rest of `Enumerable`. [Quick start](#quick-start) · [Queries](#query-a-snapshot) · [Gems](#gems) · [Guide](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/index.md) · [API reference](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/reference/api.md) · [Recipes](../../examples/recipes/) **Alpha.** APIs may change between releases. See the [initial alpha notes](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/CHANGELOG.md) and [release guide](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/releasing.md). ## Install Install the core prerelease from RubyGems: ```console $ gem install libtmux --pre ``` The [companion gems](#gems) install separately; use `--pre` for their alpha versions too. ## Install from source Use the Ruby pinned in [.tool-versions](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/.tool-versions) and have `tmux` on your `PATH`. From this checkout, install the development bundle: ```console $ mise install ``` ```console $ mise exec -- bundle config set --local path vendor/bundle ``` ```console $ mise exec -- bundle install ``` The gemspecs declare Ruby 3.3+. The [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml) tests Ruby 3.3, 3.4, and 4.0 with tmux 3.2a–3.7c on Linux and macOS. Check the results for your revision before relying on a particular combination. ## Quick start Create a session and split its `logs` window. `Server.start` owns a private tmux server and closes it when the block exits. The snapshot remains readable afterward. ```ruby require "libtmux" snapshot = LibTmux::Server.start do |server| session = server.new_session(name: "work", window_name: "main", command: ["/bin/cat"]) window = session.new_window(name: "logs", command: ["/bin/cat"]) window.split(direction: :horizontal, size: "40%", command: ["/bin/cat"]) server.snapshot end snapshot.windows.each do |window| puts "#{window.name}: #{window.panes.map(&:id).join(', ')}" end ``` Run the [complete example](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/quickstart.rb): ```console $ mise exec -- bundle exec ruby examples/quickstart.rb ``` Output: ```text main: %0 logs: %1, %2 ``` Pane commands take argument arrays. To use an existing server, open an explicit endpoint with `LibTmux::Server.open(socket_path: ...)`; closing that binding leaves the daemon running. See [ownership and errors](../ownership-errors/). ## Query a snapshot Continue with the snapshot above. `where` accepts criteria as data; `select` accepts a Ruby block. These queries make no tmux calls. ```ruby panes = snapshot.panes active_ids = panes.where(active: true).map(&:id) wide_panes = panes.select { |pane| pane.width >= 40 } panes_by_window = panes.group_by { |pane| pane.window.name } logs = snapshot.windows.one(name: "logs") missing = snapshot.windows.one_or_nil(name: "missing") ``` `one` raises `NoMatchError` or `MultipleMatchesError` unless exactly one record matches. `one_or_nil` returns `nil` for no match and still rejects duplicates. Both errors live under `LibTmux`. Selections retain captured membership. Call `server.snapshot` again while the server is open to read later changes. The [field catalog](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/reference/fields.md) lists query fields and wire names; the [list/filter recipe](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/list_filter.rb) also covers exact matches and queries after close. ## Gems Start with `libtmux`. Add the companion for your caller: | Gem | Require | Use it for | | --- | --- | --- | | [libtmux](../core/) | `libtmux` | Blocking scripts, snapshots, and control connections | | [libtmux-async](../async/) | `libtmux/async` | Concurrent commands and bounded streams in Async tasks | | [libtmux-mcp](../../mcp/source-guide/) | `libtmux/mcp` | An MCP server with explicit endpoints and tool policy | | [libtmux-workspace](../../workspace/source-guide/) | `libtmux/workspace` | YAML/JSON workspace plans and a CLI to apply them | Requiring a gem starts no tmux process, scheduler, or protocol server. [Execution modes](../execution-modes/) explains blocking calls, Async tasks, and control subscriptions. Tracked MCP captures, waits, and authored runs require tmux 3.3+ and native process identity; see the [MCP guide](../../mcp/source-guide/). Workspace client-switching semantics are in the [workspace guide](../../workspace/source-guide/). To use the core outside this checkout, build the artifacts: ```console $ mise exec -- bundle exec rake build ``` Install into the Ruby environment that will run your application. The core's runtime dependencies must already be installed for this local-only command: ```console $ gem install \ --local \ --no-document \ pkg/libtmux-0.1.0.alpha.1.gem ``` Companion gems need their declared runtime dependencies too. The [packaging check](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/.github/CONTRIBUTING.md#checks) verifies each gem in an isolated installation and runs the recipes outside the checkout. ## More examples and reference - [Send text and capture output](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/layout_io.rb): split panes, wait for output events, and round-trip binary buffers. - [Work with linked windows](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/window_links.rb): address one window at several session indexes. - [Capture concurrently](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/async_cancel.rb): read panes while another request waits, then cancel it. - [Load a workspace](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/workspace_apply.rb): parse a configuration, plan, and apply it. - [All recipes](../../examples/recipes/): cancellation, control streams, command groups, and MCP. - [API reference](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/reference/api.md): public methods, source links, and behavioral contracts. RBS declarations ship with each gem; selected installed calls are checked, without a whole-program typing guarantee. - [Benchmarks](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/docs/benchmark.md): workloads, measurements, and their limits. See [Contributing](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/.github/CONTRIBUTING.md) for setup and checks. [MIT license](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/LICENSE). --- # Ownership and errors Source: https://libtmux.org/en/ruby/latest/guides/ownership-errors/ > Source-owned Ruby guide at c6d9d2177182. An entity reference contains a binding identity, kind and tmux-assigned ID. Window links also retain session and index context. A snapshot can describe one window at several indexes without creating several window identities. Names are query values, not substitutes for stable command targets. A borrowed binding retains its private socket route. Replacing the public socket does not authorize following a new server. Closing the binding retires owned clients and pipes and preserves the borrowed daemon. A server started with `Server.start` owns its daemon and temporary directory as well. Handles, Async tasks and subscriptions cannot be transferred to another process or scheduler as if their ownership were unchanged. | Failure or empty value | Meaning | | --- | --- | | Empty `Selection` / `one_or_nil` returns `nil` | A valid complete local selection has no matching record | | `NoMatchError` / `MultipleMatchesError` | `one` cannot establish exactly one match | | `InvalidFilterError` / `FieldDecodeError` | Criteria or wire values do not satisfy the declared schema | | `IncompleteSnapshotError` | Required coverage is unavailable; do not infer absence | | `TargetNotFoundError` | The guarded target no longer matches its identity/context | | `Cancelled` / `DeadlineExceeded` | The requested wait ended; delivery evidence determines possible effects | | `SubscriptionOverflow` / gap event | A bounded stream cannot establish uninterrupted delivery | | `Workspace::ApplyError` | Inspect its immutable result ledger before deciding whether to compensate | Operation errors report a phase and delivery evidence: `not_sent`, `possibly_sent`, or `observed`. An observed nonzero exit still needs its command result. Cleanup diagnostics accompany failures; cleanup failure is not a successful close. A cancelled mutating request can already have effects. A timed-out WAIT lock request can still acquire its queued remote lock after a later unlock; retiring the client does not roll back tmux's command queue. Client cancellation sends TERM, then KILL if exit is still unobserved, and uses the remaining cleanup deadline to reap the owned child. There is no scheduled grace period for a TERM handler. This policy applies to owned command clients; cancelling their requests does not terminate borrowed panes or daemons. Create `LibTmux::Cancellation.new` for blocking requests and pass it as `cancel:`. Calling `cancel` from another thread wakes current users of that token and makes `cancelled?` true permanently; later requests with the same token refuse before dispatch. A token can cancel several requests. It owns a pipe, starts no thread, and must remain open until every request using it has returned. Join those callers before calling `close`; closing a token does not join them. Both methods are idempotent and their return values are unspecified. `reader` belongs to the token: do not consume its bytes or close it separately. After a fork, a child may close its inherited descriptors but cannot query or cancel the parent's token. The [plain-Ruby example](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/cancel.rb) proves wakeup, delivery evidence and client reaping without sleeps. Async tasks can instead use `Task#cancel` as shown in the [Async example](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/async_cancel.rb). Workspace compensation is explicit. It removes only a positively created session whose current windows and panes all belong to the creation ledger, checked in the same tmux command turn. A borrowed window or pane moved into that session prevents destructive compensation. Arbitrary shell effects cannot be rolled back, and an unacknowledged creation is never guessed by name. Parsing errors and ordinary inspection redact payloads. Explicit results, captures, plans and canonical exports contain the requested data, including commands or normalized paths; callers control their storage and display. See [execution modes](../execution-modes/) and [workspace details](../../workspace/source-guide/). --- # libtmux-mcp Source: https://libtmux.org/en/ruby/latest/mcp/source-guide/ > Source-owned Ruby guide at c6d9d2177182. Expose an existing tmux server over MCP stdio. Read snapshots, capture pane output, wait for events, or explicitly enable creation, input and shell commands. The official MCP SDK handles the protocol; bounded Async tasks handle transport. Install the alpha and its `libtmux-mcp` executable: ```console $ gem install libtmux-mcp --pre ``` ## Start the server Set `TMUX_SOCKET` to an existing tmux socket. This command borrows that daemon and serves MCP on stdin/stdout: ```console $ libtmux-mcp \ --socket "$TMUX_SOCKET" \ --endpoint local ``` Use `--socket-name NAME` instead of `--socket PATH` to select a named socket. `--endpoint` sets the public alias used in discovery and resource URIs; it does not select the socket. EOF retires owned clients and preserves the daemon. | Tools | Default | Purpose | | --- | --- | --- | | `tmux_capabilities`, `tmux_snapshot` | Enabled | Discover capabilities and query captured metadata | | `tmux_capture`, `tmux_wait` | Disabled | Capture a screen or wait for text/process exit | | `tmux_create`, `tmux_send`, `tmux_close` | Disabled | Create entities, send text/keys and tear down exact targets | | `tmux_run` | Disabled | Run a script in an explicitly enrolled zsh shell | Repeat `--enable-tool` for each additional tool. For screen capture and waits: ```console $ libtmux-mcp \ --socket "$TMUX_SOCKET" \ --enable-tool tmux_capture \ --enable-tool tmux_wait ``` Disabled tools are absent from discovery and denied on direct application calls. The [complete protocol recipe](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/mcp_protocol.rb) exercises discovery, snapshots, default denial, enabled mutations, cancellation and EOF cleanup through actual pipes, including the installed executable. ## Snapshots, capture and waits Snapshot pages retain one immutable capture and query. Cursor expiry or eviction returns an error; it never substitutes a new live listing. Defaults retain up to 16 captures and 8 MiB for 30 seconds, with a five-second acquisition deadline and one-MiB structured response limit. A cursor pages captured metadata; it does not claim that an old pane process still exists. Schema validation supplements the core decoder's stricter byte, depth, node and duplicate-key rules. Opt into `tmux_capture` for a bounded screen snapshot. Results retain line endings, encode invalid UTF-8 as base64, and distinguish truncated screen content from unknown history continuity. Tracking produces a retained cursor; subsequent calls return a splice against that exact captured state. A screen delta does not establish that every intervening output byte was observed. Capture refuses nonempty effective `after-capture-pane` hooks, including inherited sparse entries. On tmux 3.2a–3.4, callers must keep capture-hook configuration stable throughout observation: those versions require a separate hook preflight. tmux 3.5+ checks the hook in the capture command queue. Both paths retain an explicit session/pane context and refuse its removal instead of switching to another session's hooks. Process tracking retains its native identity checks on every version that supports it. `tmux_wait` observes screen text or process exit through events. Canceling it retires its observation resources without signaling the pane program. Strong process tracking requires tmux 3.3 or later and a native identity backend: Linux peer pidfds with matching process namespaces, or Darwin kqueue process observation. Acquisition verifies the live daemon and pane before retaining a cursor; unavailable evidence produces an explicit refusal. The [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml) records each exact platform/version result, including the required tmux 3.2a refusal and positive identity cases on later versions. Resource templates expose metadata pages and pane screens under encoded endpoint/generation URIs. They enforce the same policy and response limits as their tools. Metadata pages preserve capture identity; screen resources include interval, truncation and history-continuity metadata. Resource subscriptions are not advertised. ## Create, send and close Add `--enable-tool tmux_create`, `--enable-tool tmux_send` or `--enable-tool tmux_close` to authorize those tools. Creation accepts argument arrays; sending text and sending named keys are separate variants. Mutation results contain delivery evidence and positively returned references. `dispatch_only` input results do not claim program completion, and unknown effects remain unknown after cancellation. ## Run authored commands `tmux_run` requires separate policy and shell enrollment. Add `--enable-tool tmux_run --enroll-pane %ID=FILE` for each exact pane, then explicitly source the generated file in that pane's interactive zsh 5.9. The CLI creates a private setup file and never types into the terminal. It refuses existing files and symlinks. Invitations expire after 60 seconds; `--enrollment-timeout` accepts at most 300 seconds. At most eight panes may be enrolled. EOF retires pending enrollment and removes only files the CLI owns. The tool accepts an exact pane target, a POSIX `script`, and separate `stdout_limit`/`stderr_limit` byte counts. Scripts may contain at most 65,536 bytes. Each output defaults to 65,536 bytes and is capped at 262,144; the application reserves its worst-case serialized response before authorization. The helper inherits the enrolled shell's cwd and exported environment, uses closed stdin, and reports separate UTF-8 or base64 outputs. Nonzero exit and signal termination are completion results. Output overflow is an error with completion unobserved, not silently truncated success. Shell variables, functions, options and cwd changes do not persist in the interactive parent. An idle, empty primary ZLE editor receives the request through a private socket. A guarded tmux queue operation authorizes one script digest for one retained server, pane process and enrollment generation. Execution may follow that authorization; a later respawn does not redirect the prepared helper to its replacement. Error responses retain known authorization and native completion receipts. Cancellation does not prove that arbitrary descendants stopped. The Linux and macOS compatibility jobs exercise enrollment and the installed helper dependency closure; consult their results for the revision being used. ## Embed in Ruby Require `libtmux/mcp`; imports start no tmux process, scheduler or MCP server. `Application` borrows an application-owned `LibTmux::Async::Server`. Its `sdk_server` supplies the SDK server consumed by `StdioTransport`. Both objects stay on that application's reactor thread. See the [execution guide](../../guides/execution-modes/) for result and ownership boundaries. For shell enrollment, call `Application#invite_shell(reference, timeout:, expires_in:)` and pass the returned invitation to `accept_shell`. The invitation exposes an immutable `shell_arguments` array for an explicitly sourced setup command and a monotonic `expires_at`. Its acquisition deadline is separate from its enrollment lifetime. The application owns invitations and accepted connections until `close`; direct enrollment calls enforce tool policy. --- # Third-party notices Source: https://libtmux.org/en/ruby/latest/third-party-notices/ > Licences and attribution for the tools that build libtmux.org and the software libtmux depends on. libtmux and this site are built with open-source software. Several of those licences ask that their notice text travel with the work, so it is reproduced here. ## Documentation toolchain | Tool | Licence | Role | |---|---|---| | [Astro](https://astro.build) | MIT | The site shell | | [Tailwind CSS](https://tailwindcss.com) | MIT | Styling | | [Pagefind](https://pagefind.app) | MIT | Site-wide search | | [Expressive Code](https://expressive-code.com) | MIT | Code blocks | | [Sphinx](https://www.sphinx-doc.org) | BSD-2-Clause | Python and C++ reference | | [Furo](https://github.com/pradyunsg/furo) | MIT | Sphinx theme | | [Breathe](https://github.com/breathe-doc/breathe) | BSD-3-Clause | Doxygen XML into Sphinx | | [Doxygen](https://www.doxygen.nl) | GPL-2.0-only | Parses C++ headers to XML | | [API Extractor](https://api-extractor.com) | MIT | TypeScript API model | | [IBM Plex](https://github.com/IBM/plex) | OFL-1.1 | Typeface | ### A note on Doxygen Doxygen is licensed GPL-2.0-only. It runs as a build step that reads libtmux's own headers and emits XML; that XML is rendered by Breathe and Sphinx, and no Doxygen-generated HTML is published. Running a GPL program over your own input does not place its licence on the output, and libtmux does not distribute Doxygen or any modified version of it. ## Reference hosting Three ports deep-link to the canonical host their ecosystem already uses, rather than duplicating it here: - Rust: [docs.rs](https://docs.rs/libtmux) - Go: [pkg.go.dev](https://pkg.go.dev/github.com/libtmux/libtmux-go/tmux) - Java and Kotlin: [javadoc.io](https://javadoc.io/doc/io.github.libtmux/libtmux) Those sites are operated independently of this project and carry their own terms. ## libtmux itself Each port is MIT licensed. See the `LICENSE` file in that port's repository for the authoritative text. --- # MCP for Ruby Source: https://libtmux.org/en/ruby/latest/mcp/ > Run the libtmux-mcp server with an explicit tmux endpoint and tool policy. `libtmux-mcp` exposes one existing tmux server over MCP standard input and output. Requiring `libtmux/mcp` starts no server; the installed `libtmux-mcp` executable owns the protocol transport and borrows the selected tmux daemon. The default catalog contains `tmux_capabilities` and `tmux_snapshot`. Observation and mutation tools are opt-in, so a client cannot acquire them by calling an undisclosed name. ## Start here - [Install](#install) configures an MCP client to launch the Ruby server. - [Tools](./tools/) records the actual wire schemas and per-tool policy. - [Guides](./guides/) select an owned socket and enable optional tools. - [Topics](./topics/) explains retained captures, waits, and shell enrollment. - [Examples](./examples/) shows a complete client configuration. - [Language API](./reference/) documents the Ruby embedding surface. The server requires Ruby 3.3 or newer. Strong process tracking for waits and authored runs requires tmux 3.3 or newer plus native process identity support. The [source-owned MCP guide](./source-guide/) is staged from the same revision as the generated reference. --- # Ruby MCP topics Source: https://libtmux.org/en/ruby/latest/mcp/topics/ > Understand tool policy, retained observations, resources, and authored runs. ## Tool policy `tmux_capabilities` and `tmux_snapshot` are enabled by default. Capture, wait, create, send, close, and run tools remain absent until the process receives a matching `--enable-tool` option. Direct application calls enforce the same policy as protocol discovery. ## Captures and resources Snapshots and screen captures retain immutable observations. A cursor pages that retained state; it does not silently substitute a newer live listing. Resource templates expose metadata pages and pane screens using encoded endpoint and generation identities. The server advertises neither resource subscriptions nor resource-list change notifications, and it has no prompt catalog. ## Authored runs `tmux_run` needs both `--enable-tool tmux_run` and an exact `--enroll-pane %ID=FILE` entry. The operator must source the generated file in that pane's interactive zsh 5.9. Enrollment does not type into the terminal, and a later pane respawn cannot redirect an authorized request to the replacement process. The tool reports stdout and stderr independently. Nonzero exits and signals are completion results; overflow is an error, not truncated success. [Source-owned policy and enrollment guide](../source-guide/) --- # Ruby MCP guides Source: https://libtmux.org/en/ruby/latest/mcp/guides/ > Select a tmux socket, launch libtmux-mcp, and enable only the required tools. ## Select the tmux server Start tmux on an application-owned named socket, then configure the MCP server with the same name. `--socket-name` selects tmux; `--endpoint` only assigns the public alias used in discovery and resource URIs. ```console $ tmux -L libtmux-docs new-session -d -s agent ``` ```console $ libtmux-mcp \ --socket-name libtmux-docs \ --endpoint local ``` Use `--socket PATH` instead when the application owns an explicit socket path. The process serves MCP on standard input and output. End of input closes retained protocol clients without stopping the borrowed tmux daemon. ## Enable observation Screen capture and waits are absent from the default catalog. Add each one explicitly: ```console $ libtmux-mcp \ --socket-name libtmux-docs \ --enable-tool tmux_capture \ --enable-tool tmux_wait ``` `tmux_wait` can observe screen text or a process exit. Strong process tracking requires tmux 3.3 or newer and a supported native identity backend. A refusal means the required evidence is unavailable; it is not a successful wait. ## Enable mutations Repeat `--enable-tool` for `tmux_create`, `tmux_send`, or `tmux_close`. Creation accepts command argument arrays. Sending text and sending named keys are separate variants. A dispatch receipt does not claim that the pane program completed. [Source-owned MCP guide](../source-guide/) --- # Ruby MCP examples Source: https://libtmux.org/en/ruby/latest/mcp/examples/ > Configure a client for the default Ruby MCP catalog or opt-in observation tools. ## Default read-only catalog This client configuration selects a named tmux socket and exposes only capability discovery and snapshots: ```json { "mcpServers": { "tmux-ruby": { "command": "libtmux-mcp", "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"] } } } ``` Start the named tmux server separately before the client launches the MCP process. Ask the client to list tools; the result should contain `tmux_capabilities` and `tmux_snapshot`. ## Add bounded observation Append `--enable-tool`, `tmux_capture`, `--enable-tool`, and `tmux_wait` to the argument array. The discovered catalog then includes those two names. Do not enable creation, input, close, or authored execution unless the client needs those effects. The [source-owned MCP guide](../source-guide/) links the executable protocol example that drives the installed gem through pipes and an isolated server. --- # Ruby MCP API reference Source: https://libtmux.org/en/ruby/latest/mcp/reference/ > Public libtmux-mcp embedding types and methods. Require `libtmux/mcp` to embed the server. Imports start no tmux process, scheduler, or protocol transport. `Application` borrows an application-owned `LibTmux::Async::Server`; its SDK server and stdio transport remain on that application's reactor thread. The declarations below come from the public Ruby inventory, enriched with RBS signatures, YARD documentation, behavior contracts, and source coordinates at the selected revision. ## API declarations - [LibTmux::MCP](https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp/) [Protocol catalog](https://libtmux.org/en/ruby/latest/mcp/tools.json) --- # Workspace Manager for Ruby Source: https://libtmux.org/en/ruby/latest/workspace/ > Validate, plan, and load bounded YAML or JSON workspaces with libtmux-workspace. `libtmux-workspace` parses bounded YAML or JSON into an immutable creation plan. `validate` and offline `plan` do not contact tmux. `load` and `plan --live` borrow an existing server selected with `--socket`. The manager creates a new session. It does not reconcile, replace, or delete a preexisting workspace. Applying a plan authorizes declared shell commands; their delivery does not prove that the pane programs completed. ## Start here - [Guides](./guides/) covers `validate`, `plan`, and `load`. - [Topics](./topics/) explains the supported format and creation-only model. - [Examples](./examples/) provides a bounded YAML configuration. - [Language API](./reference/) documents the workspace gem. The [source-owned workspace guide](./source-guide/) is staged from the same revision as the generated reference. --- # Ruby workspace guides Source: https://libtmux.org/en/ruby/latest/workspace/guides/ > Validate, inspect, and explicitly apply a Ruby workspace plan. ## Validate without tmux Validation reads and bounds the configuration without starting or contacting a tmux server. ```console $ libtmux-workspace validate workspace.yaml ``` ## Inspect an offline plan An offline plan lists the ordered creation operations. JSON output includes configured commands and paths, so treat it as application data rather than a redacted diagnostic. ```console $ libtmux-workspace plan \ --json \ workspace.yaml ``` ## Load on an explicit socket Start an application-owned server, then pass its socket path to `load`. ```console $ libtmux-workspace load \ --socket "$TMUX_SOCKET" \ --json \ workspace.yaml ``` `plan --live` requires the same selector. `--compensate` enables guarded cleanup of positively identified created state after failure; it cannot undo shell effects. Attach and client switching are explicit post-apply choices, not defaults. [Source-owned CLI guide](../source-guide/#command-line-interface) --- # Ruby workspace topics Source: https://libtmux.org/en/ruby/latest/workspace/topics/ > Workspace format, planning boundaries, application effects, and failure ledgers. ## Configuration boundary The supported format describes one session with windows, panes, options, directories, environment values, and shell commands. YAML tags, aliases, duplicate keys, ERB, plugins, and callbacks are rejected. Environment substitution is opt-in and consults only the explicitly supplied mapping. ## Creation-only plans Plans create new state and reject a captured session-name conflict. They do not reconcile an existing workspace. Every apply takes a fresh snapshot and rechecks its binding and the target name before the first creation command. ## Effects and failure Shell commands are dispatched as literal text followed by Enter. Success means tmux accepted the dispatch, not that the shell command finished. An apply failure retains completed steps, positively identified created references, observed or dispatch-only effects, uncertainty, and cleanup diagnostics. Optional compensation kills only a positively returned new session after an atomic guard proves ownership of every current window and pane. Unknown or borrowed entities cause cleanup refusal. [Source-owned workspace guide](../source-guide/) --- # Ruby workspace examples Source: https://libtmux.org/en/ruby/latest/workspace/examples/ > A bounded creation-only workspace for libtmux-workspace. Save this as `workspace.yaml`: ```yaml session_name: work environment: PROJECT_MODE: development windows: - window_name: editor window_index: 1 layout: tiled panes: - shell_command: printf 'editor ready\n' - {} ``` Run `libtmux-workspace validate workspace.yaml`, inspect `libtmux-workspace plan --json workspace.yaml`, then pass an owned socket to `load`. Relative directories resolve against the configuration file. The [source-owned workspace guide](../source-guide/) links the installed-package example that checks inert planning, apply, and guarded compensation. --- # Ruby workspace API reference Source: https://libtmux.org/en/ruby/latest/workspace/reference/ > Public libtmux-workspace parsing, planning, application, and CLI types. Require `libtmux/workspace` for library use. Parsing and planning do not start tmux or execute commands. `Plan#apply` requires an explicitly opened core server and leaves that binding open. The declarations below come from the public Ruby inventory, enriched with RBS signatures, YARD documentation, behavior contracts, and source coordinates at the selected revision. ## API declarations - [LibTmux::Workspace](https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace/) --- # Topics Source: https://libtmux.org/en/ruby/latest/topics/ > Object traversal, cleanup, pane I/O, configuration, and failure handling. Use these pages for object traversal, cleanup, pane I/O, configuration, and failure handling. [Concepts](/concepts/) introduces the shared object model. The concept guides also cover [control mode vs one-shot](/concepts/transports/), [filtering and queries](/concepts/queries/), and [workspaces](/concepts/workspaces/). Choose a topic below for more detailed behavior: - **[Architecture](architecture/)**: locate operations and field definitions in each port's source. - **[Traversal](traversal/)**: navigate related objects, test membership, and compare identity. - **[Context managers](context-managers/)**: manage cleanup on block exit and identify objects that need an explicit kill. - **[Pane interaction](pane-interaction/)**: choose input modes, capture ranges, and completion waits. - **[Options and hooks](options-and-hooks/)**: configure tmux and register event commands at a supported scope. - **[Format-token fields](format-tokens/)**: read typed state and handle fields absent from a scope, version, or capture. - **[Waiting and retrying](waiting-and-retry/)**: wait on a condition or a named tmux signal. - **[Environment](environment/)**: locate objects from process variables and configure values inherited by new panes. - **[Socket and servers](socket-and-servers/)**: select a server, check liveness, and detect a replacement daemon. - **[Errors and exceptions](errors-and-exceptions/)**: handle command failures and determine whether a mutation can be retried. Use your port's API reference for signatures and defaults. These pages call out differences that affect how you use the APIs. --- # Architecture Source: https://libtmux.org/en/ruby/latest/topics/architecture/ > The object hierarchy underneath the API you call, and who actually holds the behavior in each port. This page describes how the language ports organize their code and where operations live. For the object hierarchy and stable IDs, start with [Server, session, window, pane](/concepts/server-session-window-pane/). ## Where behavior lives: on the object, or through the server Python, TypeScript, Go, Rust, Java, .NET, and C++ provide operations on session, window, and pane objects. Each object carries its ID and server context. These examples send input, set an option, and kill a pane: Swift's `Session`, `Window`, and `Pane` are `Sendable` value types holding IDs and state fields. [Format-token fields](../format-tokens/) lists their fields. Perform operations through `Server`, passing the target value: The practical effect is that `Server` is the one thing you hold onto in a Swift program; a `Session` or `Pane` you got back from a `snapshot()` is inert data you hand back to the server that produced it, not a handle you call things on. Every other port's `Server` is also where you start, but `Session`/`Window`/`Pane` stay live actors once you have one. ## A generated data table under a hand-written surface Ports translate object IDs into tmux targets (`-t`) and read state through tmux's `FORMATS` variables (`#{...}`). Their field definitions use generated catalogs, fixed field sets, or captured dictionaries: | Port | Generated table | Hand-written surface | |------|-------------------|----------------------| | Python | `libtmux.constants` (`FORMATS`, gated by scope and tmux version) | dataclass fields on `Obj` (`libtmux.neo`), `None` when a gate excludes a token | | TypeScript | `packages/libtmux/src/_generated/format_fields.ts` (`{ scope, since, token }` per row) | camelCase aliases on `Pane`/`Session`/`Window` (`packages/libtmux/src/_generated/field_aliases.ts`) | | Go | `format_generated.go`, `option_generated.go` (built by `internal/generate/formats`) | `(value, bool)` accessor methods: Go's own "comma ok" idiom for a gate | | Rust | `formats.rs`'s per-token macro rows (`token, wire name, scope, kind, since version, absent-handling`) | typed methods returning `Option` | | Java | (typed field accessors generated for the query layer: see `Pane_`/`Session_` in [Filtering and queries](/concepts/queries/)) | `Optional` for fields introduced after a port's tmux floor | | C++, Swift | fixed field sets; see below | a fixed, curated set of non-optional struct/class fields | | .NET | a snapshot dictionary read at capture time | typed properties that throw `IncompleteSnapshotException` for a field the capture didn't request, rather than gating on tmux version per field | Swift and C++ expose fixed sets of state fields. Swift includes indices, dimensions, active state, command, path, and edge flags. C++ declares its fields in `kFields` arrays and uses `pane->expand("#{...}")` for other tokens. See [Format-token fields](../format-tokens/) for optional fields and tokens outside the fixed sets. ## Module layout, by port Each port's own top-level organization, to orient yourself before opening its source: - **Python**: one module per tier (`libtmux.server`, `.session`, `.window`, `.pane`, `.client`), plus `libtmux.common` for shared plumbing, `libtmux.neo` for the dataclass query layer, `libtmux.options` / `libtmux.hooks` as mixins every tier includes, and `libtmux.exc` for the exception hierarchy. - **TypeScript**: `packages/libtmux/src/{server,session,window,pane,client}.ts` hold the public classes; nearly everything they call into lives under `_internal/operations/` (one file per concern: `pane_io.ts`, `hooks.ts`, `options.ts`, `topology.ts`) and `_generated/` (the format/option/hook catalogs above). Separate packages in the same monorepo cover workspaces (`@libtmux/workspace`) and an MCP server. - **Go**: a single `tmux` package, split by concern into many files rather than many packages (`model.go` for the core structs, `lifecycle_kill.go`, `pane_capture.go`, `pane_geometry.go`, `hierarchy.go`, `plan_server.go` for folded invocations); `tmuxq` is a separate package for predicate queries over an already-read snapshot ([Filtering and queries](/concepts/queries/)), and `workspace` a separate one again. - **Rust**: `crates/libtmux/src/{server,session,window,pane}/` directories, each split into files by concern (a `settings.rs` per tier holding that tier's options-and-hooks methods, matching the pattern in [Options and hooks](../options-and-hooks/)); `hooks.rs`, `options.rs`, and `formats.rs` hold the shared, scope-generic machinery those call into. Workspaces and the MCP server are separate crates in the same workspace. - **Java**: `io.github.libtmux` holds `Server`, `Session`, `Window`, and `Pane` as `final` classes; each exposes its option and hook tables through `.options()` / `.hooks()` accessor methods returning a separate `Options` / `Hooks` view scoped to that object, rather than mixing those methods directly into the entity class the way Python and Go do. `Session_`, `Window_`, and `Pane_` are a parallel set of typed-field classes that exist only for the query layer. - **.NET**: `src/LibTmux/` gives every entity its own name (`Pane.cs`, `Session.cs`, ...) but splits each into several `partial class` files by concern rather than by inheritance: `Pane.Capture.cs`, `Pane.Input.cs`, `Pane.Relations.cs`, `Pane.Scopes.cs`, `Pane.Topology.cs`, and so on all contribute to one `Pane` type. `Options`/`Hooks` are reached through `.Options` / `.Hooks` properties, structurally the same idea as Java's accessor methods. - **C++**: `include/libtmux/entities.hpp` declares `Session`, `Window`, and `Pane` together as value types (`private Row` bases), with their method bodies in `src/` rather than the header; `server.hpp`, `options.hpp`, and `capabilities.hpp` are separate headers. A private `testing` component (`include/libtmux/testing/`) ships separately from the library proper: see [Context managers](../context-managers/) for what it's for. - **Swift**: `Sources/LibTmux/Server.swift` is the hub every operation extends; `Session.swift` and `Pane.swift` declare the thin value types, `Snapshot.swift` holds the relationship queries ([Traversal](../traversal/)), and `Options.swift`, `PaneInteraction.swift`, and `Mutations.swift` are `extension Server` files grouping options/hooks, send/capture, and kill respectively: all reachable only through `Server`, per the section above. ## Naming conventions Method names follow language conventions: Python, Rust, and C++ use `snake_case`; TypeScript, Java, and Swift use `camelCase`; Go and .NET use `PascalCase`. Option and hook names remain tmux's dash-separated strings, such as `automatic-rename`, regardless of the method's spelling. --- # Traversal Source: https://libtmux.org/en/ruby/latest/topics/traversal/ > Moving up and down the server/session/window/pane tree, and the two questions that come up once you have more than one object. Use relationships to move between sessions, windows, and panes. [Server, session, window, pane](/concepts/server-session-window-pane/) explains the hierarchy and snapshot model. This page covers relationship calls, collection membership, and object identity. ## Down the hierarchy List children through the parent object or a captured snapshot. Whether a read issues another tmux command depends on the API, independently of whether the call is async; see [Server, session, window, pane](/concepts/server-session-window-pane/). | Port | Server → sessions | Session → windows | Window → panes | |------|--------------------|--------------------|-----------------| | Python | `server.sessions` | `session.windows` | `window.panes` | | TypeScript | `await server.sessions()` | `session.windows` | `window.panes` | | Go | `server.Sessions(ctx)` | `session.Windows()` | `window.Panes()` | | Rust | `await server.sessions()` | `session.windows()` | `window.panes()` | | Java | `server.sessions()` | `session.windows()` | `window.panes()` | | .NET | `server.GetSessionsAsync()` | `session.GetWindowsAsync()` | `window.GetPanesAsync()` | | C++ | `server->sessions()` | `session->windows()` | `window->panes()` | | Swift | `server.sessions()`, or `snapshot.windows(of: session)` for windows/panes once you have a `Snapshot` | see previous column | see previous column | TypeScript's `session.windows` and `window.panes` read the graph loaded by `await server.sessions()` without additional tmux commands. Rust also provides `server.attached_sessions()` to list only sessions with an attached client. ## All panes in a session Use a session-wide pane collection when the task spans several windows, such as finding a command or capturing output from every pane. A window linked to multiple sessions still refers to the same tmux panes. Check whether the API reads live state or traverses a captured graph before reusing its result. ### Python `libtmux.Session.panes` runs a session-scoped `list-panes -s` read. Use it to list panes across the session's windows without manually listing each window. ### TypeScript `session.Session.panes` returns a `Selection` from the session's captured graph. Reading it does not issue another tmux command; refresh the snapshot when you need newer state. ### Rust `session.Session.panes` performs a live listing and returns a `Result` from the async call. Handle a command failure before using the returned panes. ### Go `tmux.Session.Panes` reads captured relations without another tmux command. The relation must have been included in the read that produced the session; an uncaptured relation does not establish that the session has no panes. ### .NET `LibTmux.Session.Panes` reads the session's captured relations. It does not issue a tmux command; an incomplete capture may lack the required relation. ### C++ `libtmux::Session::panes` runs a session-scoped `list-panes` command. Inspect the returned result before traversing the panes. ### Swift `Snapshot.panes(of:)` accepts a session or a window. The session overload traverses the captured graph and deduplicates pane IDs without a tmux read. ### Java Java has no direct session-wide pane member. Traverse `Session.windows()` and then `Window.panes()` in the captured relations. Deduplicate pane IDs when combining results from sessions that may share linked windows. ## Up the hierarchy Parent lookups may read captured data or query tmux again. Check the method's read and failure semantics; [Server, session, window, pane](/concepts/server-session-window-pane/) introduces that distinction: | Port | Pane → window | Window → session | |------|----------------|--------------------| | Python | `pane.window` | `window.session` | | TypeScript | `pane.window` (getter, from the loaded graph) | `window.session` (getter) | | Go | `pane.Window()` → `(Window, bool)` | `window.Session()` → `(Session, bool)` | | Rust | `await pane.window()` → `Result, Error>` | `await window.session()` → `Result, Error>` | | Java | `pane.window()` | `window.session()` | | .NET | `pane.Window` (property) | `window.Session` (property) | | C++ | `pane->window()` | `window->session()` | | Swift | `pane.windowID`, then look it up via `Snapshot` | not a per-window field: join through the snapshot instead | Go and Rust return optional relationship results. .NET's `.Window`, `.Session`, `.ActiveWindow`, and `.ActivePane` read captured state synchronously and throw `IncompleteSnapshotException` if that capture lacks the required context. ## One walk, down and back up Start with a session, traverse to a window and pane, then look up the parent and compare its identity with the starting object: ## The active child "Which window is in front right now" and "which pane would a command actually reach" are common enough questions that most ports expose the active child directly rather than making you filter a list: | Port | Session's active window | Window's active pane | |------|---------------------------|------------------------| | Python | `session.active_window` | `window.active_pane` | | TypeScript | `session.activeWindow` (getter) | `window.activePane` (getter) | | Go | `session.ActiveWindow()` → `(Window, bool)` | `window.ActivePane()` → `(Pane, bool)` | | Rust | `await session.active_window()` → `Result, Error>` | `await window.active_pane()` → `Result, Error>` | | Java | `session.activeWindow()` → `Optional` | `window.activePane()` → `Optional` | | .NET | `session.ActiveWindow` (property) | `window.ActivePane` (property) | | C++ | `session->active_window()` | `window->active_pane()` | | Swift | filter for `isActive` on `snapshot.windows(of: session)`: `Window` carries its own `window_active` flag rather than the session exposing an accessor | same pattern, on the pane's own active flag | In Swift, filter snapshot children by `isActive`. Other ports expose an active-child method or property on the parent. [Format-token fields](../format-tokens/) describes the underlying `window_active` and `pane_active` fields. ## Is it in that collection? Checking membership generally goes through whatever your language uses for collection membership, since most of these calls already return an ordinary array, slice, or list: - **Python** overloads `in` directly on its `QueryList`: `window in session.windows`, `pane in window.panes`. - **Java**, **C++**, and **.NET** return standard collections. Use their standard membership operations with the identity comparison appropriate to the port. - **TypeScript** returns iterable `Selection` objects. Iterate over the selection and compare IDs, or spread it into an array for standard array operations. - **Go** and **Rust** return slices or vectors. Iterate and compare object IDs when testing membership by tmux identity. ## Is this the same object? Compare IDs to determine whether two handles refer to the same tmux object on the same server. Equality operators vary by port: | Port | How you check | |------|----------------| | Python | `window.window_id == other.window_id` (or `pane.pane_id == ...`) | | TypeScript | compare `.id` | | Go | `pane.ID() == other.ID()`: `PaneID` is a plain, `==`-comparable `string` | | Rust | `pane.id() == other.id()`: verified from the port's own doctests, not struct equality | | Java | `pane.equals(other)`: overridden to compare server identity plus pane ID | | .NET | `pane.Equals(other)`: overridden to compare a generation counter plus ID | | C++ | `pane == other`: `operator==` is defined directly on `Session`/`Window`/`Pane` | | Swift | compare `.id` for identity; see the equality note below | **Swift's equality compares captured state.** Compiler-synthesized equality for `Session`, `Window`, and `Pane` compares every stored property, including dimensions and current command. Two reads can compare unequal even when they describe the same tmux object. Compare `.id` when checking identity on the same server. --- # Context managers Source: https://libtmux.org/en/ruby/latest/topics/context-managers/ > Scope-based cleanup for tmux objects, and when your program must kill them explicitly. A tmux session, window, or pane normally remains until you kill it. Scope-based cleanup can kill it when your code leaves a block, including after an exception. See [Workspaces](/concepts/workspaces/) for a temporary layout example. Python provides context managers for tmux objects. .NET provides ownership scopes for servers, sessions, and windows. Other ports require explicit cleanup or offer guards for test servers: | Port | Server | Session | Window | Pane | |------|:------:|:-------:|:------:|:----:| | Python | yes | yes | yes | yes | | .NET | yes | yes | yes | - | | Java | closes conn. | - | - | - | | Rust | test-only | - | - | - | | C++ | test-only | - | - | - | | TypeScript | - | - | - | - | | Go | - | - | - | - | | Swift | - | - | - | - | "test-only" means a guard owns an entire disposable test server. Java's `AutoCloseable` server releases its transport but leaves tmux running. A dash means no built-in cleanup scope is listed for that object; use an explicit kill call with the cleanup mechanism appropriate to your language. ## Python: every level, including nested Python's `Server`, `Session`, `Window`, and `Pane` support context managers. Entry returns the existing object; exit kills it, including when the block raises: Nested scopes exit in reverse order: pane, window, session, then server. ## .NET: an explicit ownership type, stopping at Window .NET's `OwnedSessionScope` and `OwnedWindowScope` wrap the created object and implement `IAsyncDisposable`. The `Session` and `Window` handles themselves are not disposable: There is no `OwnedPaneScope`. For tests, `TmuxTestFactory.CreateHierarchyAsync()` returns a `TemporaryHierarchyScope` containing a private server, session, window, and pane. Disposing it kills the server. ## Java: `Server` is closeable, but closing one doesn't kill it Java's `Server` implements `AutoCloseable`. Exiting `try (Server server = Server.open(config))` releases the owned transport while tmux and its sessions remain running. Kill sessions, windows, panes, or the server explicitly when your program owns their cleanup. ## Rust: no async `Drop`, so cleanup is explicit or best-effort Rust's `Drop::drop` is synchronous and cannot await an async tmux kill. Use explicit shutdown when you need to observe cleanup failures: - **`kill(self)` consumes the handle.** Session, window, and pane kill methods take `self` by value, preventing subsequent use of that handle. - **`libtmux::test::TestServer` provides a test guard.** Call `guard.shutdown().await?` to handle cleanup errors. Its `Drop` implementation falls back to synchronous, best-effort `force_cleanup()`. ## C++: RAII exists, but only for a private test server C++'s `Session`, `Window`, and `Pane` are non-owning values; destroying a handle does not kill its tmux object. `libtmux::test::ScopedTmuxServer`, in the separate `testing` CMake component, owns a private test server and its temporary socket directory: ## TypeScript, Go, Swift: no built-in scoping at all TypeScript, Go, and Swift require explicit cleanup of sessions, windows, and panes. Connection or notification handles may have separate disposal APIs: - **TypeScript** implements `[Symbol.asyncDispose]` on control connections and notification streams. `await using` releases those handles; it does not kill the watched session or pane. See [Control mode vs one-shot](/concepts/transports/). Use `finally` for a session your program owns: ```typescript const session = await server.newSession({ name: "work" }); try { const window = await session.newWindow({ name: "editor" }); await window.panes.at(0)?.sendKeys("echo hi"); } finally { await session.kill(); } ``` - **Go** implements `io.Closer` on `ControlClient`, `PaneObservation`, and `NotificationStream`. Use `defer conn.Close()` for those resources and an explicit `Kill(ctx)` for tmux objects: ```go session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "work"}) if err != nil { return err } defer session.Kill(ctx) // idiomatic Go: not a library-provided guarantee ``` - **Swift** uses non-owning session, window, and pane values. Call `try await server.kill(session)` or the corresponding window or pane overload when cleanup is required. ## What this means in practice Use explicit cleanup for objects whose handles have no disposal hook. For an entire disposable test server, prefer your port's test fixture or server guard; see [Testing with libtmux](/guides/testing-with-libtmux/). --- # Pane interaction Source: https://libtmux.org/en/ruby/latest/topics/pane-interaction/ > Input defaults, screen capture, and waiting for a command to finish. Send input to a pane and capture its screen to interact with a running program. [Attach and send keys](/examples/attach-and-send-keys/) provides examples. [Sending keys](/guides/sending-keys/) and [Capturing output](/guides/capturing-output/) are task guides; this page compares input defaults, capture ranges, and completion handling. ## Typing into a pane Two questions come up every time you send something to a pane: should tmux press Enter afterward, and should tmux interpret what you sent as key names (`Enter`, `C-c`) rather than literal characters? Ports answer both, but disagree on whether that's one method with flags or two separate methods: ### Python **Type without Enter:** `pane.send_keys(text, enter=False)` **Type + Enter (default):** `pane.send_keys(text)` **How "literal" is chosen:** `literal=True` flag on the same method ### TypeScript **Type without Enter:** `pane.sendKeys(text, { enter: false })` **Type + Enter (default):** `pane.sendKeys(text)` **How "literal" is chosen:** `{ literal: true }` option ### Go **Type without Enter:** `pane.SendKeys(ctx, SendKeysRequest{Command: &text, SkipEnter: true})` **Type + Enter (default):** `pane.SendKeys(ctx, SendKeysRequest{Command: &text})` **How "literal" is chosen:** `Literal: true` field ### Rust **Type without Enter:** `pane.send_keys(keys)`: **always literal**, key names typed as text **Type + Enter (default):** `pane.send_line(text)` **How "literal" is chosen:** `send_keys` sends literal text; `send_key_names` interprets tmux key names. ### Java **Type without Enter:** `pane.send(keys)` **Type + Enter (default):** `pane.sendLine(command)` **How "literal" is chosen:** Separate text and key-sending methods. ### .NET **Type without Enter:** `SendKeysAsync(new SendKeysRequest(text, enter: false))` **Type + Enter (default):** `SendTextAsync(text)` (defaults `enter: true`) **How "literal" is chosen:** `Literal` field on `SendKeysRequest`; `SendTextAsync` hardcodes it ### C++ **Type without Enter:** `pane->send_text(text)` **Type + Enter (default):** `send_text(text)` then `send_key("Enter")` separately: no combined convenience exists **How "literal" is chosen:** `send_text` is always literal; `send_key` is always a key name ### Swift **Type without Enter:** `server.sendKeys([text], to: pane)` **Type + Enter (default):** `server.run(text, in: pane)` (sugar for `sendKeys([text, "Enter"], to: pane)`) **How "literal" is chosen:** `literally: true` option on `sendKeys` ### Examples **Rust's `send_keys` always sends literal text.** Use `send_key_names` for tmux key names. Passing `"Enter"` to `send_keys` types those characters; it does not press the key. **Text and Enter can be separate commands.** Python, TypeScript, Go, and .NET normally send Enter after the text. If the second operation fails, the text may already be in the pane; retrying the entire request can duplicate it. C++'s separate `send_text` and `send_key("Enter")` calls have the same risk. Rust's `send_line` and Java's `sendLine` append a literal `\r` to the text and send it in one `send-keys -l` command. They avoid a separate Enter dispatch. Send a command line and press Enter: ## Reading a pane back Capture methods return lines from the pane's visible screen by default: `pane.capture_pane()` in Python, `pane.capture()` in TypeScript, Rust, Java, and C++, `pane.Capture(ctx, ...)` in Go, `CaptureAsync(...)` in .NET, and `server.capture(pane)` in Swift. Request scrollback explicitly, such as with Python's `start` and `end` or Swift's `includingHistory`. ## Waiting for something to finish A send call completes when input reaches tmux. It does not wait for the shell command to finish. Wait for expected output or a completion signal: - **Python** can poll capture output for a marker. Its test-support module also provides `libtmux.test.retry_until(condition, ...)` for arbitrary conditions. [Waiting and retrying](../waiting-and-retry/) covers polling helpers across ports. - **Swift** ships a real primitive for exactly this: `server.waitForOutput(...)` returns an `OutputWait` once a pattern shows up in the pane, rather than leaving you to write the loop. - **Go** and **.NET** expose tmux's `wait-for` signal channel through `server.WaitFor(ctx, WaitForRequest{...})` and `TmuxWaitChannel`. Use a named signal when you control the command and can make it announce completion. [Capture pane output](/examples/capture-pane-output/) has the checked, per-port code for the polling-with-a-marker version of this; reach for a port's native wait primitive above it where one exists. --- # Options and hooks Source: https://libtmux.org/en/ruby/latest/topics/options-and-hooks/ > Reading and writing tmux's own configuration knobs, and binding commands to its events, at whichever scope you're holding. Use options to change tmux behavior, such as `automatic-rename` or the status-line format. Use hooks to run commands on events such as `session-renamed` or `after-split-window`. Choose the scope supported by the option or hook. ## Reading and writing options Read an option, set it, or unset it at a chosen scope. Ports differ in how they distinguish values set locally from effective values inherited from another scope: ### Python **Read all (this scope):** `pane.show_options()` **Read effective/inherited:** `pane.show_option(name, global_=True)` reaches the global fallback explicitly; no separate "resolved" call **Set:** `pane.set_option(name, value)` **Unset:** `pane.unset_option(name)` ### TypeScript **Read all (this scope):** `pane.showOptions()` **Read effective/inherited:** `pane.showResolvedOptions()` **Set:** `pane.setOption(name, value)` **Unset:** `pane.unsetOption(name)` ### Go **Read all (this scope):** `pane.Options(ctx)`: a typed struct with one accessor method per option **Read effective/inherited:** Not listed. **Set:** `pane.SetOption(ctx, ...)` **Unset:** `pane.UnsetOption(ctx, ...)` ### Rust **Read all (this scope):** `pane.options()` (typed `BTreeMap`), `pane.option_names()` **Read effective/inherited:** `pane.typed_option(name)` decodes one value by its declared kind **Set:** `pane.set_option(name, value)`, `pane.append_option(name, value)` **Unset:** `pane.unset_option(name)` ### Java **Read all (this scope):** `pane.options().all()` **Read effective/inherited:** `pane.options().get(name)` reads `show-options -A -v`: inherited, not just local **Set:** `pane.options().set(name, value)` **Unset:** `pane.options().unset(name)` ### .NET **Read all (this scope):** `pane.Options.GetAllAsync()` **Read effective/inherited:** `pane.Options.GetAsync(new GetOptionRequest(name, includeInherited: true))`: an explicit opt-in flag, mapped straight to tmux's own `-A` **Set:** `pane.Options.SetAsync(new SetOptionRequest(name, value))` **Unset:** `pane.Options.UnsetAsync(...)` ### C++ **Read all (this scope):** `pane->options()` **Read effective/inherited:** Not listed. **Set:** `pane->set_option(name, value)` **Unset:** `pane->unset_option(name)` ### Swift **Read all (this scope):** `server.options(.pane(pane))` **Read effective/inherited:** `server.option(name, scope: .pane(pane))` reads presence from the listing, then the value with `-v` **Set:** `server.setOption(name, to: value, scope: .pane(pane))` **Unset:** `server.unsetOption(name, scope: .pane(pane))` ### Examples After a successful write completes, read the option to obtain its updated value. These examples set, read, and unset a pane option: ## Hooks ### Python **Set:** `pane.set_hook(name, command)` **Unset:** `pane.unset_hook(name)` **List:** `pane.show_hook(name)`, `pane.show_hooks()` (all) **Run now, without the event:** Not listed. ### TypeScript **Set:** `pane.setHook(name, command, { append })` **Unset:** `pane.unsetHook(name)` **List:** `pane.showHooks()` (all; no singular `showHook`) **Run now, without the event:** Not listed. ### Go **Set:** `pane.SetHook(ctx, name, command)`, `pane.SetHooks(ctx, ...)` (bulk) **Unset:** `pane.UnsetHook(ctx, name)` **List:** `pane.Hooks(ctx)`: typed struct **Run now, without the event:** Not listed. ### Rust **Set:** `pane.set_hook(name, command)` **Unset:** `pane.unset_hook(name)` **List:** `pane.hook(name)`: one name only; **no listing at pane/window scope**, by design (see below) **Run now, without the event:** Not listed. ### Java **Set:** `pane.hooks().set(event, command)`, `.append(event, command)` **Unset:** `pane.hooks().unset(event)` **List:** `pane.hooks().all()` **Run now, without the event:** `pane.hooks().run(event)`: tmux's `set-hook -R` ### .NET **Set:** `pane.Hooks.SetAsync(new SetHookRequest(event, command))` **Unset:** `pane.Hooks.UnsetAsync(...)` **List:** `pane.Hooks.GetAllAsync()` **Run now, without the event:** `pane.Hooks.RunAsync(...)` ### C++ **Set:** `session.set_hook(name, command)`: no `Window`/`Pane` overload exists at all **Unset:** Not listed. **List:** `session.hooks()`, `server.global_hooks()` **Run now, without the event:** Not listed. ### Swift **Set:** `server.setHook(name, to: command, at: index, in: scope)` **Unset:** `server.unsetHook(name, in: scope)` **List:** `server.hooks(scope)` **Run now, without the event:** `server.runHook(name, in: scope)` ### Examples tmux stores hook commands in indexed arrays, such as `after-new-window[0]`. Python and TypeScript can include the index in the name. Go's `SetHooks` and Swift's `at:` parameter take it separately. Java's `.append()` and TypeScript's `{ append: true }` append without requiring the next index. Set and list a session hook. The next section explains window and pane scope limitations: ## Supported hook scopes tmux stores hooks globally or per session. Accepted `set-hook -w` or `-p` flags do not imply a separate window or pane hook table, and `show-hooks` does not provide a corresponding listing. Check the event's supported scope if a hook is accepted but never fires. Ports handle unsupported hook scopes differently: - **Java's `Hooks.java`** documents that tmux can accept a hook at an unsupported scope without an effective registration. Check `.all()` when diagnosing a hook that does not fire. - **Rust** validates scope in `Pane::set_hook` and `Window::set_hook`, returning `Error::OptionScopeMismatch` for an unsupported scope. - **Swift** restricts `HookScope` to `.global` and `.session`. **C++** exposes `set_hook` on `Session` and `global_hooks()` on `Server`, with no window or pane hook methods. Options have window and pane tables of their own. The hook-scope limitation does not apply to ordinary options. ## tmux version compatibility Python's compatibility notes list these tmux requirements: | Feature | Minimum tmux | |---------|-------------| | All options/hooks features | 3.2+ | | Window/pane hook *scope flags* (`-w`, `-p`) accepted | 3.2+; see the supported-scope caveat above | | `client-active`, `window-resized` hooks | 3.3+ | | `pane-title-changed` hook | 3.5+ | Check your port's compatibility notes before relying on a particular tmux release. --- # Format-token fields Source: https://libtmux.org/en/ruby/latest/topics/format-tokens/ > The typed fields every object exposes, mirroring tmux's own format tokens, and why a field is sometimes absent. Object fields expose values from tmux's [FORMATS](https://man.openbsd.org/tmux.1#FORMATS), such as `pane_id`, `window_zoomed_flag`, and `session_name`. The available fields depend on the port, object scope, tmux version, and data requested by the read. A token needs the right **scope** and **tmux version**. For example, a pane token needs a pane context, and a token added after your tmux release may be absent. Ports represent absence with optional values, flags, or errors, as described below. ## The absence idiom, per port | Port | What an excluded field looks like | |------|--------------------------------------| | Python | the attribute is `None` | | TypeScript | the property is `undefined` | | Go | a two-return-value accessor: `pane.DeadSignal()` returns `(string, bool)`: Go's own "comma ok" idiom | | Rust | `Option`; consult the reference for the accessor name | | Java | `Optional`: `pane.floating()` returns `Optional`, empty when the field isn't populated | | .NET | nullable values or `IncompleteSnapshotException`, depending on whether the value or captured field is absent | | C++, Swift | fixed, non-optional fields; see below for access to other tokens | These examples read optional fields, including `pane_dead_signal` on tmux 3.3 or newer: Rust's `formats.rs` marks this token as optional. Consult its generated reference for the accessor name. .NET also distinguishes missing values from incomplete captures. `Pane.Title` is nullable because tmux may report no title. `Pane.Height`, `.Width`, and `.Index` throw `IncompleteSnapshotException` when the read that produced the handle did not request those fields. A handle resolved by ID alone may therefore lack enough data to answer: ## A generated table under the accessor Several ports generate scope- and version-tagged field catalogs from tmux source or documentation. [Architecture](../architecture/) describes the layouts. Examples include: - **TypeScript** uses `_generated/format_fields.ts` rows with `scope`, `since`, and `token`. For example, `pane_zoomed_flag` has pane scope and requires tmux 3.7. `_generated/field_aliases.ts` supplies the camelCase alias `pane.zoomedFlag`. - **Rust** uses a macro row in `formats.rs` for each token's wire name, scope, tmux version, and type. `pane_dead_signal` has `Pane` scope, requires `V3_3`, and is decoded as `Text`. - **Go** generates `format_generated.go` with `internal/generate/formats`. Some accessors decode richer values: `pane.DeadTime()` returns `(time.Time, bool)` and performs timestamp parsing for the caller. Two per-token facts survive across every one of these catalogs, because they're facts about tmux, not about any one port's generator: `pane_dead_signal` and `pane_dead_time` arrived in tmux 3.3, and a cluster of pane-geometry and floating-pane tokens (`pane_floating_flag`, `pane_pb_progress`, `pane_x`, `pane_y`, `pane_z`, `pane_zoomed_flag`, `bracket_paste_flag`, `synchronized_output_flag`, among others) arrived together in 3.7. ## The two ports that didn't generate the full catalog Swift and C++ expose fixed, non-optional fields on `Session`, `Window`, and `Pane`: - **Swift** carries `index`, `width`, `height`, `isActive`, `currentCommand`, `currentPath`, and the four edge flags. - **C++** declares fields in `kFields` arrays. Pane fields include `id`, `command`, `active`, `index`, `title`, `pid`, `tty`, `path`, `width`, `height`, `dead`, `in_mode`, edge flags, and `piping`. Accessors return `std::string_view`, `bool`, or `long long`. For a token outside the fixed fields, C++ provides one-shot expansion with `pane->expand("#{pane_dead_signal}")`. Swift uses `FormatSubscription` on a control connection, delivering `SubscriptionChange` when tmux re-evaluates the token. That API observes changes over time. [Architecture](../architecture/) describes the fixed-field model. ## Fields promoted from the active child Python exposes fields promoted from an active child. For example, `session.pane_id` identifies the active pane of the session's active window: tmux's format engine includes active-child fields when listing a parent. A `list-sessions -F` row can include `window_id` and `pane_id` for the active window and pane. Check the port reference for typed access to those fields, or use the explicit relationships described in [Traversal](../traversal/). A pane context can include parent window and session fields. A session cannot identify one attached client when several clients may be attached, so client tokens such as `client_name` require a client context. --- # Waiting and retrying Source: https://libtmux.org/en/ruby/latest/topics/waiting-and-retry/ > Polling a condition instead of guessing a sleep, and tmux's own wait-for signal channel as the alternative to polling. After sending input or starting a process, wait for the state your next step requires. [Pane interaction](../pane-interaction/#waiting-for-something-to-finish) covers waiting for screen text. This page covers arbitrary conditions and tmux's named `wait-for` signal channels. ## Polling a condition Polling checks a condition repeatedly until it succeeds or a deadline expires. The helpers below expose an interval and timeout; several live in test-support packages: ### Python **Helper:** `libtmux.test.retry_until(fn, seconds=, interval=)` **Where it lives:** The `libtmux.test` module in the main package; raises `WaitTimeout`. ### TypeScript **Helper:** `connectedServer.waitFor(matches, options)` **Where it lives:** The public control-connection API. Tests a predicate over `ServerSnapshot`; see [Control mode vs one-shot](/concepts/transports/). ### Go **Helper:** `tmuxtest.WaitFor(ctx, interval, condition)` **Where it lives:** `tmuxtest`, a separate test-support package from `tmux` ### Rust **Helper:** `libtmux::test::retry_until(within, condition)` **Where it lives:** `libtmux::test`, enabled with the `test-support` Cargo feature. ### Java **Helper:** Not listed. **Where it lives:** not found in the shipped library; a package-private `Await.until(...)` exists only inside the `integration-tests` module, which downstream code cannot depend on ### .NET **Helper:** `LibTmux.Testing.TmuxWait.UntilAsync(probe, timeout, interval)` **Where it lives:** `LibTmux.Testing`, part of the same shipped `LibTmux` package ### C++ **Helper:** Not listed. **Where it lives:** no generic condition-poll helper found in the public library; a `wait_until` exists only in the private `testing` component, for waiting on a spawned child process, not on tmux state ### Swift **Helper:** Not listed. **Where it lives:** a `waitUntil` helper exists only inside the test target's own support code, not shipped ### Examples Python, Rust, Go, and .NET provide general polling helpers in their test-support APIs. TypeScript's public `waitFor` instead waits on a server-snapshot predicate through a control connection. It subscribes before reading so it does not miss a change between those steps. For Java, C++, and Swift, this page lists no public arbitrary-condition polling helper. Use a loop with a deadline and interval if a more specific wait API does not fit; [Pane interaction](../pane-interaction/#waiting-for-something-to-finish) covers output waits. ## tmux's own wait-for channel Use `tmux wait-for -S ` to signal and `tmux wait-for ` to block until signalled. This avoids repeated screen captures when the command can announce its own completion: | Port | Signal | Wait | |------|--------|------| | Python | `server.wait_for(channel, set_flag=True)` | `server.wait_for(channel)` | | TypeScript | not exposed as public API: used only inside the test-server's own startup handshake | - | | Go | `server.WaitFor(ctx, tmux.WaitForRequest{Channel: name, Mode: tmux.WaitForModeSignal})` | `tmux.WaitForRequest{Channel: name}` (the zero-value `WaitForRequest.Mode` waits) | | Rust | `server.signal_channel(name).await?` | `server.wait_for_channel(name, timeout).await?` → `ChannelWait::Signalled` or `TimedOut` | | Java | `server.channel(name).signal()` | `server.channel(name).await(timeout)` → a `WakeReason`, never silently "success" | | .NET | `server.OpenWaitChannel(name)` returns a `TmuxWaitChannel`; signalling is the same request with a different mode | `await using` the channel, then `WaitAsync(budget)` | | C++ | `server.signal(channel)` | `server.wait_for(channel, timeout)` | | Swift | `try await server.signal(channel)` | `try await server.wait(for: channel)` | tmux remembers a signal sent before a waiter starts. The next wait on that channel returns immediately, so completion is not lost when the command finishes first. A raw `wait-for` client can exit zero when the server dies, as well as when the channel is signalled. Java's `WakeReason` and Swift's `wait(for:)` distinguish server loss from a signal; Swift checks the server PID before and after the wait. Use a channel name specific to the task, or clear an old signal with Java's `drain()` when appropriate. A remembered signal can otherwise satisfy an unrelated later wait. --- # Environment Source: https://libtmux.org/en/ruby/latest/topics/environment/ > Locate tmux objects from process variables and manage the environment inherited by new panes. tmux exposes two environment APIs. Process variables such as `TMUX` and `TMUX_PANE` let code inside a pane identify its server and pane. The server also stores variables through `set-environment` and `show-environment` for new processes to inherit. Like the tables in [Options and hooks](../options-and-hooks/), this persistent store has explicit scopes. ## Locating yourself from inside a pane Inside a pane, `TMUX` contains `,,`, and `TMUX_PANE` contains the pane ID, such as `%1`. Use these variables to locate the current tmux objects. Ports expose different levels of environment lookup: ### Python **Server:** `Server.from_env()` **Session:** `Session.from_env()` **Window:** `Window.from_env()` **Pane:** `Pane.from_env()` ### TypeScript **Server:** Not listed. **Session:** `Session.fromEnv()` **Window:** Not listed. **Pane:** Not listed. ### Go **Server:** `NewServerFromEnv(env)` **Session:** `SessionFromEnv(ctx, env)` **Window:** `WindowFromEnv(ctx, env)` **Pane:** `PaneFromEnv(ctx, env)` ### Rust **Server:** `Server::from_env()` **Session:** `Session::from_env(&server)` **Window:** `Window::from_env(&server)` **Pane:** `Pane::from_env(&server)` ### Java **Server:** Not listed. **Session:** Not listed. **Window:** Not listed. **Pane:** See the Java context example below. ### .NET **Server:** `Server.FromEnvironment(env)` **Session:** `Session.FromEnvironmentAsync()` **Window:** `Window.FromEnvironmentAsync()` **Pane:** `Pane.FromEnvironmentAsync()` ### C++ **Server:** `Server::from_env()` **Session:** Not listed. **Window:** Not listed. **Pane:** Not listed. ### Swift **Server:** `TmuxContext.current()` **Session:** `TmuxContext.current()` (same call: see below) **Window:** Not listed. **Pane:** Not listed. ### Examples Python, Go, and .NET provide standalone environment lookups at each level. Rust's `Session`, `Window`, and `Pane::from_env` require an existing `&Server`. TypeScript provides `Session.fromEnv()`; this page lists no equivalent for its other object types. A process not started inside a pane has nothing truthful to answer with, so every one of these raises rather than guessing: Python's `NotInsideTmux`, Go's `FromEnvError`, .NET's `TmuxObjectNotFoundException`, and so on, each naming the missing or malformed variable rather than returning an empty or default object. ### Java and C++ stop short of the pane Neither port gives you a live object back the way the other five do, and they stop at different points: - **C++** provides `Server::from_env()` to select the socket. Use the resulting server to resolve sessions or panes. - **Java** parses `TMUX` and `TMUX_PANE` into identifiers: socket path, server PID, `SessionId`, and `Optional`. It returns context data rather than a live pane handle: ```java TmuxEnvironment here = TmuxEnvironment.current().orElseThrow(); try (Server server = Server.open(here.config())) { Session mine = server.sessions().stream() .filter(session -> session.id().equals(here.session())) .findFirst() .orElseThrow(); } ``` ### Swift context fields Swift's `TmuxContext.current()` parses the socket path, server PID, and session ID from `TMUX`. It does not read `TMUX_PANE`, so it cannot identify the current pane: Read `TMUX_PANE` separately if you need the pane ID; `TmuxContext` does not provide it. ## tmux's own environment variable store Like [Options and hooks](../options-and-hooks/#window-and-pane-hook-scopes-are-mostly-fiction), tmux's persistent environment store has global and per-session scopes. It is read with `show-environment` and updated with `set-environment`. Newly spawned processes inherit it; existing processes retain their own environments. ### Python **Set:** `server.set_environment(name, value)`, `session.set_environment(...)` **Read all:** `server.show_environment()`, `session.show_environment()` **Unset:** `server.unset_environment(name)`. See below for `.remove_environment()`. ### TypeScript **Set:** `server.setEnvironment(name, value)`, `session.setEnvironment(...)` **Read all:** `server.showEnvironment()`, `session.showEnvironment()` **Unset:** `server.unsetEnvironment(name)`, `session.unsetEnvironment(name)` ### Go **Set:** `server.SetEnvironment(ctx, name, value, opts)` (global, `-g`) **Read all:** `server.ShowEnvironment(ctx)` **Unset:** `server.UnsetEnvironment(ctx, name)` ### Rust **Set:** `server.set_environment(...)`, `session.set_environment(...)` **Read all:** `server.environment_all()`, `session.environment_all()` **Unset:** `server.unset_environment(name)`, `session.unset_environment(name)` ### Java **Set:** Not documented here; see the Java and C++ note below. **Read all:** Not documented here; see the Java and C++ note below. **Unset:** Not documented here; see the Java and C++ note below. ### .NET **Set:** `server.Environment.SetAsync(name, value)`, `session.Environment.SetAsync(...)` **Read all:** `server.Environment.GetAllAsync()` **Unset:** `server.Environment.UnsetAsync(name)`, `.RemoveAsync(name)` ### C++ **Set:** Not documented here; see the Java and C++ note below. **Read all:** Not documented here; see the Java and C++ note below. **Unset:** Not documented here; see the Java and C++ note below. ### Swift **Set:** `server.setEnvironment(name, to: value, in: scope)` **Read all:** `server.environment(scope)` **Unset:** `server.unsetEnvironment(name, in: scope)`, `.removeEnvironment(name, in:)` ### Examples Python's `set_environment` writes a value, and `unset_environment` (`-u`) removes the entry. `remove_environment` (`-r`) marks the variable for exclusion from new processes, including when tmux inherited it at server startup; the listing retains it as `-NAME`. Swift exposes the same distinction through `setEnvironment`, `unsetEnvironment`, and `removeEnvironment`. A remove operation is not documented here for TypeScript, Go, or Rust. ### Java and C++: no verified access to this table at all Java and C++ examples here cover environment values supplied at process creation, not reads or writes to an existing persistent table. Java's `SessionSpec.Builder.environment(Map)`, `WindowSpec.Builder.environment(Map)`, and `SplitSpec.Builder.environment(Map)` pass initial variables to `new-session -e`, `new-window -e`, and `split-window -e`. Consult the port reference if you need to change the environment of an existing session. --- # Socket and servers Source: https://libtmux.org/en/ruby/latest/topics/socket-and-servers/ > How a port names one specific tmux server among several, checks whether it's actually there, and tells one running instance apart from another. A tmux server is selected by its Unix-domain socket. Use different sockets for independent servers, such as a development session and an isolated test server. Ports expose tmux's default, named (`-L`), and explicit-path (`-S`) socket selectors: ## Naming a server | Port | Default | Named socket (`-L`) | Explicit path (`-S`) | |------|---------|----------------------|------------------------| | Python | `Server()` | `Server(socket_name="work")` | `Server(socket_path="/tmp/tmux-1000/work")` | | TypeScript | `new Server()` | `new Server({ socketName: "work" })` | `new Server({ socketPath: "..." })` | | Go | `tmux.NewServer(tmux.ServerOptions{})` | `tmux.ServerOptions{SocketName: "work"}` | `tmux.ServerOptions{SocketPath: "..."}` | | Rust | `Server::new()` | `Server::builder().socket_name("work").build()?` | `Server::builder().socket_path("...").build()?` | | Java | `ServerEndpoint.defaultSocket()` | `ServerEndpoint.namedSocket("work")` | `ServerEndpoint.socketPath(path)` | | .NET | `new ServerConnectionOptions()` | `new ServerConnectionOptions(socketName: "work")` | `new ServerConnectionOptions(socketPath: "...")` | | C++ | `Server::at_default()` | `Server::at_socket_name("work")` | `Server::at_socket_path("...")` | | Swift | no bare default: see below | `Server(socketName: "work")` | `Server(socketPath: "...")` | Choose either a socket name or a socket path. TypeScript rejects both together with `TypeError`; Go documents that `SocketPath` takes precedence. tmux uses `TMUX_TMPDIR` to resolve the directory for default and named sockets. Swift requires an explicit `socketPath` or `socketName` argument. To reach tmux's default socket, use `Server(socketName: "default")`. Python's `Server(socket_name_factory=...)` and .NET's `ServerConnectionOptions(socketNameFactory: ...)` accept a callable that generates socket names. Use a unique name for each isolated test server. ## Is the server actually there? A server handle does not prove that the target server is running. Use a liveness check when your program needs to distinguish a live server from an unavailable socket: | Port | Check | |------|-------| | Python | `server.is_alive()` → `bool` | | TypeScript | `await server.isAlive()` → `Promise`; `await server.raiseIfDead()` throws with tmux's own reason instead | | Go | `server.IsAlive(ctx)` → `(bool, error)`: the `error` is reserved for a question that couldn't be answered at all, not for "not alive" | | Rust | `server.is_alive().await` → `bool`; `server.check_alive().await` is the fallible twin, for when the *reason* matters | | Java | `server.isAlive()` → `boolean` | | .NET | `await server.IsAliveAsync()` → `Task` | | C++ | `server.is_alive(timeout)` → `bool` | | Swift | `try await server.isRunning()` → `Bool` | TypeScript's `isAlive()` and Rust's `is_alive()` return a boolean. Use TypeScript's `raiseIfDead()` or Rust's `check_alive()` when you need failure details. ## Killing a server, and telling two apart Kill an entire server with Python's `server.kill_server()`, TypeScript's `await server.kill()`, Go's `server.Kill(ctx)`, Rust's `server.kill().await?`, Java's or Swift's `server.killServer()`, .NET's `await server.KillAsync()`, or C++'s `server.kill()`. Java's `Server.close()` only releases the local connection; see [Context managers](../context-managers/#java-server-is-closeable-but-closing-one-doesnt-kill-it). Two handles can select the same socket. Python's `Server.__eq__` compares `socket_name` and `socket_path`. Go's `server.Equal(other)` resolves relative paths and environment-dependent socket names against each handle's captured binding. A restarted server can reuse a socket path while having different state. TypeScript's `TmuxServerRestartedError` and Go's `ErrDaemonReplaced` detect a handle encountering a replacement daemon. --- # Errors and exceptions Source: https://libtmux.org/en/ruby/latest/topics/errors-and-exceptions/ > What a failed tmux command becomes in each port, and the question every one of them has to answer before letting you retry it. A command can fail because tmux rejects it, or because the transport stops before returning a reply. Ports report these failures through typed exceptions or return values. Before retrying a mutation, determine whether tmux may already have received it. For lookup failures caused by zero or multiple matches, see [Filtering and queries](/concepts/queries/#the-cardinality-contract-side-by-side). ## A failed command, as a value | Port | How it fails | Base type | |------|--------------|-----------| | Python | throws | `LibTmuxException`: carries an optional `subcommand`; `str()` reads `": "` | | TypeScript | throws | `LibTmuxException extends Error`, with `TmuxCommandError` (tmux ran and refused) and `TmuxTransportError` (it didn't get an answer) as the two shapes that matter here | | Go | returns `(T, error)` | no shared base type: small typed `...Error` structs plus sentinel `errors.New` values, composed with `errors.Is` / `errors.As` and `%w` wrapping | | Rust | returns `Result` | one `Error` enum, `#[non_exhaustive]`, matched rather than caught | | Java | throws (unchecked) | `LibTmuxException extends RuntimeException` | | .NET | throws | `LibTmuxException`, with typed subclasses per failure (`TmuxCommandException`, `TmuxTransportException`, `TmuxObjectNotFoundException`, and a dozen more) | | C++ | returns `expected` | `CommandFailure { kind, delivery, exit_code, diagnostic }`: no exception type at all | | Swift | throws (typed) | `enum TmuxError: Error`, thrown as `throws(TmuxError)`: Swift's typed-throws syntax, not a bare `throws` | Rust and C++ return result values. Go returns an `error` that callers inspect with `errors.Is` or `errors.As`. Exception-based ports report failures through their exception hierarchies. TypeScript's own docs make the split between its two exception shapes concrete: ## Is it safe to retry? Retry a mutation automatically only when you know it was not dispatched, or when repeating it is safe for your operation. A timeout, cancellation, or dropped connection can occur after tmux has acted. These APIs expose delivery information: | Port | Name | States | |------|------|--------| | TypeScript | `TmuxTransportError.delivery` | `"not_started"` / `"written"` / `"replied"` / `"indeterminate"`: only `not_started` is safe to retry blindly | | .NET | `LibTmuxException.Dispatch` (`TmuxDispatchState`) | `NotDispatched` / `Dispatched` / `Unknown` (the default) | | Java | `DispatchOutcome`, via `TmuxTimeoutException.outcome()` | `NOT_DISPATCHED` / `COMPLETE` / `UNKNOWN` | | C++ | `DeliveryStatus` | `not_started` / `written` / `replied` / `indeterminate` | | Rust | `ControlModeErrorKind` (behind the `control-mode` feature) | `DispatchTimedOut` (safe to retry) vs. plain `TimedOut` (not: the connection may have already committed the command) | | Python, Go's one-shot path | - | inspect the exit status after normal completion; an interrupted call needs separate state verification | | Go's control-mode pool | handled internally, not exposed | a failed pooled connection is retired rather than reused, rather than handing the caller a retry-safety flag to check | | Swift | documented, not typed | `ControlSession`'s own doc comment states the same rule in prose ("a command that never reached tmux is safe to retry") without a dedicated enum | A state such as `not_started`, `NotDispatched`, `NOT_DISPATCHED`, or `DispatchTimedOut` identifies a request that did not reach tmux. Treat unknown delivery as potentially executed. C++ and TypeScript also distinguish `written`, where the transport accepted the request but no terminal reply has arrived. For subprocess calls that complete normally, inspect the exit status. If a call is interrupted or times out without a delivery state, check tmux's resulting state before repeating a mutation. --- # libtmux-workspace Source: https://libtmux.org/en/ruby/latest/workspace/source-guide/ > Source-owned Ruby guide at c6d9d2177182. Load bounded YAML or JSON, inspect an immutable creation plan, then explicitly apply it to an open libtmux server. The library and installed command-line executable share the core [compatibility matrix](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml); consult its exact per-revision results. The gem declares Ruby 3.3 or newer and depends on the same-version `libtmux` gem, JSON 3.0 and Psych 5.5. See the repository's [contribution guide](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/.github/CONTRIBUTING.md) for local builds and checks. Imports and planning do not start tmux or run commands. Install the alpha and its `libtmux-workspace` executable: ```console $ gem install libtmux-workspace --pre ``` ## Example Save this declared subset of tmuxp-style data as `workspace.yaml`. Relative directories resolve against the configuration file's directory. ```yaml session_name: work environment: PROJECT_MODE: development windows: - window_name: editor window_index: 1 layout: tiled panes: - shell_command: printf 'editor ready\n' - {} ``` The [complete workspace program](https://github.com/libtmux/libtmux-ruby/blob/c6d9d21771828e583692ca899ed817f9640d6a55/examples/workspace_apply.rb) creates an isolated server, applies this document and checks compensation after a later failure. Calling `apply` authorizes the configuration's shell commands. This excerpt runs inside that program's cleanup wrapper: ```ruby workspace = LibTmux::Workspace.load(File.join(__dir__, "workspace.yaml")) plan = workspace.plan(snapshot: server.snapshot) Example.check(server.list_sessions.size == 1, "planning changed tmux") result = plan.apply(server: server) Example.check(result.success?, "workspace apply failed") Example.check(result.effects.any? { |effect| effect.outcome == :dispatch_only }, "shell dispatch overclaims completion") crowded = LibTmux::Workspace.parse(JSON.generate({ session_name: "crowded", windows: [{window_name: "small", panes: Array.new(40) { {} }}] }), format: :json, base_directory: __dir__) error = Example.raises(LibTmux::Workspace::ApplyError) do crowded.plan.apply(server: server, compensate: true) end Example.check(!error.result.created_refs.empty?, "failure lost partial creation ledger") Example.check(error.result.compensation == :completed, "owned compensation failed") Example.check(server.list_sessions.map(&:ref).include?(borrowed.ref), "borrowed session was removed") ``` ## Configuration The plain declared subset has effective version 1. Optional `profile` and `version` must appear together as `libtmux-ruby.workspace` and `1`. `workspace.to_h` exports an immutable, normalized, reloadable configuration. It contains expanded values and absolute directories; callers control its storage and disclosure. - Root: `session_name`, nonempty `windows`, `options`, `window_options`. - Window: `window_name`, nonempty `panes`, `window_index`, `focus`, `layout`, `options`. Indexes are unique nonnegative integers; unspecified indexes use the lowest available value starting at the declared `base-index` or zero. - Pane: a command string or a mapping with `focus`, `split`, `size`. Split is `horizontal` or `vertical`. Size is positive cells or `1%` through `99%`; neither applies to the initial pane. Explicit sizes cannot accompany a final named layout. - Root, windows and pane mappings accept `start_directory`, `environment`, `shell_command` and `shell_command_before`. Environment maps merge from parent to child. Commands inherit unless overridden; before-commands append in parent-to-child order. Command values accept a string or string array. At most one window and one pane per window can declare focus; each defaults to the first. Layouts are `even-horizontal`, `even-vertical`, `main-horizontal`, `main-vertical` and `tiled`. Unknown fields and unsupported features fail with a `ConfigError` identifying their configuration position. Session options accept boolean `status`, `mouse`, `renumber-windows`; nonnegative `base-index`, `history-limit`, `status-interval`; and enumerated `status-position` and `status-justify`. Window options accept boolean `automatic-rename`, `allow-rename`, `remain-on-exit`, `synchronize-panes`, `aggressive-resize`; nonnegative `pane-base-index`, `main-pane-width`, `main-pane-height`; text `window-status-format`, `window-status-current-format`; and enumerated `pane-border-status`. Option text remains tmux option text, including any formats that tmux evaluates. `pane-base-index` cannot exceed 65535; the other numeric options accept integers through 2147483647. YAML tags, anchors, aliases, duplicate keys, multiple documents and complex mapping keys are rejected. JSON duplicate keys are rejected too. No Ruby, ERB, plugins or callbacks are evaluated. Defaults bound source and canonical bytes to 1 MiB, strings to 64 KiB, nesting to 32, nodes to 10,000, windows to 128 and panes to 1,024. The corresponding `max_*` parse/load keywords can adjust these positive limits. Configuration files must be regular files. `${NAME}` substitution is opt-in for directories and environment values: pass `expand_environment: true, environment: {"NAME" => "value"}` to load or parse. Only the supplied bounded mapping is consulted. Shell text and option values are unchanged; `~` has no special path meaning. Missing variables fail validation. Parsing checks path syntax; apply checks current accessibility. Concurrent filesystem changes can still trigger tmux's cwd fallback. ## Apply and failure results `workspace.plan(snapshot: snapshot)` retains that capture's binding identity and rejects a captured name conflict. Every apply takes a fresh snapshot and rechecks identity and name absence. The plan creates a new session; it does not reconcile, replace or remove a preexisting workspace. The first window and pane from each creation command are reused. Apply is synchronous, uses one monotonic timeout across its core operations, and accepts a cancellation token. Panes run `/bin/sh`; explicit initial pane environment overrides do not modify the session environment. Window indexes, splits, options, layout and focus follow the plan's order. Temporary local option overrides disable renumbering and pane synchronization during setup; the plan then restores declared values or inheritance. Session options apply before subsequent windows and split panes are created. On tmux 3.2a–3.6, the reused initial pane retains the global `history-limit` inherited at session creation. Later panes use the configured session value. For uniform history on these versions, configure the server's global value before applying the workspace. Apply does not change global options or replace the initial pane. On tmux 3.7+, setting the option also updates existing grids. Shell commands are sent as literal text followed by Enter. Both insertion and Enter are dispatch effects: embedded newlines can execute during text insertion. Success proves tmux accepted the dispatch, not that a shell command finished or succeeded. Shell commands can leave effects beyond tmux. An `ApplyError` exposes an immutable `result`: completed step IDs, positively identified `created_refs`, observed or dispatch-only effects, failed action, uncertainty and cleanup diagnostics. Diagnostics omit command payloads and paths. Lost creation replies stay uncertain; names are never used to guess ownership. A caller may pass `compensate: true` to kill only the positively returned new session on failure, after an atomic tmux guard proves that every current window and pane has a positively identified created reference. Unknown initial entities or borrowed entities moved into that session cause refusal. Cleanup uses a separate 0.5-second budget. Compensation status is explicit and cannot undo shell effects. The default preserves partial state for inspection. Applying or compensating does not close the supplied server binding. ## Command-line interface The gem installs `libtmux-workspace`. `validate` and offline `plan` do not contact tmux. Without a filename, discovery requires exactly one `.tmuxp.yaml`, `.tmuxp.yml` or `.tmuxp.json` in the current directory. Check the installed gem version without reading a configuration or contacting tmux: ```console $ libtmux-workspace --version ``` ```console $ libtmux-workspace validate workspace.yaml ``` Human plans list ordered operations and their effects. `--json` prints the same plan data as `Plan#to_h`, including configured command text and paths. ```console $ libtmux-workspace plan \ --json \ workspace.yaml ``` `load` and `plan --live` require `--socket` for an existing server. This detached creation example uses an explicitly supplied `TMUX_SOCKET` value: ```console $ libtmux-workspace load \ --socket "$TMUX_SOCKET" \ --json \ workspace.yaml ``` `--timeout` bounds each apply, live capture or subsequent switch operation and defaults to 5 seconds. `--compensate` enables guarded cleanup after apply failure. Environment expansion requires both `--expand-environment` and explicit `--env NAME=VALUE` arguments; ambient environment variables are not copied into that mapping. `load --attach` opens the CLI's `/dev/tty` after creation and runs an owned terminal client until the user detaches. It requires a valid `TERM`. Failure to open or attach the terminal retains the successful apply ledger and returns status 3. `load --switch CLIENT` switches the explicit current tmux client selector to the created session after apply, preserving the session's environment. It accepts a current client name, full TTY path or TTY path without `/dev/`; native first-match behavior applies. A missing client fails without fallback and retains the created session and ledger with status 3. Missing or invalid selector arguments fail before creation. Use `--switch=VALUE` for a selector beginning with `-`. The selector is resolved at dispatch; a reconnect matching it is eligible. It is not a captured client reference or proof of terminal ownership. Attach and switch are mutually exclusive. Neither operation infers a latest client, and library `Plan#apply` performs neither operation. | Exit status | Meaning | | --- | --- | | 0 | Validation, planning or apply succeeded; requested attach/switch succeeded | | 1 | Execution failed before known application effects | | 2 | Configuration or arguments are invalid | | 3 | Application was partial or uncertain, or a later attach/switch/cleanup failed | | 130 | Interrupted; available effect ledger is retained | JSON mode writes one result or error object to stdout. Apply errors include `ApplyResult#to_h`; human errors write the diagnostic and any ledger to stderr. Diagnostics omit configuration payloads. Explicit plan rendering and canonical configuration export contain those values by design.