Core LibraryunreleasedHome

Choose documentation 1

latest

Current version

latest
English

Unreleased io.github.libtmux:libtmux-clojure has not been released. Its API may change during development. This site's structure and URLs may change too.

Edit this page on GitHub

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.

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.

FunctionWraps
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)))))
Example details
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.clj

Expected output

session demo
window editor: 2 panes
window logs: 1 panes
output=hello from Clojure

Typed API

What you get over the tmux CLI

The Clojure packages have not been published. Follow the source installation steps in Getting started.

Esc

Type to search.

Reference previewOpen reference