libtmux for Lua
Lua-based tmux client
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
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")
-- A private server: a socket and an empty config of its own, so this-- neither touches nor is changed by a tmux you already use.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,-- and keep it alive with no session so the program creates them all.tmux({ "-f", "/dev/null", "start-server", ";", "set-option", "-s", "exit-empty", "off",})
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await()) local created = must(server:new_session({ name = "demo", }):await())
-- Lay out the session: name its window, split it, add a second -- window. None of them takes the focus from the first pane. local editor = created.window must(editor:rename("editor"):await()) local split = must(created.pane:split({ direction = "right", }):await()) local right = split.pane local logs = must(created.session:new_window({ name = "logs", }):await())
-- Type a command into the new pane and wait for what it prints. local command = "printf 'hello from %s\\n' tmux" must(right:send_text(command):await()) must(right:send_keys({ "Enter" }):await()) must(right:wait_for_text("hello from tmux", { timeout = 10000, }):await())
-- Move the focus an attached client sees: the pane, then the window. must(right:select():await()) must(logs.window_link:select():await())
-- Walk the objects: down from the server, then up from one pane. local snapshot = must(server:snapshot():await()) for _, each in ipairs(snapshot.sessions) do print("session " .. each.name) for _, link in ipairs(each.window_links) do local window = link.window local line = (" window %d:%s:"):format( link.index, window.name ) for _, pane in ipairs(window.panes) do line = line .. " " .. pane.id end print(line) end end local id = right:reference().id for _, pane in ipairs(snapshot.panes) do if pane.id == id then local window = pane.window local owner = window.window_links[1].session print(("%s is in window %s of session %s"):format( pane.id, window.name, owner.name )) end end
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")
-- A private server: a socket and an empty config of its own, so this-- neither touches nor is changed by a tmux you already use.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, -- and keep it alive with no session so the program creates them all. tmux({ "-f", "/dev/null", "start-server", ";", "set-option", "-s", "exit-empty", "off", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await()) local created = must(server:new_session({ name = "demo", }):await())
-- Lay out the session: name its window, split it, add a second -- window. None of them takes the focus from the first pane. local editor = created.window must(editor:rename("editor"):await()) local split = must(created.pane:split({ direction = "right", }):await()) local right = split.pane local logs = must(created.session:new_window({ name = "logs", }):await())
-- Type a command into the new pane and wait for what it prints. local command = "printf 'hello from %s\\n' tmux" must(right:send_text(command):await()) must(right:send_keys({ "Enter" }):await()) must(right:wait_for_text("hello from tmux", { timeout = 10000, }):await())
-- Move the focus an attached client sees: the pane, then the window. must(right:select():await()) must(logs.window_link:select():await())
-- Walk the objects: down from the server, then up from one pane. local snapshot = must(server:snapshot():await()) for _, each in ipairs(snapshot.sessions) do print("session " .. each.name) for _, link in ipairs(each.window_links) do local window = link.window local line = (" window %d:%s:"):format( link.index, window.name ) for _, pane in ipairs(window.panes) do line = line .. " " .. pane.id end print(line) end end local id = right:reference().id for _, pane in ipairs(snapshot.panes) do if pane.id == id then local window = pane.window local owner = window.window_links[1].session print(("%s is in window %s of session %s"):format( pane.id, window.name, owner.name )) end end
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")
-- A private server: a socket and an empty config of its own, so this-- neither touches nor is changed by a tmux you already use.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, -- and keep it alive with no session so the program creates them all. tmux({ "-f", "/dev/null", "start-server", ";", "set-option", "-s", "exit-empty", "off", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await()) local created = must(server:new_session({ name = "demo", }):await())
-- Lay out the session: name its window, split it, add a second -- window. None of them takes the focus from the first pane. local editor = created.window must(editor:rename("editor"):await()) local split = must(created.pane:split({ direction = "right", }):await()) local right = split.pane local logs = must(created.session:new_window({ name = "logs", }):await())
-- Type a command into the new pane and wait for what it prints. local command = "printf 'hello from %s\\n' tmux" must(right:send_text(command):await()) must(right:send_keys({ "Enter" }):await()) must(right:wait_for_text("hello from tmux", { timeout = 10000, }):await())
-- Move the focus an attached client sees: the pane, then the window. must(right:select():await()) must(logs.window_link:select():await())
-- Walk the objects: down from the server, then up from one pane. local snapshot = must(server:snapshot():await()) for _, each in ipairs(snapshot.sessions) do print("session " .. each.name) for _, link in ipairs(each.window_links) do local window = link.window local line = (" window %d:%s:"):format( link.index, window.name ) for _, pane in ipairs(window.panes) do line = line .. " " .. pane.id end print(line) end end local id = right:reference().id for _, pane in ipairs(snapshot.panes) do if pane.id == id then local window = pane.window local owner = window.window_links[1].session print(("%s is in window %s of session %s"):format( pane.id, window.name, owner.name )) end end
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")
-- A private server: a socket and an empty config of its own, so this-- neither touches nor is changed by a tmux you already use.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, -- and keep it alive with no session so the program creates them all. tmux({ "-f", "/dev/null", "start-server", ";", "set-option", "-s", "exit-empty", "off", })
must(adapter.run(function(runtime) local server = must(runtime:connect({ binary = binary, socket_path = socket, }):await()) local created = must(server:new_session({ name = "demo", }):await())
-- Lay out the session: name its window, split it, add a second -- window. None of them takes the focus from the first pane. local editor = created.window must(editor:rename("editor"):await()) local split = must(created.pane:split({ direction = "right", }):await()) local right = split.pane local logs = must(created.session:new_window({ name = "logs", }):await())
-- Type a command into the new pane and wait for what it prints. local command = "printf 'hello from %s\\n' tmux" must(right:send_text(command):await()) must(right:send_keys({ "Enter" }):await()) must(right:wait_for_text("hello from tmux", { timeout = 10000, }):await())
-- Move the focus an attached client sees: the pane, then the window. must(right:select():await()) must(logs.window_link:select():await())
-- Walk the objects: down from the server, then up from one pane. local snapshot = must(server:snapshot():await()) for _, each in ipairs(snapshot.sessions) do print("session " .. each.name) for _, link in ipairs(each.window_links) do local window = link.window local line = (" window %d:%s:"):format( link.index, window.name ) for _, pane in ipairs(window.panes) do line = line .. " " .. pane.id end print(line) end end local id = right:reference().id for _, pane in ipairs(snapshot.panes) do if pane.id == id then local window = pane.window local owner = window.window_links[1].session print(("%s is in window %s of session %s"):format( pane.id, window.name, owner.name )) end end
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)tmux before the first call: no server is running.
tmux after tmux: no server is running.
tmux after Runtime:connect: no server is running; the call sent no command.
tmux after Server:new_session: added session demo.
tmux after Window:rename: renamed window 0:sh to 0:editor.
tmux after Pane:split: added pane %1 to window 0:editor.
tmux after Session:new_window: added window 1:logs.
tmux after Pane:send_text: sent keys to pane %1.
tmux after Pane:send_keys: sent keys to pane %1.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane:wait_for_text: read pane %1.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane:select: selected pane %1 in window 0:editor.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after WindowLink:select: selected window 1:logs.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Server:snapshot: unchanged.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after SnapshotWindowLink.window: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after SnapshotWindowLink.window: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Pane:reference: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after SnapshotPane.window: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after SnapshotWindowLink.session: unchanged, the call sent no command.
$ printf 'hello from %s\n' tmux hello from tmux $
tmux after Server:close: unchanged, the call sent no command.
tmux after tmux: the server has exited.
Before the first call
No tmux server is running yet.
tmux returned exit status
{ "status": 0}1 tmux command, 1 that changes tmux
-
start-server ; set-option -s exit-empty offno output
Runtime:connect returned Server
{ "binary": "/usr/bin/tmux", "socketPath": "/tmp/ltx-lua-home-AbC123/tmux.sock"}The call sent no tmux command.
Server:new_session returned Creation
{ "pane": "%0", "session": "$0", "window": "@0"}1 tmux command, 1 that changes tmux
-
new-session -d -P -F 'LQ1 $a;#{q:session_id};#{q:window_id};#{q:pane_id};#{q:window_index};.' -s demo --LQ1 $a;\$0;@0;\%0;0;.
Window:rename returned boolean
true1 tmux command, 1 that changes tmux
-
rename-window -t @0 -- editorno output
Pane:split returned Creation
{ "pane": "%1"}1 tmux command, 1 that changes tmux
-
split-window -d -P -F 'LQ1 $a;#{q:session_id};#{q:window_id};#{q:pane_id};#{q:window_index};.' -t %0 -h --LQ1 $a;\$0;@0;\%1;0;.
Session:new_window returned Creation
{ "pane": "%2", "window": "@1"}1 tmux command, 1 that changes tmux
-
new-window -d -P -F 'LQ1 $a;#{q:session_id};#{q:window_id};#{q:pane_id};#{q:window_index};.' -n logs -t $0: --LQ1 $a;\$0;@1;\%2;1;.
Pane:send_text returned boolean
true1 tmux command, 1 that changes tmux
-
send-keys -t %1 -l -- 'printf '\''hello from %s\n'\'' tmux'no output
Pane:send_keys returned boolean
true1 tmux command, 1 that changes tmux
-
send-keys -t %1 -- Enterno output
Pane:wait_for_text returned TextWait
{ "found": true}1 tmux command, 1 that changes tmux
-
capture-pane -p -J -t %1 ; run-shell -C -t %1 libtmux-pane-<id>-#{q:pane_id}-#{q:pane_dead}|$ printf 'hello from %s\n' tmux hello from tmux $
Pane:select returned boolean
true1 tmux command, 1 that changes tmux
-
select-pane -t %1no output
WindowLink:select returned boolean
true1 tmux command, 1 that changes tmux
-
select-window -t $0:1no output
Server:snapshot returned Snapshot
{ "panes": 3, "sessions": 1, "windows": 2}6 tmux commands
-
list-sessions -F 'LQ1 $a;#{q:session_activity};#{q:session_attached};#{q:session_created};#{q:session_group};#{q:session_grouped};#{q:session_id};#{q:session_name};#{q:session_path};#{q:session_windows};.'LQ1 $a;1793889756;0;1793889756;;0;\$0;demo;/tmp/marked;2;. -
list-windows -a -F 'LQ1 $a;#{q:window_height};#{q:window_id};#{q:window_layout};#{q:window_name};#{q:window_panes};#{q:window_visible_layout};#{q:window_width};#{q:window_zoomed_flag};.'LQ1 $a;24;@0;\{\"V\":2,\"L\":\{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":\[\{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"l\":0,\"i\":0,\"I\":\"\%0\"\},\{\"t\":\"p\",\"w\":39,\"h\":24,\"x\":41,\"y\":0,\"a\":true,\"i\":1,\"I\":\"\%1\"\}]\}\};editor;2;\{\"V\":2,\"L\":\{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":\[\{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"l\":0,\"i\":0,\… -
list-panes -a -F 'LQ1 $a;#{q:pane_active};#{q:pane_current_command};#{q:pane_current_path};#{q:pane_dead};#{q:pane_dead_signal};#{q:pane_dead_status};#{q:pane_dead_time};#{q:pane_height};#{q:pane_id};#{q:pane_index};#{q:pane_left};#{q:pane_mode};#{q:pane_in_mode};#{q:pane_pid};#{q:pane_start_command};#{q:pane_synchronized};#{q:pane_title};#{q:pane_top};#{q:pane_tty};#{q:pane_width};#{q:window_id};…LQ1 $a;0;sh;/tmp/marked;0;;;;24;\%0;0;0;;0;3338115;;0;host;0;/dev/pts/16;40;@0;. LQ1 $a;1;sh;/tmp/marked;0;;;;24;\%1;1;41;;0;3338420;;0;host;0;/dev/pts/17;39;@0;. LQ1 $a;1;sh;/tmp/marked;0;;;;24;\%2;0;0;;0;3338614;;0;host;0;/dev/pts/18;80;@1;. -
list-windows -a -F 'LQ1 $a;#{q:window_active};#{q:window_flags};#{q:window_index};#{q:session_id};#{q:window_id};.'LQ1 $a;0;-;0;\$0;@0;. LQ1 $a;1;\*;1;\$0;@1;. -
list-clients -F 'LQ1 $a;#{q:client_activity};#{q:client_control_mode};#{q:client_created};#{q:client_height};#{q:client_name};#{q:client_pid};#{q:client_readonly};#{q:client_session};#{q:client_tty};#{q:client_width};.'no output -
list-buffers -F 'LQ1 $a;#{q:buffer_created};#{q:buffer_name};#{q:buffer_sample};#{q:buffer_size};.'no output
SnapshotWindowLink.window returned SnapshotWindow
{ "id": "@0", "name": "editor"}The call sent no tmux command.
SnapshotWindowLink.window returned SnapshotWindow
{ "id": "@1", "name": "logs"}The call sent no tmux command.
Pane:reference returned string
{ "id": "%1"}The call sent no tmux command.
SnapshotPane.window returned SnapshotWindow
{ "id": "@0", "name": "editor"}The call sent no tmux command.
SnapshotWindowLink.session returned SnapshotSession
{ "id": "$0", "name": "demo"}The call sent no tmux command.
Server:close returned boolean
trueThe call sent no tmux command.
tmux returned exit status
{ "status": 0}1 tmux command, 1 that changes tmux
-
kill-serverno output
Run this example
POSIX shell, 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 and libtmux 0.1.1.
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
luarocks --tree .rocks install luv 1.52.1-0luarocks --tree .rocks install libtmux 0.1.1-1unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sheval "$(luarocks --tree .rocks path)"lua quickstart.luaExpected output
session demo window 0:editor: %0 %1 window 1:logs: %2%1 is in window editor of session demoInstall libtmux
$ luarocks --local install libtmuxTopics
Subject-by-subject, with Lua in every code block.
Guides
Task guides for Lua.
Examples
Complete programs, sourced from the code each port tests.
Concepts
The domain model every port shares.