# libtmux.control/subscribe!

- **Module:** libtmux.control
- **Package:** io.github.libtmux:libtmux-clojure
- **Language:** Clojure
- **Kind:** function
- **Source:** https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/libtmux-clojure/src/main/clojure/libtmux/control.clj#L110
- **Public owner:** libtmux.control
- **Page:** https://libtmux.org/en/clojure/latest/reference/libtmux-control-subscribe/

```
(subscribe! connection)

(subscribe! connection options)
```

Creates a closeable output subscription borrowing its connection.

`:encoding` is `:bytes` (caller-owned byte arrays) or `:utf-8` (incremental text
per pane, reset after this subscription's gaps; malformed bytes use \xHH).
`:decoder-capacity` bounds partial-pane state (default 1024); an eviction is
reported with `:decoder/reset` `:capacity` and a cumulative `:decoder/resets.`
`:capacity` bounds queued events; `:max-bytes` bounds their unescaped payload
(default 16 MiB).
Oldest events are dropped until both bounds fit; an oversized event drops
the queued prefix and itself. The next read is a `:gap` with byte/event loss.
Readers compete; create separate subscriptions for independent fan-out.
[[next!]] reads deliveries; [[terminal]] reports the drained end; [[close!]] stops locally.

## Examples

Read bounded UTF-8 output on an owned wait worker and inspect local termination. Use JDK 25 or newer, the Clojure CLI, Git and tmux on Linux or macOS, with /bin/cat and /bin/sh available. These packages are unpublished. In an empty directory, install this exact source into the consumer's isolated Maven repository:

```console
$ git clone https://github.com/libtmux/libtmux-java libtmux-source && \
  git -C libtmux-source checkout --detach 5f88402151ff08fc0390dd438b90963692ec3fe4 && \
  (cd libtmux-source && ./gradlew \
    :libtmux:publishToMavenLocal \
    :libtmux-clojure:publishToMavenLocal \
    --console=plain \
    "-Dmaven.repo.local=$PWD/../m2")
```

Save this file as `deps.edn`. Dependencies resolve from the isolated m2 directory; the core.async dependency is only enabled by its alias.

[Source example](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/examples/api/clojure/deps.edn).

```clojure
{:paths ["src/main/clojure"]
 :mvn/local-repo "m2"
 :deps {org.clojure/clojure {:mvn/version "1.12.6"}
        io.github.libtmux/libtmux-clojure {:mvn/version "0.1.2-SNAPSHOT"}}
 :aliases
 {:core-async
  {:extra-deps
   {io.github.libtmux/libtmux-clojure-core-async {:mvn/version "0.1.2-SNAPSHOT"}}}}}
```

Save this file as `run.sh`. The launcher creates and stops a private server, including after program failure, and reports leaked servers as errors.

[Source example](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/examples/api/run.sh).

```sh
#!/bin/sh
set -eu

if [ "$#" -eq 0 ]; then
    printf 'Usage: sh run.sh program [arguments...]\n' >&2
    exit 2
fi
binary=$(command -v "${TMUX_BIN:-tmux}")
root=${API_RUN_ROOT:-/tmp/libtmux-java-dev}

# A client that names no socket follows TMUX or LIBTMUX_SOCKET*, and otherwise lands on the default
# socket under TMUX_TMPDIR. The program gets none of them, and a TMUX_TMPDIR of its own below.
unset TMUX TMUX_PANE
for name in $(env | sed -n 's/^\(LIBTMUX_SOCKET[A-Za-z0-9_]*\)=.*/\1/p'); do
    unset "$name"
done

mkdir -p "$root"
directory=$(mktemp -d "$root/api.XXXXXX")
tmpdir=$(mktemp -d "$root/tmpdir.XXXXXX")
socket="$directory/tmux.sock"
config="$directory/tmux.conf"

serves() {
    "$binary" -S "$1" display-message -p '#{pid}' >/dev/null 2>&1
}

# Stops the server on the socket, then waits for it to stop answering.
stop() {
    "$binary" -S "$1" kill-server || return 1
    attempts=0
    while serves "$1" && [ "$attempts" -lt 60 ]; do
        sleep 0.05
        attempts=$((attempts + 1))
    done
    ! serves "$1"
}

cleanup() {
    status=$?
    trap - 0 HUP INT TERM PIPE
    # A closed output must not end the cleanup, whose messages are the only thing left to write.
    trap '' PIPE
    set +e
    stuck=
    left=
    if [ -S "$socket" ] && serves "$socket" && ! stop "$socket"; then
        stuck="$stuck tmux.sock"
    fi
    # Every socket anywhere under either directory, without following links. Only one a server answers
    # on is a leak; the runner's own is expected to be there.
    old_ifs=$IFS
    IFS='
'
    set -f
    for candidate in $(find "$tmpdir" "$directory" -type s 2>/dev/null); do
        [ "$candidate" = "$socket" ] && continue
        serves "$candidate" || continue
        case "$candidate" in
            "$tmpdir"/*) name=${candidate#"$tmpdir"/} ;;
            *) name=${candidate#"$directory"/} ;;
        esac
        left="$left $name"
        stop "$candidate" || stuck="$stuck $name"
    done
    set +f
    IFS=$old_ifs
    if [ -n "$stuck" ]; then
        printf 'tmux still answers on%s after kill-server; kept %s and %s under the runner root\n' \
            "$stuck" "${directory##*/}" "${tmpdir##*/}" >&2
        exit 1
    fi
    rm -rf "$directory" "$tmpdir" || exit 1
    if [ -n "$left" ]; then
        printf 'The program left a tmux server running:%s\n' "$left" >&2
        exit 1
    fi
    exit "$status"
}
trap cleanup 0
trap 'exit 1' HUP INT TERM PIPE

# A default socket path longer than this does not fit in sun_path.
default_socket="$tmpdir/tmux-$(id -u)/default"
if [ "$(printf '%s' "$default_socket" | wc -c)" -gt 100 ]; then
    printf 'API_RUN_ROOT is too long for a tmux socket\n' >&2
    exit 1
fi
TMUX_TMPDIR=$tmpdir
export TMUX_TMPDIR

printf 'set-option -g default-shell /bin/sh\n' > "$config"
"$binary" -S "$socket" -f "$config" new-session -d -s work-one -n editor /bin/cat
"$binary" -S "$socket" split-window -h -t '=work-one:editor' /bin/cat
"$binary" -S "$socket" new-window -d -t '=work-one' -n logs /bin/cat
"$binary" -S "$socket" new-session -d -s work-two -n editor /bin/cat
"$@" "$binary" "$socket" "$config"
"$binary" -S "$socket" has-session -t '=work-one'
"$binary" -S "$socket" has-session -t '=work-two'
```

Save the complete program as `src/main/clojure/libtmux/examples/api/watch_pane_output.clj`, creating its parent directories. Closing its client leaves the launcher's server running.

[Source example](https://github.com/libtmux/libtmux-java/blob/5f88402151ff08fc0390dd438b90963692ec3fe4/examples/src/main/clojure/libtmux/examples/api/watch_pane_output.clj).

```clojure
(ns libtmux.examples.api.watch-pane-output
  "Observe bounded UTF-8 output on an owned virtual worker, then close locally."
  (:require [clojure.string :as str]
            [libtmux.async :as async]
            [libtmux.control :as control]
            [libtmux.core :as tm]
            [libtmux.data :as data])
  (:import [java.lang AutoCloseable]
           [java.time Duration]))

(defn -main [& args]
  (when-not (= 3 (count args))
    (throw (IllegalArgumentException. "expected: tmux-binary socket-path config-file")))
  (let [[binary socket config] args]
    (with-open [server (tm/open! {:binary binary :socket-path socket :config-file config})]
      (let [session (tm/new-session! server {:name "stream-example" :command ["/bin/sh"]})
            pane (data/exactly-one (data/panes session))
            marker "stream-ready"]
        (try
          (with-open [^AutoCloseable runtime (async/runtime! {:max-running 2 :queue-limit 2})
                      ^AutoCloseable connection (control/attach! (:tmux/ref session) (Duration/ofMillis 900))
                      ;; One terminal line can arrive as many separate output events.
                      ^AutoCloseable stream (control/subscribe! connection
                                                               {:encoding :utf-8 :max-bytes 8192})]
            (let [observed
                  (async/submit! runtime {:kind :wait :timeout-ms 900}
                                 #(loop [suffix ""]
                                    (let [step (control/next! stream)]
                                      (when-not (= :event (:stream/status step))
                                        (throw (ex-info "Observation lost output or ended" step)))
                                      (if (= (:pane/id pane) (:pane/id step))
                                        (let [text (str suffix (:output/text step))]
                                          (if (str/includes? text marker)
                                            marker
                                            (recur (subs text (max 0 (- (count text) (dec (count marker))))))))
                                        (recur suffix)))))]
              (tm/send-literal! (:tmux/ref pane) "printf 'stream-%s\\n' ready")
              (tm/send-keys! (:tmux/ref pane) ["Enter"])
              (assert (= marker @observed))
              (control/close! stream)
              (let [ended (control/next! stream Duration/ZERO)]
                (assert (= :terminal (:stream/status ended)))
                (assert (= :closed (:reason (control/terminal stream)))))
              (println (str "observed=" marker))
              (println "closed=true")))
          (catch clojure.lang.ExceptionInfo failure
            (if (= :tmux/unsupported-feature (:tmux/error (ex-data failure)))
              (do
                (assert (= :not-dispatched (:tmux/dispatch (ex-data failure))))
                (assert (empty? (tm/clients! server)))
                (println "requires tmux 3.7+"))
              (throw failure))))))))
```

Run it with the private fixture. Set TMUX_BIN to select another tmux binary:

```console
$ sh run.sh clojure \
  -J-Dclojure.main.report=stderr \
  -M \
  -m libtmux.examples.api.watch-pane-output
```

Expected output on tmux 3.7 or newer:

```text
observed=stream-ready
closed=true
```

On an older supported tmux, the program reports the control capability refusal:

```text
requires tmux 3.7+
```

## Parameters

- `connection`
- `options` (for `subscribe!(connection, options)`)
