tmux client library for Clojure
Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents.
Guides
Install from source and work with captures, effects and streams.
API reference
Public namespaces, function arities and ownership contracts.
core.async adapter
Optional event channels with explicit cancellation and closure.
Install
git clone --branch master https://github.com/libtmux/libtmux-java && (cd libtmux-java && ./gradlew :libtmux:publishToMavenLocal :libtmux-clojure:publishToMavenLocal)At a glance
Server → Session → Window → Pane. Each level of the tmux hierarchy has its own entry in the Clojure API, with the tmux object it wraps.
| Function | Wraps |
|---|---|
libtmux.core/open! | A tmux server |
libtmux.core/sessions! | A session, ID $n |
libtmux.data/windows | A window in a session, ID @n |
libtmux.data/panes | A pane in a window, ID %n |
What it does
(require '[libtmux.core :as tm] '[libtmux.data :as data])(import '[java.lang ProcessHandle] '[java.nio.file Files Path] '[java.nio.file.attribute FileAttribute] '[java.time Duration] [java.util.concurrent TimeUnit])
(let [attrs (make-array FileAttribute 0) root (Path/of "/tmp/libtmux-java-dev" (make-array String 0)) _ (Files/createDirectories root attrs) directory (Files/createTempDirectory root "clojure-home-" attrs) socket (.resolve directory "tmux.sock") daemon (atom nil)] (try (with-open [server (tm/open! {:socket-path socket :binary (or (System/getenv "TMUX_BIN") "tmux") :config-file "/dev/null"})] (try (let [session (tm/new-session! server {:name "demo" :window-name "work" :command ["/bin/sh"]}) window (data/exactly-one (data/windows session)) pane (data/exactly-one (data/panes window)) _ (reset! daemon (.get (ProcessHandle/of (get-in session [:tmux/identity :tmux/pid]))))] (tm/rename! (:tmux/ref window) "editor") (assert (= "work" (:window/name window))) (tm/split-pane! (:tmux/ref pane) {:direction :right :command ["/bin/cat"]}) (tm/new-window! (:tmux/ref session) {:name "logs" :detached? true :command ["/bin/cat"]}) (tm/send-literal! (:tmux/ref pane) "printf 'hello from %s\\n' Clojure") (tm/send-keys! (:tmux/ref pane) ["Enter"]) (assert (#{:appeared :present-at-entry} (tm/await-text! (:tmux/ref pane) "hello from Clojure" (Duration/ofSeconds 2)))) (let [snapshot (tm/snapshot! server) observed (data/exactly-one (:tmux/sessions snapshot))] (assert (= "demo" (:session/name observed))) (println "session" (:session/name observed)) (doseq [entry (data/windows observed)] (println (str " window " (:window/name entry) ": " (count (data/panes entry)) " panes"))) (assert (some #{"hello from Clojure"} (tm/capture! (:tmux/ref pane)))) (println "output=hello from Clojure"))) (finally (tm/kill-server! server) (when-let [^ProcessHandle process @daemon] (.get (.onExit process) 1 TimeUnit/SECONDS))))) (finally (if-let [^ProcessHandle process @daemon] (when-not (.isAlive process) (Files/deleteIfExists socket) (Files/deleteIfExists directory)) (when-not (Files/exists socket (make-array java.nio.file.LinkOption 0)) (Files/deleteIfExists directory))))))(require '[libtmux.core :as tm] '[libtmux.data :as data])(import '[java.lang ProcessHandle] '[java.nio.file Files Path] '[java.nio.file.attribute FileAttribute] '[java.time Duration] [java.util.concurrent TimeUnit])
(let [attrs (make-array FileAttribute 0) root (Path/of "/tmp/libtmux-java-dev" (make-array String 0)) _ (Files/createDirectories root attrs) directory (Files/createTempDirectory root "clojure-home-" attrs) socket (.resolve directory "tmux.sock") daemon (atom nil)] (try (with-open [server (tm/open! {:socket-path socket :binary (or (System/getenv "TMUX_BIN") "tmux") :config-file "/dev/null"})] (try (let [session (tm/new-session! server {:name "demo" :window-name "work" :command ["/bin/sh"]}) window (data/exactly-one (data/windows session)) pane (data/exactly-one (data/panes window)) _ (reset! daemon (.get (ProcessHandle/of (get-in session [:tmux/identity :tmux/pid]))))] (tm/rename! (:tmux/ref window) "editor") (assert (= "work" (:window/name window))) (tm/split-pane! (:tmux/ref pane) {:direction :right :command ["/bin/cat"]}) (tm/new-window! (:tmux/ref session) {:name "logs" :detached? true :command ["/bin/cat"]}) (tm/send-literal! (:tmux/ref pane) "printf 'hello from %s\\n' Clojure") (tm/send-keys! (:tmux/ref pane) ["Enter"]) (assert (#{:appeared :present-at-entry} (tm/await-text! (:tmux/ref pane) "hello from Clojure" (Duration/ofSeconds 2)))) (let [snapshot (tm/snapshot! server) observed (data/exactly-one (:tmux/sessions snapshot))] (assert (= "demo" (:session/name observed))) (println "session" (:session/name observed)) (doseq [entry (data/windows observed)] (println (str " window " (:window/name entry) ": " (count (data/panes entry)) " panes"))) (assert (some #{"hello from Clojure"} (tm/capture! (:tmux/ref pane)))) (println "output=hello from Clojure"))) (catch Exception failure (throw (ex-info "Clojure tmux quickstart failed" {:tmux/failure (tm/error-summary failure)} failure))) (finally (tm/kill-server! server) (when-let [^ProcessHandle process @daemon] (.get (.onExit process) 1 TimeUnit/SECONDS))))) (finally (if-let [^ProcessHandle process @daemon] (when-not (.isAlive process) (Files/deleteIfExists socket) (Files/deleteIfExists directory)) (when-not (Files/exists socket (make-array java.nio.file.LinkOption 0)) (Files/deleteIfExists directory))))))(require '[libtmux.core :as tm] '[libtmux.data :as data])(import '[java.lang ProcessHandle] '[java.nio.file Files Path] '[java.nio.file.attribute FileAttribute] '[java.time Duration] [java.util.concurrent TimeUnit])
(let [attrs (make-array FileAttribute 0) root (Path/of "/tmp/libtmux-java-dev" (make-array String 0)) _ (Files/createDirectories root attrs) directory (Files/createTempDirectory root "clojure-home-" attrs) socket (.resolve directory "tmux.sock") daemon (atom nil)] (try (with-open [server (tm/open! {:socket-path socket :binary (or (System/getenv "TMUX_BIN") "tmux") :config-file "/dev/null"})] (try (let [session (tm/new-session! server {:name "demo" :window-name "work" :command ["/bin/sh"]}) window (data/exactly-one (data/windows session)) pane (data/exactly-one (data/panes window)) _ (reset! daemon (.get (ProcessHandle/of (get-in session [:tmux/identity :tmux/pid]))))] (tm/rename! (:tmux/ref window) "editor") (assert (= "work" (:window/name window))) (tm/split-pane! (:tmux/ref pane) {:direction :right :command ["/bin/cat"]}) (tm/new-window! (:tmux/ref session) {:name "logs" :detached? true :command ["/bin/cat"]}) (tm/send-literal! (:tmux/ref pane) "printf 'hello from %s\\n' Clojure") (tm/send-keys! (:tmux/ref pane) ["Enter"]) (assert (#{:appeared :present-at-entry} (tm/await-text! (:tmux/ref pane) "hello from Clojure" (Duration/ofSeconds 2)))) (let [snapshot (tm/snapshot! server) observed (data/exactly-one (:tmux/sessions snapshot))] (assert (= "demo" (:session/name observed))) (println "session" (:session/name observed)) (doseq [entry (data/windows observed)] (println (str " window " (:window/name entry) ": " (count (data/panes entry)) " panes"))) (assert (some #{"hello from Clojure"} (tm/capture! (:tmux/ref pane)))) (println "output=hello from Clojure"))) (finally (tm/kill-server! server) (when-let [^ProcessHandle process @daemon] (.get (.onExit process) 1 TimeUnit/SECONDS))))) (finally (if-let [^ProcessHandle process @daemon] (when-not (.isAlive process) (Files/deleteIfExists socket) (Files/deleteIfExists directory)) (when-not (Files/exists socket (make-array java.nio.file.LinkOption 0)) (Files/deleteIfExists directory))))) (assert (not (Files/exists directory (make-array java.nio.file.LinkOption 0)))))(require '[libtmux.core :as tm] '[libtmux.data :as data])(import '[java.lang ProcessHandle] '[java.nio.file Files Path] '[java.nio.file.attribute FileAttribute] '[java.time Duration] [java.util.concurrent TimeUnit])
(let [attrs (make-array FileAttribute 0) root (Path/of "/tmp/libtmux-java-dev" (make-array String 0)) _ (Files/createDirectories root attrs) directory (Files/createTempDirectory root "clojure-home-" attrs) socket (.resolve directory "tmux.sock") daemon (atom nil)] (try (with-open [server (tm/open! {:socket-path socket :binary (or (System/getenv "TMUX_BIN") "tmux") :config-file "/dev/null"})] (try (let [session (tm/new-session! server {:name "demo" :window-name "work" :command ["/bin/sh"]}) window (data/exactly-one (data/windows session)) pane (data/exactly-one (data/panes window)) _ (reset! daemon (.get (ProcessHandle/of (get-in session [:tmux/identity :tmux/pid]))))] (tm/rename! (:tmux/ref window) "editor") (assert (= "work" (:window/name window))) (tm/split-pane! (:tmux/ref pane) {:direction :right :command ["/bin/cat"]}) (tm/new-window! (:tmux/ref session) {:name "logs" :detached? true :command ["/bin/cat"]}) (tm/send-literal! (:tmux/ref pane) "printf 'hello from %s\\n' Clojure") (tm/send-keys! (:tmux/ref pane) ["Enter"]) (assert (#{:appeared :present-at-entry} (tm/await-text! (:tmux/ref pane) "hello from Clojure" (Duration/ofSeconds 2)))) (let [snapshot (tm/snapshot! server) observed (data/exactly-one (:tmux/sessions snapshot))] (assert (= "demo" (:session/name observed))) (println "session" (:session/name observed)) (doseq [entry (data/windows observed)] (println (str " window " (:window/name entry) ": " (count (data/panes entry)) " panes"))) (assert (some #{"hello from Clojure"} (tm/capture! (:tmux/ref pane)))) (println "output=hello from Clojure"))) (catch Exception failure (throw (ex-info "Clojure tmux quickstart failed" {:tmux/failure (tm/error-summary failure)} failure))) (finally (tm/kill-server! server) (when-let [^ProcessHandle process @daemon] (.get (.onExit process) 1 TimeUnit/SECONDS))))) (finally (if-let [^ProcessHandle process @daemon] (when-not (.isAlive process) (Files/deleteIfExists socket) (Files/deleteIfExists directory)) (when-not (Files/exists socket (make-array java.nio.file.LinkOption 0)) (Files/deleteIfExists directory))))) (assert (not (Files/exists directory (make-array java.nio.file.LinkOption 0)))))Run this example
JDK 25 or newer, Clojure CLI 1.12, Git, tmux 3.3a or newer, /bin/sh and /bin/cat on Linux or macOS. The Clojure artifacts are unpublished; install the pinned source into the isolated Maven repository first.
Save the program above as quickstart.clj in a new directory, along with the files below.
Every view closes its client and stops its private server. If shutdown fails, its socket directory stays available for recovery. Cleanup also checks directory removal. The program stops its private tmux server and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
deps.edn
{:paths ["."] :mvn/local-repo "m2" :deps {org.clojure/clojure {:mvn/version "1.12.6"} io.github.libtmux/libtmux-clojure {:mvn/version "0.1.2-SNAPSHOT"}}}Setup and run
git clone \ --branch master \ --single-branch \ https://github.com/libtmux/libtmux-java libtmux-source && \ git -C libtmux-source checkout --detach 941f2162062b6d118c68b7d1c23417e0d2cea124 && \ (cd libtmux-source && ./gradlew \ :libtmux:publishToMavenLocal \ :libtmux-clojure:publishToMavenLocal \ --max-workers=1 \ --console=plain \ "-Dmaven.repo.local=$PWD/../m2")clojure -J-Dclojure.main.report=stderr -M quickstart.cljExpected output
session demo window editor: 2 panes window logs: 1 panesoutput=hello from ClojureTyped API
- Typed objects instead of parsed format strings:
libtmux.data - Typed errors instead of exit codes and stderr:
libtmux.core/error-summary
What you get over the tmux CLI
- Connect to or start a tmux server:
libtmux.core/open! - Traversal from a server down to its panes:
libtmux.data/panes - Send keys to a pane:
libtmux.core/send-keys! - Watch for changes through tmux control mode:
libtmux.control/subscribe! - Capture pane contents:
libtmux.core/capture! - Async operations:
libtmux.async
The Clojure packages have not been published. Follow the source installation steps in Getting started.