libtmux.control/next!
Reads one event, gap, timeout, or terminal map; never uses nil for termination.
Source and package
- Qualified name
-
libtmux.control/ next! - Module
- libtmux.control
- Declared in
libtmux.control- Package
- io.github.libtmux:libtmux-clojure
- Source
- libtmux-clojure/src/main/clojure/libtmux/control.clj
-
Public owner:
libtmux.controlDescription#
A
:gapnames 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:
Terminal window $ 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. View source{: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. View source#!/bin/shset -euif [ "$#" -eq 0 ]; thenprintf 'Usage: sh run.sh program [arguments...]\n' >&2exit 2fibinary=$(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_PANEfor name in $(env | sed -n 's/^\(LIBTMUX_SOCKET[A-Za-z0-9_]*\)=.*/\1/p'); dounset "$name"donemkdir -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 1attempts=0while serves "$1" && [ "$attempts" -lt 60 ]; dosleep 0.05attempts=$((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 '' PIPEset +estuck=left=if [ -S "$socket" ] && serves "$socket" && ! stop "$socket"; thenstuck="$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=$IFSIFS=''set -ffor candidate in $(find "$tmpdir" "$directory" -type s 2>/dev/null); do[ "$candidate" = "$socket" ] && continueserves "$candidate" || continuecase "$candidate" in"$tmpdir"/*) name=${candidate#"$tmpdir"/} ;;*) name=${candidate#"$directory"/} ;;esacleft="$left $name"stop "$candidate" || stuck="$stuck $name"doneset +fIFS=$old_ifsif [ -n "$stuck" ]; thenprintf 'tmux still answers on%s after kill-server; kept %s and %s under the runner root\n' \"$stuck" "${directory##*/}" "${tmpdir##*/}" >&2exit 1firm -rf "$directory" "$tmpdir" || exit 1if [ -n "$left" ]; thenprintf 'The program left a tmux server running:%s\n' "$left" >&2exit 1fiexit "$status"}trap cleanup 0trap '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 ]; thenprintf 'API_RUN_ROOT is too long for a tmux socket\n' >&2exit 1fiTMUX_TMPDIR=$tmpdirexport TMUX_TMPDIRprintf '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. View source(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:
Terminal window $ sh run.sh clojure \-J-Dclojure.main.report=stderr \-M \-m libtmux.examples.api.watch-pane-outputExpected output on tmux 3.7 or newer:
observed=stream-readyclosed=trueOn an older supported tmux, the program reports the control capability refusal:
requires tmux 3.7+