# Captures, effects and streams

Source: https://libtmux.org/en/clojure/latest/guides/captures-and-effects/

> Captures, effects and streams: io.github.libtmux:libtmux-clojure documentation.

`libtmux-clojure` is a JVM façade over the Java library. It keeps tmux command
encoding, transport, server identity and compatibility in Java, while exposing
captured persistent Clojure values and ordinary collection operations.

Start with the package [README](https://libtmux.org/en/clojure/latest/guides/getting-started/) for the
install boundary and one capture. This guide explains the choices that matter
after the first program: what is captured, what can perform I/O, who owns
workers and streams, and where model-facing MCP tooling begins.

The native API references list every documented public var in
[the core façade](https://libtmux.org/en/clojure/latest/reference/) and
[the optional adapter](https://libtmux.org/en/clojure/latest/reference/libtmux-core-async/).
They derive arities and docstrings from source; module checks reject stale
output. [Captured values](https://libtmux.org/en/clojure/latest/guides/captured-values/) documents the maps separately.

For complete files with their own entry points and a private-server launcher,
use the [Clojure API programs](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/examples/api/README.md#clojure-programs).

Every Clojure fence in reader-facing documentation is discovered and run against
an owned tmux server. A fence must open and close its own client with
`with-open` and end with one `;; =>` EDN result. The test fixture supplies
`libtmux.docs.socket` and `libtmux.docs.binary`; applications supply their
socket path and use their selected binary or the default `tmux` command.

## Values, links and projection

[`sessions!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-sessions/>), [`windows!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-windows/>), [`panes!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-panes/>) and [`clients!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-clients/>) acquire a graph. `data/windows`
and `data/panes` traverse that graph without I/O. A window occurrence has a
contextual `:tmux/link`; `data/same-link?` includes the session, window and
index, while `data/same-entity?` compares only the physical identity.

`nil` means an optional relationship or lookup was absent in a successful
capture. Failed acquisition throws an [`ExceptionInfo`](<https://github.com/clojure/clojure/blob/clojure-1.12.6/src/jvm/clojure/lang/ExceptionInfo.java>); it is never represented
as an empty collection. `data/data` returns a detached EDN projection without
the Java refs required for effects or Java filter evaluation.

## Resource scopes and laziness

Acquisition returns eager captured values. A lazy `filter` or `map` over those
values reads only Clojure data, so it can escape the `with-open` scope. An I/O
operation such as [`capture!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-capture/>) is different: realize it inside the scope that
owns its server. Do not return `(map #(tm/capture! (:tmux/ref %)) panes)` from
`with-open`; use `mapv`, `into`, or another eager reduction before closing.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.core :as tm]
         '[libtmux.data :as data])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))
      {:keys [lazy-names captures]}
      (with-open [server (tm/open! {:socket-path socket-path})]
        (let [session (tm/new-session! server {:name "docs-lazy"})
              pane (first (data/panes (first (data/windows session))))]
          {:lazy-names (->> (tm/sessions! server)
                            (filter #(= "docs-lazy" (:session/name %)))
                            (map :session/name))
           :captures (mapv #(tm/capture! (:tmux/ref %) {:from :history})
                           [pane])}))]
  [(vec lazy-names) (count captures)])
;; => [["docs-lazy"] 1]
```

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.core :as tm]
         '[libtmux.data :as data])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})]
    (let [session (tm/new-session! server {:name "docs-projection"})]
      (data/data (select-keys session [:tmux/ref :session/name])))))
;; => {:session/name "docs-projection"}
```

## Native filtering and Java expressions

Use `filter`, `map`, `into` and transducers for unrestricted local Clojure
logic. Java [`FilterExpr`](<https://libtmux.org/en/java/latest/reference/io-github-libtmux-query-filterexpr-filterexpr/>) remains available when an expression must use its
canonical Java fields and relationship semantics. `data/predicate` returns an
IFn. `data/matching` returns a transducer with one argument and a lazy sequence
with an expression and collection. Both evaluate preserved `:tmux/ref` values,
not fields edited in a Clojure map.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.core :as tm]
         '[libtmux.data :as data])
(import '[io.github.libtmux Session_])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})]
    (tm/new-session! server {:name "docs-query"})
    (->> (tm/sessions! server)
         (data/matching (.is (Session_/name) "docs-query"))
         (mapv :session/name))))
;; => ["docs-query"]
```

Use `data/exactly-one` when zero or many candidates is an error, and
`data/one-or-none` when absence is allowed. Establish cardinality before any
effect.

## Effects, errors and execution modes

Every effect in [`libtmux.core`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core/>) takes a Java ref from `:tmux/ref`; a captured map
is not an editable remote object. [`new-session!`](https://libtmux.org/en/tmux/latest/manual/new-session/), [`rename!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-rename/>), [`split-pane!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-split-pane/>) and
[`refresh!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-refresh/>) return new captured values when they can recapture the result. Void
operations return `nil`.

Validation errors include `:tmux/phase :validation` and
`:tmux/dispatch :not-dispatched`. Transport and mutation failures retain the
operation, dispatch certainty and whether effects are possible. Inspect the
stable subset with `core/error-summary`; do not parse exception messages.

[`batch!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-batch/>) and [`chain!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-chain/>) use fresh Java groups. Their per-position results can be
`:complete`, `:failed`, `:skipped` or `:unknown`; neither operation promises a
transaction or rollback. A group that can wait needs `{:kind :wait}` so its
release can reserve capacity.

### Bounded capture work and cancellation

[`libtmux.async/runtime!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-async-runtime/>) bounds asynchronous work. Submit each capture as a
task, then dereference its result after the runtime has admitted it. Task
submission does not add another tmux transport or alter the captured values.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.async :as async]
         '[libtmux.core :as tm])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})
              runtime (async/runtime! {:max-running 3})]
    (tm/new-session! server {:name "docs-parallel"})
    (let [tasks (mapv (fn [_] (async/submit! runtime #(tm/snapshot! server)))
                      (range 2))]
      (mapv #(boolean (some (fn [session]
                              (= "docs-parallel" (:session/name session)))
                            (:tmux/sessions @%)))
            tasks))))
;; => [true true]
```

[`cancel!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-async-cancel/>) is explicit. A cancellation accepted after a task starts reports
unknown dispatch certainty, because the enclosing tmux operation might have
started. Use a separate `:wait` task for operations that wait for a release.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.async :as async]
         '[libtmux.core :as tm])
(import '[java.time Duration])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})
              runtime (async/runtime! {})]
    (let [started (promise)
          task (async/submit! runtime {:kind :wait}
                              #(do (deliver started true)
                                   (tm/await-channel! server "docs-cancel"
                                      (Duration/ofSeconds 20))))]
      @started
      (async/cancel! task)
      (try
        @task
        (catch clojure.lang.ExceptionInfo failure
          [(:tmux/error (ex-data failure))
           (:tmux/dispatch (ex-data failure))])))))
;; => [:cancelled :unknown]
```

### Layouts and literal input

Creation and layout functions modify tmux and return fresh observations when
they create a target. [`apply-layout!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-apply-layout/>) accepts tmux's explicit layout text.
[`send-literal!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-send-literal/>) writes characters without treating them as tmux key names;
[`send-keys!`](https://libtmux.org/en/tmux/latest/manual/send-keys/) takes an explicit key vector. Their complete return does not mean
the program in the pane has finished processing input.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.core :as tm]
         '[libtmux.data :as data])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})]
    (let [session (tm/new-session! server {:name "docs-layout"})
          window (tm/new-window! (:tmux/ref session)
                                 {:name "docs-layout-window" :detached? true})
          pane (first (data/panes window))
          _ (tm/apply-layout! (:tmux/ref window) (:window/layout window))
          split (tm/split-pane! (:tmux/ref pane)
                                {:direction :right :percent 30})]
      [(:window/name window) (= (:tmux/link pane) (:tmux/link split))])))
;; => ["docs-layout-window" true]
```

<!-- clojure-snippet: fixture -->
```clojure
(require '[clojure.string :as str]
         '[libtmux.core :as tm]
         '[libtmux.data :as data])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})]
    (let [session (tm/new-session! server {:name "docs-literal"})
          pane (first (data/panes (first (data/windows session))))]
      (tm/send-literal! (:tmux/ref pane) "printf docs-literal")
      (tm/send-keys! (:tmux/ref pane) ["Enter"])
      (boolean (some #(str/includes? % "docs-literal")
                     (tm/capture! (:tmux/ref pane) {:from :history}))))))
;; => true
```

### Contextual targeting

A linked window keeps the same physical window identity but has another
occurrence context. Effects use its preserved ref, so an unlinked occurrence
does not silently target a remaining link in another session.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.core :as tm]
         '[libtmux.data :as data])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})]
    (let [first-session (tm/new-session! server {:name "docs-link-first"})
          second-session (tm/new-session! server {:name "docs-link-second"})
          window (first (data/windows first-session))]
      (tm/link-window! (:tmux/ref window) (:tmux/ref second-session))
      (let [linked (first (filter #(= (:window/id window) (:window/id %))
                                  (data/windows
                                   (tm/refresh! (:tmux/ref second-session)))))]
        [(data/same-entity? window linked) (data/same-link? window linked)]))))
;; => [true false]
```

## Lifecycle, streaming and optional adapters

[`libtmux.async/runtime!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-async-runtime/>) owns bounded JVM workers. Close it, or use `with-open`;
closing a borrowed server or executor is never an adapter side effect. Tasks
have explicit admission, cancellation and deadline results, and expose a Java
[`CompletionStage`](<https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/CompletionStage.html>) for Manifold or other JVM integration.

Control needs tmux 3.7 or newer. On an older daemon [`attach!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-control-attach/>) is
refused before any client exists, with `:tmux/error` `:tmux/unsupported-feature`
and `:tmux/dispatch` `:not-dispatched`; the rest of the façade keeps the 3.3a
minimum. Control observations have an explicit owner and terminal result. A
bounded consumer can lose events: the next read is a `:gap` naming how many were
dropped, ahead of the events that remain, and an adapter loss stays distinct
from it. `:capacity` bounds queued events; `:max-bytes` bounds their unescaped
payload, defaulting to 16 MiB. An oversized event drops the buffered prefix
and itself. `:loss` reports cumulative upstream event and byte counts.

Text subscriptions decode preserved bytes independently per pane and reset
after their own gaps. Malformed bytes become `\\xHH`; partial characters at
termination appear in `:trailing-text`, keyed by pane ID. `:decoder-capacity`
bounds panes with partial characters, defaulting to 1024. A reset reports
`:decoder/reset` as `:gap` or `:capacity` and increments `:decoder/resets`.

The terminal `:reason` distinguishes `:closed`, `:completed`, `:control-exit`,
`:transport-failure`, `:protocol-failure`, and `:unknown`. A `%exit`
notification ends the client; it does not prove daemon death. An unexplained
EOF stays unknown. `:cause` retains the original exception, including any
sensitive text.

Do not model a subscription as an unbounded lazy sequence, and do not perform
blocking observation work in a core.async `go` block. Add the optional
`io.github.libtmux/libtmux-clojure-core-async` dependency only for
[`libtmux.core-async`](<https://libtmux.org/en/clojure/latest/reference/libtmux-core-async/>).

Stopping the channel adapter preserves the owned subscription's final outcome
under `:upstream/terminal`, including loss, cause, and trailing partial text.
Its `:adapter/events` loss counter stays separate from upstream counters.

An empty slow stream reports a timeout without becoming terminal. A later event
remains readable from the same owned stream.

<!-- clojure-snippet: control -->
```clojure
(require '[libtmux.control :as control]
         '[libtmux.core :as tm])
(import '[java.lang AutoCloseable]
        '[java.time Duration])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path
                                :binary (System/getProperty
                                         "libtmux.docs.binary")})
              connection (control/attach!
                          (:tmux/ref (first (tm/sessions! server)))
                          (Duration/ofSeconds 2))
              stream (control/subscribe! connection {:capacity 64
                                                      :max-bytes 65536})]
    (let [initial (control/next! stream (Duration/ofMillis 25))
          pane (first (:reply/lines
                       (control/send! connection
                                      ["new-window" "-P" "-F" "#{pane_id}"
                                       (str "stty -echo; printf docs-slow;"
                                            " read stop")])))
          event (control/next! stream (Duration/ofMillis 900))]
      [(= :timeout (:stream/status initial))
       (= pane (:pane/id event))])))
;; => [true true]
```

<!-- clojure-snippet: control -->
```clojure
(require '[clojure.core.async :as async]
         '[libtmux.control :as control]
         '[libtmux.core :as tm]
         '[libtmux.core-async :as adapter])
(import '[java.time Duration])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path
                                :binary (System/getProperty
                                         "libtmux.docs.binary")})
              connection (control/attach!
                          (:tmux/ref (first (tm/sessions! server)))
                          (Duration/ofSeconds 2))]
    (let [owner (adapter/observe! connection)
          pane (first (:reply/lines
                       (control/send! connection
                                      ["new-window" "-P" "-F" "#{pane_id}"
                                       (str "stty -echo; printf docs-async;"
                                            " read stop")])))
          timeout (async/timeout 900)
          [event port] (async/alts!! [(adapter/events owner) timeout]
                                     :priority true)]
      (async/close! (adapter/events owner))
      [(and (identical? port (adapter/events owner)) (= pane (:pane/id event)))
       (:reason (async/<!! (adapter/terminal owner)))])))
;; => [true :adapter-stopped]
```

### Manifold CompletionStage recipe

Manifold remains a consumer dependency. Add
`manifold/manifold {:mvn/version "0.5.0"}` to the application, then adapt the
public [`CompletionStage`](<https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/CompletionStage.html>); it does not become a `libtmux` dependency. [`then!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-async-then/>)
uses the runtime's bounded callback executor. A Manifold deferred observes the
stage; cancelling that deferred does not cancel the task, so call [`cancel!`](<https://libtmux.org/en/clojure/latest/reference/libtmux-async-cancel/>)
explicitly when ownership requires it.

<!-- clojure-snippet: fixture -->
```clojure
(require '[libtmux.async :as async]
         '[libtmux.core :as tm]
         '[manifold.deferred :as deferred])
(import '[java.util.concurrent CompletionStage])

(let [socket-path (java.nio.file.Path/of
                   (System/getProperty "libtmux.docs.socket")
                   (make-array String 0))]
  (with-open [server (tm/open! {:socket-path socket-path})
              runtime (async/runtime! {})]
    (tm/new-session! server {:name "docs-manifold"})
    (let [task (async/submit! runtime #(tm/snapshot! server))
          stage (async/then! runtime task
                             #(boolean (some (fn [session]
                                               (= "docs-manifold"
                                                  (:session/name session)))
                                             (:tmux/sessions %))))
          observed (deferred/->deferred stage)]
      [(instance? CompletionStage stage) @observed])))
;; => [true true]
```

## Compatibility and Java interop

The façade supports JDK 25 or newer, the floor of the Java core it wraps, and
the repository's tmux 3.3a through 3.7c matrix; control needs tmux 3.7 or
newer. It is JVM-only: it does not run in ClojureScript, Babashka, or a native
image. Existing Java handles, [`FilterExpr`](<https://libtmux.org/en/java/latest/reference/io-github-libtmux-query-filterexpr-filterexpr/>), the optional Jackson codec, and
Java async types remain directly usable when the Clojure functions do not cover
an operation.

[`clojure-reference.md`](https://libtmux.org/en/clojure/latest/guides/captured-values/) lists every captured key and
relation, its availability, identity meaning and Java filter support.

## MCP boundary and measurement

This façade is for a Clojure process that owns its own tmux decisions. It does
not turn arbitrary persistent maps, predicates, or Clojure functions into MCP
tools. For an MCP client such as Codex or Claude Code, use
[libtmux-mcp](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/docs/guide/mcp.md): it has named tools, endpoint selection, toolsets, and
bounded reads. A Clojure application can use both libraries, but their
contracts remain separate.

The [operation benchmark](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/docs/benchmarks/operation-costs.md) measures Java
operations under documented conditions. It is useful when a Clojure program
changes its acquisition pattern, but it is not a Clojure façade benchmark.
There is no published façade throughput or allocation claim. Measure the
application's capture count, filtering/projection allocation, and any owned
runtime queue under its real tmux workload before setting a capacity.
