tmux client library for Common Lisp
Control tmux, navigate sessions, windows, and panes, send keys, capture pane contents.
Guides
Install the source checkout, acquire snapshots, and issue explicit operations.
Concepts
Understand captured identity, transport ownership, and local query behavior.
Examples
Run tested programs against an owned tmux server.
API reference
Browse public packages, CLOS types, conditions, macros, and functions.
Install
git clone https://github.com/libtmux/libtmux-lisp.git vendor/libtmux-lispAt a glance
Server → Session → Window → Pane. Each level of the tmux hierarchy has its own entry in the Common Lisp API, with the tmux object it wraps.
| Type | Wraps |
|---|---|
libtmux:client | A tmux server |
libtmux:session | A session, ID $n |
libtmux:window | A window in a session, ID @n |
libtmux:pane | A pane in a window, ID %n |
What it does
(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 demoTyped API
- Typed objects instead of parsed format strings:
libtmux:snapshot - Addressing by tmux ID:
libtmux:id - Filtering and queries over tmux objects:
libtmux.query:compile-filter - Typed errors instead of exit codes and stderr:
libtmux:libtmux-error
What you get over the tmux CLI
- Traversal from a server down to its panes:
libtmux:fetch-snapshot - Send keys to a pane:
libtmux:send-keys - Capture pane contents:
libtmux:capture-pane - Arguments passed as a list, with no shell quoting:
libtmux:run-command - A typed error when the running tmux lacks a feature:
libtmux:unsupported-capability