Getting started
libtmux-lisp is a native Common Lisp tmux client library. Acquire a CLOS object graph once, process its panes with ordinary sequences, and make mutations through an explicit client.
Implemented: captured sessions, physical windows, window links and panes;
bounded subprocess transport; cancellation; named operations, byte buffers,
validated native queries, eager QuerySets, optional regex and bounded JSON;
bounded declarative workspaces with pure plans, explicit
apply and an explicit-endpoint CLI;
decoded configuration reads,
printer inspection and
typed option, hook and environment mutations
for the exact reviewed tmux catalogs through 3.8; explicit entity and server
termination with retained outcome evidence; owned daemon scopes;
optional queued control commands, pane-output streams and explicit reconnection on
tmux 3.8+.
In development: broader event qualification and source filtering.
MCP is intentionally out of scope. The 0.1.0
systems are unreleased.
Install from source¶
Use Linux x86-64 (including WSL), kernel 6.9+, Bash and a native tmux binary. The supported tmux range is 3.3a through 3.8, including intermediate releases. Plain tmux 3.3 is rejected. The pinned toolchain is SBCL 2.6.10, ASDF/UIOP 3.3.7 and Qlot 1.8.4. The process requirements also apply to WSL; its distribution name alone does not establish kernel capabilities.
Core and query loading require ASDF/UIOP only. Mise reproduces the contributor toolchain; Qlot installs development and optional dependencies. ASDF is the Common Lisp build system.
$ git clone https://github.com/libtmux/libtmux-lisp.git && cd libtmux-lispIn an existing SBCL application with ASDF loaded, register this checkout from its root:
(asdf:load-asd (truename "libtmux.asd"))Load core and its local query API:
(asdf:load-system "libtmux/query")To reproduce the pinned contributor environment and run all optional examples, use mise 2026.11.3+. Review mise.toml, then trust the checkout:
$ mise trust$ mise installInstall the locked dependencies. This is the network step:
$ mise run setupRun the complete workspace example. It creates a private tmux socket, checks the results and removes its daemon; it never addresses your default server.
$ mise run examplesSuccess includes EXAMPLES PASS: graph, links, native, compiled, relation, regex and JSON selection, declarative workspace, false/presence, ownership, refresh, bytes, capture, close, terminate. Core and query loading need ASDF/UIOP only; optional
regex/JSON use the locked CL-PPCRE/Jzon systems. The example runner also needs
Python 3. No Java runtime is involved.
Choose an ASDF system¶
All systems share this source distribution, version and changelog. Load only the systems your application uses. Each system has its own README:
| System | Use it for | Additional dependencies |
|---|---|---|
libtmux¶ | Captures, CLOS objects and explicit tmux operations | ASDF/UIOP |
libtmux/query¶ | Native predicates, relations and eager QuerySets | Core |
libtmux/regex¶ | CL-PPCRE search and full-match filters | Query, CL-PPCRE |
libtmux/json¶ | Bounded query document interchange | Query, Jzon |
libtmux/control¶ | Queued requests, output streams and reconnection on tmux 3.8+ | Core |
libtmux/workspace¶ | Native declarations, topology previews and explicit apply | Core, Jzon |
libtmux/workspace/cli¶ | Workspace validation, previews and explicit-endpoint apply | Workspace |
These directories provide system documentation. ASDF definitions remain in
libtmux.asd; source stays in src/. Secondary systems are
optional parts of one library release.
Start an isolated daemon¶
This complete function starts an empty-config daemon, creates one session and returns pane data after closing its client and daemon. The runnable program includes loading and assertions.
(defun inspect-owned-workspace (&key (program "tmux")) "Start an isolated daemon; return owned pane data after closing both resources." (libtmux:with-daemon (daemon :program program) (libtmux:with-client (client :program (libtmux:daemon-program daemon) :socket-path (libtmux:daemon-socket-path daemon)) (let ((outcome (libtmux:new-session client :name "demo" :command '("/bin/cat")))) (when (libtmux:outcome-observation-error outcome) (error (libtmux:outcome-observation-error outcome)))) (map 'list #'libtmux:entity-data (libtmux:panes (libtmux:fetch-snapshot client))))))Run it from the checkout with the pinned toolchain:
$ mise exec -- tools/lisp.sh examples/managed-daemon.lispIt prints MANAGED DAEMON PASS and a pane record. Read
daemon scopes for environment, readiness and cleanup contracts.
Use an ordinary client alone when connecting to an existing server.
Inspect a workspace¶
The REPL tutorial loads the example package, whose local
nickname tm refers to libtmux. This complete function returns ordinary
owned data after closing its client:
(defun inspect-workspace (socket-path &key (program "tmux")) "Acquire one graph from SOCKET-PATH; return owned session/window/pane data." (tm:with-client (client :program program :socket-path socket-path) (let ((snapshot (tm:fetch-snapshot client))) (loop for session across (tm:sessions snapshot) collect (list :session (tm:name session) :windows (loop for link across (tm:window-links session) for window = (tm:window link) collect (list :index (tm:index link) :name (tm:name window) :panes (map 'list #'tm:entity-data (tm:panes window)))))))))libtmux:fetch-snapshot contacts tmux. Traversal and projection inside the loops are local. Returned strings and sequence containers belong to the caller. Acquisition spans an interval; it is not a transaction over a changing daemon.
The walkthrough also creates two sessions, splits a pane and links one physical window into both sessions:
graph LR work[Session: work] --> a[Window link: index 0] work --> b[Window link: logs] review[Session: review] --> c[Window link: index 7] review --> d[Window link: notes] a --> editor[Physical window: editor] c --> editor editor --> p1[Pane 1] editor --> p2[Pane 2] b --> logs[Physical window: logs] d --> notes[Physical window: notes]Select a window link when session/index context matters. Rename the physical window to affect every link. Refresh returns a new observation; earlier captures retain their values. See identity and ownership.
Process the retained capture with ordinary Lisp. This function uses only local accessors and returns a list of owned pane data:
(defun wide-active-panes (snapshot) "Select captured active panes at least 40 cells wide; no tmux commands." (loop for pane across (tm:panes snapshot) when (and (tm:active-p pane) (>= (tm:width pane) 40)) collect (tm:entity-data pane)))Compile a local query¶
The tutorial’s example package gives libtmux and libtmux.query the local
nicknames tm and q. This checked function compiles once, filters a captured
vector locally, and returns ordinary owned data:
(defparameter *wide-active-filter* (q:compile-filter :pane '(:and (:= :pane/active t) (:>= :pane/width 40))))
(defun wide-active-pane-data (snapshot) "Return owned data for captured active panes at least 40 cells wide; no I/O." (map 'vector #'tm:entity-data (q:matching *wide-active-filter* (tm:panes snapshot))))matching returns a fresh simple vector and preserves order and duplicates.
The compiler owns mutable operands, rejects cycles and mistyped fields, and
never evaluates query data. Scalar filtering performs no tmux calls. Read the
query guide for presence, limits, inspection and strict
cardinality.
Relations traverse the same captured graph. This checked query follows session links to physical windows and panes, then uses the eager local QuerySet:
(defparameter *active-session-filter* (q:compile-filter :session '(:any :session/window-links (:is :link/window (:any :window/panes (:= :pane/active t))))))
(defun active-session-data (snapshot) "Return owned data for sessions with an active pane; no I/O." (map 'vector #'tm:entity-data (q:items (q:where (q:from (tm:sessions snapshot) :model :session) *active-session-filter*))))Declare and apply a workspace¶
The optional libtmux/workspace ASDF system reads a closed, bounded JSON
document, plans locally, and applies only through an explicit client. The
shipped workspace.json creates two windows and
three panes from argv arrays. This tested function returns both the inspectable
plan and retained application evidence:
(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))))An exact second apply returns an empty plan and no command outcomes. A conflict
fails before dispatch. A partial failure retains the failed step and every core
outcome; no mutation is retried or rolled back. The CLI’s validate and plan
modes need no daemon, while apply requires --socket-path. Read the
workspace guide and
generated workspace API.
Add regex and a versioned filter document¶
Optional systems keep core/query dependency-free while providing a compiled
CL-PPCRE scanner and a bounded native JSON envelope. These checked definitions
match the demo’s cat panes, serialize the filter, read it with explicit regex
permission, and evaluate the restored predicate against the same capture:
(defparameter *cat-command-filter* (q:compile-filter :pane '(:regex-full-match :pane/current-command (:cl-ppcre "CAT" :case-insensitive-mode t))))
(defun cat-pane-data (snapshot) "Return owned data for captured panes whose command is exactly cat; no I/O." (map 'vector #'tm:entity-data (q:matching *cat-command-filter* (tm:panes snapshot))))(defun round-trip-filter-document (snapshot) "Round-trip the checked regex filter and apply it to one retained snapshot." (let* ((document (j:write-filter *cat-command-filter*)) (restored (j:read-filter document :allow-regex t))) (values document (map 'vector #'tm:entity-data (q:matching restored (tm:panes snapshot))))))The JSON decoder rejects duplicates, unknown names, null operands and exhausted input/depth/node/string/membership/numeric bounds. It never interns external names. See optional query interchange.
Find the right guide¶
| Task | Start here |
|---|---|
| Load the library; inspect and build a workspace¶ | REPL tutorial |
| Start and stop a private empty-config daemon¶ | Daemon scopes |
Filter with loop, handle false/absence, export plists¶ | Local data recipes |
| Compile typed filters, traverse relations and compose QuerySets¶ | Native queries · Query API |
| Add CL-PPCRE or exchange bounded filter documents¶ | Query interchange · JSON API |
| Validate, plan or explicitly apply a declarative workspace¶ | Workspaces · Workspace API |
| Rename, resize, move, send input, capture, use buffers or terminate explicit targets¶ | Operation contracts |
| Read typed options, hooks and environment¶ | Decoded configuration |
| Inspect native option, hook and environment output¶ | Configuration inspection |
| Look up signatures, multiple values and conditions¶ | Public API · Captured fields |
| Understand deadlines, cancellation and cleanup¶ | Transport |
| Reconnect without replaying commands¶ | Fresh control observations |
| Evaluate async, streaming and performance support¶ | Capabilities and limits |
| Rerun FiveAM tests at the REPL or inspect CI coverage¶ | Testing · Verification |
| Navigate all documentation¶ | Guide index |
Core subprocess calls block. Each client accepts one external operation at a
time; concurrent reuse signals client-busy. Optional control
submit-command queues a request
without awaiting its reply; use
await-request or
next-event for bounded waits.
Closing a client releases its resources and leaves tmux running.
kill-server
is a separate explicit operation that proves the daemon accepted during its
preflight exited. Daemon scopes separately own a new private
daemon; the tutorial also documents explicit tmux setup and cleanup.
Named mutations preserve dispatch evidence and separate successful changes from follow-up observation failures. Inspect command-outcome or the condition’s error-outcome; never retry an uncertain mutation blindly. See outcome handling.
The examples are shipped source, executed on isolated servers and checked for excerpt drift. All tmux releases from 3.3a through 3.8 are supported. CI uses the latest stable SBCL pin and tests only the range’s endpoints, 3.3a and 3.8, to reduce build time. No benchmark result is claimed.
For changes, read Contributing and Writing. Licensed under MIT.
Branding assets include the logo, icons, and social cards.