# libtmux.control/next!

- **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#L210
- **Public owner:** libtmux.control
- **Page:** https://libtmux.org/en/clojure/latest/reference/libtmux-control-next/

```
(next! stream)

(next! stream timeout)
```

Reads one event, gap, timeout, or terminal map; never uses `nil` for termination.

A `:gap` names the events dropped since the previous read and precedes the
events that remain. The optional nonnegative Duration includes waiting for
another reader. Sequence numbers order the events of this stream. Close wakes
a reader. Termination drains queued deliveries before the final outcome;
intentionally closing this subscription discards its pending deliveries.
[[subscribe!]] sets bounds; [[close!]] wakes a blocked reader.

## 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

- `stream`
- `timeout` (for `next!(stream, timeout)`)
