LuaHome

Choose documentation 1

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

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 value
end
local binary
for 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
end
end
assert(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 true
end))
local adapter = require("libtmux.runtime.luv")
local uv = require("luv")
local function must(value, err)
if err ~= nil then error(err, 0) end
return value
end
local binary
for 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
end
end
assert(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) end
local adapter = require("libtmux.runtime.luv")
local uv = require("luv")
local function must(value, err)
if err ~= nil then error(err, 0) end
return value
end
local binary
for 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
end
end
assert(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") end
if not ok then error(failure, 0) end
assert(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 value
end
local binary
for 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
end
end
assert(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") end
if not ok then error(failure, 0) end
assert(cleaned, "cleanup failed; retained " .. directory)
Example details
No tmux server yet

tmux before the first call: no server is running.

No tmux server yet

tmux after tmux: no server is running.

No tmux server yet

tmux after Runtime:connect: no server is running; the call sent no command.

%0 sh
[demo]

tmux after Server:new_session: added session demo.

%0 sh
[demo]

tmux after Window:rename: renamed window 0:sh to 0:editor.

%0 sh
%1 sh
[demo]

tmux after Pane:split: added pane %1 to window 0:editor.

%0 sh
%1 sh
[demo]

tmux after Session:new_window: added window 1:logs.

%0 sh
%1 sh
[demo]

tmux after Pane:send_text: sent keys to pane %1.

%0 sh
%1 sh
[demo]

tmux after Pane:send_keys: sent keys to pane %1.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
[demo]

tmux after Pane:wait_for_text: read pane %1.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
[demo]

tmux after Pane:select: selected pane %1 in window 0:editor.

%2 sh
[demo]

tmux after WindowLink:select: selected window 1:logs.

%2 sh
[demo]

tmux after Server:snapshot: unchanged.

%2 sh
[demo]

tmux after SnapshotWindowLink.window: unchanged, the call sent no command.

%2 sh
[demo]

tmux after SnapshotWindowLink.window: unchanged, the call sent no command.

%2 sh
[demo]

tmux after Pane:reference: unchanged, the call sent no command.

%2 sh
[demo]

tmux after SnapshotPane.window: unchanged, the call sent no command.

%2 sh
[demo]

tmux after SnapshotWindowLink.session: unchanged, the call sent no command.

%2 sh
[demo]

tmux after Server:close: unchanged, the call sent no command.

No sessions

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
  1. start-server ; set-option -s exit-empty off no 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
  1. 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

true
1 tmux command, 1 that changes tmux
  1. rename-window -t @0 -- editor no output

Pane:split returned Creation

{
"pane": "%1"
}
1 tmux command, 1 that changes tmux
  1. 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
  1. 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

true
1 tmux command, 1 that changes tmux
  1. send-keys -t %1 -l -- 'printf '\''hello from %s\n'\'' tmux' no output

Pane:send_keys returned boolean

true
1 tmux command, 1 that changes tmux
  1. send-keys -t %1 -- Enter no output

Pane:wait_for_text returned TextWait

{
"found": true
}
1 tmux command, 1 that changes tmux
  1. 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

true
1 tmux command, 1 that changes tmux
  1. select-pane -t %1 no output

WindowLink:select returned boolean

true
1 tmux command, 1 that changes tmux
  1. select-window -t $0:1 no output

Server:snapshot returned Snapshot

{
"panes": 3,
"sessions": 1,
"windows": 2
}
6 tmux commands
  1. 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;.
  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,\…
  3. 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;.
  4. 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;.
  5. 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
  6. 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

true

The call sent no tmux command.

tmux returned exit status

{
"status": 0
}
1 tmux command, 1 that changes tmux
  1. kill-server no output
0/18
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-0
luarocks --tree .rocks install libtmux 0.1.1-1
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
eval "$(luarocks --tree .rocks path)"
lua quickstart.lua

Expected output

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

Install libtmux

$ luarocks --local install libtmux

Packages and integrations

API references

Esc

Type to search.

Reference previewOpen reference