# libtmux for Erlang
> The Erlang library (libtmux_erlang). These pages include its guides, tested examples and API documentation.
- [Erlang API reference](https://libtmux.org/en/erlang/latest/reference/): every public symbol, generated from the source. Hosted on libtmux.org.
---
# Concepts
Source: https://libtmux.org/en/erlang/latest/concepts/
> Concepts for libtmux_erlang.
Understand captured queries, object identities, and operations.
- [Architecture decisions](https://libtmux.org/en/erlang/latest/concepts/architecture/): Read about adr 0001: immutable values and owned tmux execution.
- [Batched graph capture](https://libtmux.org/en/erlang/latest/concepts/capture-strategy/): Read about adr 0002: batch complete-graph field hydration.
- [Object graph](https://libtmux.org/en/erlang/latest/concepts/object-graph/): Read about query a shared window without losing its memberships.
- [Selections and queries](https://libtmux.org/en/erlang/latest/concepts/queries/): Read about capture locally; fetch explicitly.
---
# Architecture decisions
Source: https://libtmux.org/en/erlang/latest/concepts/architecture/
> Architecture decisions: libtmux_erlang documentation.
## Status
Accepted architecture, 2026-11-08. Implementation and delivery remain partial.
This record describes the design selected for the
[recorded source baseline](https://github.com/libtmux/libtmux-erlang/tree/9a1ca749f7bf6f082c910c99f961bb586781ab0b). It does not qualify unfinished APIs or
announce a release. [Support and gaps](https://libtmux.org/en/erlang/latest/guides/compatibility/) owns the current
implementation and platform inventory. A later decision that changes these
boundaries should identify which part of this record it supersedes.
[ADR 0002](https://libtmux.org/en/erlang/latest/concepts/capture-strategy/) supersedes AD-4's field hydration
strategy with escaped entity scans and an explicit isolated option.
Upstream code references use release tags. This repository has no release tag
at this baseline; implementation links pin the recorded source commit. The
decisions below distinguish that implementation from required follow-up work.
## Context
tmux owns mutable sessions, physical windows, membership links and panes in a
separate server. Several sessions can link the same window, and one session
can contain several links to it. tmux's [pane-list traversal](https://github.com/tmux/tmux/blob/3.3/cmd-list-panes.c#L70-L84)
walks those links. Treating a window as a session-owned object would lose that
structure and could count one physical pane several times.
Observing the graph, acting on it and managing a client process have different
lifetimes. A captured pane can disappear before an action. A timed-out waiter
can leave a command running. A client can retire after tmux has already made
an irreversible change. The API must preserve those distinctions while fitting
ordinary Erlang programs.
The core targets the single current stable OTP baseline and tmux 3.3a–3.8 on
Linux, including intermediate releases. It does not carry older-OTP
compatibility branches. The [project policy](https://libtmux.org/en/erlang/latest/topics/project-policy/) defines the
runtime dependency and application boundaries.
## Decision drivers
- Preserve tmux identity, linked-window membership and observed coverage.
- Give callers predictable errors, ownership and cleanup evidence.
- Bound native I/O, queued operations and retained results.
- Make source-call cost and observation intervals inspectable.
- Keep ordinary Erlang use concise and downstream installation reproducible.
- Review public record compatibility independently from OTP upgrades.
## Architectural decisions
### AD-1: Use native Erlang values and explicit module functions
Use exported OTP native records for session, window, window-link and pane rows.
Keep selections, queries, filters and operation handles opaque. Criteria and
options use maps; text and terminal bytes use binaries; ordinary lists remain
the exchange format for collections.
[Native records in OTP 29](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/ref_man_native_records.md#L31) provide module-owned definitions and
record export/import without distributing a header. Their experimental status
is an explicit dependency: assess record construction, consumers and upgrades
before accepting another OTP line. Do not serialize record internals as a
public format. [Constructors](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_model.erl#L100) and action boundaries validate values;
typespecs and nominal IDs assist tooling without enforcing runtime invariants.
Exported record definitions are a versioned library contract. A value retains
the definition used when it was constructed; adding a field does not add that
field to retained values. Each release must state whether older rows and
snapshots remain usable, require conversion, or must be discarded and freshly
observed. Field renames, defaults and model-version changes need the same
review. A documented consumer restart is an acceptable initial upgrade policy.
Experimental status permits future incompatibilities; it does not establish
that every future OTP release or code upgrade will break these values.
Use [to_list/1](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_select.erl#L159) and [fold/3](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_select.erl#L165) at library boundaries.
The opaque selection itself is not an Erlang list. Arbitrary transformations
return ordinary values rather than preserving an entity selection after its
rows have changed type.
Keeping a selection retains its snapshot context. Store the extracted rows
when relationship evaluation is finished. A future compact selection must
declare its retained relationship closure and coverage; dropping related rows
cannot turn an incomplete relation into an empty complete one. Measure that
need before adding another extraction alias or pruning API.
Companion APIs provide behavioral references, with their own language
interfaces: Python's [QueryList](https://github.com/tmux-python/libtmux/blob/v0.62.0/src/libtmux/_internal/query_list.py#L327) extends a list, Elixir's
[Selection](https://github.com/libtmux/libtmux-elixir/blob/v0.1.1/lib/libtmux/selection.ex#L1-L13) implements Enumerable, and the OCaml core
[Map.S interface](https://github.com/ocaml/ocaml/blob/5.5.2/stdlib/map.mli#L65) describes an abstract container. Erlang uses
module functions and pattern matching. None of those companion runtimes is a
core dependency.
### AD-2: Separate physical identity, membership and observation
[tmux_model](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_model.erl#L4) distinguishes Session, Window, WindowLink and Pane.
A physical window has no universal session or session-relative index.
A window link records the observed session, index and expected physical
window; it is not a stable tmux link ID. Session window traversal deduplicates
physical windows while preserving every membership in window-link traversal.
[Snapshots](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_snapshot.erl#L3) retain immutable indexes, coverage, source calls, capture
intervals and endpoint/server generation. Server-wide physical entities use
numeric ID order. Link and pane traversal use their documented observed
indexes. Filtering preserves that order instead of exposing map iteration.
Coverage distinguishes a known value, known absence, not-loaded data and an
unavailable capability. An unloaded relationship cannot become an empty list.
[require/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_snapshot.erl#L187) checks the fields and relationships needed by an
evaluation, including when its selection is empty.
Constructed rows are detached. [bind/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_server.erl#L267) performs an explicit fresh
observation before registering an action-capable row. Actions validate the
registered origin, generation and expected membership. A vanished target does
not permit fallback to a positional target.
At the recorded baseline, the [origin registry](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_server.erl#L282) identifies an endpoint and
generation, while [target resolution](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_target.erl#L9) takes the ID from the supplied row.
Exported records permit construction and updates, including that ID and their
provenance fields. The origin therefore does not authenticate an unchanged
observation. The selected correction binds the captured target in a private,
unexported native record carried by the public row. Actions compare its entity,
origin, snapshot ID and target fields with the supplied row before dispatch.
Edited identities require fresh [`tmux_server:bind/2`]() observation. The private
value retains identifiers rather than a graph or a per-row registry entry.
Edited-row regressions cover IDs, origins, snapshot IDs and link memberships.
The [model upgrade policy](https://libtmux.org/en/erlang/latest/guides/compatibility/#retained-rows-and-upgrades) requires
discarding version-1 values when adopting this model version 2 representation.
These checks provide library provenance, not authorization against trusted
code in the Erlang VM or callers of internal construction functions.
### AD-3: Keep selections, portable filters and source queries distinct
| Value | Meaning | Execution boundary |
| --- | --- | --- |
| Selection | Ordered rows from one captured graph | Local reads and transformations |
| Filter | Validated, versioned predicate data | Preparation against explicit snapshot coverage |
| Query | A future source observation and its plan | Explicit fetch |
[where/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_select.erl#L107) accepts declarative criteria or a portable filter.
[filter/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_select.erl#L132) accepts a unary Boolean function; callback failures
propagate. A trusted matcher behaviour prepares local executable code. A
serialized filter cannot select a module or carry a closure.
Criteria use one map grammar with field-specific operators, Boolean
composition and explicit relationship quantifiers. A single existential child
must satisfy all conditions inside that child. Separate existential clauses
may match different children and must not be merged during normalization.
Complete coverage is required for relationship counts, every and none.
[Portable filters](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_filter.erl#L3) contain stable schema identifiers and normalized
values. [Prepared evaluation](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_filter.erl#L169) may cache membership sets, compiled
patterns and relation indexes, but remains bound to the captured context.
Checked cardinality distinguishes absence from ambiguity and stops after a
second match establishes the latter.
Composition should depend on the normalized expression, not the sequence of
query-builder calls. Compose validated filters internally and flatten adjacent
Boolean conjunctions before applying normalized limits. Preserve bounds during
external parsing and preserve separate existential nodes. The recorded
implementation still nests conjunctions through wire conversion; chained and
flat construction need an equivalent-semantics and resource-accounting test.
[OTP JSON](https://github.com/erlang/otp/blob/OTP-29.1.2/lib/stdlib/src/json.erl#L880) owns parsing and encoding. The [wire boundary](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_json.erl#L5)
adds document and nesting limits, duplicate-key and trailing-input rejection,
exact-number bounds and explicit field mappings. Payload keys never create
atoms. Shared regex support uses the tested restricted dialect; full local
regex behavior belongs in trusted callbacks, without an interoperability claim.
### AD-4: Make observation and optimization explicit
[snapshot/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_server.erl#L245) and [fetch/2](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_query.erl#L212) perform source I/O.
Query construction, explanation and local selection reads do not refresh or
hydrate data implicitly.
Capture brackets hydration with identity/scalar scans and rejects detected
disagreements. It remains an interval of observations. A change followed by a
reversal can escape detection; immutable storage does not make capture atomic.
Stable IDs and observed process identity do not make later actions atomic.
The current `fetch/2` permits one opt-in inconsistent-capture retry within the
original deadline; `snapshot/2` has no retry option. Changing that default or
accepting disagreements in volatile fields changes the observation contract.
Measure churn before making that decision. `max_rows` bounds each raw scan and
the aggregate session/window/pane observations in each spine; repeated linked
memberships count before physical deduplication.
Native tmux filtering is a candidate optimization. For the same observed state,
the full predicate must imply the candidate predicate. The complete residual
still runs locally, required relationships remain complete, and limits apply
after residual filtering and identity handling. Unknown translations broaden
the candidate; negation requires an exact child translation.
Numeric formats need their own conversion and range qualification. Erlang term
ordering and tmux string comparison must not stand in for schema numeric
semantics. [explain/1](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_query.erl#L172) exposes the plan and its temporal limits.
Disabling pushdown avoids an earlier candidate exclusion, while retaining the
capture interval. Later filtering cannot recover a row excluded earlier.
#### Capture cost and optimization gate
The [capture sequence](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_observe.erl#L29) hydrates the complete graph even when a
candidate filter is present. With the recorded field schema, a successful
nonempty capture performs `6 + S + 2W + 3P + C` logical data commands: two
three-command spine scans, one text field per session, two per physical window,
three text/byte fields per physical pane, and an optional candidate scan `C` of
zero or one.
Each successful command also performs an identity preflight. Three sessions,
nine windows and 27 panes therefore require 108 logical commands and 216 tmux
client launches without a candidate scan. Startup, retries and failures are
outside this calculation; it is not a latency measurement.
Complete-graph queries default to pushdown disabled. Enabling it can reduce
local residual work, but does not reduce hydration and adds an earlier
exclusion point. `explain/1` and the query guide report this choice. Equivalent
workload measurements are required before changing the default.
Measure the Erlang/helper path on small, linked, changing and 200-pane graphs.
Record commands, client launches, elapsed distributions, bytes, retained memory,
capture failures and cleanup. Require identical successful payloads, IDs,
ordering and attribution before comparing one-shot and control timings. A
literal newline inside a line-framed control command can change the request;
a successful reply guard alone cannot validate a benchmark's work.
Compare qualified persistent control, lossless batching and an in-server
guard before changing the source strategy. tmux's [command queue](https://github.com/tmux/tmux/blob/3.3/cmd-queue.c#L719-L790)
can wait for external work, so chaining commands does not establish general
snapshot atomicity. [if-shell -F](https://github.com/tmux/tmux/blob/3.3/cmd-if-shell.c#L73-L103)
provides a candidate synchronous guard, subject to identity precision, alias,
hook, membership, parsing and concurrent-client qualification. Qualify text
framing per field and tmux version; paths cannot inherit a title codec merely
because one release sanitizes titles.
Candidate-scoped hydration is a later strategy if measurements justify its
coverage complexity. A subset of matching children must never be represented
as the complete relationship. Candidate implication tests must execute the
translated predicate through the CI endpoints, tmux 3.3a and 3.8.
### AD-5: Supervise resource owners and keep domain rows as values
The endpoint, transport and owned-daemon lifecycle require supervised
processes. Sessions, panes, snapshots and selections remain values; there is
no process for every domain row.
[start_owned/1](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_server.erl#L118) creates a private server namespace and returns
a separate ownership token. Borrowed endpoint close retires library clients
while preserving the borrowed daemon. Stopping an owned daemon requires its
ownership token and cleanup proof. No operation gains permission to stop a
default or borrowed server from a socket path alone.
The initiating process owns an [operation](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_operation.erl#L3). Ownership transfer
registers the new owner before acknowledging the old owner. Submission is
non-blocking with respect to command completion. Waiting, command deadlines,
cancellation, remote outcome and native cleanup are separate contracts.
[OTP response abandonment](https://github.com/erlang/otp/blob/OTP-29.1.2/lib/stdlib/src/gen_server.erl#L1524) does not undo remote effects.
Cancellation after dispatch can leave the tmux outcome unknown; an uncertain
mutation must not be retried automatically.
A transfer timeout can also leave an uncertain ownership outcome: abandoning
the response does not retract the queued owner change. Callers reconcile the
current owner through [tmux_operation:status/1](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_operation.erl#L104) before relying on ownership. A delayed-endpoint
regression must cover the late transfer and owner-death ordering.
Keep an executable transition model for cancellation before dispatch, partial
writes, terminal responses and late retirement. Duplicate or stale events must
release admission once; stronger cleanup evidence cannot invent a known remote
outcome. Adopt a different OTP behaviour or supervisor layout only when it
improves these invariants or a measured bottleneck.
### AD-6: Use a separate bounded Linux helper for native I/O
The OTP transport launches the [bundled C11 helper](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_transport.erl#L324) with
spawn_executable. It sends the child's executable and argument vector through
the helper's framed protocol. The helper runs outside the VM and uses Linux
process and pipe interfaces. It is built from packaged source; there is no
runtime executable download.
The [private protocol](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/apps/libtmux_erlang/c_src/README.md) separates stdout and stderr, grants output read
credits, bounds frames and queues, acknowledges input writes, and distinguishes
terminal delivery from observed helper exit. The helper owns its child tree
and retains incomplete cleanup as an explicit failure. A successful tmux
reply, pipe write or helper exit alone cannot prove every later aftereffect.
[Descendant retirement](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/c_src/owned_io.c#L291) reads `/proc/self/task//children`; the Linux kernel
must expose that interface through [CONFIG_PROC_CHILDREN](https://man7.org/linux/man-pages/man5/proc_tid_children.5.html). A mounted procfs
alone is insufficient. The recorded helper first attempts the interface during
cleanup and ignores open failure; remaining descendants can then produce
`cleanup_incomplete`. Required follow-up is admission before child launch, with a
specific capability error and underlying OS error, plus a negative test that
proves no child started. Missing support must not silently select a weaker
cleanup contract.
The helper is reused from the [tagged Elixir source](https://github.com/libtmux/libtmux-elixir/blob/v0.1.1/c_src/owned_io.c#L380), with
[provenance and license records](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/apps/libtmux_erlang/c_src/provenance.json). Its source identity does
not transfer Elixir runtime qualification to Erlang. Separate processes also
do not provide a sandbox: inherited authority, hostile forks, helper SIGKILL
and concurrently replaced filesystem entries retain documented limits.
Carry helper fixes with their source/protocol provenance and qualify each port
independently. Focused frame fuzzing, sanitizer checks and installed source
consumers complement lifecycle tests; matching the upstream digest is not a
substitute for them.
Executable argv, tmux command syntax, formats and explicit shell-command
arguments are separate interpreter boundaries. Avoiding an implicit host
shell does not suppress tmux aliases, hooks or terminal-program behavior.
### AD-7: Qualify persistent control separately from one-shot execution
[attach/3](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang/src/tmux_control.erl#L55) currently establishes explicit control-client lifetime
and identity on that persistent connection. Attachment changes attached_count.
The selected no-output/ignore-size and environment-update policies remain
visible. Existing hooks and destruction policies can still produce effects;
closing a client is not a rollback.
The [control guide](https://libtmux.org/en/erlang/latest/topics/control/) limits current qualification to attachment.
Command submission and groups need separate framing, ordering, eligibility,
certainty and cleanup evidence. tmux's [reply guards](https://github.com/tmux/tmux/blob/3.8/control.c#L523-L543) coexist with
asynchronous notifications. Its [alias-expansion path](https://github.com/tmux/tmux/blob/3.3/cmd-parse.y#L744-L779) means a
canonical command name alone cannot establish what configured tmux will do.
The ambient-alias contract also applies to the existing attachment command and
its identity probe. Qualify those paths against configured aliases while
preserving borrowed-server configuration. Record the library's attached client
where possible; retain the raw attached count and hook effects. Any adjusted
view needs a separately named contract rather than silently subtracting the
observer.
Public output subscriptions and sampled observation streams are also unfinished.
The helper's internal credit protocol does not establish a public streaming
API. Any such API must define credits, gaps, acknowledgement and retirement,
and describe sampled observations without promising unobserved terminal history.
### AD-8: Keep workspace execution downstream and MCP excluded
Use a Rebar3 umbrella to develop independent OTP applications together.
libtmux_erlang owns the core; libtmux_erlang_workspace consumes its public API.
The core has no dependency on the workspace application. Each package owns
metadata, license and changelog material, and needs an installed-consumer check
outside the umbrella before independent delivery.
The [workspace loader](https://github.com/libtmux/libtmux-erlang/blob/9a1ca749f7bf6f082c910c99f961bb586781ab0b/apps/libtmux_erlang_workspace/src/tmux_workspace.erl#L71) validates versioned JSON into
inert desired state. The later
[planner](https://github.com/libtmux/libtmux-erlang/blob/2e681477ed015e61f03c393ded111c2d5e6dd665/apps/libtmux_erlang_workspace/src/tmux_workspace_plan.erl#L34)
stores canonical operations and observed preconditions; the
[apply engine](https://github.com/libtmux/libtmux-erlang/blob/2e681477ed015e61f03c393ded111c2d5e6dd665/apps/libtmux_erlang_workspace/src/tmux_workspace_apply.erl#L40)
checks their derivation against the stored observation and hash-checked input.
Fresh tmux state can reject a plan without supplying replacement operations.
Ownership tags bind resource keys, parent manifests, server incarnation and
launch hashes. Explicit adoption establishes ownership; changed launch intent
requires respawning. Ordered windows/panes use stable IDs for explicit swaps.
Pane creation uses the saved, unzoomed tiled layout to schedule fixed targets
and discard exhausted split cells. New windows use a breadth-first schedule
until their geometry is observable. Axis selection remains an explicit stored
policy, evaluated at each checked step. Custom tmux options and available area
can reject this geometric schedule; it is not a complete packing algorithm.
Apply recaptures the complete projected graph around each step, retaining a
verified prefix on failure. Source work therefore grows with both graph size
and step count. This initial strategy favors explicit drift evidence; measured
large-workspace performance remains a qualification gate. No atomic reservation
or automatic rollback is claimed. The standalone Rebar3 escript materializes
its bundled helper, license and provenance in a private runtime directory;
BEAM code stays in the archive. SIGTERM follows explicit cancellation, while
Ctrl-C receipt semantics remain unqualified. See the
[workspace guide](https://libtmux.org/en/erlang/latest/guides/workspace-json/) for current contracts and executable examples.
Ownership metadata reads use bounded batches of three workers. A private
coordinator monitors the caller and joins its workers; the caller gates each
new batch after checking cancellation. Results retain row order and the worst
cleanup evidence across the drained batch. Worker exit alone cannot prove
native retirement. [ADR 0003](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/docs/adr-0003-scoped-option-batches.md) further groups
up to seven local scalar reads per native sequence. Every graph and ownership
read remains; complete apply cost still grows with graph size and step count.
Endpoint names need host policy; loading shell text does not authorize its
execution. Shell execution needs both host policy and a recorded plan opt-in.
File loading and path resolution are synchronous trusted-filesystem
operations without an I/O deadline or atomic filesystem-snapshot guarantee.
Erlang runtime dependencies stay within OTP, alongside the recorded external
helper exception. Build, test and documentation dependencies have separate
roles. An additional runtime dependency needs a concrete capability requirement
and a new recorded decision. MCP packages, servers and SDK integration are
excluded by [project policy](https://libtmux.org/en/erlang/latest/topics/project-policy/); they are not pending packages.
### AD-9: Keep contracts and verification native to Erlang
Keep specs, exported type/callback documentation and module/function docs beside
their definitions. Native record exports and compiler documentation support
API browsers. [ct_doctest](https://github.com/erlang/otp/blob/OTP-29.1.2/lib/common_test/src/ct_doctest.erl#L24) executes Erlang-shell examples;
reader programs exercise real tmux through owned fixtures and installed core
packages. Generated field/operator documentation shares the validation schema.
Keep failure examples useful when both an operation and cleanup fail: retain
both outcomes instead of masking the original exception in an `after` clause.
Validate more precise query/constructor/error specs through an external
Dialyzer consumer with deliberate entity mixups. Generated recursive filters
and linked graphs should agree with a small reference evaluator.
Use bounded [OTP Logger reports](https://libtmux.org/en/erlang/latest/topics/diagnostics/) for transport failures,
cleanup, IPC duration, capture rejection and held read credit. The implemented
one-shot and attachment lanes emit these reports; later lanes must preserve
their data and cost boundaries. Give structured programming errors
useful native error formatting. Keep payloads and environment secrets out of
ordinary diagnostics, and keep optional telemetry outside core dependencies.
EUnit, Common Test, properties, Dialyzer, documentation and installed-consumer
checks cover different contracts. Preserve failed or skipped evidence instead
of treating it as a passing subset. Exact source/tool/platform identities and
individual matrix conclusions determine qualification. Continuous CI uses the
latest stable Erlang/OTP release and the oldest supported and newest stable
tmux releases on both Linux architectures. Support includes the releases
between those endpoints; earlier results retain their recorded scope.
Test-loop durations do not establish throughput or a control-versus-streaming
performance claim.
Source installation is a separate gate from consuming a compiled `ebin`/`priv`
archive. Build the exact source package outside the umbrella without a prebuilt
helper or checkout-relative resources, on each supported architecture. This
gate remains open at the recorded baseline.
## Alternatives considered
| Alternative | Decision and consequence |
| --- | --- |
| Map-only domain rows | Native records provide named shapes and downstream patterns; maps remain appropriate for criteria and options. Runtime validation is still required. |
| A general collection or ORM framework | Keep the entity-aware operations small and exchange ordinary lists. Arbitrary transformations leave the selection type. |
| Mutable shared storage as the snapshot contract | Use immutable captured indexes. An internal cache or optional adapter must preserve observation identity and coverage. |
| Direct ports without the helper | A replacement must prove separate bounded output, input accounting and owned-tree retirement. The selected helper exposes those contracts explicitly. |
| A native extension inside the VM | Keep the transport helper outside the VM, accepting Linux build and IPC costs. |
| Transparent lazy relationship reads | Reject incomplete local coverage. Arrange hydration only during explicit source execution. |
| One application for core and consumers | Separate OTP applications preserve dependency direction and expose installed-package integration failures. |
| QLC, graph or ETS as the public predicate language | Defer adapters; portable filters remain data and local callers retain native Erlang tools. |
## Consequences
The public API follows Erlang conventions and supports local reuse without
transport calls. Captured context makes relationship filtering predictable,
but retaining a selection can retain its complete snapshot graph. Callers that
only need rows should exchange lists.
Native records require deliberate upgrade qualification. Linux helper builds
require a C11 compiler, make, libc and the documented Linux interfaces. Platform
support follows the tested matrix rather than the availability of an Erlang VM.
Resource ownership and bounded protocols make failure evidence explicit. They
do not make multi-command tmux actions transactional, turn socket access into
confinement, or bound arbitrary application callbacks. Observed identity checks
and filesystem cleanup retain concurrency races stated in their contracts.
At the recorded baseline, the core graph/query/one-shot path and control
attachment exist; workspace JSON loading exists; control submission, groups,
public streams, workspace plan/apply/CLI and complete delivery remain open.
Subsequent workspace planning/apply and CLI work follows AD-8; the baseline
statement above remains the historical review scope. Control submission,
groups, public streams, full adoption and complete delivery remain open.
Registry and shared-site publication require their own verified delivery steps.
Consult [support and gaps](https://libtmux.org/en/erlang/latest/guides/compatibility/) before selecting an execution lane.
## When to reconsider
Revisit the native-record choice when another OTP line changes representation,
construction or upgrade behavior. Revisit the helper boundary when another
platform or transport can prove the same bounded-I/O and retirement contracts.
Revisit observation guarantees if tmux supplies a server-side atomic capture
or compare-and-act primitive. Qualify control and streams before changing the
default execution lane; compare equal workloads, errors and cleanup outcomes.
Revisit caching or advanced query adapters when measured workloads justify
their additional state and lifecycle costs.
Record a separate consumer decision if MCP is brought back into scope or if a
new runtime dependency is proposed. Do not infer either change from artwork,
an empty package directory or an implementation plan.
## References
The linked upstream code is pinned to OTP 29.1.2, tmux 3.3/3.8, Python libtmux
v0.62.0, Elixir libtmux v0.1.1 and OCaml 5.5.2. Companion references explain
interfaces or source provenance, without adding runtime dependencies.
---
# Batched graph capture
Source: https://libtmux.org/en/erlang/latest/concepts/capture-strategy/
> Batched graph capture: libtmux_erlang documentation.
## Status
Accepted architecture. Implementation qualification remains separate from
release and full-product adoption.
This supersedes the capture execution strategy in
[ADR 0001, AD-4](https://libtmux.org/en/erlang/latest/concepts/architecture/#ad-4-make-observation-and-optimization-explicit).
The graph model, interval semantics, identity checks, coverage, limits and
explicit execution boundary remain the governing contracts.
## Context
The original one-shot capture reads each text or byte field separately. A
successful nonempty capture needs `6 + S + 2W + 3P + C` logical commands for
sessions, physical windows, physical panes and an optional candidate scan.
Each successful command also starts an identity-preflight client. Complete
hydration dominates the cost even when a query returns one pane.
A delimiter alone cannot frame paths containing tabs or newlines. A separate
length expression is also insufficient: tmux's
[format lookup](https://github.com/tmux/tmux/blob/3.3a/format.c#L3432)
invokes built-in callbacks on each lookup, and
[pane_current_path](https://github.com/tmux/tmux/blob/3.3a/format.c#L844)
reads an external process. Length and value could therefore describe different
observations. The field must be observed once before framing.
## Decision
Hydrate each nonempty entity collection with one bounded list command.
The schema explicitly admits each current text/byte field to
`escaped_columns_v1`. A field without that admission retains isolated reads.
Each field expansion applies three literal
[substitutions](https://github.com/tmux/tmux/blob/3.3a/format.c#L4513) to one
value: escape backslashes, then replace tabs and newlines with their textual
escapes. Only then add column tabs and row newlines. No payload is expanded
again as a format, command or shell program. List commands use ordinary
format expansion, preserving literal percent sequences.
The decoder requires the final newline, exact column counts, valid escapes
and bounded raw row counts. It reverses the selected tmux print layer before
field validation, including the older VIS and dollar encodings. Native
qualification covers every non-NUL byte, empty values and protocol-looking
text; NUL is outside tmux's string representation. Text fields retain schema
UTF-8 validation, while paths remain bytes.
Validate every hydrated identity against the initial spine. Repeated physical
rows from linked memberships must agree before deduplication. Missing or new
identities reject the capture; conflicting repeated values reject it too.
The final identity/scalar spine must still agree with the first. Multiple
commands and external process reads retain interval semantics, including the
possibility of undetected change and reversal.
Default snapshot and query hydration is `batched`. The existing options maps
also accept `hydration => isolated` for one field per physical row.
[Query plans](https://libtmux.org/en/erlang/latest/concepts/queries/#source-plans) describe both command templates;
[snapshot metadata](https://libtmux.org/en/erlang/latest/guides/api-documentation/) records the requested strategy and actual calls.
There is no automatic fallback after an overflowing batch.
The current successful nonempty batched capture needs `9 + C` logical commands
and `18 + 2C` tmux clients: six spine scans, three hydration scans, and the
optional candidate scan. Empty collections skip their hydration command.
Unqualified schema fields can increase this cost through isolated fallback.
Native candidate filtering remains opt-in and still hydrates the complete
graph. Limits apply after full residual evaluation and identity handling.
## Resource tradeoffs
The command stdout bound remains 1 MiB, and the capture total remains 8 MiB by
default. A batch combines entity output under that command bound. Callers with
larger entity output can explicitly choose isolated hydration, distributing it
across commands under the same total and deadline. Output overflow remains an
explicit error with cleanup evidence; the library neither enlarges bounds nor
silently retries the observation. See the native capture options in the
[API guide](https://libtmux.org/en/erlang/latest/guides/api-documentation/).
The native limit regression uses 37 panes with 30,000-byte titles. Batched
capture returns `output_limit` with its bounded prefix and completed cleanup;
an explicitly requested isolated capture returns every full title.
Linked windows can increase raw batch output because the list traversal
repeats memberships. Row bounds count those observations before physical
deduplication. Fewer commands do not imply fewer transferred bytes.
The decoder rebuilds field binaries rather than retaining slices of the batch
frame. A snapshot-backed selection still retains the complete captured graph.
After relationship evaluation, callers can keep extracted rows and release
their other snapshot and selection references. Extraction preserves field
values, including any backing allocations shared by caller-created rows; see
the selection contracts in the [API guide](https://libtmux.org/en/erlang/latest/guides/api-documentation/).
## Qualification and measurements
The linked-graph regression first returned 21 logical commands, then returned
nine with equal row maps under both strategies. The real-path regression
retains tabs, newlines, carriage returns, format syntax, backslashes and a
non-UTF-8 byte. Native framing tests run at the CI endpoints, tmux 3.3a and
3.8; focused local probes also checked the distinct older print encodings.
Support remains continuous through the intermediate releases.
A controlled Linux x86_64 experiment used OTP 29.1.2, tmux 3.8, the real
Erlang/helper path and cooperating pane programs with per-process readiness.
Both strategies observed the same owned graph, and all successful full row
maps had equal hashes. Three repetitions alternated strategy order after one
default-deadline attempt per strategy. The figures below are capture medians;
they exclude fixture setup and are observations on that host, not latency
promises.
| Graph | Batched commands | Isolated commands | Batched median | Isolated median |
| --- | ---: | ---: | ---: | ---: |
| 1 session, 1 window, 1 pane | 9 | 12 | 0.065 s | 0.084 s |
| 1 session, 5 windows, 20 panes | 9 | 77 | 0.066 s | 0.556 s |
| 2 sessions, 9 linked windows, 27 panes | 9 | 107 | 0.072 s | 0.858 s |
| 2 sessions, 20 linked windows, 200 panes | 9 | 648 | 0.125 s | 6.772 s |
The isolated 200-pane default attempt reached its deadline. Its median uses
three successful repetitions with an explicit 60-second deadline; the other
cells each have four successful observations. All owned helpers, servers and
pane roots retired, and their namespaces were removed. The complete sweep,
including fresh source builds and fixture setup, took about 40 seconds.
These measurements qualify this one-shot strategy choice. They establish no
control-mode speedup, churn rate or general workload distribution.
## Alternatives and reconsideration
Persistent control submission remains a separate qualification task with
alias, framing, attribution and lifecycle obligations. A single command batch
does not establish a general atomic snapshot. In-server guards also retain
their identity and configuration qualification requirements.
Candidate-scoped hydration would reduce complete coverage and needs a distinct
contract before adoption. Reconsider this strategy if measured transferred
bytes, regex-substitution cost, large-field overflow or repeated memberships
outweigh the saved round trips. A future strategy must preserve the current
byte, coverage, identity and cleanup contracts or name its weaker guarantees.
---
# Object graph
Source: https://libtmux.org/en/erlang/latest/concepts/object-graph/
> Object graph: libtmux_erlang documentation.
This program creates two sessions, a shared two-pane window, and two extra
window links. It then checks the physical object graph and session-relative
membership counts. [`tmux_window_link`]() acts on a session/index membership and
checks its expected physical window before dispatch.
Selecting a window membership changes that session's current window.
[`tmux_session:select/2`]() switches one explicitly named attached client to a
session. It never attaches a client or chooses a current client implicitly.
Its [`tmux_session:select/3`]() variant updates the destination environment only
when requested. Client names can be reused after disconnection; this call
does not provide an atomic client-incarnation check. The native API contract
documents name lookup and environment-removal semantics.
Run it on a private server after compiling:
```console
$ mise exec -- escript tools/run_examples.escript graph
```
```erlang
-module(linked_graph).
-moduledoc "Distinguish physical windows from their session memberships.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
Endpoint = #{socket_path => Socket, executable => Executable},
{ok, Server} = tmux_server:open(Endpoint),
tmux_scope:run_or_error(fun() ->
{ok, Work} = tmux_session:create(Server,
#{name => <<"work">>, launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
{ok, Mirror} = tmux_session:create(Server,
#{name => <<"mirror">>, launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
{ok, Shared} = tmux_window:create(Work,
#{name => <<"shared">>,
launch => #{argv => [<<"/bin/cat">>]}}),
Launch = #{launch => #{argv => [<<"/bin/cat">>]}},
{ok, _} = tmux_pane:split(Shared, Launch),
WorkId = tmux_model:get(id, Work),
WindowId = tmux_model:get(id, Shared),
{ok, Before} = tmux_server:snapshot(Server),
{ok, Source} = tmux_select:one(#{window_id => WindowId},
tmux_select:window_links(Before)),
{ok, _} = tmux_window_link:link(Source, Work, #{index => 7}),
{ok, _} = tmux_window_link:link(
Source, Mirror, #{index => 9}),
{ok, Snapshot} = tmux_server:snapshot(Server),
3 = length(tmux_snapshot:windows(Snapshot)),
4 = length(tmux_snapshot:panes(Snapshot)),
5 = length(tmux_snapshot:window_links(Snapshot)),
{ok, CapturedWork} = tmux_snapshot:get(
session, WorkId, Snapshot),
{ok, Physical} = tmux_snapshot:related(
windows, CapturedWork, Snapshot),
{ok, Memberships} = tmux_snapshot:related(
window_links, CapturedWork, Snapshot),
2 = length(Physical),
3 = length(Memberships),
Criteria = #{window_links => #{count => 3},
panes => #{every => #{current_command => <<"cat">>},
count => 2}},
{ok, Only} = tmux_select:one(
Criteria, tmux_select:windows(Snapshot)),
WindowId = tmux_model:get(id, Only),
#{physical_windows => 3, physical_panes => 4, window_links => 5,
work_windows => 2, work_memberships => 3,
shared_window_panes => 2}
end, fun() ->
{ok, _} = tmux_session:kill(Mirror),
ok
end)
end, fun() ->
{ok, _} = tmux_session:kill(Work),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
There are three physical windows and four physical panes. Five window links
remain because the shared window occurs at two indexes in `work` and once
in `mirror`. The windows relationship yields two unique windows for `work`;
its window_links relationship retains all three memberships. Both traversals
use [`tmux_snapshot:related/3`]().
A link is identified within a capture by session, index and expected physical
window. It has no stable tmux link ID. Captured index references cannot promise
atomic compare-and-act after another client moves a link.
Use [`tmux_window_link:get/3`]() for a fresh membership. `link/3` adds a membership;
`move/3` moves just the checked membership; `swap/3` exchanges two memberships'
physical windows. Their input rows must share one registered owner. Actions
return command results and leave captured values unchanged.
`select/1` selects the checked window in its session. `unlink/2` removes one
membership, retaining tmux's last-link protection by default. Explicit
`force => true` can destroy the physical window if its final membership goes.
[`tmux_window:kill/1`]() destroys that window across all links.
If a captured index disappears or names another physical window, actions
return `stale_link` before dispatch. [`tmux_server:bind/2`]() applies this same
precondition when rebinding a link to an owner. It does not adopt the new
occupant of an old index. The one-shot preflight and action are separate;
another client can still change that membership between them.
The relation query checks the full captured graph. Filtering a window selection
does not replace its panes with only matching panes. `some`, `every`, `none`
and `count` require complete membership coverage, including empty selections.
The [query guide](https://libtmux.org/en/erlang/latest/concepts/queries/) explains validation and correlation.
## Compare captured server incarnations
Live [`tmux_snapshot:metadata/1`]() includes an opaque server_instance value.
[`tmux_identity:fingerprint/1`]() derives a versioned binary comparison key from
that captured identity without I/O. The key survives reconnecting through
another endpoint handle and changes when the observed process incarnation
changes. The installed consumer checks its JSON round trip.
An endpoint's generation identifies that handle's lifetime; it changes when
the handle is reopened. A persisted incarnation key serves a different
purpose: comparing the server observed by those handles. It cannot make a
stale row actionable, establish current liveness or authorize an action.
Use [`tmux_server:bind/2`]() to obtain a fresh actionable row.
---
# Selections and queries
Source: https://libtmux.org/en/erlang/latest/concepts/queries/
> Selections and queries: libtmux_erlang documentation.
A snapshot is observed data. A selection is ordered rows plus their captured
context. A filter is portable predicate data. A query describes a future tmux
observation. Only `fetch` executes a query.
The [README program](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/examples/readme_demo.erl) shows both paths against
real tmux. It retains a captured name after renaming the live window, then
executes a fresh source query. All ordinary snapshot and selection reads stay
local, even after the endpoint closes.
## Criteria
Local criteria use atom-keyed maps and binary text. Bare scalars mean equality;
fields and operators in one map conjoin. Boolean branches use quoted `'and'`,
`'or'` and `'not'`. Empty AND is true and empty OR is false. Callbacks belong
to [`tmux_select:filter/2`]() and must return Booleans; callback failures propagate.
Relations use `some/every/none/count` for to-many values and `is/is_not` for
to-one values. One `some` containing two conditions requires the same related
row. Two separate `some` conditions can match different rows. Empty to-many
relations make `some` false and `every` and `none` true.
[Fields and operators](https://libtmux.org/en/erlang/latest/reference/fields/) come from the runtime validator. Unknown
fields, invalid operations, type mismatches and incomplete coverage fail before
row traversal. An unloaded relationship never becomes an empty relationship.
`from_list` keeps supplied order and duplicates but cannot infer relationships.
## Cardinality
| Call | Result |
| --- | --- |
| `first` | `{ok, Row}` or `none`; more matches are allowed |
| `one` | `{ok, Row}`, `{error, no_match}`, or a multiple-match error |
| `one_or_none` | `{ok, Row}`, `{ok, none}`, or a multiple-match error |
| `one_or_error` | Row directly; raises on zero or multiple matches |
| `size` | Cached materialized size |
| `count` | Current size, or exact count after supplied criteria |
Criteria come first and the selection last. Ambiguity stops after the second
match once validation is complete. `to_list/1` and `fold/3` keep ordinary Erlang
list consumers available; a selection is not a list protocol.
## Portable input
[`tmux_filter:decode_json/1`]() rejects duplicate keys, trailing input and excessive
size/depth. `from_wire/2` maps generated wire names explicitly; it never creates
atoms from payload keys. `to_wire/1` exports filter data, not closures or native
record internals. Integral numbers normalize only within the exact shared JSON
integer range. Text is case-sensitive and is not implicitly normalized.
Run this complete local recipe. It uses captured rows and needs no tmux server:
```console
$ mise exec -- escript tools/run_examples.escript filter
```
```erlang
-module(filter_demo).
-moduledoc "Validate portable JSON and filter captured rows without tmux I/O.".
-export([run/0]).
-spec run() -> map().
run() ->
{ok, Editor} = tmux_model:pane(#{id => <<"%1">>, active => true,
width => 120, current_command => <<"nvim">>}),
{ok, Shell} = tmux_model:pane(#{id => <<"%2">>, active => false,
width => 80, current_command => <<"bash">>}),
Panes = tmux_select:from_list(pane, [Editor, Shell]),
Criteria = #{active => true, 'or' => [
#{current_command => #{in => [<<"nvim">>, <<"vim">>]}},
#{width => #{gte => 160}}
]},
{ok, Filter} = tmux_filter:new(pane, Criteria),
Encoded = iolist_to_binary(json:encode(tmux_filter:to_wire(Filter))),
{ok, Document} = tmux_filter:decode_json(Encoded),
{ok, RoundTrip} = tmux_filter:from_wire(pane, Document),
{ok, Editor} = tmux_select:one(RoundTrip, Panes),
[Editor] = tmux_select:to_list(tmux_select:where(Filter, Panes)),
{error, #{code := duplicate_key}} = tmux_filter:decode_json(
<<"{\"version\":1,\"version\":2}">>),
Invalid = #{active => #{contains => <<"yes">>}},
try tmux_select:where(Invalid, Panes) of
_ -> error(expected_validation_error)
catch
error:{bad_filter, #{code := unsupported_operator} = Detail}:Stack ->
[active, contains] = maps:get(path, Detail),
Reason = {bad_filter, Detail},
Message = erl_error:format_exception(error, Reason, Stack),
Text = unicode:characters_to_binary(Message),
{_, _} = binary:match(Text, <<"Allowed:">>)
end,
#{matched_ids => [tmux_model:get(id, Editor)],
wire_round_trip => true, duplicate_keys_rejected => true}.
```
It selects `%1`, preserves the meaning of the JSON round trip, and rejects
ambiguous duplicate keys. The installed-archive check runs the same program.
The [restricted ASCII regex guide](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/tools/regex-conformance/README.md) gives
its grammar and resource limits. Non-ASCII subjects and exhausted match limits
are errors, not nonmatches. Local callbacks provide other regex semantics.
## Source plans
Queries filter the complete captured graph locally by default. Enable native
candidate exclusion explicitly with `tmux_query:pushdown(true, Query)`. It can add
a candidate scan and an earlier observation without reducing graph hydration.
[`tmux_query:explain/1`]() reports the choice in its `pushdown` field.
[`tmux_query:explain/1`]() performs no I/O. It reports command templates, native
candidate conditions, full residual data, complete hydration, required fields,
ordering, identity policy and temporal assumptions. ID, Boolean and qualified
unsigned numeric conditions can reach tmux's `-f`. Text, nullable fields and
relations remain local. Numeric comparisons use tmux arithmetic formats,
not string ordering.
Hydration defaults to one framed list scan per nonempty entity collection.
Each field is expanded once and escaped before row/column framing, preserving
paths containing tabs, newlines and non-UTF-8 bytes. A successful nonempty
capture uses nine logical commands, plus an optional candidate scan. Identity
preflights add one tmux client per command. Linked memberships can increase
the bytes returned even though physical rows remain unique.
Each command remains bounded to 1 MiB of stdout. If a whole entity exceeds
that bound, use `hydration => isolated` in snapshot or fetch options to read
one field per physical row. The total capture bound and deadline still apply.
The hydration entry of the commands metadata describes the default templates;
its isolated_hydration entry describes isolated reads and unqualified-schema
fallback. Snapshot metadata records the chosen strategy and actual calls.
[ADR 0002](https://libtmux.org/en/erlang/latest/concepts/capture-strategy/) records the framing, measurements and
tradeoffs.
A candidate is a necessary condition: it can admit extra rows but must not
exclude a same-state full-filter match. Unsupported leaves become true;
AND and OR compose conservatively; NOT translates only exact children.
Membership trees are balanced to respect tmux's recursion limit.
Fetch retains a complete captured graph, validates any candidate scalar agreement,
evaluates the full local filter, and only then applies limits. Physical entities
deduplicate by ID; window links retain membership identity. `pushdown(false, Q)`
disables earlier candidate exclusion. Capture options bound rows, output and
time; `retry => 1` requests at most one inconsistent-capture retry within the
original capture deadline. The default is no retry.
Captures span several commands and are not atomic. Local re-evaluation cannot
recover rows excluded before their state changed. Atomic consistency and a
same-connection identity promise remain unsupported by this one-shot path.
---
# tmux client library for Erlang
Source: https://libtmux.org/en/erlang/latest/core/
> tmux client library for Erlang. Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, capture pane contents
Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, capture pane contents.
## At a glance
| Type | Wraps |
| ---- | ----- |
| [`tmux_server:t/0`](https://libtmux.org/en/erlang/latest/reference/t-tmux_server-t-0/) | A tmux server |
| [`#tmux_model:session{}`](https://libtmux.org/en/erlang/latest/reference/tmux_model-session/) | A session, ID `$n` |
| [`#tmux_model:window{}`](https://libtmux.org/en/erlang/latest/reference/tmux_model-window/) | A physical tmux window; session membership uses window links, ID `@n` |
| [`#tmux_model:pane{}`](https://libtmux.org/en/erlang/latest/reference/tmux_model-pane/) | A pane in a window, ID `%n` |
## Typed API
- Typed objects instead of parsed format strings: [`#tmux_model:pane{}`](https://libtmux.org/en/erlang/latest/reference/tmux_model-pane/)
- Addressing by tmux ID: [`tmux_model:pane_id/0`](https://libtmux.org/en/erlang/latest/reference/t-tmux_model-pane_id-0/)
- Filtering and queries over tmux objects: [`tmux_filter`](https://libtmux.org/en/erlang/latest/reference/tmux_filter/)
- Typed errors instead of exit codes and stderr: [`tmux_operation:failure/0`](https://libtmux.org/en/erlang/latest/reference/t-tmux_operation-failure-0/)
## What you get over the tmux CLI
- Connect to or start a tmux server: [`tmux_server:open/1`](https://libtmux.org/en/erlang/latest/reference/tmux_server-open-1/)
- Traversal from a server down to its panes: [`tmux_snapshot:panes/1`](https://libtmux.org/en/erlang/latest/reference/tmux_snapshot-panes-1/)
- Send keys to a pane: [`tmux_pane:send_keys/2`](https://libtmux.org/en/erlang/latest/reference/tmux_pane-send_keys-2/)
- Capture pane contents: [`tmux_pane:capture/2`](https://libtmux.org/en/erlang/latest/reference/tmux_pane-capture-2/)
- Arguments passed as a list, with no shell quoting: [`tmux_command`](https://libtmux.org/en/erlang/latest/reference/tmux_command/)
- A typed error when the running tmux lacks a feature: [`tmux_capability`](https://libtmux.org/en/erlang/latest/reference/tmux_capability/)
- Isolated tmux servers for tests: [`tmux_server:start_owned/1`](https://libtmux.org/en/erlang/latest/reference/tmux_server-start_owned-1/)
- Async operations: [`tmux_operation`](https://libtmux.org/en/erlang/latest/reference/tmux_operation/)
- [Guides](https://libtmux.org/en/erlang/latest/guides/): Installation, queries and owned operations.
- [Topics](https://libtmux.org/en/erlang/latest/topics/): Object identity, command execution and compatibility.
- [Examples](https://libtmux.org/en/erlang/latest/examples/): Complete programs from the tested source checkout.
- [API reference](https://libtmux.org/en/erlang/latest/reference/): Native records, modules, functions and types.
---
# API glossary
Source: https://libtmux.org/en/erlang/latest/reference/glossary/
> Kinds, modifiers and package labels in the Erlang API reference.
## Symbol kinds
Kinds identify declarations in the Erlang API reference.
### module
An Erlang module grouping function, type and record declarations under an atom name.
Exports identify which functions, types and native records callers can use. Maps, lists and records are values passed explicitly to module functions.
References: [Erlang modules](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/modules.md).
### struct
An exported Erlang native record with a fixed set of named fields, owned by its defining module.
Session, window, window-link and pane rows use native records. Import an exported definition with -import_record(tmux_model, [pane]). Native records remain experimental in OTP 29.1.2; their internals are not a wire format.
References: [Erlang native records](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/ref_man_native_records.md).
### function
An Erlang function identified by its module, name and arity, the number of arguments.
Functions sharing a name with different arities are distinct. Multiple clauses belong to one function; patterns and guards select the clause to execute.
References: [Erlang functions](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/ref_man_functions.md).
### typealias
An Erlang type declaration written with -type, -opaque or -nominal, identified by module, name and arity.
The shared typealias badge includes nominal IDs: Dialyzer distinguishes their declared names even when each is represented by a binary. Typespecs support analysis and documentation; runtime input validation remains necessary.
References: [Erlang user-defined types](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/typespec.md#type-declarations-of-user-defined-types).
## Modifiers
Modifiers add information about calling, implementing, or accessing a declaration. Their precise effect follows the language and the signature.
### opaque
An Erlang type declared with -opaque, whose representation callers must not depend on.
Selections and queries are opaque values. Use tmux_select:to_list/1 to obtain rows for lists functions, and tmux_query:fetch/1 to execute a query. Ordinary selection reads and query construction perform no tmux I/O.
References: [Erlang opaque types](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/typespec.md#type-declarations-of-user-defined-types).
### abstract
An Erlang behaviour callback declared with -callback, whose implementation belongs to a callback module.
The declaration describes the callback contract; it does not provide a function body to call on the behaviour module.
References: [Erlang behaviour callbacks](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/reference_manual/modules.md#behaviour-module-attribute).
## Package context
Package labels identify where an API is supplied.
### package
The OTP application supplying these Erlang modules, described by an application resource file.
The core application is libtmux_erlang. Its application name and the names of its modules serve different roles; use the documented source dependency to install it.
References: [OTP applications](https://github.com/erlang/otp/blob/OTP-29.1.2/system/doc/design_principles/applications.md).
---
# Erlang workspace manager is not published
Source: https://libtmux.org/en/erlang/latest/workspace/
> The Erlang source application validates JSON desired state. Its plan and apply workflow and command-line tool are unfinished, and no package or loader command is available.
Erlang has no published libtmux workspace manager. The source application
validates JSON desired state, but its plan and apply workflow and its
command-line tool are unfinished.
There is no workspace package to install, loader command to run, or workspace
API reference yet. The current public core observes tmux, holds the object
graph as native records and sends explicit tmux operations.
Start with the [core library](https://libtmux.org/en/erlang/latest/core/). The [guides](https://libtmux.org/en/erlang/latest/guides/) cover
installation and the first connection.
---
# Examples
Source: https://libtmux.org/en/erlang/latest/examples/
> Run Erlang programs with private tmux servers and checked cleanup.
Each guide includes its complete program, setup command and expected result.
The core checks run these programs against private tmux servers and repeat
them as installed consumers. Follow the guide's cleanup contract when adapting
a program to an existing server.
- [Create and query a graph](https://libtmux.org/en/erlang/latest/guides/getting-started/): Create sessions, windows and panes, then filter captured rows.
- [Traverse a linked window](https://libtmux.org/en/erlang/latest/concepts/object-graph/): Compare physical windows with their session-relative memberships.
- [Capture and send text](https://libtmux.org/en/erlang/latest/guides/panes/): Send literal text and inspect pane output on an owned server.
- [Work with byte buffers](https://libtmux.org/en/erlang/latest/guides/buffers/): Load, inspect, paste and delete named byte buffers.
---
# Getting started
Source: https://libtmux.org/en/erlang/latest/guides/getting-started/
> Getting started: libtmux_erlang documentation.
Create tmux sessions, windows and panes from Erlang. Capture their object graph,
filter it as immutable data, and execute source queries and owned operations
explicitly.
## Install
Use Erlang/OTP **29.1.2**, Rebar3 **3.28.0**, Linux, tmux **3.3a–3.8**, and
`make` with a C11 compiler. Native records are experimental in OTP 29. The core
uses OTP libraries and a compiled external helper, with no NIF or third-party
runtime packages.
This checkout is unreleased. Add its core application to your `rebar.config`:
```erlang
{deps, [
{libtmux_erlang, {git_subdir,
"https://github.com/libtmux/libtmux-erlang.git",
{branch, "master"}, "apps/libtmux_erlang"}}
]}.
```
Commit `rebar.lock` to keep the resolved commit. The
[installation guide](https://libtmux.org/en/erlang/latest/guides/installation/) covers application startup and
building a consumer. No Hex release is advertised.
## Create, capture and query
From this checkout, compile with the pinned tools:
```console
$ mise exec -- rebar3 compile
```
Run the complete program below on a private tmux server:
```console
$ mise exec -- escript tools/run_examples.escript readme
```
The program starts an empty daemon on a unique private socket, verifies its
assertions, and retires the owned server and clients. It leaves your existing
tmux sessions alone. It uses only public core APIs and OTP runtime libraries;
the archive check also runs it outside this checkout.
Save it as `readme_demo.erl` when reusing it in an application.
```erlang
-module(readme_demo).
-moduledoc "Create a workspace, query its graph and preserve captured values.".
-export([run/1]).
-import_record(tmux_model, [pane]).
-spec run(binary()) -> map().
run(Executable) ->
Options = #{executable => Executable},
{ok, Result} = tmux_server:with_owned(Options, fun(Server) ->
{ok, Session} = tmux_session:create(Server, #{name => <<"workspace">>,
window_name => <<"editor">>, width => 120, height => 30,
launch => #{argv => [<<"/bin/cat">>]}}),
{ok, Worker} = tmux_window:create(Session,
#{name => <<"workers">>,
launch => #{argv => [<<"/bin/cat">>]}}),
{ok, Right} = tmux_pane:split(Worker, #{direction => horizontal,
launch => #{argv => [<<"/bin/cat">>]}}),
{ok, _} = tmux_window:select_layout(Worker, even_horizontal),
{ok, _} = tmux_pane:resize(Right, #{width => 40}),
{ok, Snapshot} = tmux_server:snapshot(Server),
Windows = tmux_select:windows(Snapshot),
Workers = #{name => <<"workers">>, panes => #{count => 2,
every => #{current_command => <<"cat">>}}},
{ok, Captured} = tmux_select:one(Workers, Windows),
{ok, Children} = tmux_snapshot:related(panes, Captured, Snapshot),
Ids = [Id || #pane{id = Id} <:- Children],
2 = length(Ids),
Criteria = #{width => #{gte => 20},
window => #{is => #{name => <<"workers">>}}},
Query = tmux_query:where(Criteria, tmux_query:panes(Server)),
{ok, Fetched} = tmux_query:fetch(Query),
2 = tmux_select:size(Fetched),
{ok, _} = tmux_window:rename(Worker, <<"finished">>),
<<"workers">> = tmux_model:get(name, Captured),
{ok, Fresh} = tmux_query:fetch(tmux_query:where(
#{name => <<"finished">>}, tmux_query:windows(Server))),
{ok, Renamed} = tmux_select:one(Fresh),
<<"finished">> = tmux_model:get(name, Renamed),
1 = length(tmux_snapshot:sessions(Snapshot)),
2 = length(tmux_snapshot:windows(Snapshot)),
3 = length(tmux_snapshot:panes(Snapshot)),
#{sessions => 1, windows => 2, panes => 3, matched_panes => 2,
captured_name => <<"workers">>, immutable => true}
end),
Result.
```
The result includes this checked output. Captured rows keep their original
values after a rename; actions and `fetch` perform explicit I/O.
[`tmux_server:with_owned/2`]() returns success after verified daemon cleanup. If cleanup fails,
its error preserves the callback outcome and ownership token for recovery.
```json
{
"sessions": 1,
"windows": 2,
"panes": 3,
"matched_panes": 2,
"captured_name": "workers",
"immutable": true
}
```
## Follow the graph
A window can have several links in one session and belong to more than one
session. Physical windows and panes remain unique; window links retain each
session/index membership.
- [Linked windows and relationship queries](https://libtmux.org/en/erlang/latest/concepts/object-graph/): build two sessions,
link a shared window twice, and verify physical and membership counts.
- [Daemon ownership](https://libtmux.org/en/erlang/latest/topics/ownership/): choose private startup or a borrowed
socket, keep their lifetimes explicit and handle incomplete cleanup.
- [Build command values](https://libtmux.org/en/erlang/latest/guides/commands/): prepare a rename and resize, submit
them explicitly and inspect fresh state without changing captured rows.
- [Control attachments](https://libtmux.org/en/erlang/latest/topics/control/): verify connection identity, inspect
attachment effects and retire the client explicitly.
- [Plan and apply a workspace](https://libtmux.org/en/erlang/latest/guides/workspace-json/): validate desired sessions,
windows and panes, review saved operations, and handle drift or cancellation.
- [Run a job and capture its result](https://libtmux.org/en/erlang/latest/guides/operations/): wait for a pane's completion
event, check its exit status, capture bytes, and cancel a separate operation.
- [Local filters and source queries](https://libtmux.org/en/erlang/latest/concepts/queries/): Boolean criteria,
quantifiers, portable JSON, cardinality, coverage and execution plans.
- [Fields and operators](https://libtmux.org/en/erlang/latest/reference/fields/): generated from the runtime schema.
- [Scoped options](https://libtmux.org/en/erlang/latest/guides/options/): local ownership markers, explicit
inheritance and sparse arrays, with a source-pinned option catalogue.
- [Buffers and paste](https://libtmux.org/en/erlang/latest/guides/buffers/): preserve binary artifacts, inspect
names and control terminal delivery.
- [Rearrange panes](https://libtmux.org/en/erlang/latest/guides/panes/): isolate a running job in a window and join
it back, preserving its process and the original snapshot.
- [Apply configuration and await a signal](https://libtmux.org/en/erlang/latest/guides/configuration/): load settings
from stdin, observe the event and verify parse-only behavior.
- [Pipe terminal output](https://libtmux.org/en/erlang/latest/guides/piping/): save a job's bytes, await its consumer
acknowledgement and verify the resulting log.
- [Hooks and completion](https://libtmux.org/en/erlang/latest/guides/hooks/): register a split callback, await its
acknowledgement and remove the local hook.
- [Key tables](https://libtmux.org/en/erlang/latest/guides/keys/): register application bindings, inspect native
listings and remove the owned table.
- [Native commands](https://libtmux.org/en/erlang/latest/reference/commands/): source-derived parser metadata
and feature minimums, separate from library API and runtime qualification.
- [API browser and source docs](https://libtmux.org/en/erlang/latest/guides/api-documentation/): build searchable native reference
pages with specs, types and links to the exact source revision.
## Decide whether it fits
The current path uses bounded one-shot tmux clients. Operations support
non-blocking submission, explicit waits, cancellation and ownership transfer.
An await timeout abandons that wait; it does not cancel the operation.
Cancellation after dispatch can leave the tmux outcome unknown.
An explicit control attachment verifies identity on its persistent connection
and owns its client cleanup. The workspace application plans and applies JSON
files through a standalone escript. Control command submission and output
streams remain implementation work. The
[support and gaps](https://libtmux.org/en/erlang/latest/guides/compatibility/) page lists
what is exercised, what remains open, and the required Linux/tmux matrix.
There is no control-versus-streaming performance claim.
The [project policy](https://libtmux.org/en/erlang/latest/topics/project-policy/) keeps core and workspace in
separate OTP applications with OTP runtime dependencies. MCP is excluded from
this port's scope.
## Verify and contribute
```console
$ time python3 tools/check.py mid
```
```console
$ time python3 tools/check.py outer
```
The outer check owns its tmux fixtures and tests installed consumers. See
[contributor setup](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/.github/CONTRIBUTING.md) for prerequisites and test budgets,
[CI](https://github.com/libtmux/libtmux-erlang/actions/workflows/ci.yml) for
individual release/architecture results, and the [MIT license](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/LICENSE).
[Branding assets](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/docs/branding.md) include the logo, icons, and social cards.
---
# Guides
Source: https://libtmux.org/en/erlang/latest/guides/
> Guides for libtmux_erlang.
Connect to tmux and work with its sessions, windows, and panes.
- [Getting started](https://libtmux.org/en/erlang/latest/guides/getting-started/): Read about libtmux for erlang.
- [Installation](https://libtmux.org/en/erlang/latest/guides/installation/): Read about install the core application.
- [Apply configuration and await its completion signal](https://libtmux.org/en/erlang/latest/guides/configuration/): Read about apply configuration and await its completion signal.
- [Build commands before running them](https://libtmux.org/en/erlang/latest/guides/commands/): Read about build commands before running them.
- [Install application keybindings](https://libtmux.org/en/erlang/latest/guides/keys/): Read about install application keybindings.
- [Isolate and rejoin a running pane](https://libtmux.org/en/erlang/latest/guides/panes/): Read about isolate and rejoin a running pane.
- [Native API documentation](https://libtmux.org/en/erlang/latest/guides/api-documentation/): Read about build and browse the native api.
- [Owned operations](https://libtmux.org/en/erlang/latest/guides/operations/): Read about wait for a terminal job, then capture its output.
- [Pipe pane output](https://libtmux.org/en/erlang/latest/guides/piping/): Read about save a job's output and verify the consumer received it.
- [Preserve byte buffers and paste deliberately](https://libtmux.org/en/erlang/latest/guides/buffers/): Read about preserve byte buffers and paste deliberately.
- [Read and change scoped options](https://libtmux.org/en/erlang/latest/guides/options/): Read about read and change scoped options.
- [Register a hook and await its effect](https://libtmux.org/en/erlang/latest/guides/hooks/): Read about register a hook and await its effect.
- [Support and remaining gaps](https://libtmux.org/en/erlang/latest/guides/compatibility/): Read about support and remaining gaps.
- [Workspace JSON loader](https://libtmux.org/en/erlang/latest/guides/workspace-json/): Read about plan and apply a workspace.
---
# Installation
Source: https://libtmux.org/en/erlang/latest/guides/installation/
> Installation: libtmux_erlang documentation.
The core OTP application is `libtmux_erlang`. It lives in an umbrella, so a
Rebar3 source dependency must use `git_subdir` with `apps/libtmux_erlang`.
Using the repository root as an ordinary single-app Git dependency does not
select the core application.
The [README](https://libtmux.org/en/erlang/latest/guides/getting-started/#install) contains the dependency declaration tested
in an isolated Rebar3 consumer. Rebar3 resolves the branch to a commit in
`rebar.lock`; commit that file. To choose a reviewed commit directly, use
Rebar3's `{ref, Commit}` form instead of `{branch, "master"}`.
Add `libtmux_erlang` to your application's `applications` list in its
`.app.src`. For an interactive consumer, compile and start the Rebar3 shell:
```console
$ rebar3 compile
```
```console
$ rebar3 shell
```
## Inspect an existing server
`open/1` borrows an existing daemon. It does not start tmux or create a session.
From a shell attached to the server you intend to inspect, obtain its socket:
```console
$ export LIBTMUX_SOCKET="$(tmux display-message -p '#{socket_path}')"
```
Save this module as `src/inspect_demo.erl` in the consumer application:
```erlang
-module(inspect_demo).
-moduledoc "Inspect an existing tmux socket without changing its sessions.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, _} = application:ensure_all_started(libtmux_erlang),
Endpoint = #{socket_path => Socket, executable => Executable},
{ok, Result} = tmux_server:with_server(Endpoint, fun(Server) ->
{ok, Snapshot} = tmux_server:snapshot(Server),
#{sessions => length(tmux_snapshot:sessions(Snapshot)),
windows => length(tmux_snapshot:windows(Snapshot)),
panes => length(tmux_snapshot:panes(Snapshot))}
end),
Result.
```
Run it through the consumer's Rebar shell. The executable path comes from PATH;
the socket must be the absolute path selected above:
```console
$ rebar3 shell \
--eval '
Socket = list_to_binary(os:getenv("LIBTMUX_SOCKET")),
Tmux = list_to_binary(os:find_executable("tmux")),
io:format("~p~n", [inspect_demo:run(Socket, Tmux)]),
halt().'
```
The returned map counts physical objects in one captured view. Closing the
library handle preserves the borrowed daemon and its sessions. This module
also runs against the installed archive in the outer check. The private-server
[README program](https://libtmux.org/en/erlang/latest/guides/getting-started/#create-capture-and-query) adds creation, native
record patterns, graph traversal and a fresh query.
Direct native-record construction needs [`tmux_model`]() loaded first; its
constructors load normally and validate observations. Do not distribute record
headers or serialize record internals as a wire format.
## Requirements
| Component | Current requirement |
| --- | --- |
| Erlang/OTP | Exactly the qualified latest-stable baseline, 29.1.2 |
| Build tool | Rebar3 3.28.0 |
| tmux | 3.3a–3.8, including intermediate releases; CI tests both endpoints |
| Runtime platform | Linux x86_64 and arm64 qualification cells |
| Native build | C11 compiler, libc headers and make |
| Core dependencies | OTP kernel, stdlib and crypto; no JSON package or NIF |
| Checkout example runner | Python 3.12+ for owned-server readiness observation |
The helper builds from its packaged C source. Installation must fail if that
build fails; there is no runtime executable download. The helper's original
license is retained separately from the project's MIT license.
There is no published Hex package or release artifact for this checkout.
[API docs](https://libtmux.org/en/erlang/latest/guides/api-documentation/) describe the local browser build. [Compatibility](https://libtmux.org/en/erlang/latest/guides/compatibility/)
separates current checks from the remaining suite and delivery gates.
---
# Apply configuration and await its completion signal
Source: https://libtmux.org/en/erlang/latest/guides/configuration/
> Apply configuration and await its completion signal: libtmux_erlang documentation.
Submit a wait without blocking the Erlang caller, then load configuration that
sets an owned session option and signals the channel. The signal works whether
the waiter arrives before or after it. The program verifies the stored value
and checks that parse-only loading leaves it unchanged. The runner owns the
daemon and verifies session and process cleanup. Compile the project first:
```console
$ mise exec -- rebar3 compile
```
Run the complete program:
```console
$ mise exec -- escript tools/run_examples.escript config
```
```erlang
-module(config_demo).
-moduledoc "Apply session configuration and wait for its explicit signal.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
{ok, Session} = tmux_session:create(Server,
#{name => <<"configured-job">>,
launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
Id = tmux_model:get(id, Session),
Suffix = binary:encode_hex(crypto:strong_rand_bytes(8), lowercase),
Channel = <<"configured-", Suffix/binary>>,
{ok, Wait} = tmux_command:wait_for(Channel, wait),
{ok, Operation} = tmux_operation:start(Server, Wait, #{}),
{ok, Source} = tmux_command:source_file(stdin, #{}),
Scope = [<<"-t">>, Id, <<"@state">>],
{ok, Settings} = tmux_command:raw(<<"set-option">>,
Scope ++ [<<"configured">>]),
{ok, Signal} = tmux_command:wait_for(Channel, signal),
{ok, Text} = tmux_command:to_config([Settings, Signal]),
{ok, _} = tmux_server:command(Server, Source, #{stdin => Text}),
{ok, #{cleanup := complete}} =
tmux_operation:await(Operation, 900),
State = <<"@state">>,
{ok, <<"configured">>} = tmux_option:get(Session, State, #{}),
ParseOptions = #{parse_only => true},
{ok, Parse} = tmux_command:source_file(stdin, ParseOptions),
{ok, NewValue} = tmux_command:raw(<<"set-option">>,
Scope ++ [<<"changed">>]),
{ok, Change} = tmux_command:to_config(NewValue),
{ok, _} = tmux_server:command(Server, Parse, #{stdin => Change}),
{ok, <<"configured">>} = tmux_option:get(Session, State, #{}),
#{configuration_applied => true, event_received => true,
parse_only_kept_value => true}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"configuration_applied": true,
"event_received": true,
"parse_only_kept_value": true
}
```
The archive check repeats this program using the installed core package.
[Jobs and operations](https://libtmux.org/en/erlang/latest/guides/operations/) covers operation ownership, cancellation and
application-level completion events.
## Choose input and execution separately
[`tmux_command:to_config/1`]() renders one validated command value or a nonempty
flat list as native configuration text. Lists contain at most 256 commands and
share the 32 KiB rendered limit, including native separators. Rendering a list
does not provide atomic execution or rollback. Argument quoting preserves
literal bytes through the lexer, including variables, semicolons, newline
indentation and non-UTF-8 byte values. It returns ASCII text without
performing I/O. Escaping growth is bounded by a 32 KiB rendered limit. This
covers one lexer pass; native commands that accept command strings can parse
those strings again. Render each inner command separately for its own
boundary. Native commands still interpret their arguments and formats
normally; this renderer does not quote shell code.
[`tmux_command:source_file/2`]() builds a command value without I/O. The source is
`stdin` or one absolute POSIX byte path. Path glob characters and backslashes
are quoted for tmux's glob parser; hash formats are not expanded. The builder
never opens the file. Reading and parsing occur when the command executes.
The file's size is not bounded by the command builder.
For stdin, pass bytes through the execution options of
[`tmux_server:command/3`]() or [`tmux_operation:start/3`](). The transport bounds stdin
and rendered arguments together to 256 KiB. `parse_only` requests native
`-n`: parse the input without executing its command list. `quiet` requests
native missing-file suppression; syntax and execution errors remain errors.
Both options default to false.
Configuration is trusted tmux code, with native directives and variable
expansion. Parse-only is not a sandbox. Execution can change state before
reporting an error, and cancellation cannot roll back those effects. A
successful source command does not establish that background work launched
by that configuration has finished. Check its actual completion event.
## Coordinate channels explicitly
[`tmux_command:wait_for/2`]() accepts a binary channel name and one action: `wait`,
`signal`, `lock` or `unlock`. Submit its result through the same execution
APIs. Native channel names are server-wide; use a name unique to the operation
when other clients share the server.
A signal wakes current waiters. On an unlocked channel with no waiters or
pending wakeup, it retains one wakeup for a later wait. Signaling again before
a wait can clear that pending state. Send once per expected wait; repeated
signals are neither counted nor reliably coalesced. Use separate names for
signaling and locking. `lock` and `unlock` expose native advisory channel
locks. A lock remains held after the acquiring command
client exits, until an explicit unlock. They are not library-owned leases.
Cancelling a command does not undo a signal or release a possibly acquired
lock. An uncertain lock outcome requires application coordination before
unlocking; blindly unlocking can release another client's lock.
---
# Build commands before running them
Source: https://libtmux.org/en/erlang/latest/guides/commands/
> Build commands before running them: libtmux_erlang documentation.
Prepare a literal session rename and a pane resize, then submit the values
explicitly. Building validates their captured target and arguments without
contacting tmux. Execution checks the endpoint origin and returns the native
one-shot result. The resize uses an operation handle so waiting is explicit.
This complete program checks the name before execution, awaits the resize,
reads fresh dimensions and verifies that earlier rows keep their values. The
runner owns the private fixture and verifies cleanup:
```console
$ mise exec -- escript tools/run_examples.escript commands
```
```erlang
-module(command_demo).
-moduledoc "Build captured-target commands, then execute and inspect them.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
Endpoint = #{socket_path => Socket, executable => Executable},
{ok, Server} = tmux_server:open(Endpoint),
tmux_scope:run_or_error(fun() ->
{ok, Session} = tmux_session:create(Server,
#{name => <<"commands">>, width => 120, height => 30,
launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
SessionId = tmux_model:get(id, Session),
{ok, Snapshot} = tmux_server:snapshot(Server),
[Anchor] = tmux_snapshot:panes(Snapshot),
{ok, Pane} = tmux_pane:split(Anchor,
#{direction => horizontal, size => {cells, 40},
launch => #{argv => [<<"/bin/cat">>]}}),
{ok, Rename} = tmux_session:rename_command(
Session, <<"commands-ready">>),
{ok, Resize} = tmux_pane:resize_command(Pane, #{width => 60}),
{ok, OtherEndpoint} = tmux_server:open(Endpoint),
tmux_scope:run_or_error(fun() ->
{error, #{code := different_owner, submitted := false}} =
tmux_server:command(OtherEndpoint, Rename, #{})
end, fun() ->
ok = tmux_server:close(OtherEndpoint)
end),
{ok, Before} = tmux_session:get(Server, SessionId),
<<"commands">> = tmux_model:get(name, Before),
40 = tmux_model:get(width, Pane),
{ok, #{exit_status := 0, cleanup := complete}} =
tmux_server:command(Server, Rename, #{}),
{ok, Operation} = tmux_operation:start(Server, Resize, #{}),
{ok, #{exit_status := 0, cleanup := complete}} =
tmux_operation:await(Operation, 900),
{ok, Current} = tmux_pane:get(Server, tmux_model:get(id, Pane)),
60 = tmux_model:get(width, Current),
{ok, Renamed} = tmux_session:get(Server, SessionId),
<<"commands-ready">> = tmux_model:get(name, Renamed),
<<"commands">> = tmux_model:get(name, Session),
40 = tmux_model:get(width, Pane),
#{name => <<"commands-ready">>, initial_width => 40,
actual_width => 60, captured_values_unchanged => true,
foreign_origin_rejected => true}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"name": "commands-ready",
"initial_width": 40,
"actual_width": 60,
"captured_values_unchanged": true,
"foreign_origin_rejected": true
}
```
The archive check runs this program through the installed public package.
[`tmux_session:rename/2`]() and [`tmux_pane:resize/2`]() perform their operations
immediately; their command builders return reusable values. Reusing a value
requires explicit intent each time. An uncertain mutation result must not
trigger an automatic retry.
A command keeps the originating row's endpoint token. Submitting it through
another endpoint returns `different_owner` with `submitted = false`, even if
both servers happen to use the same pane or session ID. Building alone does
not establish endpoint liveness. The example rejects a second endpoint handle
to the same daemon too. To act through another endpoint, bind the row there
and build a new command. Bind detached rows with
[`tmux_server:bind/2`]() before building their commands.
[`tmux_command:to_config/1`]() renders command syntax without
executing it. Serialized configuration carries no origin token; the caller
must choose and validate that destination explicitly. See
[jobs and operations](https://libtmux.org/en/erlang/latest/guides/operations/) for wait timeouts, ownership and cancellation,
the [configuration guide](https://libtmux.org/en/erlang/latest/guides/configuration/) for rendering, and
[current support](https://libtmux.org/en/erlang/latest/guides/compatibility/) for transport qualification.
---
# Install application keybindings
Source: https://libtmux.org/en/erlang/latest/guides/keys/
> Install application keybindings: libtmux_erlang documentation.
Use a separate key table when an application needs its own bindings. This
program binds `z` to zoom and show a status message, and `s` to show the
workspace name. It reads the native listing and removes its table. It does not
attach a client or press a key.
From the checkout, after [building the core](https://libtmux.org/en/erlang/latest/guides/installation/), run:
```console
$ mise exec -- escript tools/run_examples.escript keys
```
```erlang
-module(key_demo).
-moduledoc "Install and remove an application-owned key table.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
Suffix = binary:encode_hex(crypto:strong_rand_bytes(8), lowercase),
Table = <<"workspace-", Suffix/binary>>,
{ok, Zoom} = tmux_command:raw(<<"resize-pane">>, [<<"-Z">>]),
{ok, Status} = tmux_command:raw(<<"display-message">>,
[<<"Workspace: #{session_name}">>]),
{ok, _} = tmux_key:bind(Server, <<"z">>, [Zoom, Status],
#{table => Table, note => <<"Zoom the current workspace pane">>}),
tmux_scope:run_or_error(fun() ->
{ok, _} = tmux_key:bind(Server, <<"s">>, Status,
#{table => Table, note => <<"Show the workspace name">>}),
{ok, Listing} = tmux_key:listing(Server, #{table => Table}),
true = binary:match(Listing, <<"resize-pane">>) =/= nomatch,
true = binary:match(Listing, <<"display-message">>) =/= nomatch,
#{bindings => 2, native_listing => true,
execution => <<"requires a client key press">>}
end, fun() ->
{ok, _} = tmux_key:clear_table(Server, Table),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"bindings": 2,
"native_listing": true,
"execution": "requires a client key press"
}
```
The runner owns an isolated server. The program uses the public core API, and
the installed-archive check repeats it outside the source checkout. Common
Test separately drives a real terminal client's key input, verifies the bound
command's bytes and acknowledgement, and proves client retirement on success,
failure and cancellation.
## Registration and execution are separate
[`tmux_key:bind/3`]() registers in the native prefix table.
[`tmux_key:bind/4`]() accepts an explicit `table`, Boolean `repeat` and byte `note`.
It accepts one command or a flat nonempty command list, with the bounds of
[`tmux_command:to_config/1`](): at most 256 commands and 32 KiB of rendered text.
tmux parses the body before replacing an existing binding. Replacement resets
the previous commands, repeat flag and note.
Tables belong to the server. Creating one does not select it for any client.
Choose a session's default through the [`key-table`](https://libtmux.org/en/tmux/latest/catalog/options/key-table/) option in
[`tmux_option`](https://libtmux.org/en/erlang/latest/guides/options/); native prefix, repeat and client table-switching
rules still apply. The example leaves client selection to the application.
Registration quotes arguments for the stored command parser. A command can
still interpret formats when a client executes it: the example's
[`#{session_name}`](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/) is evaluated then. See
[configuration boundaries](https://libtmux.org/en/erlang/latest/guides/configuration/) before storing nested command
strings or explicit shell commands. An accepted registration does not prove
future execution; arrange a completion signal when an application needs one,
as in the [hook example](https://libtmux.org/en/erlang/latest/guides/hooks/).
## Read listings and remove owned bindings
[`tmux_key:listing/1`]() and [`tmux_key:listing/2`]() return native bytes unchanged.
Options can restrict `table` and `key`. Spacing, quoting and included metadata
follow the daemon release. This is a presentation view: retain original command
values for reinstalling bindings. tmux 3.4 may escape dollar signs in this
view. On tmux 3.7c, a single matching entry can go to the native status-message
path and produce empty stdout when no target client is attached. Empty output
does not prove a binding is absent. The example lists multiple bindings to
avoid that path. The tmux 3.3 baseline has no custom listing format option.
Explicit newer-server options remain available through
[`tmux_command:raw/2`]().
[`tmux_key:unbind/2`]() and [`tmux_key:unbind/3`]() remove one binding; an absent key in
an existing table is a native no-op. Removing the final custom binding may
remove its table.
Missing explicitly named tables and invalid native key names remain errors.
[`tmux_key:clear_table/2`]() requires an explicit name and removes the entire
table. It can affect attached clients and does not restore default bindings
or cancel commands already queued by a client.
Calls use the registered endpoint identity and preserve backend diagnostics.
They do not start a missing server or retry uncertain mutations. The native
[`tmux_key` API browser](https://libtmux.org/en/erlang/latest/guides/api-documentation/) documents input bounds, results and errors.
---
# Isolate and rejoin a running pane
Source: https://libtmux.org/en/erlang/latest/guides/panes/
> Isolate and rejoin a running pane: libtmux_erlang documentation.
Move a job into its own window, then join it back without respawning it.
This complete program checks the process ID, changed parent window, removal of
the temporary window and immutability of the earlier capture. The runner owns
the server and verifies cleanup:
```console
$ mise exec -- escript tools/run_examples.escript panes
```
```erlang
-module(pane_demo).
-moduledoc "Isolate a running pane, then join it back without respawning.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
{ok, Session} = tmux_session:create(Server,
#{name => <<"pane-workflow">>, window_name => <<"editor">>,
launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
{ok, Initial} = tmux_server:snapshot(Server),
{ok, Link} = tmux_select:one(
#{session_id => tmux_model:get(id, Session)},
tmux_select:window_links(Initial)),
{ok, Window} = tmux_snapshot:related(window, Link, Initial),
{ok, [Anchor]} = tmux_snapshot:related(panes, Window, Initial),
WindowId = tmux_model:get(id, Window),
{ok, Pane} = tmux_pane:split(Anchor,
#{launch => #{argv => [<<"/bin/cat">>]}}),
Id = tmux_model:get(id, Pane),
Pid = tmux_model:get(pid, Pane),
{ok, Before} = tmux_server:snapshot(Server),
{ok, _} = tmux_pane:break(Pane, Link, Session,
#{index => 9, name => <<"isolated #{pane_id}">>}),
{ok, Isolated} = tmux_pane:get(Server, Id),
Pid = tmux_model:get(pid, Isolated),
Temporary = tmux_model:get(window_id, Isolated),
true = Temporary =/= WindowId,
{ok, OldPane} = tmux_snapshot:get(pane, Id, Before),
WindowId = tmux_model:get(window_id, OldPane),
{ok, _} = tmux_pane:join(Pane, Anchor,
#{direction => horizontal, size => {percent, 40}}),
{ok, Joined} = tmux_pane:get(Server, Id),
WindowId = tmux_model:get(window_id, Joined),
Pid = tmux_model:get(pid, Joined),
{error, #{code := target_gone}} =
tmux_window:get(Server, Temporary),
{ok, Final} = tmux_server:snapshot(Server),
2 = length(tmux_snapshot:panes(Final)),
#{same_process => true, snapshot_unchanged => true,
temporary_window_removed => true, panes_after_join => 2}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"same_process": true,
"snapshot_unchanged": true,
"temporary_window_removed": true,
"panes_after_join": 2
}
```
The archive check repeats this reader program outside the source checkout.
Its separate native consumer also exercises same-window move and pane swap.
## Supply a membership when breaking a pane
A physical window can be linked several times, even within one session. A pane
therefore has no single source membership. [`tmux_pane:break/4`]() takes the pane,
an explicit source window link, the destination session and placement options.
`index` is required. `detach` defaults to true; false selects the destination
membership. `name` is a literal UTF-8 argument, without format expansion.
| Source window | What tmux changes |
| --- | --- |
| Several panes | Creates a physical window and moves the pane into it; the original links remain |
| Only one pane | Moves the selected membership of the existing physical window; other links remain |
Pane IDs and running processes survive both cases. Moving the final membership
out of a session can destroy that source session. Session groups retain tmux's
native synchronization behavior. An occupied destination index is refused.
The API checks the link's expected window and the pane's observed parent. It
then sends a session/index selector qualified by the stable pane ID. An absent
pane cannot become whichever pane later occupies its old positional index.
The preflight and action remain separate commands: concurrent changes after
the check, including an index moving away and back, can remain undetected.
There is no atomic compare-and-act guarantee.
Literal means unexpanded, not stored verbatim. tmux's own name cleaning still
applies. For example, 3.3/3.4 stores an added backslash before dollar-variable
prefixes; later releases changed that behavior. The library returns the
stored name instead of guessing which escapes a caller intended. See the
[object graph guide](https://libtmux.org/en/erlang/latest/concepts/object-graph/) for physical windows and memberships.
## Move and swap by physical pane ID
[`tmux_pane:join/3`]() and [`tmux_pane:move/3`]() use the source and destination pane
IDs. Their native commands share placement behavior and permit rearrangement
within one window. Geometry uses [`t:tmux_pane:join_options/0`](): vertical by
default, optional direction, cells/percent size, `before` and `full_size`.
Both reject identical source/destination IDs.
[`tmux_pane:swap/3`]() exchanges layout positions within or across windows. Its
`detach` defaults to true; `zoom` maps to tmux's `-Z` retention flag. Swapping
a pane with itself exchanges nothing, but native zoom handling still runs:
the default can unzoom even that same-pane call. Across windows, active
positions can receive different pane IDs even in detached mode.
Moving the last pane out destroys its physical window and every link to it,
and can remove sessions. Shared windows affect all memberships. Existing
processes keep their process environment and working directory; inherited
window options can change when the pane changes parent. With `detach` false,
join/move selection uses tmux's resolved destination-session context. Supply
window-link actions separately when a particular session membership must be
selected.
## Keep captures and action results separate
These calls return the completed command result for both [new-window](https://libtmux.org/en/tmux/latest/manual/new-window/) and
existing-window cases. They never refresh input records or captures. Use
[`tmux_pane:get/2`](), [`tmux_window_link:get/3`]() or [`tmux_server:snapshot/1`]() to observe
the resulting graph. Source/destination rows must use the same registered
owner; rebind explicitly when working through another endpoint owner.
A disappeared pane or session remains an operational error. A replaced source
link returns `stale_link`; a different observed pane parent returns
`pane_link_mismatch`. Even an eventual native layout error can unzoom windows.
These operations are not transactions and are never automatically retried.
The [support page](https://libtmux.org/en/erlang/latest/guides/compatibility/) separates implemented APIs from remaining
control, consumer and delivery qualification.
---
# Native API documentation
Source: https://libtmux.org/en/erlang/latest/guides/api-documentation/
> Native API documentation: libtmux_erlang documentation.
Core and workspace contracts live in Erlang `-moduledoc`, `-doc` and specs.
OTP's compiler stores EEP-48 documentation chunks; ExDoc renders searchable module,
function and type pages. Source links open captured files with line anchors
and content digests. An unchanged file also links to its exact Git commit;
changed files are labelled as working-tree previews.
Compile the project:
```console
$ mise exec -- rebar3 compile
```
Prepare the checksum-pinned ExDoc tool. This step uses the network:
```console
$ python3 tools/build_docs.py --prepare
```
Build the browser without fetching dependencies:
```console
$ mise exec -- python3 tools/build_docs.py
```
Open `_build/docs/core/index.html`. This is a local build, not a published
HexDocs URL. The build refreshes compiled modules before rendering. The
renderer treats warnings as failures and uses the OTP 29
build of ExDoc. It consumes compiler chunks directly because the older EDoc
source-extraction path rejects native-record attributes.
## Read by task
| Task | API modules |
| --- | --- |
| Open, inspect and close an endpoint | [`m:tmux_server`]() |
| Preserve work and cleanup failures | [`m:tmux_scope`](), [`tmux_server:with_owned/2`](), [`tmux_server:with_server/2`]() |
| Create and act on terminal objects | [`m:tmux_session`](), [`m:tmux_window`](), [`m:tmux_pane`]() |
| Switch one explicitly named attached client | [`tmux_session:select/2`](), [`tmux_session:select/3`]() |
| Break, join, move and swap running panes | [`tmux_pane:break/4`](), [`tmux_pane:join/3`](), [`tmux_pane:move/3`](), [`tmux_pane:swap/3`]() |
| Act on session-relative window memberships | [`m:tmux_window_link`]() |
| Read local or inherited options and change explicit tables | [`m:tmux_option`]() |
| Register, inspect and explicitly run native hooks | [`m:tmux_hook`]() |
| Register and remove keys in explicit server tables | [`m:tmux_key`]() |
| Inspect native command declarations and feature minimums | [`m:tmux_capability`]() |
| Store byte buffers and paste into a pane | [`m:tmux_buffer`]() |
| Read a captured graph | [`m:tmux_snapshot`](), [`m:tmux_model`]() |
| Filter captured data | [`m:tmux_select`](), [`m:tmux_filter`](), [`m:tmux_matcher`]() |
| Plan source retrieval | [`m:tmux_query`]() |
| Submit, await and cancel work | [`m:tmux_operation`](), [`m:tmux_command`]() |
| Render/load configuration and build channel waits or signals | [`tmux_command:to_config/1`](), [`tmux_command:source_file/2`](), [`tmux_command:wait_for/2`]() |
| Connect a shell consumer to live pane bytes | [`tmux_pane:pipe/3`](), [`tmux_pane:stop_pipe/1`]() |
| Inspect schema and wire validation | [`m:tmux_schema`](), [`m:tmux_json`](), [`m:tmux_version`]() |
| Explain local library exceptions in the shell | [`m:tmux_error`]() |
| Load and inspect desired workspace files | `m:tmux_workspace` |
| Inspect observed process identities | [`m:tmux_identity`]() |
The [field reference](https://libtmux.org/en/erlang/latest/reference/fields/) is generated from the same registry and
operator validator used by filtering. [Contributor checks](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/.github/CONTRIBUTING.md)
execute native shell examples and complete programs; compilation alone is
not the example gate.
## Types at application boundaries
Entity constructors accept generated maps such as
[`t:tmux_model:pane_input/0`](). Required fields and scalar categories are visible
to Dialyzer. Optional observations retain `not_loaded` and `unavailable`;
nullable fields also accept `none`. Numeric fields accept integral floats,
which normalize to integers. Runtime validation checks bounds, ID syntax,
UTF-8 and derived values. Use [`tmux_model:new/2`]() when validating dynamic maps
and handle its [`t:tmux_schema:error_detail/0`]() result.
[`t:tmux_select:t/1`]() describes the intended row type. Dialyzer in OTP 29.1.2
does not always substitute actual argument types into generic return
variables. For example, a selection passed through [`tmux_select:one/1`]() can
lose its specific entity type during analysis. A successful analysis does
not prove every pane/window relationship. Keep native patterns and runtime
action validation at those boundaries. The
[OTP contract solver](https://github.com/erlang/otp/blob/OTP-29.1.2/lib/dialyzer/src/dialyzer_contracts.erl#L477-L502)
records this generic constraint limitation.
The installed-consumer outer check executes valid constructor calls and
requires Dialyzer to reject wrong Boolean/numeric fields and missing session
identity or window-link index. It analyzes the installed core outside the
umbrella; a green library analysis alone does not establish those diagnostics.
---
# Owned operations
Source: https://libtmux.org/en/erlang/latest/guides/operations/
> Owned operations: libtmux_erlang documentation.
A successful send or command frame does not prove that a terminal program
processed input. This example runs an owned job that acknowledges terminal
parsing before it exits, then waits for tmux's [`pane-died`](https://libtmux.org/en/tmux/latest/catalog/hooks/pane-died/) event, checks the
exit status and captures the completed output.
The companion [terminal job](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/examples/terminal_job.py) writes its result and
an ANSI device-status request. It exits successfully only after reading tmux's
reply. tmux generates that reply after parsing the preceding output. The job
writes no more output afterward. Python 3.12+ is an example dependency only.
[`pane-died`](https://libtmux.org/en/tmux/latest/catalog/hooks/pane-died/) alone is **not** a general output-drain barrier on tmux 3.3. This
protocol applies to this cooperating job; it does not establish completion for
an arbitrary program or reconstruct output that scrolled out of history.
The example changes options and a hook only on its new window. [`remain-on-exit`](https://libtmux.org/en/tmux/latest/catalog/options/remain-on-exit/)
keeps the pane addressable; an empty exit banner preserves the first line.
The hook signals a tmux [`wait-for`](https://libtmux.org/en/tmux/latest/manual/wait-for/) channel. There are no sleeps or capture
retries. Run from the checkout so the companion job can be located:
```console
$ mise exec -- escript tools/run_examples.escript job
```
```erlang
-module(job_demo).
-moduledoc "Capture a job that acknowledges terminal parsing before it exits.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
Endpoint = #{socket_path => Socket, executable => Executable},
{ok, Server} = tmux_server:open(Endpoint),
tmux_scope:run_or_error(fun() ->
{ok, Session} = tmux_session:create(Server,
#{name => <<"jobs">>, width => 120, height => 30,
launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
{ok, Window} = tmux_window:create(Session,
#{launch => #{argv => [<<"/bin/cat">>]}}),
Target = [<<"-w">>, <<"-t">>, tmux_model:get(id, Window)],
Deadline = #{deadline_ms => 2000},
{ok, _} = tmux_option:set(Window, <<"remain-on-exit">>, <<"on">>),
{ok, _} = tmux_option:set(
Window, <<"remain-on-exit-format">>, <<>>),
{ok, Hook} = tmux_command:raw(<<"set-hook">>,
Target ++ [<<"pane-died">>, <<"wait-for -S job-finished">>]),
{ok, _} = tmux_server:command(Server, Hook, #{}),
{ok, Wait} = tmux_command:raw(<<"wait-for">>, [<<"job-finished">>]),
{ok, Operation} = tmux_operation:start(Server, Wait, Deadline),
{error, #{code := await_timeout}} =
tmux_operation:await(Operation, 0),
JobFile = filename:absname("examples/terminal_job.py"),
Launch = #{argv => [<<"/usr/bin/python3">>,
list_to_binary(JobFile)]},
{ok, Job} = tmux_pane:split(Window, #{launch => #{shell => <<>>}}),
{ok, _} = tmux_pane:respawn(Job, #{launch => Launch}),
{ok, #{exit_status := 0, cleanup := complete}} =
tmux_operation:await(Operation, 900),
{ok, Completed} = tmux_pane:get(Server, tmux_model:get(id, Job)),
true = tmux_model:get(dead, Completed),
0 = tmux_model:get(exit_status, Completed),
{ok, <<"build complete\n">>} = tmux_pane:capture(
Completed, #{start => 0, 'end' => 0}),
{ok, Never} = tmux_command:raw(
<<"wait-for">>, [<<"not-signalled">>]),
{ok, Cancelled} = tmux_operation:start(Server, Never, Deadline),
{ok, _} = tmux_operation:await_started(Cancelled, 900),
{ok, cancel_requested} = tmux_operation:cancel(Cancelled),
{error, #{outcome := unknown, cleanup := complete}} =
tmux_operation:await(Cancelled, 900),
#{output => <<"build complete\n">>, job_status => 0,
await_timeout_kept_operation => true,
cancelled_outcome => unknown}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
The output is `build complete` followed by a newline, and the exit status is
zero. The zero-duration await proves that abandoning a wait leaves the
operation usable. The separate cancelled wait reports an unknown tmux outcome
and complete client cleanup; cancellation does not undo submitted work.
The example prepares an empty pane, then starts its job with
[`tmux_pane:respawn/2`](). Respawn keeps the pane ID. Its default launch mode,
`previous`, reuses tmux's saved command; this example supplies new argv instead.
Running panes require explicit `force => true`. Use a job-specific completion
condition and fresh observations to check the result.
Use an empty shell command to prepare a process-free pane with
[`tmux_pane:split/2`](). Session creation on every supported version, and window
creation before tmux 3.8, can finish that command immediately and remove the
new object. This example uses `/bin/cat` to keep its parent session and window
available across versions.
`await_started/2` returns lifecycle status, including when an operation already
failed or retired. Use `await/2` for its command result. Successful command
results preserve separate stdout/stderr binaries, stdin bytes delivered to the
client pipe, exit status, outcome and cleanup. Validation and wait errors can
contain only a code; do not assume every error contains output or certainty.
The native API defines [`t:tmux_operation:completed/0`](),
[`t:tmux_operation:failure/0`]() and [`t:tmux_operation:state/0`]().
Treat a pane's exit_status field independently from its dead flag. tmux exposes
a numeric status after collecting a normal child exit; signal termination has
no numeric value in that field. The captured absence value is none, which does
not mean success. The native pane_dead_signal format can identify a signal
when diagnosing an unexpected exit. This distinction appears in the
[tmux 3.3a status callback](https://github.com/tmux/tmux/blob/3.3a/format.c#L1792)
and the
[tmux 3.8 status callback](https://github.com/tmux/tmux/blob/3.8/format.c#L2288).
## Operation ownership
The process that starts an operation owns it. Other processes can await its
handle without becoming owners. The endpoint confirms a transfer after
monitoring the new owner and releasing the old monitor; the new process does
not perform a handshake. Owner death and cancellation retire queued or
possibly-sent clients; they do not replay mutations.
An `await_timeout` from [`tmux_operation:transfer/2`]() leaves the transfer outcome
uncertain: the endpoint may process the owner change after the caller stops
waiting. Read [`tmux_operation:status/1`]() to establish the current owner before
relying on ownership. The timeout does not prove the old owner still owns the
operation.
Retired results are retained for at most 60 seconds. The endpoint also evicts
the oldest results when retention exceeds 64 results or 4 MiB of stdout/stderr.
Use [`tmux_operation:status/1`]() while a handle is valid, and handle
`operation_expired` after retention. Closing an endpoint retires its library
clients and preserves the tmux server. [Daemon ownership](https://libtmux.org/en/erlang/latest/topics/ownership/)
explains the separate token used to stop an owned daemon.
Current operations use one-shot clients. Persistent control, grouping and
stream delivery are not yet available; see [support and gaps](https://libtmux.org/en/erlang/latest/guides/compatibility/).
---
# Pipe pane output
Source: https://libtmux.org/en/erlang/latest/guides/piping/
> Pipe pane output: libtmux_erlang documentation.
This program opens a pane pipe before releasing the producer. A shell consumer
reads the expected bytes into a log and signals completion only after that
write succeeds. The Erlang caller verifies the file contents before stopping
the pipe. It does not infer delivery from pipe creation or command completion.
Compile the project:
```console
$ mise exec -- rebar3 compile
```
Run on an owned server. The runner verifies session and process cleanup:
```console
$ mise exec -- escript tools/run_examples.escript pipe
```
```erlang
-module(pipe_demo).
-moduledoc "Save terminal bytes after a consumer acknowledgement.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
Suffix = binary:encode_hex(crypto:strong_rand_bytes(8), lowercase),
Directory = <<"/tmp/ltm-pipe-example-", Suffix/binary>>,
ok = file:make_dir(Directory),
tmux_scope:run_or_error(fun() ->
ok = file:change_mode(Directory, 8#700),
Log = <>,
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
Native = <<(quote(Executable))/binary,
" -N -S ", (quote(Socket))/binary, " wait-for ">>,
Start = <<"start-", Suffix/binary>>,
Done = <<"consumed-", Suffix/binary>>,
Producer = <<"stty -onlcr && ", Native/binary, Start/binary,
" && printf 'build complete\\n' && exec /bin/cat">>,
{ok, Session} = tmux_session:create(Server,
#{name => <<"pipe-example">>, launch => #{argv =>
[<<"/bin/sh">>, <<"-c">>, Producer]}}),
tmux_scope:run_or_error(fun() ->
{ok, Snapshot} = tmux_server:snapshot(Server),
[Pane] = tmux_snapshot:panes(Snapshot),
Payload = <<"build complete\n">>,
Size = integer_to_binary(byte_size(Payload)),
Consumer = <<"/usr/bin/head -c ", Size/binary,
" > ", (quote(Log))/binary, " && ", Native/binary,
"-S ", Done/binary, " && exec /bin/cat >/dev/null">>,
{ok, Wait} = tmux_command:wait_for(Done, wait),
{ok, Operation} = tmux_operation:start(Server, Wait, #{}),
{ok, _} = tmux_pane:pipe(Pane, #{shell => Consumer}, #{}),
{ok, Signal} = tmux_command:wait_for(Start, signal),
{ok, _} = tmux_server:command(Server, Signal, #{}),
{ok, #{cleanup := complete}} =
tmux_operation:await(Operation, 900),
{ok, Payload} = file:read_file(Log),
{ok, _} = tmux_pane:stop_pipe(Pane),
#{log => Payload, bytes_written => byte_size(Payload),
consumer_acknowledged => true}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end)
end, fun() ->
ok = file:del_dir_r(Directory)
end).
quote(Value) ->
Escaped = binary:replace(Value, <<"'">>, <<"'\\''">>, [global]),
<<"'", Escaped/binary, "'">>.
```
```json
{
"log": "build complete\n",
"bytes_written": 15,
"consumer_acknowledged": true
}
```
The example uses Linux shell utilities `stty`, `/usr/bin/head` and `/bin/cat`.
It disables newline translation on the producer's terminal, so the expected
log contains LF bytes. The temporary log is removed after verification. The
archive check repeats the program with the installed core package.
## Choose direction and replacement behavior
[`tmux_pane:pipe/3`]() requires an explicit shell-command map and a registered
physical pane. It escapes hash and strftime directives before tmux expands
that text. The shell still interprets the command; the example's `quote/1`
handles values inserted into its shell code. This native operation takes shell
code rather than executable argv.
[`tmux_command:literal_format/2`]() documents the encoding limits. Percent-bearing
commands above tmux's 8191-byte intermediate time-format limit return
`format_limit` before dispatch. That check preserves an existing pipe when a
replacement cannot be represented safely.
| Direction | Connection |
| --- | --- |
| `output` (default) | Pane PTY output to child stdin |
| `input` | Child stdout to pane input |
| `both` | Both connections |
Terminal modes affect these bytes. This is neither rendered screen capture
nor a replay of prior terminal history. It is not an Erlang subscription with
library flow control. Child stderr is discarded; unused directions use tmux's
native null-device routing. The API does not copy the pane's cwd or environment
into the child process.
Starting a pipe normally replaces an existing one. `toggle => true` maps to
native `-o`: an open pipe is closed without starting another, while a closed
pipe is opened. It does not mean preserve an already open pipe.
## Distinguish the pipe from its process
A successful action proves command completion, not shell execution success,
consumer readiness, delivered bytes or process exit. The example observes the
consumer's actual file write. Native integration tests independently verify
all 256 byte values in both directions, literal hash/percent shell text,
replacement, toggle and process retirement.
[`tmux_pane:stop_pipe/1`]() closes the native connection and can discard pending
bytes. Wait for the consumer's acknowledgement before closing when delivery
matters. Closing does not kill or wait for an arbitrary shell process; a
consumer that does not finish on EOF needs application-level coordination.
[`tmux_server:close/1`]() preserves the borrowed pane and its tmux-owned pipe.
[Jobs and operations](https://libtmux.org/en/erlang/latest/guides/operations/) covers library-owned command clients;
[configuration and signals](https://libtmux.org/en/erlang/latest/guides/configuration/) explains native wait channels.
---
# Preserve byte buffers and paste deliberately
Source: https://libtmux.org/en/erlang/latest/guides/buffers/
> Preserve byte buffers and paste deliberately: libtmux_erlang documentation.
Store a binary artifact without sending it through a shell, terminal renderer
or Unicode conversion. Loading uses tmux client stdin; showing a buffer reads
its exact bytes, including NUL, invalid UTF-8 and protocol-like lines.
Run this complete program against the runner's isolated server:
```console
$ mise exec -- escript tools/run_examples.escript buffers
```
```erlang
-module(buffer_demo).
-moduledoc "Store a binary artifact without terminal or text conversion.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
Name = <<"libtmux-example-",
(binary:encode_hex(crypto:strong_rand_bytes(8), lowercase))/binary>>,
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
tmux_scope:run_or_error(fun() ->
Bytes = <<0, 255, "build\n%end 1 2 3\n\\r">>,
{ok, _} = tmux_buffer:load(Server, Name, Bytes),
{ok, Bytes} = tmux_buffer:show(Server, Name),
{ok, [#{name := Name, size := Size} | _]} =
tmux_buffer:list(Server),
Size = byte_size(Bytes),
{ok, _} = tmux_buffer:load(Server, Name, <<>>),
{ok, Bytes} = tmux_buffer:show(Server, Name),
#{byte_count => Size, exact_round_trip => true,
empty_load_kept_bytes => true}
end, fun() ->
case tmux_buffer:delete(Server, Name) of
{ok, _} -> ok;
{error, #{code := buffer_not_found}} -> ok;
Failure -> error({buffer_cleanup_failed, Failure})
end,
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"byte_count": 21,
"exact_round_trip": true,
"empty_load_kept_bytes": true
}
```
The program uses a unique name and removes its buffer in cleanup. The runner
also retires the owned server; the archive check repeats this program outside
the source checkout against the installed core application.
## Choose a name or the newest buffer
[`tmux_buffer:load/3`]() accepts a binary name or `automatic`, which lets tmux
allocate a name. `show/2`, `delete/2` and `paste/3` accept an observed binary
name or `latest`. `list/1` returns ordinary metadata maps in tmux's newest-first
insertion order, with name, size in bytes and Unix creation time. It does not
load payloads. Renaming keeps the order; loading replacement data inserts a
new buffer.
Names are NUL-free bytes. tmux 3.7+ applies its own name cleaning, which can
reject controls and escape backslashes. Subsequent calls need the actual
stored name from `list/1`. The list codec preserves embedded delimiters,
newlines and invalid UTF-8 on releases that allow those names. It never uses
a newline inside a name as a row boundary.
Empty load input is a native no-op: it neither creates a buffer nor truncates
an existing one. Delete explicitly when the buffer should disappear. A named
rename refuses an occupied destination on tmux 3.3/3.3a; 3.4+ replaces it.
Identical rename inputs fail before dispatch because some legacy versions
have unsafe handling of that case.
On 3.3/3.3a, native named delete/rename falls back to the newest buffer when
the requested name is absent. These APIs first perform a targeted existence
read with zero output allowance and return `buffer_not_found` for a missing
name. The read retains no payload and is independent of list bounds.
That preflight is not atomic:
concurrent removal after the check can still trigger the native fallback.
Serialize external changes or use tmux 3.4+ when that race is unacceptable.
Buffer names do not provide stable object identities or transactional updates.
## Paste bytes into a registered pane
[`tmux_buffer:paste/3`]() takes a registered pane, a buffer name or `latest`, and
an options map. Defaults preserve bytes: LF separators, no sanitization, no
bracketing and no deletion. Set `separator` to replace each LF with a chosen
NUL-free byte string. It is passed literally, without format expansion.
`sanitize => true` requires tmux 3.7+; older releases return
`unsupported_capability` before dispatch. The default raw path uses `-S` on
newer releases. `bracketed => true` adds markers only when the target has
enabled bracketed paste. `delete => true` removes the buffer after the native
paste call, including cases where native input-off handling discards input.
A completed paste is a tmux command result, not proof that a child consumed
input. The integration test uses a raw terminal receiver and a separate socket
acknowledgment to verify all 256 byte values and custom separators. The
[job guide](https://libtmux.org/en/erlang/latest/guides/operations/) explains completion signals and operation ownership.
## Bound reads and preserve errors
Load input shares the endpoint's 256 KiB command admission budget with argv.
`show/3` accepts `max_bytes` from 0 through 1 MiB and `deadline_ms` from 1 through
600000. Defaults are 1 MiB and 2500 ms. Overflow returns `output_limit` with
partial command evidence; it never returns a successful truncated buffer.
Lists are bounded to 4096 rows and 1 MiB of encoded output.
Missing buffers, gone panes and closed endpoints remain distinct errors.
Timeouts do not undo a completed load or paste, and mutations are not retried.
[`tmux_buffer:capabilities/1`]() describes source-defined behavior without I/O;
the [support page](https://libtmux.org/en/erlang/latest/guides/compatibility/) separates that metadata from qualification.
---
# Read and change scoped options
Source: https://libtmux.org/en/erlang/latest/guides/options/
> Read and change scoped options: libtmux_erlang documentation.
Use a local user option to record which workspace created a session. Read it
without inheritance when deciding whether the session belongs to that workspace.
`only_if_unset` refuses an existing local value; it does not compare an expected
owner or provide a workspace transaction.
The complete example also sets and removes a window override, checks that the
inherited value is restored, and preserves an empty sparse array entry. It
opens the supplied endpoint and creates/removes its own session. The repository
runner supplies an isolated server and verifies cleanup:
```console
$ mise exec -- escript tools/run_examples.escript options
```
```erlang
-module(options_demo).
-moduledoc "Use local options to mark workspace ownership without inheritance.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
{ok, Session} = tmux_session:create(Server,
#{name => <<"options">>, launch => #{argv => [<<"/bin/cat">>]}}),
tmux_scope:run_or_error(fun() ->
Owner = <<"@libtmux-owner">>,
none = tmux_option:get(Session, Owner, #{}),
{ok, _} = tmux_option:set(Session, Owner, <<"build-workspace">>,
#{only_if_unset => true}),
{ok, <<"build-workspace">>} = tmux_option:get(Session, Owner, #{}),
{ok, Window} = tmux_window:create(Session,
#{launch => #{argv => [<<"/bin/cat">>]}}),
{ok, [<<"build-workspace">>, none]} =
tmux_option:get_many([Session, Window], Owner, #{}),
none = tmux_option:get(Window, <<"remain-on-exit">>, #{}),
{ok, Effective} = tmux_option:get(Window, <<"remain-on-exit">>,
#{inherit => true}),
{ok, _} = tmux_option:set(Window, <<"remain-on-exit">>, <<"on">>),
{ok, <<"on">>} = tmux_option:get(Window, <<"remain-on-exit">>, #{}),
{ok, _} = tmux_option:unset(Window, <<"remain-on-exit">>),
none = tmux_option:get(Window, <<"remain-on-exit">>, #{}),
{ok, Effective} = tmux_option:get(Window, <<"remain-on-exit">>,
#{inherit => true}),
Env = <<"update-environment">>,
{ok, _} = tmux_option:set(Session, {Env, 7}, <<>>),
{ok, #{7 := <<>>}} = tmux_option:get(Session, Env, #{}),
none = tmux_option:get(Session, {Env, 8}, #{}),
#{owner => <<"build-workspace">>, restored_inheritance => true,
empty_entry_preserved => true, missing_entry => true}
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"owner": "build-workspace",
"restored_inheritance": true,
"empty_entry_preserved": true,
"missing_entry": true
}
```
## Select the table explicitly
| Scope argument | Table |
| --- | --- |
| Registered session record | Local session options |
| Registered physical window record | Local window options, shared by its memberships |
| Registered pane record | Local pane options |
| `{server, Server}` | Server-wide options |
| `{session_defaults, Server}` | Global session defaults |
| `{window_defaults, Server}` | Global window defaults |
A physical window has no universal parent session. Session options require a
session record; passing a pane to a session option fails rather than choosing
an incidental session. Built-in names can override tmux's native scope flags;
the wrapper checks their source metadata before dispatch to prevent that.
`get/3` uses local-only reads with `#{}`. `#{inherit => true}` follows the
selected table's parents: session defaults, or window/window defaults for a
pane. User options follow the same table ancestry. Server options have no
parent. There is no inheritance from a session table to a window table.
[`tmux_option:get_many/3`]() reads one scalar key from one to seven local rows on
the same registered endpoint. Results preserve input order, including `none`
for absent values. The example checks session ownership and the window's
independent local table together. Arrays, indexed entries, command options,
global tables and names outside the batch API's ASCII grammar use `get/3`.
Each batch uses one native command sequence plus the normal identity-preflight
client, with native ID observations around every row's read. Aggregate stdout
is limited to 1 MiB and stderr to 64 KiB, with a
2500 ms command deadline. Command or decoding failures reject the entire
batch. These are interval observations: a queued reference may outlive a
removed window, and target checks do not establish atomic topology or stronger
server-incarnation identity. See [ADR 0003](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/docs/adr-0003-scoped-option-batches.md).
## Preserve absence and bytes
`none` means the selected local option or requested array entry is absent.
`{ok, <<>>}` means a known empty value. A whole array returns `{ok, Entries}`
with numeric indexes as map keys; an empty local array returns `{ok, #{}}`.
An unloaded graph field is unrelated to these live reads.
Set array entries with `{Name, Index}`. The API deliberately avoids whole-array
assignment and its separator grammar. `unset/2` removes an entry or local option;
removing a local option restores inheritance. Unsetting a global built-in resets
its tmux default. Non-numeric array keys are outside this API; an observed array
with such keys fails decoding instead of silently dropping entries.
Values are NUL-free binaries, including arbitrary bytes. Names are literal;
`#{...}` in a user-option name does not expand. Prefixes, aliases and bracket
syntax in names are rejected. A scalar value is passed without `-F`, although
tmux may interpret a stored format or executable option later. Use this API
only with trusted option names and values; it is not an external write policy.
`append` uses tmux's `-a` behavior for the selected type. `only_if_unset` uses
`-o` and reports already-set as an error. Empty flag values retain tmux's toggle
behavior; use explicit `<<"on">>` or `<<"off">>` when state must be predictable.
Mutations are not retried after a timeout or unknown outcome.
## Discover version support
The [generated catalogue](https://libtmux.org/en/erlang/latest/reference/options/) lists canonical names, scopes,
parser types, arrays and hooks from pinned release commits.
[`tmux_option:describe/2`]() and `catalogue/1` read that metadata without I/O.
Built-ins have exact source catalogues for every release from 3.3a through
3.8, including intermediate patch releases. Tracking builds and unknown
versions return `unsupported_option_catalogue`; unavailable built-ins return
`unsupported_tmux_option`. User options do not depend on a built-in catalogue.
Hooks are executable commands and are rejected by this API, including hook
names discovered in the catalogue. Use [`tmux_hook:get/3`]() to read hooks and
[`tmux_hook:set/3`]() for registration; the [hook guide](https://libtmux.org/en/erlang/latest/guides/hooks/) covers their
scope, execution and cleanup.
Raw commands are also available for deliberate newer-version integration.
A missing target remains an operational error, never a missing option.
---
# Register a hook and await its effect
Source: https://libtmux.org/en/erlang/latest/guides/hooks/
> Register a hook and await its effect: libtmux_erlang documentation.
Register a split hook, submit a wait without blocking the Erlang caller, then
split a pane. The hook writes a session option and signals the unique channel.
The program checks that effect, removes the local hook and verifies cleanup.
The runner owns the daemon and checks that its sessions and processes exit.
Compile the project first:
```console
$ mise exec -- rebar3 compile
```
Run the complete program:
```console
$ mise exec -- escript tools/run_examples.escript hooks
```
```erlang
-module(hook_demo).
-moduledoc "Acknowledge a registered split hook before reading its effect.".
-export([run/2]).
-spec run(binary(), binary()) -> map().
run(Socket, Executable) ->
{ok, Server} = tmux_server:open(
#{socket_path => Socket, executable => Executable}),
tmux_scope:run_or_error(fun() ->
Launch = #{launch => #{argv => [<<"/bin/cat">>]}},
{ok, Session} = tmux_session:create(Server,
Launch#{name => <<"hooks">>}),
tmux_scope:run_or_error(fun() ->
{ok, Snapshot} = tmux_server:snapshot(Server),
[Pane] = tmux_snapshot:panes(Snapshot),
Suffix = binary:encode_hex(crypto:strong_rand_bytes(8), lowercase),
Channel = <<"hook-", Suffix/binary>>,
{ok, Mark} = tmux_command:raw(<<"set-option">>,
[<<"-t">>, tmux_model:get(id, Session),
<<"@last-event">>, <<"split complete">>]),
{ok, Signal} = tmux_command:wait_for(Channel, signal),
{ok, Wait} = tmux_command:wait_for(Channel, wait),
{ok, Operation} = tmux_operation:start(Server, Wait, #{}),
tmux_scope:run_or_error(fun() ->
Name = <<"after-split-window">>,
{ok, _} = tmux_hook:set(Session, {Name, 4}, [Mark, Signal]),
{ok, _} = tmux_pane:split(Pane, Launch),
{ok, #{cleanup := complete}} =
tmux_operation:await(Operation, 900),
{ok, <<"split complete">>} =
tmux_option:get(Session, <<"@last-event">>, #{}),
{ok, #{4 := _}} = tmux_hook:get(Session, Name, #{}),
{ok, _} = tmux_hook:unset(Session, Name),
none = tmux_hook:get(Session, Name, #{}),
#{event => <<"split complete">>, hook_acknowledged => true,
local_hook_removed => true}
end, fun() ->
tmux_scope:run_or_error(fun() ->
{ok, _} = tmux_operation:cancel(Operation)
end, fun() ->
case tmux_operation:await(Operation, 900) of
{ok, #{cleanup := complete}} -> ok;
{error, #{cleanup := complete}} -> ok
end
end), ok
end)
end, fun() ->
{ok, _} = tmux_session:kill(Session),
ok
end)
end, fun() ->
ok = tmux_server:close(Server)
end).
```
```json
{
"event": "split complete",
"hook_acknowledged": true,
"local_hook_removed": true
}
```
The archive check repeats this program using the installed core package.
[Jobs and operations](https://libtmux.org/en/erlang/latest/guides/operations/) explains why an await timeout abandons only
that wait. The program cancels and awaits its operation during cleanup even
if registration or the split fails.
## Choose the table explicitly
[`tmux_hook:set/3`]() requires a `{Name, Index}` key and validated
[`tmux_command`]() values. It replaces that indexed entry while preserving other
entries. `append/3` chooses tmux's first vacant local index, which can precede
existing sparse entries. Commands run in index order. Bodies can be one
command or a nonempty list; rendering is bounded before dispatch.
Local tables use registered session, physical window or pane rows. Global
tables use `{session_defaults, Server}` or `{window_defaults, Server}`.
The [source catalogue](https://libtmux.org/en/erlang/latest/reference/options/) defines each builtin hook's
permitted scope and version. Prefixes, aliases, ordinary options and custom
event/monitor interfaces are rejected by this API.
`get/3` reads local values by default. `inherit => true` follows parent tables.
An absent local array returns `none`; an empty array returns `{ok, #{}}` and
masks inheritance. `clear/2` leaves that empty override. `unset/2` removes an
entry or restores inheritance for the whole array. Unsetting an entry can
leave its local array empty. Global unset resets the native default.
Read values are tmux's canonical command-list text, including its own quoting
and escapes. They are not individual argument values or the original source
text. A read never executes them.
## Separate registration, context and completion
Registration confirms that tmux stored the commands. Those commands still
interpret their values and formats when they run. Native parsing can fail;
a failed first indexed write can already leave an empty local override.
There is no rollback or automatic retry of an uncertain mutation.
[`tmux_hook:run/2`]() requests native execution in an explicit context. Use a
session for its current window/pane, a checked window link for one membership,
or `{pane, Pane, Link}` for that pane and membership. This preserves the
chosen session and index when a physical window is linked more than once. A
physical window alone cannot choose that execution context. Existence and
membership checks precede dispatch. If the target disappears after preflight,
tmux can still fall back to another context. These separate observations do
not provide atomic compare-and-act.
A native command result does not acknowledge background work launched by a
hook. The program waits for its own signal after the option write. Clearing
or unsetting a hook does not cancel effects tmux has already queued.
[Window memberships](https://libtmux.org/en/erlang/latest/concepts/object-graph/) and [scoped options](https://libtmux.org/en/erlang/latest/guides/options/) explain the
related graph and inheritance rules.
---
# Support and remaining gaps
Source: https://libtmux.org/en/erlang/latest/guides/compatibility/
> Support and remaining gaps: libtmux_erlang documentation.
Support covers tmux **3.3a–3.8**, including intermediate releases, on Linux
x86_64 and arm64. The [workflow](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/.github/workflows/ci.yml) tests the oldest
supported and newest stable tmux releases on both architectures.
OTP 29.1.2 is the only supported Erlang baseline;
older-OTP compatibility branches are not maintained. Native records remain an
experimental language dependency in OTP 29. Do not persist their internals or
assume compatibility with another OTP line.
| Platform | tmux releases in the required matrix |
| --- | --- |
| Ubuntu 24.04 x86_64 | 3.3a, 3.8 |
| Ubuntu 24.04 arm64 | 3.3a, 3.8 |
CI uses only the latest stable Erlang/OTP release. Testing the endpoints keeps
the routine matrix small; it does not exclude releases between them from
support. Earlier intermediate-version results retain their recorded scope.
Inspect the individual jobs at the commit you will consume. A minimum-version
check does not prove runtime support on every newer release. Tracking builds,
macOS, native Windows and psmux are not advertised as qualified platforms.
Linux must provide readable `/proc/self/task//children`, enabled by
CONFIG_PROC_CHILDREN. The helper checks this interface before launching a
child. An unavailable interface returns `unsupported_capability` with
`capability => proc_children`, the OS `errno` and `outcome => not_started`.
See the [helper protocol](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/apps/libtmux_erlang/c_src/README.md) for cleanup
evidence and the limits of native process ownership.
## Retained rows and upgrades
Native model version 2 adds a private captured target association to each
observed row. Actions check the entity, stable ID, origin and snapshot ID;
pane associations also retain the observed window and index, and window-link
associations retain their session, index and expected physical window.
Actions reject edited target fields before dispatch. Valid-shaped binding
mismatches return `observation_mismatch`; detached and malformed rows retain
their validation errors. [`tmux_server:bind/2`]() reads the requested target and returns
a new actionable row. Ordinary record matching and detached constructors
remain available.
The private value retains identifiers, not the snapshot graph. It prevents
accidental retargeting through public row updates; it is not an authorization
boundary against trusted code running inside the VM. The internal construction
functions remain implementation details.
Discard version-1 rows and snapshots when upgrading to model version 2. Stop
consumers that retain them, clear their process/ETS state and freshly observe
the server after restart. This release supplies no `appup`/`relup` conversion.
Old or unknown model versions fail row validation. New code must validate a
retained value before accessing fields added by another model version.
Every exported-field addition, rename, removal or default change requires a
model compatibility review independently of the OTP version. Its changelog
must state whether retained values remain usable, need an explicit validated
conversion, or must be discarded. Do not assume hot loading changes fields in
existing native values.
[`tmux_model:to_map/1`]() extracts the documented domain fields from a valid row.
Validate those fields with the destination model's constructor when migrating
data. The map loses actionable provenance; bind it through a freshly
constructed row before an action. It is not a versioned storage or cross-port
wire format: absence atoms, binary bytes and field inventories still require
an application-defined schema.
## Implemented surfaces
| Surface | Current behavior |
| --- | --- |
| Model | Exported native session/window/window-link/pane records; runtime validation |
| Capture | Immutable complete graph, coverage, ordering, conflict detection and read intervals |
| Local query | Map predicates, Boolean callbacks, quantifiers, cardinality and bounded JSON |
| Source query | Explicit plans/fetch, conservative candidates, full residuals and final limits |
| Operations | Bounded one-shot submission, waits, cancellation, transfer and cleanup |
| Control attachment | Explicit session, same-connection identity, no-output/ignore-size, skipped environment update and separate native cleanup |
| Daemon ownership | Empty private startup, separate ownership token, caller lifetime, explicit stop and identity-checked namespace cleanup |
| Domain commands | Initial create/read/rename/kill calls, explicit client/session selection, checked window memberships, pane split/break/join/move/swap/resize, pane/window respawn, window layouts, literal/key input and capture |
| Options | Explicit local/global tables, local or inherited reads, indexed writes/unsets and generated source metadata; hooks remain outside the option API |
| Hooks | Versioned builtin arrays, indexed command values, inheritance and explicit execution contexts |
| Buffers | Named or automatic binary load, exact show, metadata lists, delete/rename and paste; native version differences are explicit |
| Server utilities | Command values for literal source-file/stdin configuration and native [wait-for](https://libtmux.org/en/tmux/latest/manual/wait-for/) signals, waits and explicit advisory locks |
| Pane pipe | Literal shell command, output/input/both directions, native toggle and explicit stop; tmux-owned process and delivery checks |
The [examples](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/examples) exercise the current public API. Real integration
checks use private sockets and verify owned process retirement. A captured row
can disappear before an action; stable IDs are not permission to reinterpret a
missing target positionally.
## Not yet shipped
Control command submission and groups and output subscriptions/streams remain
implementation work. Workspace planning/apply and the CLI use the one-shot
path. The [workspace guide](https://libtmux.org/en/erlang/latest/guides/workspace-json/) includes executable loader and
apply examples. SIGTERM exercises explicit cancellation and returns JSON.
Installed-consumer checks also cover terminal Ctrl-C and SIGKILL: both exit by
signal without a receipt, retire observed helper/client processes, and preserve
the borrowed server and existing panes. Remote steps can remain applied and
temporary files can remain; see [CLI interruption](https://libtmux.org/en/erlang/latest/guides/workspace-json/#interrupt-the-cli).
Complete performance qualification remains open.
MCP packages, servers and SDK integration are excluded by
[project policy](https://libtmux.org/en/erlang/latest/topics/project-policy/).
Typed command coverage, generated capability inventory, registry delivery,
public cross-port filter conformance, published native API pages, shared-site documentation and
independent implementation reviews remain open. Current test success does not
pass those unfinished surfaces. No registry or documentation publication is
claimed.
## Source installation
The [source-package check](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/tools/check_source_package.py) builds each
application separately with pinned `rebar3_hex` tooling. It serves the exact
tarballs through a signed local registry, then creates a fresh Rebar3 consumer
whose only dependency is workspace. Core must resolve transitively and compile
its helper from packaged C source. Missing compiler support fails installation.
The installed examples, native-record imports, model-upgrade check and owned
transport checks run against that consumer.
CI runs this gate on both supported architectures with tmux 3.3a and 3.8.
Inspect its receipt for the commit being consumed. These local source artifacts
retain the development version; the check neither publishes to Hex nor proves
an independently retrieved registry release. The older compiled-archive check
remains separate evidence for its own payload.
## Performance evidence
The current suite checks behavior and resource limits. It does not establish a
control/grouping/streaming comparison because those execution lanes are not implemented.
Any published benchmark must compare equal workloads and values, state the
source/tool/platform identities, and report errors and cleanup as well as time.
Test-loop durations are not throughput benchmarks.
---
# Workspace JSON loader
Source: https://libtmux.org/en/erlang/latest/guides/workspace-json/
> Workspace JSON loader: libtmux_erlang documentation.
Load desired sessions, windows and panes with the workspace application's
load function, then inspect the normalized graph with its to_map function. The
workspace application depends on the core's public JSON API and OTP libraries.
Loading performs filesystem I/O. Planning observes tmux without mutations;
applying executes the saved operations. The standalone CLI uses the same
workspace application and the core's public one-shot operations.
## Describe the desired graph
[This input file](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/examples/workspace.json) declares one session, one window
and two panes. `endpoint` names a host-configured connection; it cannot choose
a socket. `argv` is literal executable data. `shell` explicitly requests shell
syntax. Apply requires both the plan's shell opt-in and host policy approval.
```json
{
"version": 1,
"id": "demo",
"endpoint": "dev",
"sessions": [
{
"name": "editor",
"windows": [
{
"name": "api",
"layout": "even-horizontal",
"panes": [
{"cwd": ".", "argv": ["rebar3", "shell"]},
{"cwd": ".", "shell": "exec erl"}
]
}
]
}
]
}
```
Relative working directories use the canonical workspace file directory.
Symlinks resolve before `..`; directories must already exist. There is no
environment interpolation. Each pane requires exactly one launch form.
An explicit `id` keeps workspace identity stable when moving the file.
Otherwise the loader hashes its canonical filename. Optional entity `key`
fields identify sessions, windows and panes across edits. Without a key, the
loader uses the entry's JSON pointer; reordering entries changes identity.
Keys must be unique across the workspace.
Explicit keys preserve identity when reordering entries. Apply orders managed
windows and panes as listed, preserving unmanaged resources. Changing argv or
cwd plans a respawn: it replaces that pane's running process. Current command
names cannot reconstruct the original launch.
## Build the CLI
Use the pinned OTP and Rebar3 toolchain plus the Linux C11 build prerequisites
in [Contributing](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/.github/CONTRIBUTING.md#setup). From this source checkout:
```console
$ mise exec -- rebar3 escriptize
```
Inspect the command help:
```console
$ mise exec -- _build/default/bin/libtmux-workspace --help
```
The resulting escript includes the compiled helper, its MIT license and source
provenance. Each invocation materializes those assets in an owned temporary
directory, starts its core application, closes its endpoint and removes the
directory before returning its JSON result. BEAM modules remain in the archive.
Run the copied escript with OTP 29.1.2 installed on the same Linux architecture.
It needs neither a compiler nor this checkout at runtime. Registry publication
is a separate gate; no published workspace version is claimed here.
## Configure the endpoint
Create a trusted host configuration file. Replace the socket path below with
the absolute path of your existing tmux server; no default server is selected
or started. The optional executable is an absolute tmux path, otherwise PATH
selects it. Workspace input cannot override either path.
```json
{
"version": 1,
"allowShell": false,
"endpoints": {
"dev": {"socketPath": "/tmp/project/tmux.sock"}
}
}
```
Save that file as host.json. The CLI also accepts LIBTMUX_WORKSPACE_CONFIG
when the config option is omitted. Shell launches need host allowShell true
and an explicit allow-shell option when creating the plan.
## Review a plan, then execute it
The [argv-only example](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/examples/workspace_apply.json) declares a session,
one window and two long-lived panes. Replace cat with your own programs after
trying the complete owned-server example below. From the checkout, plan it:
```console
$ mise exec -- _build/default/bin/libtmux-workspace plan \
examples/workspace_apply.json \
--config host.json \
--output plan.json
```
Planning writes canonical JSON with mode 0600 and prints its hash and step
count. The file records stable targets, ownership, preconditions, schema and
input hashes. It retains the source filename and observed names, while omitting
launch argv, shell text and cwd. Review it before applying:
```console
$ mise exec -- _build/default/bin/libtmux-workspace apply plan.json \
--config host.json
```
Apply reloads the desired file and verifies its hash. It checks that the stored
operations follow from that file and the stored observation, then executes
exactly those operations. Fresh state can reject the plan; it never supplies
replacement operations. Each step checks IDs, names, memberships, pane order,
layouts and ownership before and after its command. Captures span commands,
so concurrent changes can still intervene.
There is no atomic reservation, automatic retry or terminal-effect rollback.
Ownership reads group up to seven rows of one entity kind per native sequence,
using [`tmux_option:get_many/3`]() with inheritance disabled. At most three
workers run concurrently, retaining the captured row order. Every row's
metadata is still read; graph observations and per-step preconditions remain.
Cancellation stops the next worker batch and drains reads already in flight
before returning. The core deadline applies to each native sequence; there is
no additional workspace batch deadline. Lost results leave cleanup unverified,
and a later cleanup failure remains visible alongside an earlier metadata
error. [ADR 0003](https://github.com/libtmux/libtmux-erlang/blob/d1b6308440cf9eb8f6647e7ae434bce60e849368/docs/adr-0003-scoped-option-batches.md) records the client-cost
tradeoff and interval semantics.
Pane creation uses fixed targets and the saved unzoomed layout. It distributes
splits across tiled cells, then applies the requested arrangement and pane
order. Floating cells are excluded. For a new window, the plan stores a
breadth-first target schedule; apply chooses the longer usable axis from each
checked cell. Available area and options such as reserved scrollbars can
still reject a split. The failure retains the completed prefix; apply never
substitutes another target. A pane count within the file limit does not promise
that the server's window geometry can accommodate it.
Run the plan command again with unchanged managed state: its step count is
zero. An existing untagged session/window name instead conflicts. To adopt it,
add one adopt option for each explicit resource key you intend to manage;
pane adoption respawns its ordered target. Linked windows with another
membership, duplicate ownership and missing/tampered metadata conflict.
Removal requires matching ownership for the whole affected subtree.
### Interrupt the CLI
Completed CLI calls write one JSON line to stdout; help writes text. OTP
diagnostics use stderr. Exit codes are 0 for success, 2 for validation,
3 for changed input or observed conflict,
and 4 for operational failure, cancellation or uncertain outcome. Failures
retain the verified completed prefix and native command outcome separately.
A native creation can return success while its process exits before observation;
that returns creation_not_observed with the reported IDs for explicit recovery.
SIGTERM requests cancellation and waits for owned client cleanup. A command
possibly delivered to tmux can return unknown; earlier steps remain applied.
Ctrl-C sends SIGINT. OTP 29's escript launcher disables the VM break handler,
so the standalone CLI exits by signal without a JSON receipt. SIGKILL also
returns no receipt. These exits retire observed helpers and tmux clients
in the installed-consumer checks; the borrowed server and existing panes remain.
Completed remote steps remain applied. Abrupt exit can leave the CLI's private
temporary directory behind, so callers must not infer rollback or file cleanup
from process exit. See the
[OTP escript launcher](https://github.com/erlang/otp/blob/OTP-29.1.2/erts/etc/common/escript.c#L509-L522)
and [emulator flags](https://www.erlang.org/doc/apps/erts/erl_cmd.html#emulator-flags).
Cancellation in Erlang uses an application-owned reference and a cancel message,
as documented by the apply module.
The initial execution strategy recaptures the complete projected graph per
step. It favors drift detection over source-call cost; large-workspace
performance remains unqualified. See [snapshot costs](https://libtmux.org/en/erlang/latest/concepts/queries/).
## Run the complete apply example
This program owns its tmux server, applies a real input/plan file, inspects the
resulting object graph and verifies an empty second plan. Scope cleanup keeps
operation and cleanup failures together, then retires the owned server.
```console
$ mise exec -- escript tools/run_examples.escript workspace-apply
```
```erlang
-module(workspace_apply_demo).
-moduledoc "Apply a workspace and verify an empty second plan.".
-export([run/1]).
-spec run(binary()) -> map().
run(Executable) ->
Suffix = binary:encode_hex(crypto:strong_rand_bytes(10), lowercase),
Directory = <<"/tmp/ltm-workspace-apply-", Suffix/binary>>,
ok = file:make_dir(Directory),
Result = tmux_scope:run_or_error(fun() ->
ok = file:change_mode(Directory, 8#700),
Source = filename:join(Directory, <<"workspace.json">>),
PlanFile = filename:join(Directory, <<"plan.json">>),
{ok, _} = file:copy("examples/workspace_apply.json", Source),
{ok, Output} = tmux_server:with_owned(#{executable => Executable},
fun(Server) ->
{ok, Plan} = tmux_workspace_plan:new(Source, Server, #{}),
ok = tmux_workspace_plan:write(Plan, PlanFile),
{ok, Stored} = tmux_workspace_plan:read(PlanFile),
{ok, #{completed := Completed}} =
tmux_workspace_apply:run(Stored, Server, #{}),
true = length(Completed) > 0,
{ok, Snapshot} = tmux_server:snapshot(Server),
[Session] = tmux_snapshot:sessions(Snapshot),
<<"project">> = tmux_model:get(name, Session),
{ok, [Window]} =
tmux_snapshot:related(windows, Session, Snapshot),
<<"api">> = tmux_model:get(name, Window),
{ok, Panes} = tmux_snapshot:related(panes, Window, Snapshot),
2 = length(Panes),
{ok, Settled} = tmux_workspace_plan:new(Source, Server, #{}),
#{<<"steps">> := []} = tmux_workspace_plan:to_map(Settled),
{ok, #{completed := []}} =
tmux_workspace_apply:run(Settled, Server, #{}),
#{sessions => 1, windows => 1, panes => 2,
plan_round_trip => true, second_plan_empty => true}
end),
Output
end, fun() -> file:del_dir_r(Directory) end),
{error, enoent} = file:read_file_info(Directory),
Result#{files_removed => true}.
```
```json
{
"sessions": 1,
"windows": 1,
"panes": 2,
"plan_round_trip": true,
"second_plan_empty": true,
"files_removed": true
}
```
## Read and inspect it
Compile this checkout first:
```console
$ mise exec -- rebar3 compile
```
Run the complete program below:
```console
$ mise exec -- escript tools/run_examples.escript workspace
```
The program copies the real JSON file to an owned temporary directory, checks
the desired object graph and canonical cwd, and verifies removal of its files.
The outer check repeats it using installed core and workspace OTP archives.
```erlang
-module(workspace_demo).
-moduledoc "Load a real workspace file and inspect its desired object graph.".
-export([run/0]).
-spec run() -> map().
run() ->
Suffix = binary:encode_hex(crypto:strong_rand_bytes(10), lowercase),
Directory = <<"/tmp/ltm-workspace-demo-", Suffix/binary>>,
ok = file:make_dir(Directory),
Output = tmux_scope:run_or_error(fun() ->
ok = file:change_mode(Directory, 8#700),
Path = filename:join(Directory, <<"workspace.json">>),
{ok, _} = file:copy("examples/workspace.json", Path),
{ok, Workspace} = tmux_workspace:load(Path),
#{id := Id, endpoint := Endpoint, sessions := [Session]} =
tmux_workspace:to_map(Workspace),
#{name := SessionName, windows := [Window]} = Session,
#{name := WindowName, layout := <<"even-horizontal">>,
panes := [Build, Shell]} = Window,
#{cwd := Directory, launch := #{argv := Arguments}} = Build,
[<<"rebar3">>, <<"shell">>] = Arguments,
#{cwd := Directory, launch := #{shell := <<"exec erl">>}} = Shell,
#{workspace_id => Id, endpoint => Endpoint,
session => SessionName, window => WindowName, panes => 2,
first_pane_key => maps:get(key, Build), canonical_cwd => true,
shell_declared => true, launched => false}
end, fun() ->
ok = file:del_dir_r(Directory)
end),
{error, enoent} = file:read_file_info(Directory),
Output#{files_removed => true}.
```
```json
{
"workspace_id": "demo",
"endpoint": "dev",
"session": "editor",
"window": "api",
"panes": 2,
"first_pane_key": "/sessions/0/windows/0/panes/0",
"canonical_cwd": true,
"shell_declared": true,
"launched": false,
"files_removed": true
}
```
## Handle validation failures
Errors include a source basename, a `code` and a JSON `path` with zero-based
array indexes. Unknown fields, duplicate JSON object keys and duplicate
entity keys fail before any tmux operation. A pane with both launch forms
returns `ambiguous_launch`; a nonexistent directory returns `invalid_directory`.
The loader accepts version 1, at most 256 KiB of JSON and 32 nested
containers. Collections contain 1–64 entries and a workspace has at most
2048 entities. IDs and keys use at most 128 UTF-8 bytes; names use at most
256. Names exclude controls and backslashes; session names also exclude
periods and colons because tmux rewrites them. Launch argv has at most 256
elements and a 32 KiB aggregate bound. Layouts use the five standard named
tmux arrangements.
Local filesystem checks are synchronous and assume trusted directories.
They provide byte bounds and canonicalization, not a filesystem sandbox,
an I/O deadline or protection against concurrent replacement of path entries.
See the [API browser](https://libtmux.org/en/erlang/latest/guides/api-documentation/) for native types and
[project policy](https://libtmux.org/en/erlang/latest/topics/project-policy/) for application boundaries.
---
# Linking and consuming our docs
Source: https://libtmux.org/en/erlang/latest/meta/linking/
> Find documentation exports for search engines, bots, LLMs and other documentation sites.
We welcome bots, search engines, SEO tools, LLMs and other clients to use our
documentation. Read a page in your browser, fetch its Markdown, discover
pages through a manifest, or link to documented objects from another site.
## Choose an export
The page footer links to the exports for its documentation tree. Use those
links to select a language, version or preview.
| File or format | Use it for |
| --- | --- |
| [llms.txt](https://libtmux.org/en/llms.txt) | Discover guides, API references and agent prompts through a Markdown index. |
| [llms-full.txt](https://libtmux.org/en/llms-full.txt) | Fetch guides and MCP contracts together as Markdown. API references are linked separately. |
| [docs.json](https://libtmux.org/en/docs.json) | Discover page titles, URLs, descriptions, headings and Markdown URLs in a structured manifest. |
| Markdown | Read one page as text using its footer's **Markdown** link. |
| Raw source | Inspect the source file using its footer's **Raw source** link, where available. |
| [objects-full.inv](https://libtmux.org/en/objects-full.inv) | Resolve Sphinx references across shared docs, each port's default version, or its latest until the default has published a catalog, and tmux reference versions. |
| objects.inv | Resolve Sphinx references within one port and version, or one tmux documentation tree. |
| [targets.json](https://libtmux.org/en/targets.json) | Browse inventory keys, display names and destination URLs as JSON. A scoped catalog sits beside each root inventory. The root one lists fallbacks and ports left out. |
The root exports above cover the assembled site. Port exports describe the
selected port and version. For a single API declaration, start with
`docs.json` and follow its reference and symbol-index links.
## Link from Sphinx
The [Intersphinx guide](https://libtmux.org/en/meta/intersphinx/) explains how to configure
inventories, choose native roles or portable labels, and preserve references
when a page moves or changes its title.
Browse [Intersphinx examples](https://libtmux.org/en/meta/intersphinx/examples/) for copyable RST
and MyST references to library APIs, tmux C declarations, hooks, format
variables, CLI commands and schema fields, paired with their destinations.
## Discover and index pages
Use the [sitemap](https://libtmux.org/en/sitemap-index.xml) to discover indexable pages. Follow
[robots.txt](https://libtmux.org/robots.txt) and each page's robots metadata:
pull-request previews, component demos and search-index shards are excluded
from crawling, and alternate versions can carry `noindex`.
Choose a versioned documentation tree when a reference needs a particular
release. The full inventory follows each port's advertised default; a
port's `objects.inv` describes the selected version.
---
# Link to libtmux from Sphinx
Source: https://libtmux.org/en/erlang/latest/meta/intersphinx/
> Use Intersphinx inventories to reference libtmux APIs and documentation.
Sphinx can resolve references to libtmux APIs, guides, headings, MCP objects,
schema fields, and other documented targets. The footer links to the full
inventory and the inventory for the page's port or tmux version.
Browse [Intersphinx examples](https://libtmux.org/en/meta/intersphinx/examples/) for copyable RST
and MyST references paired with their destinations.
For Markdown exports, manifests and crawler discovery, see
[Linking and consuming our docs](https://libtmux.org/en/meta/linking/).
## Configure Intersphinx
Enable the built-in extension in your project's `conf.py`:
Python's [`None`]() discovers `objects.inv` at the configured base URL. The port mapping
above selects the Python `latest` tree. Copy the base URL of another port
or version to select its inventory.
The full inventory includes shared documentation, the configured default
version of each assembled port, and distinct tmux reference versions.
It is generated after site assembly. Each port inventory includes the
rendered targets in that exact version.
The tmux inventory includes the shared tmux guides and the latest CLI manual,
options, hooks, formats, and C source reference. A version mapping such as
`tmux-3-7c` includes only that release's manual, catalog, and C declarations.
Manual and C reference pages link to the same version inventory in the footer.
Use mapping names without dots when writing MyST reference roles.
## Choose a reference
Every exported target has a portable label that works with `std:ref`, without
installing a language or MCP domain. Open `targets.json` beside the inventory
and copy its `key`. Its `href` and `display` identify the destination.
For example, the Python [`libtmux.Pane`]() declaration has this reStructuredText
reference:
```rst
:external+libtmux:ref:`Pane `
```
With MyST enabled in your project, the same label works in Markdown:
```markdown
{external+libtmux:ref}`Pane `
```
Native Python, C++, JavaScript, and C objects also have aliases for their
built-in roles. A scoped Python inventory supports:
```rst
:external+libtmux-python:py:class:`libtmux.Pane`
```
The tmux mapping provides native C roles and portable manual references:
```rst
:external+tmux:c:struct:`cmd`
:external+tmux:ref:`capture-pane `
```
Command options also work with Sphinx's program context:
```rst
.. program:: tmuxp load
:external+libtmux-python:option:`--append`
```
The full inventory qualifies native names by port, such as
`py:libtmux.Pane`. A fully qualified alias includes the product too:
`py:core:libtmux.Pane`. Short aliases are offered only when they identify
one destination. Configuration values, command options, environment
variables, and glossary terms use standard-domain types where their
renderers identify those semantics.
## Stable labels
A portable key contains `scope:product:category:identity`. Its encoding
preserves uppercase letters, punctuation, and Unicode, because `std:ref`
lowercases targets. `%50ane` and `pane` therefore remain distinct.
Copy the key from the catalog without decoding its percent escapes.
Keys are independent of display titles and destination URLs. API public IDs,
protocol wire identities, JSON Pointers, named parameters, and native method
identities supply their names. Authored documents can retain `inventoryId`
and explicit HTML fragment IDs when their titles or routes change.
Contributors preserve renamed keys in the inventory alias catalog and use
the existing page redirect mechanism for retired routes.
### Preserve published links
If `meta/intersphinx.md` moves to `meta/linking.md`, retain its logical ID
and list the retired route in the new page's frontmatter:
```yaml
inventoryId: intersphinx
aliases:
- meta/intersphinx
```
The document key remains `site:core:doc:intersphinx`, and the old route
redirects to the new page. Retain existing heading fragment IDs when their
text changes.
When an identity or fragment changes intentionally, map the former keys to
current targets in `site/src/data/inventory-aliases.json`. For example,
choosing `inventoryId: linking` and changing `stable-labels` to
`stable-references` needs these entries:
```json
[
{
"scope": "site",
"from": "site:core:doc:intersphinx",
"to": "site:core:doc:linking"
},
{
"scope": "site",
"from": "site:core:heading:intersphinx%23stable-labels",
"to": "site:core:heading:linking%23stable-references",
"fragment": "stable-labels"
}
]
```
The document entry preserves `std:ref` and `std:doc` references. The heading
entry preserves `std:ref`; `fragment` also installs the retired HTML anchor
at the new destination. Keep the route alias too: inventory aliases do not
redirect retired URLs.
Copy `to` from the current target catalog. Retain earlier `from` entries
and update their destinations when renaming again. Former native role names,
such as a Python class name, need preservation in the native metadata.
Existing `objects.inv` and `reference/objects.inv` endpoints remain compatibility
exports. New integrations should use the full inventory, a port's root
inventory, or a tmux root inventory to obtain the complete target catalog
and case-safe labels.
See the [Sphinx Intersphinx documentation](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html)
for named inventories and cross-reference syntax.
---
# Intersphinx examples
Source: https://libtmux.org/en/erlang/latest/meta/intersphinx/examples/
> Compare Sphinx and MyST references to APIs, tmux C declarations, hooks, format variables, CLI commands and schema fields.
import { referenceHref } from '../../../../lib/api-models'
Copy a reference and compare its destination below. These examples cover
native language roles, portable labels, full and scoped inventories, and
links into tmux C declarations, hooks, format variables and CLI commands.
## Configure the inventories
Add these mappings to your Sphinx project's `conf.py`:
Python's [`None`]() discovers `objects.inv` at the mapping's base URL. The full inventory
uses each port's advertised default version. The `py` mapping explicitly
selects `latest`; `tmux` selects the latest tmux manual and C reference.
Use the root URL from an inventory's footer link when selecting a preview
or another release.
## Native Python roles
A named inventory selects the Python tree:
```rst
:external+py:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
The full inventory qualifies native names by port:
```rst
:external+full:py:class:`py:libtmux.Pane`
```
Destination: Pane in the Python default version.
For automatic lookup, use the ordinary role. A local definition takes
precedence over Intersphinx:
```rst
:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
## Portable labels
Portable labels work with the standard reference role, including languages
that have no built-in Sphinx domain. Copy the key from `targets.json`.
```rst
:external+full:ref:`Pane `
:external+py:ref:`Pane `
```
Destinations: Full inventory: Pane,
[Scoped inventory: Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
The external role searches the configured inventories without selecting one.
This native name is unqualified only in the scoped Python inventory:
```rst
:external:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
## MyST roles and inventory URLs
Enable `myst_parser` alongside
[`sphinx.ext.intersphinx`](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html)
for Markdown sources.
Named roles accept the same targets as reStructuredText:
```markdown
{external+py:py:class}`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
Portable labels retain their percent escapes inside reference roles:
```markdown
{external+full:ref}`Pane `
```
Destination: Pane in the Python default version.
MyST's inventory URL syntax also supports native aliases:
```markdown
[Pane](inv:py:py:class#libtmux.Pane)
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
Use role syntax for percent-encoded portable keys: MyST normalizes escapes
in `inv:` URLs. Keep mapping names free of dots, as in `tmux-3-7c`, so MyST
can parse the role name. When using an inventory URL, specify its domain and
object type if a name is both a document and a label.
## tmux C declarations
The tmux mapping provides Sphinx's built-in C roles:
```rst
:external+tmux:c:struct:`cmd`
:external+tmux:c:func:`cmd_find_target`
```
Destinations: [cmd](https://libtmux.org/en/tmux/latest/reference/c-struct-cmd/#c%3Astruct%3Acmd),
[cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
Select a release with its own mapping. The full inventory also offers
version-qualified native aliases:
```rst
:external+tmux-3-7c:c:func:`cmd_find_target`
:external+full:c:func:`tmux@3.7c:cmd_find_target`
:external+full:c:func:`tmux@latest:cmd_find_target`
```
Destinations: [Scoped: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Full: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Full: cmd_find_target in latest](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
MyST supports the same C roles and a typed inventory URL:
```markdown
{external+tmux:c:struct}`cmd`
{external+tmux:c:func}`cmd_find_target`
{external+full:c:func}`tmux@3.7c:cmd_find_target`
[cmd_find_target](inv:tmux:c:function#cmd_find_target)
```
Destinations: [MyST: cmd](https://libtmux.org/en/tmux/latest/reference/c-struct-cmd/#c%3Astruct%3Acmd),
[MyST: cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[MyST full: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Inventory URL: cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
The C reference links each declaration to its source file and revision.
Portable labels also address its signatures, parameters, and sections.
## tmux hooks
Link a hook name to its catalog page with a portable label. The same key
works in the scoped tmux inventory and the full inventory:
```rst
:external+tmux:ref:`after-new-session `
:external+full:ref:`after-new-session `
```
Destinations: [Scoped: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/),
[Full: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/).
In MyST, use the same key inside a reference role. Preserve its percent
escapes:
```markdown
{external+tmux:ref}`after-new-session `
{external+full:ref}`after-new-session `
```
Destinations: [MyST scoped: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/),
[MyST full: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/).
## tmux formats
Link a format variable such as [`#{session_name}`](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/) to its catalog entry:
```rst
:external+tmux:ref:`session_name `
:external+full:ref:`session_name `
```
Destinations: [Scoped: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/),
[Full: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/).
The MyST references select the same scoped or full inventory:
```markdown
{external+tmux:ref}`session_name `
{external+full:ref}`session_name `
```
Destinations: [MyST scoped: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/),
[MyST full: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/).
## tmux CLI manual
The same tmux inventory covers command pages:
```rst
:external+tmux:ref:`capture-pane `
```
Destination: [capture-pane](https://libtmux.org/en/tmux/latest/manual/capture-pane/).
The document role works when the target is a whole page:
```rst
:external+tmux:doc:`Full manual `
```
Destination: [Full tmux manual](https://libtmux.org/en/tmux/latest/manual/full/).
Select a command's manual page with the document role. Its portable key
also works in the full inventory:
```rst
:external+tmux:doc:`tmux new-session `
:external+full:doc:`tmux new-session `
```
Destinations: [Scoped: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/),
[Full: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/).
Use the same document role in MyST:
```markdown
{external+tmux:doc}`tmux new-session `
{external+full:doc}`tmux new-session `
```
Destinations: [MyST scoped: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/),
[MyST full: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/).
## Command options, environment variables, and terms
The Python port's workspace documentation exports standard-domain objects.
Set the program context before referencing an option:
```rst
.. program:: tmuxp load
:external+py:option:`--append`
```
Destination: [tmuxp load --append](https://libtmux.org/en/py/latest/workspace/cli/load/#arguments-and-flags-a-append).
Environment variables and glossary terms use their own roles. Qualify the
product when a term appears in more than one glossary:
```rst
:external+py:envvar:`COLUMNS`
:external+py:term:`async `
```
Destinations: [COLUMNS](https://libtmux.org/en/py/latest/workspace/configuration/environment/#cli-and-runtime-variables-columns-lines-rows),
[async](https://libtmux.org/en/py/latest/reference/glossary/#async).
## MCP tools and schema fields
Portable labels also address protocol objects and individual JSON Schema
fields. No custom MCP domain is required:
```markdown
{external+py:ref}`Read tools batch `
{external+py:ref}`on_error `
```
Destinations: [Read tools batch](https://libtmux.org/en/py/latest/mcp/tools/call_read_tools_batch/#mcp.tool.call_read_tools_batch),
[on_error](https://libtmux.org/en/py/latest/mcp/tools/call_read_tools_batch/#mcp.tool.call_read_tools_batch.input%2Fproperties%2Fon_error).
## Shared documentation and headings
The full inventory includes the shared documentation:
```rst
:external+full:doc:`Inventory guide `
:external+full:ref:`Configure Intersphinx `
```
Destinations: [Inventory guide](https://libtmux.org/en/meta/intersphinx/),
[Configure Intersphinx](https://libtmux.org/en/meta/intersphinx/#configure-intersphinx).
## Local files and fallback locations
Download an inventory and pass its filename as the second mapping value.
The first value remains the documentation URL used in generated links:
An ordered list tries a local copy before the HTTP inventory:
See the [inventory guide](https://libtmux.org/en/meta/intersphinx/) for key stability and the
[Sphinx Intersphinx reference](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html)
for configuration options.
---
# AI policy
Source: https://libtmux.org/en/erlang/latest/meta/ai-policy/
> Use the libtmux packages with AI tools. When contributing, check what you submit and wait for a maintainer's invitation before preparing a pull request.
Last revised October 25, 2026
Use these packages with AI tools as you like. When contributing, check what
you submit and wait for a maintainer's invitation before preparing a pull
request.
This policy covers the libraries, ports, and documentation in the
[libtmux](https://github.com/libtmux) and
[tmux-python](https://github.com/tmux-python) organizations, including their
issues, comments, discussions, reviews, and pull requests.
## 1. For users: About these packages
We encourage you to use AI tools with our libraries and documentation,
including to generate code that calls the libraries. We want these projects to
be useful in the way you work. Some of what we publish supports that workflow:
- [MCP integrations](https://libtmux.org/en/py/latest/mcp/) that let assistants interact with tmux.
- Markdown documentation and `llms.txt` and `llms-full.txt` resources linked
from documentation pages.
- `AGENTS.md` and other repository instructions for tools working on the
codebase.
Use the documentation for the package, language, and version you are working
with.
### How the ports are made
The ports build on [tmuxp](https://tmuxp.git-pull.com/), started in 2013, and
[libtmux](https://libtmux.git-pull.com/), started in 2016. Both projects
predate today's LLM coding tools. Current work on the original projects and the
ports uses AI assistance.
Maintainers guide the design, review changes, test behavior against real tmux,
and decide what ships. They remain responsible for what the projects include
and maintain.
### Stability
Packages in the [libtmux organization](https://github.com/libtmux) are
experimental unless their README says otherwise. Names and APIs may change
between releases. Pin an exact version and review changes before upgrading.
## 2. For contributors: How to report issues, ideas, suggestions, and security issues
A useful issue is a contribution in its own right. Your experience helps us see
what breaks, what feels awkward, and what is missing. You do not need to arrive
with a patch.
Open bugs, ideas, and suggestions in the repository of the package they
concern. Report suspected security vulnerabilities privately using the route
below.
### Bugs
Tell us what you tried, what happened, and what you expected. Include a small
example we can run, the library and tmux versions, and the actual output.
Include relevant logs and error messages as they appeared, and distinguish what
you observed from possible explanations.
Bug reports are welcome whether the code you were running was handwritten or
generated. Include the code that ran. A conversation transcript is usually
unnecessary; include relevant prompts or tool calls when they are needed to
reproduce the problem.
### Ideas and suggestions
Explain what you wanted to accomplish and where the current design gets in
your way. We welcome feedback on:
- Usability: what was hard to find or get right, and where the documentation
misled you.
- Language conventions: whether a port feels natural in the language you use.
- Aesthetics: how an API, a page, or an error message reads.
- Missing capabilities: what you could not accomplish with the current API.
If an API you expected is missing, describe the task and why that API would
help.
### Security issues
Report suspected security vulnerabilities privately. Use **Report a
vulnerability** on the affected repository's security page where available.
Include the affected package and version, the tmux version, what you observed,
and reproduction steps or relevant code references if available. Distinguish
verified behavior from suspected impact. Please coordinate public disclosure
with us so we can investigate and, where needed, prepare a fix.
Command execution explicitly requested through documented features is expected
behavior and does not, by itself, establish a vulnerability. Explain any
additional failure, such as data being interpreted as commands unexpectedly or
an intended security boundary being crossed.
### Writing your report
Short reports, typos, and imperfect English are fine. Translation and writing
assistance are welcome; check that the result says what you mean and preserves
the facts you observed. Your experience matters more than a polished report.
### Using AI tools
If AI materially helped prepare your issue, comment, discussion, review, or
pull request, say which tools you used, what they did, and what you checked
yourself. Using these tools does not count against a contribution. You remain
responsible for what you submit.
Do not send generated code, reports, or explanations you have not checked.
### Pull requests
Before preparing a pull request, open an issue or comment on an existing issue
about the proposed change. Wait for a maintainer to invite a PR for an agreed
scope. Opening an issue alone is not an invitation. PRs submitted without that
agreement may be closed without review. Arrange security fixes through the
private reporting route.
Even when we agree that something should change, we may prefer to write the
implementation ourselves. Review must consider correctness, supported tmux
versions, language conventions, documentation, and ongoing maintenance.
Checking those can take more time than writing the change, whether it was
handwritten or generated.
If we invite a PR, understand the changes, test them, and be able to explain
them. Follow the project's contribution instructions, and describe the tests
you ran and the versions you used. Generated code and tests can share the same
mistake, so passing tests alone do not show that a change fits the project.
Submit only material you have the right to contribute on terms compatible with
the project's license. Identify outside code, tests, or text incorporated into
the contribution, including material adapted with AI assistance, and provide
its source and license. Keep required notices, and raise unclear permissions
before including the material. These source and licensing requirements apply
to maintainers too.