Prerelease This site documents an alpha of libtmux. Its structure, URLs and APIs are subject to change.

libtmux for Lua

Lua
  • Python Unavailable
  • Ruby Unavailable
  • Lua
  • TypeScript Unavailable
  • Rust Unavailable
  • Go Unavailable
  • Java Unavailable
  • .NET Unavailable
  • C++ Unavailable
  • Swift Unavailable

Script tmux from Lua: create sessions and panes, capture terminal output, query server state, and watch pane output. Use standalone Lua with luv or Neovim’s event loop.

Alpha software. The core API is still changing. The separate MCP and workspace packages are unpublished scaffolds.

Install · Read a server · Query · Create panes · Neovim · Guides · CI · MIT license

InstallLink to section

Install the core alpha with LuaRocks configured for your Lua interpreter:

Terminal window
$ luarocks --local install libtmux 0.1.0alpha1-1

To install from a checkout:

Terminal window
$ luarocks --local make rockspecs/libtmux-scm-1.rockspec

See the changelog and release instructions for release history and verification. Linux is tested; macOS remains unverified.

Add the local rocks tree to Lua’s module paths:

Terminal window
$ eval "$(luarocks --local path)"

For standalone scripts, also install luv. Building it requires a C compiler and CMake. Neovim provides its own libuv binding.

Terminal window
$ luarocks --local install luv 1.52.1-0

Core and local queries require only Lua. Importing a module does not start tmux or an event loop. CI runs unit tests on Lua 5.1–5.5 and LuaJIT, plus live tests across tmux 3.2a–3.7c. See the compatibility matrix for exact versions and remaining platform coverage.

Read a serverLink to section

Connect to an existing server by its explicit socket path. This is the full snapshot example, which prints pane and window IDs:

local adapter = require("libtmux.runtime.luv")
local function must(value, err)
if err ~= nil then
error(err, 0)
end
return value
end
must(adapter.run(function(runtime)
local server = must(runtime
:connect({
binary = assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable"),
socket_path = assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to an explicit socket"),
})
:await())
local snapshot = must(server:snapshot({ strict = true }):await())
for _, pane in ipairs(snapshot.panes) do
io.stdout:write(pane.id, "\t", pane.window_id, "\n")
end
must(server:close():await())
return true
end))

Live operations return Requests; :await() yields inside the runtime body and returns value, err. The must helper propagates errors. Closing the connection leaves the tmux server and its sessions running.

Set TMUX_BIN and TMUX_SOCKET to absolute paths for your server, then run:

Terminal window
$ lua examples/snapshot.lua
Try it on a temporary server

Run from the checkout after installing the dependencies above. This starts a private tmux server and removes it when the example finishes.

Terminal window
$ sh <<'SH'
set -eu
unset TMUX TMUX_PANE
TMUX_BIN=$(command -v tmux)
demo_dir=$(mktemp -d /tmp/libtmux-lua-XXXXXX)
TMUX_SOCKET="$demo_dir/tmux.sock"
export TMUX_BIN TMUX_SOCKET
cleanup() {
"$TMUX_BIN" -S "$TMUX_SOCKET" kill-server 2>/dev/null || true
rm -rf "$demo_dir"
}
trap cleanup EXIT
"$TMUX_BIN" -f /dev/null -S "$TMUX_SOCKET" new-session -d -s demo
lua examples/snapshot.lua
SH

Query captured stateLink to section

After capturing snapshot in the example above, filter its panes with structured criteria or an ordinary Lua function:

local editors = snapshot.panes:where({
current_command = { one_of = { "nvim", "vim" } },
})
local inactive = snapshot.panes:filter(function(pane)
return not pane.active
end)
print(#editors, #inactive)
for _, pane in ipairs(editors) do
print(pane.id, pane.current_command)
end

Selections are dense, one-based Lua tables. Both queries read the snapshot without calling tmux. Capture again to refresh it; a snapshot spans multiple tmux commands and is not an atomic view.

See criteria, relationships and live queries, the field reference, or run the standalone table-query example:

Terminal window
$ lua examples/native_query.lua

Create sessions and panesLink to section

Before closing server in that runtime body, create a session and split a window. These calls use the same must helper:

local work = must(server:new_session({ name = "work" }):await())
local editor = must(work.session:new_window({ name = "editor" }):await())
local split = must(editor.pane:split({ direction = "right", percent = 40 }):await())
must(split.pane:send_text("printf '%s\\n' hello"):await())
must(split.pane:send_keys({ "Enter" }):await())

Creation returns a table with session, window, pane and window_link handles. These operations leave the new sessions and panes running. Sending keys confirms that tmux accepted input; it does not wait for a shell command to finish. See creation and pane operations for literal argv, capture, resize and cleanup.

NeovimLink to section

From the checkout, start Neovim with the library on its runtimepath:

Terminal window
$ nvim --cmd 'set runtimepath+=.'

With TMUX_SOCKET set to an existing server’s absolute socket path, run this Lua code. start uses the editor’s loop and reports the result in a callback:

local adapter = require("libtmux.runtime.nvim")
adapter.start(function(runtime)
local server, err = runtime:connect({
binary = vim.fn.exepath("tmux"),
socket_path = assert(os.getenv("TMUX_SOCKET")),
}):await()
if err then
return nil, err
end
return server:snapshot({ strict = true }):await()
end, function(snapshot, err)
if err then
vim.notify(tostring(err), vim.log.levels.ERROR)
return
end
vim.notify(("Panes: %d"):format(#snapshot.panes))
end)

The runtime closes its connections when the body finishes and leaves the tmux server running. See runtime ownership and cancellation.

GuidesLink to section

Live tests use private sockets and clean up their own servers. See the contributing guide for the same offline checks that run in CI.

Esc

Type to search.