tmux client library for Lua
Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents.
Guides
Task guides for Lua.
Topics
Subject-by-subject, with Lua in every code block.
Examples
Complete programs, sourced from the code each port tests.
API reference
Types and functions of the Lua API.
Install
luarocks --local install libtmuxAt a glance
Server → Session → Window → Pane. Each level of the tmux hierarchy has its own entry in the Lua API, with the tmux object it wraps.
| Type | Wraps |
|---|---|
libtmux.Server | 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
Start with Lua
local adapter = require("libtmux.runtime.luv")local uv = require("luv")
local function must(value, err) if err ~= nil then error(err, 0) end return valueend
local binaryfor directory in (os.getenv("PATH") or ""):gmatch("[^:]+") do local candidate = uv.fs_realpath(directory .. "/tmux") if candidate and uv.fs_access(candidate, "X") then binary = candidate break endendassert(binary, "tmux must be installed on PATH")
local directory = assert(uv.fs_mkdtemp("/tmp/ltx-lua-home-XXXXXX"))local socket = directory .. "/tmux.sock"
local function tmux(arguments) local args = { "-S", socket } for _, argument in ipairs(arguments) do args[#args + 1] = argument end
local timer = assert(uv.new_timer()) local process, status process = assert(uv.spawn(binary, { args = args, stdio = { nil, 1, 2 }, }, function(code) status = code process:close() timer:stop() timer:close() end)) timer:start(10000, 0, function() process:kill("sigkill") end) uv.run() assert(status == 0, "tmux command failed or timed out")end
-- This Lua API connects to an existing daemon. Start our own first.tmux({ "-f", "/dev/null", "new-session", "-d", "-s", "work", "-n", "main", "/bin/sh",})
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await())
local created = must(server:new_session({ name = "quickstart", window_name = "main", argv = { "/bin/sh" }, }):await())
local logs = must(created.session:new_window({ name = "logs", argv = { "/bin/cat" }, }):await()) must(logs.pane:split({ direction = "right", percent = 40, argv = { "/bin/cat" }, }):await())
local marker = "libtmux-ready" local quoted = "'" .. binary:gsub("'", "'\\''") .. "'" local command = ("printf '\\nlibtmux ready\\n'; " .. "%s wait-for -S %s"):format(quoted, marker) must(created.pane:send_text(command):await()) must(created.pane:send_keys({ "Enter" }):await()) must(server:command( { "wait-for", marker }, { timeout = 10000 } ):await())
local capture = must(created.pane:capture():await()) assert(must(capture:text()):find( "\nlibtmux ready\n", 1, true ), "pane output was not captured") print("libtmux ready")
must(server:close():await()) return trueend))local adapter = require("libtmux.runtime.luv")local uv = require("luv")
local function must(value, err) if err ~= nil then error(err, 0) end return valueend
local binaryfor directory in (os.getenv("PATH") or ""):gmatch("[^:]+") do local candidate = uv.fs_realpath(directory .. "/tmux") if candidate and uv.fs_access(candidate, "X") then binary = candidate break endendassert(binary, "tmux must be installed on PATH")
local directory = assert(uv.fs_mkdtemp("/tmp/ltx-lua-home-XXXXXX"))local socket = directory .. "/tmux.sock"
local function tmux(arguments) local args = { "-S", socket } for _, argument in ipairs(arguments) do args[#args + 1] = argument end
local timer = assert(uv.new_timer()) local process, status process = assert(uv.spawn(binary, { args = args, stdio = { nil, 1, 2 }, }, function(code) status = code process:close() timer:stop() timer:close() end)) timer:start(10000, 0, function() process:kill("sigkill") end) uv.run() assert(status == 0, "tmux command failed or timed out")end
local ok, failure = xpcall(function() -- This Lua API connects to an existing daemon. Start our own first. tmux({ "-f", "/dev/null", "new-session", "-d", "-s", "work", "-n", "main", "/bin/sh", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await())
local created = must(server:new_session({ name = "quickstart", window_name = "main", argv = { "/bin/sh" }, }):await())
local logs = must(created.session:new_window({ name = "logs", argv = { "/bin/cat" }, }):await()) must(logs.pane:split({ direction = "right", percent = 40, argv = { "/bin/cat" }, }):await())
local marker = "libtmux-ready" local quoted = "'" .. binary:gsub("'", "'\\''") .. "'" local command = ("printf '\\nlibtmux ready\\n'; " .. "%s wait-for -S %s"):format(quoted, marker) must(created.pane:send_text(command):await()) must(created.pane:send_keys({ "Enter" }):await()) must(server:command( { "wait-for", marker }, { timeout = 10000 } ):await())
local capture = must(created.pane:capture():await()) assert(must(capture:text()):find( "\nlibtmux ready\n", 1, true ), "pane output was not captured") print("libtmux ready")
must(server:close():await()) return true end))end, debug.traceback)
if not ok then error(failure, 0) endlocal adapter = require("libtmux.runtime.luv")local uv = require("luv")
local function must(value, err) if err ~= nil then error(err, 0) end return valueend
local binaryfor directory in (os.getenv("PATH") or ""):gmatch("[^:]+") do local candidate = uv.fs_realpath(directory .. "/tmux") if candidate and uv.fs_access(candidate, "X") then binary = candidate break endendassert(binary, "tmux must be installed on PATH")
local directory = assert(uv.fs_mkdtemp("/tmp/ltx-lua-home-XXXXXX"))local socket = directory .. "/tmux.sock"
local function tmux(arguments) local args = { "-S", socket } for _, argument in ipairs(arguments) do args[#args + 1] = argument end
local timer = assert(uv.new_timer()) local process, status process = assert(uv.spawn(binary, { args = args, stdio = { nil, 1, 2 }, }, function(code) status = code process:close() timer:stop() timer:close() end)) timer:start(10000, 0, function() process:kill("sigkill") end) uv.run() assert(status == 0, "tmux command failed or timed out")end
local ok, failure = pcall(function() -- This Lua API connects to an existing daemon. Start our own first. tmux({ "-f", "/dev/null", "new-session", "-d", "-s", "work", "-n", "main", "/bin/sh", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await())
local created = must(server:new_session({ name = "quickstart", window_name = "main", argv = { "/bin/sh" }, }):await())
local logs = must(created.session:new_window({ name = "logs", argv = { "/bin/cat" }, }):await()) must(logs.pane:split({ direction = "right", percent = 40, argv = { "/bin/cat" }, }):await())
local marker = "libtmux-ready" local quoted = "'" .. binary:gsub("'", "'\\''") .. "'" local command = ("printf '\\nlibtmux ready\\n'; " .. "%s wait-for -S %s"):format(quoted, marker) must(created.pane:send_text(command):await()) must(created.pane:send_keys({ "Enter" }):await()) must(server:command( { "wait-for", marker }, { timeout = 10000 } ):await())
local capture = must(created.pane:capture():await()) assert(must(capture:text()):find( "\nlibtmux ready\n", 1, true ), "pane output was not captured") print("libtmux ready")
must(server:close():await()) return true end))end)
local cleaned, cleanup_error = pcall(function() if uv.fs_stat(socket) then tmux({ "kill-server" }) end local removed, _, code = uv.fs_unlink(socket) assert(removed or code == "ENOENT", "could not remove owned socket") assert(uv.fs_rmdir(directory))end)
if not cleaned then io.stderr:write(tostring(cleanup_error), "\n") endif not ok then error(failure, 0) endassert(cleaned, "cleanup failed; retained " .. directory)local adapter = require("libtmux.runtime.luv")local uv = require("luv")
local function must(value, err) if err ~= nil then error(err, 0) end return valueend
local binaryfor directory in (os.getenv("PATH") or ""):gmatch("[^:]+") do local candidate = uv.fs_realpath(directory .. "/tmux") if candidate and uv.fs_access(candidate, "X") then binary = candidate break endendassert(binary, "tmux must be installed on PATH")
local directory = assert(uv.fs_mkdtemp("/tmp/ltx-lua-home-XXXXXX"))local socket = directory .. "/tmux.sock"
local function tmux(arguments) local args = { "-S", socket } for _, argument in ipairs(arguments) do args[#args + 1] = argument end
local timer = assert(uv.new_timer()) local process, status process = assert(uv.spawn(binary, { args = args, stdio = { nil, 1, 2 }, }, function(code) status = code process:close() timer:stop() timer:close() end)) timer:start(10000, 0, function() process:kill("sigkill") end) uv.run() assert(status == 0, "tmux command failed or timed out")end
local ok, failure = xpcall(function() -- This Lua API connects to an existing daemon. Start our own first. tmux({ "-f", "/dev/null", "new-session", "-d", "-s", "work", "-n", "main", "/bin/sh", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await())
local created = must(server:new_session({ name = "quickstart", window_name = "main", argv = { "/bin/sh" }, }):await())
local logs = must(created.session:new_window({ name = "logs", argv = { "/bin/cat" }, }):await()) must(logs.pane:split({ direction = "right", percent = 40, argv = { "/bin/cat" }, }):await())
local marker = "libtmux-ready" local quoted = "'" .. binary:gsub("'", "'\\''") .. "'" local command = ("printf '\\nlibtmux ready\\n'; " .. "%s wait-for -S %s"):format(quoted, marker) must(created.pane:send_text(command):await()) must(created.pane:send_keys({ "Enter" }):await()) must(server:command( { "wait-for", marker }, { timeout = 10000 } ):await())
local capture = must(created.pane:capture():await()) assert(must(capture:text()):find( "\nlibtmux ready\n", 1, true ), "pane output was not captured") print("libtmux ready")
must(server:close():await()) return true end))end, debug.traceback)
local cleaned, cleanup_error = pcall(function() if uv.fs_stat(socket) then tmux({ "kill-server" }) end local removed, _, code = uv.fs_unlink(socket) assert(removed or code == "ENOENT", "could not remove owned socket") assert(uv.fs_rmdir(directory))end)
if not cleaned then io.stderr:write(tostring(cleanup_error), "\n") endif not ok then error(failure, 0) endassert(cleaned, "cleanup failed; retained " .. directory)Run this example
POSIX shell, Git, tmux 3.3a or newer, Lua 5.5.1, LuaRocks 3.13.0, CMake and a C compiler; setup installs luv 1.52.1-0.
Save the program above as quickstart.lua 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. Error handling adds diagnostics; every version checks required results and deadlines.
Setup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-lua.git \ 0395620d362ef75c74cd488f9577afda68922586git -C source checkout -q --detach FETCH_HEADluarocks --tree .rocks install luv 1.52.1-0(cd source && luarocks --tree ../.rocks make \ rockspecs/libtmux-scm-1.rockspec)unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sheval "$(luarocks --tree .rocks path)"lua quickstart.luaExpected output
libtmux readyLibrary revision: 0395620d362e.
Typed API
- Typed objects instead of parsed format strings:
libtmux.SnapshotPane - Filtering and queries over tmux objects:
libtmux.Selection - Typed errors instead of exit codes and stderr:
libtmux.Error
What you get over the tmux CLI
- Connect to or start a tmux server:
libtmux.Runtime:connect - Traversal from a server down to its panes:
libtmux.Server:snapshot - Send keys to a pane:
libtmux.Pane:send_keys - Watch for changes through tmux control mode:
libtmux.Observation:watch_notifications - Capture pane contents:
libtmux.Pane:capture - Arguments passed as a list, with no shell quoting:
libtmux.Server:command