libtmux for Common Lisp
Common Lisp-based tmux client
tmux client library for Common Lisp. Control tmux, navigate sessions, windows, and panes, send keys, capture pane contents
(require :asdf)
(defparameter *source-root* (or (uiop:getenv "LIBTMUX_SOURCE") "."))(defparameter *program* (or (uiop:getenv "LIBTMUX_PROGRAM") "tmux"))(defparameter *socket-path* (or (uiop:getenv "LIBTMUX_EXAMPLE_SOCKET") (error "Set LIBTMUX_EXAMPLE_SOCKET to the owned tmux socket path")))
(asdf:load-asd (truename (merge-pathnames "libtmux.asd" (uiop:ensure-directory-pathname *source-root*))))(asdf:load-system "libtmux")
(defun observed-value (outcome) "Return OUTCOME's observed value, or signal its observation failure." (when (libtmux:outcome-observation-error outcome) (error (libtmux:outcome-observation-error outcome))) (multiple-value-bind (value present-p) (libtmux:outcome-value outcome) (unless present-p (error "The operation did not return an observation")) value))
(libtmux:with-client (client :program *program* :socket-path *socket-path* :config-file "/dev/null") (let* ((before (libtmux:fetch-snapshot client)) (demo (find "demo" (libtmux:sessions before) :key #'libtmux:name :test #'string=)) (editor-link (aref (libtmux:window-links demo) 0)) (editor (libtmux:window editor-link)) (left (aref (libtmux:panes editor) 0)) (right (observed-value (libtmux:split-pane client left :direction :horizontal :command '("/bin/cat")))) (logs (observed-value (libtmux:new-window client demo :name "logs" :command '("/bin/cat"))))) (declare (ignore logs)) (libtmux:send-text client right "hello from tmux" :enter t) (multiple-value-bind (text outcome) (libtmux:capture-pane client right) (assert (eq :succeeded (libtmux:outcome-status outcome))) (assert (search "hello from tmux" text))) (let* ((snapshot (libtmux:fetch-snapshot client)) (pane (find (libtmux:id right) (libtmux:panes snapshot) :key #'libtmux:id :test #'string=)) (window (libtmux:window pane)) (owner-link (aref (libtmux:window-links window) 0)) (owner (libtmux:session owner-link))) (loop for session across (libtmux:sessions snapshot) do (format t "session ~A~%" (libtmux:name session)) (loop for link across (libtmux:window-links session) for each = (libtmux:window link) do (format t " window ~D:~A:" (libtmux:index link) (libtmux:name each)) (loop for item across (libtmux:panes each) do (format t " ~A" (libtmux:id item))) (terpri))) (format t "~A is in window ~A of session ~A~%" (libtmux:id pane) (libtmux:name window) (libtmux:name owner)))) (libtmux:kill-server client))(require :asdf)
(defparameter *source-root* (or (uiop:getenv "LIBTMUX_SOURCE") "."))(defparameter *program* (or (uiop:getenv "LIBTMUX_PROGRAM") "tmux"))(defparameter *socket-path* (or (uiop:getenv "LIBTMUX_EXAMPLE_SOCKET") (error "Set LIBTMUX_EXAMPLE_SOCKET to the owned tmux socket path")))
(asdf:load-asd (truename (merge-pathnames "libtmux.asd" (uiop:ensure-directory-pathname *source-root*))))(asdf:load-system "libtmux")
(defun observed-value (outcome) "Return OUTCOME's observed value, or signal its observation failure." (when (libtmux:outcome-observation-error outcome) (error (libtmux:outcome-observation-error outcome))) (multiple-value-bind (value present-p) (libtmux:outcome-value outcome) (unless present-p (error "The operation did not return an observation")) value))
(libtmux:with-client (client :program *program* :socket-path *socket-path* :config-file "/dev/null") (let* ((before (libtmux:fetch-snapshot client)) (demo (find "demo" (libtmux:sessions before) :key #'libtmux:name :test #'string=)) (editor-link (aref (libtmux:window-links demo) 0)) (editor (libtmux:window editor-link)) (left (aref (libtmux:panes editor) 0)) (right (observed-value (libtmux:split-pane client left :direction :horizontal :command '("/bin/cat")))) (logs (observed-value (libtmux:new-window client demo :name "logs" :command '("/bin/cat"))))) (declare (ignore logs)) (libtmux:send-text client right "hello from tmux" :enter t) (multiple-value-bind (text outcome) (libtmux:capture-pane client right) (assert (eq :succeeded (libtmux:outcome-status outcome))) (assert (search "hello from tmux" text))) (let* ((snapshot (libtmux:fetch-snapshot client)) (pane (find (libtmux:id right) (libtmux:panes snapshot) :key #'libtmux:id :test #'string=)) (window (libtmux:window pane)) (owner-link (aref (libtmux:window-links window) 0)) (owner (libtmux:session owner-link))) (loop for session across (libtmux:sessions snapshot) do (format t "session ~A~%" (libtmux:name session)) (loop for link across (libtmux:window-links session) for each = (libtmux:window link) do (format t " window ~D:~A:" (libtmux:index link) (libtmux:name each)) (loop for item across (libtmux:panes each) do (format t " ~A" (libtmux:id item))) (terpri))) (format t "~A is in window ~A of session ~A~%" (libtmux:id pane) (libtmux:name window) (libtmux:name owner)))) (libtmux:kill-server client))(require :asdf)
(defparameter *source-root* (or (uiop:getenv "LIBTMUX_SOURCE") "."))(defparameter *program* (or (uiop:getenv "LIBTMUX_PROGRAM") "tmux"))(defparameter *socket-path* (or (uiop:getenv "LIBTMUX_EXAMPLE_SOCKET") (error "Set LIBTMUX_EXAMPLE_SOCKET to the owned tmux socket path")))
(asdf:load-asd (truename (merge-pathnames "libtmux.asd" (uiop:ensure-directory-pathname *source-root*))))(asdf:load-system "libtmux")
(defun observed-value (outcome) "Return OUTCOME's observed value, or signal its observation failure." (when (libtmux:outcome-observation-error outcome) (error (libtmux:outcome-observation-error outcome))) (multiple-value-bind (value present-p) (libtmux:outcome-value outcome) (unless present-p (error "The operation did not return an observation")) value))
(libtmux:with-client (client :program *program* :socket-path *socket-path* :config-file "/dev/null") (let ((normal nil)) (unwind-protect (multiple-value-prog1 (let* ((before (libtmux:fetch-snapshot client)) (demo (find "demo" (libtmux:sessions before) :key #'libtmux:name :test #'string=)) (editor-link (aref (libtmux:window-links demo) 0)) (editor (libtmux:window editor-link)) (left (aref (libtmux:panes editor) 0)) (right (observed-value (libtmux:split-pane client left :direction :horizontal :command '("/bin/cat")))) (logs (observed-value (libtmux:new-window client demo :name "logs" :command '("/bin/cat"))))) (declare (ignore logs)) (libtmux:send-text client right "hello from tmux" :enter t) (multiple-value-bind (text outcome) (libtmux:capture-pane client right) (assert (eq :succeeded (libtmux:outcome-status outcome))) (assert (search "hello from tmux" text))) (let* ((snapshot (libtmux:fetch-snapshot client)) (pane (find (libtmux:id right) (libtmux:panes snapshot) :key #'libtmux:id :test #'string=)) (window (libtmux:window pane)) (owner-link (aref (libtmux:window-links window) 0)) (owner (libtmux:session owner-link))) (loop for session across (libtmux:sessions snapshot) do (format t "session ~A~%" (libtmux:name session)) (loop for link across (libtmux:window-links session) for each = (libtmux:window link) do (format t " window ~D:~A:" (libtmux:index link) (libtmux:name each)) (loop for item across (libtmux:panes each) do (format t " ~A" (libtmux:id item))) (terpri))) (format t "~A is in window ~A of session ~A~%" (libtmux:id pane) (libtmux:name window) (libtmux:name owner)))) (setf normal t)) (handler-case (libtmux:kill-server client) (error (condition) (if normal (error condition) (warn "Owned server cleanup also failed: ~A" condition)))))))(require :asdf)
(defparameter *source-root* (or (uiop:getenv "LIBTMUX_SOURCE") "."))(defparameter *program* (or (uiop:getenv "LIBTMUX_PROGRAM") "tmux"))(defparameter *socket-path* (or (uiop:getenv "LIBTMUX_EXAMPLE_SOCKET") (error "Set LIBTMUX_EXAMPLE_SOCKET to the owned tmux socket path")))
(asdf:load-asd (truename (merge-pathnames "libtmux.asd" (uiop:ensure-directory-pathname *source-root*))))(asdf:load-system "libtmux")
(defun observed-value (outcome) "Return OUTCOME's observed value, or signal its observation failure." (when (libtmux:outcome-observation-error outcome) (error (libtmux:outcome-observation-error outcome))) (multiple-value-bind (value present-p) (libtmux:outcome-value outcome) (unless present-p (error "The operation did not return an observation")) value))
(libtmux:with-client (client :program *program* :socket-path *socket-path* :config-file "/dev/null") (let ((normal nil)) (unwind-protect (multiple-value-prog1 (let* ((before (libtmux:fetch-snapshot client)) (demo (find "demo" (libtmux:sessions before) :key #'libtmux:name :test #'string=)) (editor-link (aref (libtmux:window-links demo) 0)) (editor (libtmux:window editor-link)) (left (aref (libtmux:panes editor) 0)) (right (observed-value (libtmux:split-pane client left :direction :horizontal :command '("/bin/cat")))) (logs (observed-value (libtmux:new-window client demo :name "logs" :command '("/bin/cat"))))) (declare (ignore logs)) (libtmux:send-text client right "hello from tmux" :enter t) (multiple-value-bind (text outcome) (libtmux:capture-pane client right) (assert (eq :succeeded (libtmux:outcome-status outcome))) (assert (search "hello from tmux" text))) (let* ((snapshot (libtmux:fetch-snapshot client)) (pane (find (libtmux:id right) (libtmux:panes snapshot) :key #'libtmux:id :test #'string=)) (window (libtmux:window pane)) (owner-link (aref (libtmux:window-links window) 0)) (owner (libtmux:session owner-link))) (loop for session across (libtmux:sessions snapshot) do (format t "session ~A~%" (libtmux:name session)) (loop for link across (libtmux:window-links session) for each = (libtmux:window link) do (format t " window ~D:~A:" (libtmux:index link) (libtmux:name each)) (loop for item across (libtmux:panes each) do (format t " ~A" (libtmux:id item))) (terpri))) (format t "~A is in window ~A of session ~A~%" (libtmux:id pane) (libtmux:name window) (libtmux:name owner)))) (setf normal t)) (handler-case (libtmux:kill-server client) (error (condition) (if normal (error condition) (warn "Owned server cleanup also failed: ~A" condition)))))))Run this example
mise 2026.11.3, Git, POSIX shell and tmux 3.3a or newer on Linux x86-64. The source lock installs SBCL 2.6.10 and ASDF/UIOP 3.3.7.
Save the program above as quickstart.lisp in a new directory, along with the files below.
Cleanup is omitted; the private tmux server and its temporary files may remain. Select Cleanup to remove them when the program finishes. The program stops its private tmux server and removes any temporary files it created when it finishes. Unhandled conditions already end the script with SBCL's diagnostic and backtrace, so there is no separate error wrapper to add.
Setup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-lisp.git \ 99874b44dbe29b3651597a5debbacccde235d96cgit -C source checkout -q --detach FETCH_HEADmise trust sourcemise -C source installunset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shexport LIBTMUX_SOURCE="$PWD/source"export LIBTMUX_PROGRAM="${LIBTMUX_NATIVE_TMUX:-$(command -v tmux)}"LIBTMUX_DEMO_DIR="$(mktemp -d)"export LIBTMUX_EXAMPLE_SOCKET="$LIBTMUX_DEMO_DIR/tmux.sock"trap 'rm -rf "$LIBTMUX_DEMO_DIR"' EXITtmux -S "$LIBTMUX_EXAMPLE_SOCKET" -f /dev/null \ new-session -d -s demo -n editor -x 80 -y 24 -- /bin/catLIBTMUX_ASDF="$(mise -C source where http:asdf-lisp)/asdf.lisp"mise -C source exec -- sbcl --noinform --no-sysinit --no-userinit \ --load "$LIBTMUX_ASDF" --script "$PWD/quickstart.lisp"Expected output
session demo window 0:editor: %0 %1 window 1:logs: %2%1 is in window editor of session demoInstall libtmux
The 0.1.0 ASDF systems are unreleased. This installs the pinned SBCL, ASDF and Qlot toolchain from source.
Workspace Manager
Install libtmux/workspace
The workspace package is separate from the core library.
Loads the workspace ASDF systems and command from the same locked source checkout as the core library.
Build the workspace from a program
(defun apply-workspace-document (client pathname) "Plan and apply one bounded workspace document through explicit CLIENT.Return the preview steps and application result as two values. Applicationshares a 30-second budget; file loading and the preview precede that budget." (let* ((workspace (ws:read-workspace pathname)) (snapshot (tm:fetch-snapshot client)) (plan (ws:plan-workspace workspace snapshot))) (values (ws:plan-steps plan) (ws:apply-workspace client workspace :total-timeout 30))))
Excerpt of examples/workspace.lisp,
which mise run package-examples runs in the repository's CI
(.github/workflows/tests.yml).
Topics
Subject-by-subject, with Common Lisp in every code block.
Guides
Task guides for Common Lisp.
Examples
Complete programs, sourced from the code each port tests.
Concepts
The domain model every port shares.