# libtmux for Common Lisp

Source: https://libtmux.org/en/lisp/latest/

> libtmux for Common Lisp, version latest.

![](https://libtmux.org/en/brand/lisp/library/logo.svg)

Common Lisp-based tmux client

tmux client library for Common Lisp. Control tmux, navigate sessions, windows, and panes, send keys, capture pane contents

Product

Core API Workspace Manager

```lisp
(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))
```

```lisp
(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))
```

```lisp
(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)))))))
```

```lisp
(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)))))))
```

Example details

\+ Error handling + Cleanup

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

```bash
git init -q source
git -C source fetch -q --depth=1 \
  https://github.com/libtmux/libtmux-lisp.git \
  99874b44dbe29b3651597a5debbacccde235d96c
git -C source checkout -q --detach FETCH_HEAD
mise trust source
mise -C source install
```

```bash
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
export 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"' EXIT
tmux -S "$LIBTMUX_EXAMPLE_SOCKET" -f /dev/null \
  new-session -d -s demo -n editor -x 80 -y 24 -- /bin/cat
LIBTMUX_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

```text
session demo
  window 0:editor: %0 %1
  window 1:logs: %2
%1 is in window editor of session demo
```

[Core Library documentation →](https://libtmux.org/en/lisp/latest/core/)[More Common Lisp examples →](https://libtmux.org/en/lisp/latest/examples/)

### Install `libtmux`

```console
$ git clone https://github.com/libtmux/libtmux-lisp.git
cd libtmux-lisp
mise trust
mise install
mise run setup
```

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.

```console
$ git clone https://github.com/libtmux/libtmux-lisp.git
cd libtmux-lisp
mise trust
mise install
mise run setup
```

Loads the workspace ASDF systems and command from the same locked source checkout as the core library.

* [Workspace Manager documentation →](https://libtmux.org/en/lisp/latest/workspace/)

Build the workspace from a program

examples/workspace.lisp

```lisp
(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. Application
shares 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`](https://github.com/libtmux/libtmux-lisp/blob/c2e5fc4ed3befda21f45dfa5d5868c38fd138311/examples/workspace.lisp), which `mise run package-examples` runs in the repository's CI (`.github/workflows/tests.yml`).

### [Topics](https://libtmux.org/en/lisp/latest/topics/)

[Subject-by-subject, with Common Lisp in every code block.](https://libtmux.org/en/lisp/latest/topics/)

### [Guides](https://libtmux.org/en/lisp/latest/guides/)

[Task guides for Common Lisp.](https://libtmux.org/en/lisp/latest/guides/)

### [Examples](https://libtmux.org/en/lisp/latest/examples/)

[Complete programs, sourced from the code each port tests.](https://libtmux.org/en/lisp/latest/examples/)

### [Concepts](https://libtmux.org/en/lisp/latest/concepts/)

[The domain model every port shares.](https://libtmux.org/en/lisp/latest/concepts/)

## Packages and integrations

### [Workspace Manager](https://libtmux.org/en/lisp/latest/workspace/)

[Validate, plan and explicitly apply bounded JSON workspaces through the native Lisp engine.](https://libtmux.org/en/lisp/latest/workspace/)

## API references

### [Official reference](https://libtmux.org/en/lisp/latest/reference/)

[Common Lisp API signatures and documentation, with links to related guides and APIs in other languages.](https://libtmux.org/en/lisp/latest/reference/)
