tmux, in your language
Control tmux from your application.
Create sessions, arrange panes, send input, and capture output.
Start with Python
from __future__ import annotations
from pathlib import Pathimport tempfileimport time
import libtmux
directory = Path(tempfile.mkdtemp(prefix="libtmux-py-home-"))socket = directory / "tmux.sock"server = libtmux.Server(socket_path=socket, config_file="/dev/null")
session = server.new_session(session_name="demo")window = session.active_windowassert window is not Nonepane = window.split(shell="sh")
pane.send_keys("printf '\\nHello world\\n'", enter=True)
deadline = time.monotonic() + 5while "Hello world" not in pane.capture_pane(): if time.monotonic() >= deadline: raise TimeoutError("The pane did not print Hello world") time.sleep(0.02)
print("Hello world")from __future__ import annotations
from pathlib import Pathimport tempfileimport time
import libtmux
directory = Path(tempfile.mkdtemp(prefix="libtmux-py-home-"))socket = directory / "tmux.sock"server = libtmux.Server(socket_path=socket, config_file="/dev/null")
try: session = server.new_session(session_name="demo") window = session.active_window assert window is not None pane = window.split(shell="sh")
pane.send_keys("printf '\\nHello world\\n'", enter=True)
deadline = time.monotonic() + 5 while "Hello world" not in pane.capture_pane(): if time.monotonic() >= deadline: raise TimeoutError("The pane did not print Hello world") time.sleep(0.02)
print("Hello world")except Exception as error: raise RuntimeError("tmux quickstart failed") from errorfrom __future__ import annotations
from pathlib import Pathimport shutilimport tempfileimport time
import libtmux
directory = Path(tempfile.mkdtemp(prefix="libtmux-py-home-"))socket = directory / "tmux.sock"server = libtmux.Server(socket_path=socket, config_file="/dev/null")
try: session = server.new_session(session_name="demo") window = session.active_window assert window is not None pane = window.split(shell="sh")
pane.send_keys("printf '\\nHello world\\n'", enter=True)
deadline = time.monotonic() + 5 while "Hello world" not in pane.capture_pane(): if time.monotonic() >= deadline: raise TimeoutError("The pane did not print Hello world") time.sleep(0.02)
print("Hello world")finally: if socket.exists(): server.kill() shutil.rmtree(directory)from __future__ import annotations
from pathlib import Pathimport shutilimport tempfileimport time
import libtmux
directory = Path(tempfile.mkdtemp(prefix="libtmux-py-home-"))socket = directory / "tmux.sock"server = libtmux.Server(socket_path=socket, config_file="/dev/null")
try: session = server.new_session(session_name="demo") window = session.active_window assert window is not None pane = window.split(shell="sh")
pane.send_keys("printf '\\nHello world\\n'", enter=True)
deadline = time.monotonic() + 5 while "Hello world" not in pane.capture_pane(): if time.monotonic() >= deadline: raise TimeoutError("The pane did not print Hello world") time.sleep(0.02)
print("Hello world")except Exception as error: raise RuntimeError("tmux quickstart failed") from errorfinally: if socket.exists(): server.kill() shutil.rmtree(directory)Run this example
POSIX shell, Git, tmux 3.2a or newer, Python 3.14.6 and uv (tested 0.12.0).
Save the program above as quickstart.py in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server 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/tmux-python/libtmux.git \ 9fdd083a8181a827889337b63cd4e6daea661a8cgit -C source checkout -q --detach FETCH_HEADuv venv .venvuv pip install --python .venv/bin/python ./sourceunset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh.venv/bin/python quickstart.pyExpected output
Hello worldLibrary revision: 9fdd083a8181.
Dependency cooldowns
The checkbox turns a cooldown on and off for every picker on the site. It applies to what an install pulls in; the package itself, and the libtmux it pins, stay current.
What are cooldowns?
Cooldowns delay picking up newly uploaded packages so the community has time to spot supply-chain attacks before you install. Most package managers don't apply one by default; you opt in through a global setting in your tool's configuration.
Learn more at cooldowns.dev · Datadog Security Labs writeup.
$ pip install libtmux$ pip install libtmuxpip cannot exempt libtmux from its own cooldown, so a cooldown here would also hold back the release this installs. This command runs without one.
$ pip install libtmuxpip cannot exempt libtmux from its own cooldown, so a cooldown here would also hold back the release this installs. This command runs without one.
$ uv add libtmux$ uv add --no-config libtmux$ pipx install libtmux$ pipx install libtmuxpipx cannot exempt libtmux from its own cooldown, so a cooldown here would also hold back the release this installs. This command runs without one.
$ pipx install libtmuxpipx cannot exempt libtmux from its own cooldown, so a cooldown here would also hold back the release this installs. This command runs without one.
Start with Ruby
require "libtmux"
snapshot = LibTmux::Server.start do |server| session = server.new_session( name: "work", window_name: "main", command: ["/bin/cat"] )
window = session.new_window(name: "logs", command: ["/bin/cat"]) window.split( direction: :horizontal, size: "40%", command: ["/bin/cat"] )
server.snapshotend
snapshot.windows.each do |window| pane_ids = window.panes.map(&:id).join(", ") puts "#{window.name}: #{pane_ids}"endrequire "libtmux"
begin snapshot = LibTmux::Server.start do |server| session = server.new_session( name: "work", window_name: "main", command: ["/bin/cat"] )
window = session.new_window(name: "logs", command: ["/bin/cat"]) window.split( direction: :horizontal, size: "40%", command: ["/bin/cat"] )
server.snapshot end
snapshot.windows.each do |window| pane_ids = window.panes.map(&:id).join(", ") puts "#{window.name}: #{pane_ids}" endrescue StandardError => error warn "tmux quickstart failed: #{error.message}" raiseendrequire "libtmux"
server = LibTmux::Server.start
begin session = server.new_session( name: "work", window_name: "main", command: ["/bin/cat"] )
window = session.new_window(name: "logs", command: ["/bin/cat"]) window.split( direction: :horizontal, size: "40%", command: ["/bin/cat"] )
snapshot = server.snapshot snapshot.windows.each do |window| pane_ids = window.panes.map(&:id).join(", ") puts "#{window.name}: #{pane_ids}" endensure server.closeendrequire "libtmux"
server = LibTmux::Server.start
begin session = server.new_session( name: "work", window_name: "main", command: ["/bin/cat"] )
window = session.new_window(name: "logs", command: ["/bin/cat"]) window.split( direction: :horizontal, size: "40%", command: ["/bin/cat"] )
snapshot = server.snapshot snapshot.windows.each do |window| pane_ids = window.panes.map(&:id).join(", ") puts "#{window.name}: #{pane_ids}" endrescue StandardError => error warn "tmux quickstart failed: #{error.message}" raiseensure server.closeendRun this example
POSIX shell, Git, tmux 3.2a or newer, Ruby 4.0.7, Bundler 4.0.20 and a C build toolchain for gems.
Save the program above as quickstart.rb in a new directory, along with the files below.
The Ruby block closes its server automatically. Cleanup shows the explicit ensure form. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
Gemfile
source "https://rubygems.org"gem "libtmux", path: "source/gems/libtmux"Gemfile.lock
PATH remote: source/gems/libtmux specs: libtmux (0.1.0.alpha.1) fiddle (~> 1.1) io-console (~> 0.8) json (~> 3.0.2) securerandom (~> 0.4) tmpdir (~> 0.3)
GEM remote: https://rubygems.org/ specs: fiddle (1.1.8) fileutils (1.8.0) io-console (0.9.4) json (3.0.2) securerandom (0.4.1) tmpdir (0.3.1) fileutils
PLATFORMS ruby x86_64-linux
DEPENDENCIES libtmux!
CHECKSUMS fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02 io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df libtmux (0.1.0.alpha.1) securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 tmpdir (0.3.1) sha256=908e9729bb31ce104ba27fc03cfc8293a7f858a8dc9e3a1d7cfd112919a3513d
BUNDLED WITH 4.0.20Setup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-ruby.git \ 9b1545562a112353c2c893a1d3e8c0d9b4b51f8dgit -C source checkout -q --detach FETCH_HEADbundle config set --local path vendor/bundlebundle install --jobs 2unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shbundle exec ruby quickstart.rbExpected output
main: %0logs: %1, %2Library revision: 9b1545562a11.
$ gem install --version 0.1.0.alpha.1 libtmuxgem "libtmux", "0.1.0.alpha.1"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.2a 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 may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server 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.
$ luarocks --local install libtmux 0.1.0alpha1-1Start with PowerShell
$ErrorActionPreference = 'Stop'Import-Module "$PSScriptRoot/modules/LibTmux/0.1.0/LibTmux.psd1"
$socket = 'libtmux-home-' + [Guid]::NewGuid().ToString('N')$server = New-TmuxServer -SocketName $socket -ConfigurationFile /dev/null
$session = $server | New-TmuxSession ` -Name demo -WindowName editor -Command 'exec /bin/cat'$pane = $session | Get-TmuxPane$null = $pane | Split-TmuxPane -Horizontal -Command 'exec /bin/cat'$null = $session | New-TmuxWindow -Name logs -Command 'exec /bin/cat'
$captured = ($server | Get-TmuxSnapshot).Sessions | Select-TmuxSession -Criteria @{ Name = 'demo' } -ExactlyOneforeach ($window in $captured.Windows) { '{0}: {1}' -f $window.Name, ($window.Panes.Id -join ', ')}$ErrorActionPreference = 'Stop'Import-Module "$PSScriptRoot/modules/LibTmux/0.1.0/LibTmux.psd1"
try { $socket = 'libtmux-home-' + [Guid]::NewGuid().ToString('N') $server = New-TmuxServer -SocketName $socket -ConfigurationFile /dev/null
$session = $server | New-TmuxSession ` -Name demo -WindowName editor -Command 'exec /bin/cat' $pane = $session | Get-TmuxPane $null = $pane | Split-TmuxPane -Horizontal -Command 'exec /bin/cat' $null = $session | New-TmuxWindow -Name logs -Command 'exec /bin/cat'
$captured = ($server | Get-TmuxSnapshot).Sessions | Select-TmuxSession -Criteria @{ Name = 'demo' } -ExactlyOne foreach ($window in $captured.Windows) { '{0}: {1}' -f $window.Name, ($window.Panes.Id -join ', ') }} catch { $failure = [InvalidOperationException]::new( "tmux quickstart failed: $($_.Exception.Message)", $_.Exception) throw $failure}$ErrorActionPreference = 'Stop'Import-Module "$PSScriptRoot/modules/LibTmux/0.1.0/LibTmux.psd1"
$session = $null$failure = $nulltry { $socket = 'libtmux-home-' + [Guid]::NewGuid().ToString('N') $server = New-TmuxServer -SocketName $socket -ConfigurationFile /dev/null
$session = $server | New-TmuxSession ` -Name demo -WindowName editor -Command 'exec /bin/cat' $pane = $session | Get-TmuxPane $null = $pane | Split-TmuxPane -Horizontal -Command 'exec /bin/cat' $null = $session | New-TmuxWindow -Name logs -Command 'exec /bin/cat'
$captured = ($server | Get-TmuxSnapshot).Sessions | Select-TmuxSession -Criteria @{ Name = 'demo' } -ExactlyOne foreach ($window in $captured.Windows) { '{0}: {1}' -f $window.Name, ($window.Panes.Id -join ', ') }} catch { $failure = $_.Exception throw} finally { if ($session) { $cleanupErrors = @() $session | Remove-TmuxSession -Confirm:$false ` -ErrorAction SilentlyContinue -ErrorVariable cleanupErrors if ($cleanupErrors) { if ($failure) { $errors = @($failure) + $cleanupErrors.Exception throw [AggregateException]::new( 'tmux operation and cleanup failed.', [Exception[]]$errors) } throw $cleanupErrors[0] } }}$ErrorActionPreference = 'Stop'Import-Module "$PSScriptRoot/modules/LibTmux/0.1.0/LibTmux.psd1"
$session = $null$failure = $nulltry { $socket = 'libtmux-home-' + [Guid]::NewGuid().ToString('N') $server = New-TmuxServer -SocketName $socket -ConfigurationFile /dev/null
$session = $server | New-TmuxSession ` -Name demo -WindowName editor -Command 'exec /bin/cat' $pane = $session | Get-TmuxPane $null = $pane | Split-TmuxPane -Horizontal -Command 'exec /bin/cat' $null = $session | New-TmuxWindow -Name logs -Command 'exec /bin/cat'
$captured = ($server | Get-TmuxSnapshot).Sessions | Select-TmuxSession -Criteria @{ Name = 'demo' } -ExactlyOne foreach ($window in $captured.Windows) { '{0}: {1}' -f $window.Name, ($window.Panes.Id -join ', ') }} catch { $failure = [InvalidOperationException]::new( "tmux quickstart failed: $($_.Exception.Message)", $_.Exception) throw $failure} finally { if ($session) { $cleanupErrors = @() $session | Remove-TmuxSession -Confirm:$false ` -ErrorAction SilentlyContinue -ErrorVariable cleanupErrors if ($cleanupErrors) { if ($failure) { $errors = @($failure) + $cleanupErrors.Exception throw [AggregateException]::new( 'tmux operation and cleanup failed.', [Exception[]]$errors) } throw $cleanupErrors[0] } }}Run this example
PowerShell 7.4.20 with PSResourceGet 1.1.1, POSIX shell and tmux on Linux.
Save the program above as quickstart.ps1 in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
Setup and run
pwsh -NoLogo -NoProfile -Command ' $ErrorActionPreference = "Stop" $null = New-Item -ItemType Directory -Path ./modules -Force Save-PSResource -Name LibTmux -Version 0.1.0-alpha2 ` -Prerelease -Path ./modules -TrustRepository'unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shpwsh -NoLogo -NoProfile -File quickstart.ps1Expected output
editor: %0, %1logs: %2Library revision: fcd0634e9ca8.
Install-Module -Name LibTmux -AllowPrereleaseRequires PowerShell 7.4 or later and tmux on the same Unix host.
Install-PSResource -Name LibTmux -PrereleaseRequires PowerShell 7.4 or later and tmux on the same Unix host.
Start with Julia
using LibTmux
owned = open_server()server = owned.server
session = new_session(server; name="demo", command=["/bin/cat"])pane = only(panes(snapshot(server))).refsplit_window(server, pane; direction=:below, command=["/bin/cat"])new_window(server, session; name="logs", command=["/bin/cat"])
snap = snapshot(server)@assert isempty(strip(capture_pane(server, pane)))println(only(sessions(snap)).name, ": ", length(windows(snap)), " windows, ", length(panes(snap)), " panes")using LibTmux
try owned = open_server() server = owned.server
session = new_session(server; name="demo", command=["/bin/cat"]) pane = only(panes(snapshot(server))).ref split_window(server, pane; direction=:below, command=["/bin/cat"]) new_window(server, session; name="logs", command=["/bin/cat"])
snap = snapshot(server) @assert isempty(strip(capture_pane(server, pane))) println(only(sessions(snap)).name, ": ", length(windows(snap)), " windows, ", length(panes(snap)), " panes")catch error println(stderr, "tmux quickstart failed: ", sprint(showerror, error)) rethrow()endusing LibTmux
with_server() do server session = new_session(server; name="demo", command=["/bin/cat"]) pane = only(panes(snapshot(server))).ref split_window(server, pane; direction=:below, command=["/bin/cat"]) new_window(server, session; name="logs", command=["/bin/cat"])
snap = snapshot(server) @assert isempty(strip(capture_pane(server, pane))) println(only(sessions(snap)).name, ": ", length(windows(snap)), " windows, ", length(panes(snap)), " panes")endusing LibTmux
try with_server() do server session = new_session(server; name="demo", command=["/bin/cat"]) pane = only(panes(snapshot(server))).ref split_window(server, pane; direction=:below, command=["/bin/cat"]) new_window(server, session; name="logs", command=["/bin/cat"])
snap = snapshot(server) @assert isempty(strip(capture_pane(server, pane))) println(only(sessions(snap)).name, ": ", length(windows(snap)), " windows, ", length(panes(snap)), " panes") endcatch error println(stderr, "tmux quickstart failed: ", sprint(showerror, error)) rethrow()endRun this example
Julia 1.13.0, Git, POSIX shell and tmux on Linux.
Save the program above as quickstart.jl in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server 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-julia.git \ 6f62bd72cb8d99f3759ca5e2cc3591165a03c768git -C source checkout -q --detach FETCH_HEADunset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shjulia --startup-file=no --project=source quickstart.jlExpected output
demo: 2 windows, 3 panesLibrary revision: 6f62bd72cb8d.
Run in Julia with tmux on PATH. This alpha release is installed from Git; it is not registered in General.
Start with TypeScript
import { mkdtemp } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { setTimeout as delay } from "node:timers/promises";import { Server } from "libtmux";
const directory = await mkdtemp(join(tmpdir(), "ltx-home-"));const server = new Server({ socketPath: join(directory, "tmux.sock"), configFile: "/dev/null", timeoutMs: 5_000,});
const signal = AbortSignal.timeout(5_000);const session = await server.newSession({ name: "work", signal });
const editor = await session.newWindow({ name: "editor", shellCommand: "sh", signal,});await editor.split({ signal });
const pane = editor.activePane;if (!pane) throw new Error("The window has no active pane");
await pane.sendKeys("printf '\\nhello\\n'", { signal });
for (;;) { const lines = await pane.capture({ signal }); if (lines.includes("hello")) { console.log("hello"); break; }
await delay(20, undefined, { signal });}import { mkdtemp } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { setTimeout as delay } from "node:timers/promises";import { Server } from "libtmux";
const directory = await mkdtemp(join(tmpdir(), "ltx-home-"));const server = new Server({ socketPath: join(directory, "tmux.sock"), configFile: "/dev/null", timeoutMs: 5_000,});
try { const signal = AbortSignal.timeout(5_000); const session = await server.newSession({ name: "work", signal });
const editor = await session.newWindow({ name: "editor", shellCommand: "sh", signal, }); await editor.split({ signal });
const pane = editor.activePane; if (!pane) throw new Error("The window has no active pane");
await pane.sendKeys("printf '\\nhello\\n'", { signal });
for (;;) { const lines = await pane.capture({ signal }); if (lines.includes("hello")) { console.log("hello"); break; }
await delay(20, undefined, { signal }); }} catch (error) { throw new Error("tmux quickstart failed", { cause: error });}import { mkdtemp, readdir, rm } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { setTimeout as delay } from "node:timers/promises";import { Server } from "libtmux";
const directory = await mkdtemp(join(tmpdir(), "ltx-home-"));const server = new Server({ socketPath: join(directory, "tmux.sock"), configFile: "/dev/null", timeoutMs: 5_000,});
try { const signal = AbortSignal.timeout(5_000); const session = await server.newSession({ name: "work", signal });
const editor = await session.newWindow({ name: "editor", shellCommand: "sh", signal, }); await editor.split({ signal });
const pane = editor.activePane; if (!pane) throw new Error("The window has no active pane");
await pane.sendKeys("printf '\\nhello\\n'", { signal });
for (;;) { const lines = await pane.capture({ signal }); if (lines.includes("hello")) { console.log("hello"); break; }
await delay(20, undefined, { signal }); }} finally { if ((await readdir(directory)).includes("tmux.sock")) { await server.kill(); } await rm(directory, { recursive: true });}import { mkdtemp, readdir, rm } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { setTimeout as delay } from "node:timers/promises";import { Server } from "libtmux";
const directory = await mkdtemp(join(tmpdir(), "ltx-home-"));const server = new Server({ socketPath: join(directory, "tmux.sock"), configFile: "/dev/null", timeoutMs: 5_000,});
const failures: unknown[] = [];try { const signal = AbortSignal.timeout(5_000); const session = await server.newSession({ name: "work", signal });
const editor = await session.newWindow({ name: "editor", shellCommand: "sh", signal, }); await editor.split({ signal });
const pane = editor.activePane; if (!pane) throw new Error("The window has no active pane");
await pane.sendKeys("printf '\\nhello\\n'", { signal });
for (;;) { const lines = await pane.capture({ signal }); if (lines.includes("hello")) { console.log("hello"); break; }
await delay(20, undefined, { signal }); }} catch (error) { failures.push(error);}
try { if ((await readdir(directory)).includes("tmux.sock")) { await server.kill(); }
await rm(directory, { recursive: true });} catch (error) { failures.push(new Error(`Cleanup failed: ${directory}`, { cause: error, }));}
if (failures.length) throw new AggregateError(failures);Run this example
POSIX shell, Git, tmux 3.2a or newer and Bun 1.4.2.
Save the program above as quickstart.ts in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
package.json
{"type":"module","private":true,"dependencies":{"libtmux":"file:./source/packages/libtmux"}}Setup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-ts.git \ 46cccfef2a546de55ce8b308249b8c76339089a0git -C source checkout -q --detach FETCH_HEADbun installunset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shbun run quickstart.tsExpected output
helloLibrary revision: 46cccfef2a54.
Dependency cooldowns
The checkbox turns a cooldown on and off for every picker on the site. It applies to what an install pulls in; the package itself, and the libtmux it pins, stay current.
What are cooldowns?
Cooldowns delay picking up newly uploaded packages so the community has time to spot supply-chain attacks before you install. Most package managers don't apply one by default; you opt in through a global setting in your tool's configuration.
Learn more at cooldowns.dev · Datadog Security Labs writeup.
$ npm install libtmux$ npm install --min-release-age=0 libtmux$ pnpm add libtmux$ pnpm_config_minimum_release_age=0 pnpm add libtmux$ yarn add libtmux$ YARN_NPM_MINIMAL_AGE_GATE=0 yarn add libtmux$ bun add libtmux$ bun add libtmuxBun cannot exempt libtmux from its own cooldown, so a cooldown here would also hold back the release this installs. This command runs without one.
$ bun add --minimum-release-age=0 libtmuxStart with Rust
use libtmux::{PaneWait, Server};use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> { let session = server.new_session("work").await?; let window = session.new_window("editor").await?; let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?; let seen = pane .wait_until(Duration::from_secs(5), |lines| { lines.iter().any(|line| *line == "hello") }) .await?; if seen != PaneWait::Arrived { return Err("The pane did not print hello".into()); }
for line in pane.capture().await? { if line == "hello" { println!("{}", line.to_string_lossy()); } } Ok(())}
#[tokio::main(flavor = "current_thread")]async fn main() -> Result<(), ExampleError> { let root = std::path::Path::new("/tmp/libtmux-rs-dev"); std::fs::create_dir_all(root)?; let directory = tempfile::Builder::new().prefix("home-").tempdir_in(root)?.keep();
let server = Server::builder() .socket_path(directory.join("tmux.sock")) .config_file("/dev/null") .default_timeout(Duration::from_secs(5)) .build()?;
let outcome = tokio::time::timeout(Duration::from_secs(10), work(&server)) .await;
server.shutdown().await?; outcome??; Ok(())}use libtmux::{PaneWait, Server};use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> { let session = server.new_session("work").await?; let window = session.new_window("editor").await?; let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?; let seen = pane .wait_until(Duration::from_secs(5), |lines| { lines.iter().any(|line| *line == "hello") }) .await?; if seen != PaneWait::Arrived { return Err("The pane did not print hello".into()); }
for line in pane.capture().await? { if line == "hello" { println!("{}", line.to_string_lossy()); } } Ok(())}
#[tokio::main(flavor = "current_thread")]async fn main() -> Result<(), ExampleError> { let root = std::path::Path::new("/tmp/libtmux-rs-dev"); std::fs::create_dir_all(root)?; let directory = tempfile::Builder::new().prefix("home-").tempdir_in(root)?.keep();
let server = Server::builder() .socket_path(directory.join("tmux.sock")) .config_file("/dev/null") .default_timeout(Duration::from_secs(5)) .build()?;
let outcome = tokio::time::timeout(Duration::from_secs(10), work(&server)) .await;
let closed = server.shutdown().await; let mut failures = Vec::new(); match outcome { Ok(Ok(())) => {} Ok(Err(error)) => failures.push(format!("work: {error}")), Err(error) => failures.push(format!("deadline: {error}")), } if let Err(error) = closed { failures.push(format!("close executor: {error}")); }
if failures.is_empty() { Ok(()) } else { Err(failures.join("; ").into()) }}use libtmux::{PaneWait, Server};use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> { let session = server.new_session("work").await?; let window = session.new_window("editor").await?; let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?; let seen = pane .wait_until(Duration::from_secs(5), |lines| { lines.iter().any(|line| *line == "hello") }) .await?; if seen != PaneWait::Arrived { return Err("The pane did not print hello".into()); }
for line in pane.capture().await? { if line == "hello" { println!("{}", line.to_string_lossy()); } } Ok(())}
#[tokio::main(flavor = "current_thread")]async fn main() -> Result<(), ExampleError> { let root = std::path::Path::new("/tmp/libtmux-rs-dev"); std::fs::create_dir_all(root)?; let directory = tempfile::Builder::new().prefix("home-").tempdir_in(root)?;
let server = Server::builder() .socket_path(directory.path().join("tmux.sock")) .config_file("/dev/null") .default_timeout(Duration::from_secs(5)) .build()?;
let outcome = tokio::time::timeout(Duration::from_secs(10), work(&server)) .await;
let killed = server.kill().await; let closed = server.shutdown().await; if killed.is_err() || closed.is_err() { directory.keep(); } else { directory.close()?; }
killed?; closed?; outcome??; Ok(())}use libtmux::{PaneWait, Server};use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> { let session = server.new_session("work").await?; let window = session.new_window("editor").await?; let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?; let seen = pane .wait_until(Duration::from_secs(5), |lines| { lines.iter().any(|line| *line == "hello") }) .await?; if seen != PaneWait::Arrived { return Err("The pane did not print hello".into()); }
for line in pane.capture().await? { if line == "hello" { println!("{}", line.to_string_lossy()); } } Ok(())}
#[tokio::main(flavor = "current_thread")]async fn main() -> Result<(), ExampleError> { let root = std::path::Path::new("/tmp/libtmux-rs-dev"); std::fs::create_dir_all(root)?; let directory = tempfile::Builder::new().prefix("home-").tempdir_in(root)?;
let server = Server::builder() .socket_path(directory.path().join("tmux.sock")) .config_file("/dev/null") .default_timeout(Duration::from_secs(5)) .build()?;
let outcome = tokio::time::timeout(Duration::from_secs(10), work(&server)) .await;
// kill stops our daemon; shutdown closes this client executor. let killed = server.kill().await; let closed = server.shutdown().await; let cleanup_failed = killed.is_err() || closed.is_err();
let mut failures = Vec::new(); match outcome { Ok(Ok(())) => {} Ok(Err(error)) => failures.push(format!("work: {error}")), Err(error) => failures.push(format!("deadline: {error}")), }
if let Err(error) = killed { failures.push(format!("stop owned daemon: {error}")); } if let Err(error) = closed { failures.push(format!("close executor: {error}")); }
if cleanup_failed { let retained = directory.keep(); failures.push(format!("inspect {}", retained.display())); } else if let Err(error) = directory.close() { failures.push(format!("remove directory: {error}")); }
if failures.is_empty() { Ok(()) } else { Err(failures.join("; ").into()) }}Run this example
POSIX shell, Git, tmux 3.2a or newer, rustup with Rust 1.97.1 (rust-toolchain.toml), and a C linker.
Save the program above as src/main.rs in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
Cargo.toml
[package]name = "libtmux-home-quickstart"version = "0.0.0"edition = "2024"publish = false
[dependencies]libtmux = { path = "source/crates/libtmux" }tempfile = "3.20"tokio = { version = "1.40", features = ["macros", "rt", "time"] }Cargo.lock
# This file is automatically @generated by Cargo.# It is not intended for manual editing.version = 4
[[package]]name = "aho-corasick"version = "1.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"dependencies = [ "memchr",]
[[package]]name = "bitflags"version = "2.13.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]name = "bytes"version = "1.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]name = "caseless"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"dependencies = [ "unicode-normalization",]
[[package]]name = "cfg-if"version = "1.0.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]name = "errno"version = "0.3.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"dependencies = [ "libc", "windows-sys",]
[[package]]name = "fastrand"version = "2.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]name = "getrandom"version = "0.4.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"dependencies = [ "cfg-if", "libc", "r-efi",]
[[package]]name = "libc"version = "0.2.190"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]name = "libtmux"version = "0.1.0-alpha.15"dependencies = [ "caseless", "regex", "rustix", "thiserror", "tokio",]
[[package]]name = "libtmux-home-quickstart"version = "0.0.0"dependencies = [ "libtmux", "tempfile", "tokio",]
[[package]]name = "linux-raw-sys"version = "0.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]name = "memchr"version = "2.8.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]name = "mio"version = "1.2.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"dependencies = [ "libc", "wasi", "windows-sys",]
[[package]]name = "once_cell"version = "1.21.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]name = "pin-project-lite"version = "0.2.17"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]name = "proc-macro2"version = "1.0.107"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"dependencies = [ "unicode-ident",]
[[package]]name = "quote"version = "1.0.47"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"dependencies = [ "proc-macro2",]
[[package]]name = "r-efi"version = "6.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]name = "regex"version = "1.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax",]
[[package]]name = "regex-automata"version = "0.4.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"dependencies = [ "aho-corasick", "memchr", "regex-syntax",]
[[package]]name = "regex-syntax"version = "0.8.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]name = "rustix"version = "1.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys",]
[[package]]name = "signal-hook-registry"version = "1.4.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"dependencies = [ "errno", "libc",]
[[package]]name = "syn"version = "3.0.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"dependencies = [ "proc-macro2", "quote", "unicode-ident",]
[[package]]name = "tempfile"version = "3.27.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys",]
[[package]]name = "thiserror"version = "2.0.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"dependencies = [ "thiserror-impl",]
[[package]]name = "thiserror-impl"version = "2.0.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"dependencies = [ "proc-macro2", "quote", "syn",]
[[package]]name = "tinyvec"version = "1.13.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]name = "tokio"version = "1.53.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "tokio-macros", "windows-sys",]
[[package]]name = "tokio-macros"version = "2.7.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"dependencies = [ "proc-macro2", "quote", "syn",]
[[package]]name = "unicode-ident"version = "1.0.26"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]name = "unicode-normalization"version = "0.1.25"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"dependencies = [ "tinyvec",]
[[package]]name = "wasi"version = "0.11.1+wasi-snapshot-preview1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]name = "windows-link"version = "0.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]name = "windows-sys"version = "0.61.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"dependencies = [ "windows-link",]rust-toolchain.toml
[toolchain]channel = "1.97.1"components = ["clippy", "rust-src", "rustfmt"]profile = "minimal"Setup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-rs.git \ a6fc2a65674177b92b17fa380757155d2ba150fdgit -C source checkout -q --detach FETCH_HEADcargo build --locked --jobs 2unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shcargo run --quiet --lockedExpected output
helloLibrary revision: a6fc2a656741.
$ cargo add libtmuxStart with Go
package main
import ( "bufio" "context" "errors" "fmt" "io" "log" "os" "path/filepath" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
func start() (err error) { root := "/tmp/libtmux-go-test" if err := os.MkdirAll(root, 0700); err != nil { return err } directory, err := os.MkdirTemp(root, "home-") if err != nil { return err }
server, err := tmux.NewServer(tmux.ServerOptions{ Binary: os.Getenv("TMUX_BIN"), SocketPath: filepath.Join(directory, "tmux.sock"), ConfigFile: "/dev/null", }) if err != nil { return err }
ctx, cancel := context.WithTimeout( context.Background(), 10*time.Second, ) defer cancel()
return run(ctx, server)}
func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "libtmux-go-quickstart", WindowName: "start", }) if err != nil { return err }
window, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: new("work"), }) if err != nil { return err }
pane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, Command: "sh", }) if err != nil { return err }
output, err := pane.OpenObservation(ctx) if err != nil { return err } defer func() { err = errors.Join(err, output.Close()) }()
_, err = fmt.Fprintln( pane.Writer(ctx), "printf 'libtmux ready\\n'", ) if err != nil { return err } scanner := bufio.NewScanner(output.Reader(ctx)) for scanner.Scan() { if scanner.Text() == "libtmux ready" { fmt.Println("libtmux ready") return nil } }
if err := scanner.Err(); err != nil { return err } return io.ErrUnexpectedEOF}package main
import ( "bufio" "context" "errors" "fmt" "io" "log" "os" "path/filepath" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
func start() (err error) { root := "/tmp/libtmux-go-test" if err := os.MkdirAll(root, 0700); err != nil { return err } directory, err := os.MkdirTemp(root, "home-") if err != nil { return err }
server, err := tmux.NewServer(tmux.ServerOptions{ Binary: os.Getenv("TMUX_BIN"), SocketPath: filepath.Join(directory, "tmux.sock"), ConfigFile: "/dev/null", }) if err != nil { return err }
ctx, cancel := context.WithTimeout( context.Background(), 10*time.Second, ) defer cancel()
return run(ctx, server)}
func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "libtmux-go-quickstart", WindowName: "start", }) if err != nil { return fmt.Errorf("create session: %w", err) }
window, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: new("work"), }) if err != nil { return fmt.Errorf("create window: %w", err) }
pane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, Command: "sh", }) if err != nil { return fmt.Errorf("split window: %w", err) }
output, err := pane.OpenObservation(ctx) if err != nil { return fmt.Errorf("watch pane: %w", err) } defer func() { err = errors.Join(err, output.Close()) }()
_, err = fmt.Fprintln( pane.Writer(ctx), "printf 'libtmux ready\\n'", ) if err != nil { return fmt.Errorf("send command: %w", err) } scanner := bufio.NewScanner(output.Reader(ctx)) for scanner.Scan() { if scanner.Text() == "libtmux ready" { fmt.Println("libtmux ready") return nil } }
if err := scanner.Err(); err != nil { return fmt.Errorf("read pane: %w", err) } return io.ErrUnexpectedEOF}package main
import ( "bufio" "context" "errors" "fmt" "io" "log" "os" "path/filepath" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
func start() (err error) { root := "/tmp/libtmux-go-test" if err := os.MkdirAll(root, 0700); err != nil { return err } directory, err := os.MkdirTemp(root, "home-") if err != nil { return err }
server, err := tmux.NewServer(tmux.ServerOptions{ Binary: os.Getenv("TMUX_BIN"), SocketPath: filepath.Join(directory, "tmux.sock"), ConfigFile: "/dev/null", }) if err != nil { return errors.Join(err, os.RemoveAll(directory)) }
defer func() { cleanup, cancel := context.WithTimeout( context.Background(), 3*time.Second, ) defer cancel()
if stopErr := server.Kill(cleanup); stopErr != nil { err = errors.Join(err, stopErr) return // Keep the directory if cleanup fails. } err = errors.Join(err, os.RemoveAll(directory)) }()
ctx, cancel := context.WithTimeout( context.Background(), 10*time.Second, ) defer cancel()
return run(ctx, server)}
func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "libtmux-go-quickstart", WindowName: "start", }) if err != nil { return err }
window, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: new("work"), }) if err != nil { return err }
pane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, Command: "sh", }) if err != nil { return err }
output, err := pane.OpenObservation(ctx) if err != nil { return err } defer func() { err = errors.Join(err, output.Close()) }()
_, err = fmt.Fprintln( pane.Writer(ctx), "printf 'libtmux ready\\n'", ) if err != nil { return err } scanner := bufio.NewScanner(output.Reader(ctx)) for scanner.Scan() { if scanner.Text() == "libtmux ready" { fmt.Println("libtmux ready") return nil } }
if err := scanner.Err(); err != nil { return err } return io.ErrUnexpectedEOF}package main
import ( "bufio" "context" "errors" "fmt" "io" "log" "os" "path/filepath" "time"
"github.com/libtmux/libtmux-go/tmux")
func main() { if err := start(); err != nil { log.Fatal(err) }}
func start() (err error) { root := "/tmp/libtmux-go-test" if err := os.MkdirAll(root, 0700); err != nil { return err } directory, err := os.MkdirTemp(root, "home-") if err != nil { return err }
server, err := tmux.NewServer(tmux.ServerOptions{ Binary: os.Getenv("TMUX_BIN"), SocketPath: filepath.Join(directory, "tmux.sock"), ConfigFile: "/dev/null", }) if err != nil { return errors.Join(err, os.RemoveAll(directory)) }
defer func() { cleanup, cancel := context.WithTimeout( context.Background(), 3*time.Second, ) defer cancel()
if stopErr := server.Kill(cleanup); stopErr != nil { err = errors.Join(err, stopErr) return // Keep the directory if cleanup fails. } err = errors.Join(err, os.RemoveAll(directory)) }()
ctx, cancel := context.WithTimeout( context.Background(), 10*time.Second, ) defer cancel()
return run(ctx, server)}
func run(ctx context.Context, server tmux.Server) (err error) { session, err := server.NewSession(ctx, tmux.NewSessionRequest{ Name: "libtmux-go-quickstart", WindowName: "start", }) if err != nil { return fmt.Errorf("create session: %w", err) }
window, err := session.NewWindow(ctx, tmux.NewWindowRequest{ Name: new("work"), }) if err != nil { return fmt.Errorf("create window: %w", err) }
pane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{ Direction: tmux.PaneDirectionRight, Command: "sh", }) if err != nil { return fmt.Errorf("split window: %w", err) }
output, err := pane.OpenObservation(ctx) if err != nil { return fmt.Errorf("watch pane: %w", err) } defer func() { err = errors.Join(err, output.Close()) }()
_, err = fmt.Fprintln( pane.Writer(ctx), "printf 'libtmux ready\\n'", ) if err != nil { return fmt.Errorf("send command: %w", err) } scanner := bufio.NewScanner(output.Reader(ctx)) for scanner.Scan() { if scanner.Text() == "libtmux ready" { fmt.Println("libtmux ready") return nil } }
if err := scanner.Err(); err != nil { return fmt.Errorf("read pane: %w", err) } return io.ErrUnexpectedEOF}Run this example
POSIX shell, Git, tmux 3.2a or newer and Go 1.26.5.
Save the program above as quickstart.go in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
go.mod
module example.com/libtmux-home
go 1.26.0
require github.com/libtmux/libtmux-go v0.0.0
replace github.com/libtmux/libtmux-go => ./sourceSetup and run
git init -q sourcegit -C source fetch -q --depth=1 \ https://github.com/libtmux/libtmux-go.git \ 6f3df55f8a41999e41225eefb3940545b834e6a2git -C source checkout -q --detach FETCH_HEADgo build -p 2 -o quickstart quickstart.gounset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh./quickstartExpected output
libtmux readyLibrary revision: 6f3df55f8a41.
$ go get github.com/libtmux/libtmux-go/tmux@latestStart with Java
import io.github.libtmux.Server;import io.github.libtmux.ServerConfig;import io.github.libtmux.ServerEndpoint;import io.github.libtmux.TextOutcome;import java.nio.file.Path;import java.time.Duration;import java.util.UUID;
public final class Main { public static void main(String[] args) throws Exception { var socket = "libtmux-home-" + UUID.randomUUID(); var config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build();
try (Server server = Server.open(config)) { var session = server.newSession("demo"); var window = session.newWindow("build"); var pane = window.split();
pane.sendLine("printf '\\nhello from %s\\n' libtmux"); var seen = pane.awaitText( "hello from libtmux", Duration.ofSeconds(5)); if (seen == TextOutcome.TIMED_OUT) { throw new IllegalStateException("output did not arrive"); }
var lines = pane.capture(); if (lines.stream().noneMatch( line -> line.strip().equals("hello from libtmux"))) { throw new IllegalStateException( "capture is missing the output line"); }
System.out.println("hello from libtmux"); } }}import io.github.libtmux.Server;import io.github.libtmux.ServerConfig;import io.github.libtmux.ServerEndpoint;import io.github.libtmux.TextOutcome;import java.nio.file.Path;import java.time.Duration;import java.util.UUID;
public final class Main { public static void main(String[] args) throws Exception { var socket = "libtmux-home-" + UUID.randomUUID(); var config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build();
try (Server server = Server.open(config)) { try { var session = server.newSession("demo"); var window = session.newWindow("build"); var pane = window.split();
pane.sendLine("printf '\\nhello from %s\\n' libtmux"); var seen = pane.awaitText( "hello from libtmux", Duration.ofSeconds(5)); if (seen == TextOutcome.TIMED_OUT) { throw new IllegalStateException("output did not arrive"); }
var lines = pane.capture(); if (lines.stream().noneMatch( line -> line.strip().equals("hello from libtmux"))) { throw new IllegalStateException( "capture is missing the output line"); }
System.out.println("hello from libtmux"); } catch (Exception error) { throw new IllegalStateException( "tmux quickstart failed", error); } } }}import io.github.libtmux.Server;import io.github.libtmux.ServerConfig;import io.github.libtmux.ServerEndpoint;import io.github.libtmux.TextOutcome;import java.nio.file.Path;import java.time.Duration;import java.util.UUID;
public final class Main { public static void main(String[] args) throws Exception { var socket = "libtmux-home-" + UUID.randomUUID(); var config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build();
try (Server server = Server.open(config)) { try { var session = server.newSession("demo"); var window = session.newWindow("build"); var pane = window.split();
pane.sendLine("printf '\\nhello from %s\\n' libtmux"); var seen = pane.awaitText( "hello from libtmux", Duration.ofSeconds(5)); if (seen == TextOutcome.TIMED_OUT) { throw new IllegalStateException("output did not arrive"); }
var lines = pane.capture(); if (lines.stream().noneMatch( line -> line.strip().equals("hello from libtmux"))) { throw new IllegalStateException( "capture is missing the output line"); }
System.out.println("hello from libtmux"); } finally { server.killServer(); } } }}import io.github.libtmux.Server;import io.github.libtmux.ServerConfig;import io.github.libtmux.ServerEndpoint;import io.github.libtmux.TextOutcome;import java.nio.file.Path;import java.time.Duration;import java.util.UUID;
public final class Main { public static void main(String[] args) throws Exception { var socket = "libtmux-home-" + UUID.randomUUID(); var config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build();
try (Server server = Server.open(config)) { try { var session = server.newSession("demo"); var window = session.newWindow("build"); var pane = window.split();
pane.sendLine("printf '\\nhello from %s\\n' libtmux"); var seen = pane.awaitText( "hello from libtmux", Duration.ofSeconds(5)); if (seen == TextOutcome.TIMED_OUT) { throw new IllegalStateException("output did not arrive"); }
var lines = pane.capture(); if (lines.stream().noneMatch( line -> line.strip().equals("hello from libtmux"))) { throw new IllegalStateException( "capture is missing the output line"); }
System.out.println("hello from libtmux"); } catch (Exception error) { throw new IllegalStateException( "tmux quickstart failed", error); } finally { server.killServer(); } } }}Run this example
JDK 25 and tmux on Linux/macOS
Save the program above as src/main/java/Main.java in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
build.gradle.kts
plugins { application}
repositories { mavenCentral() }
dependencies { implementation("io.github.libtmux:libtmux:0.0.1-alpha.17-SNAPSHOT")}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
application { mainClass.set(providers.gradleProperty("exampleMain")) }settings.gradle.kts
rootProject.name = "api-example"
includeBuild("libtmux-source")gradle.properties
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8kotlin.daemon.jvmargs=-Xmx2gorg.gradle.workers.max=2Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-java.git libtmux-sourcegit -C libtmux-source checkout --detach a0ecbc16da00140e2520462d478a2af470878272./libtmux-source/gradlew --project-dir . --console=plain --no-daemon --max-workers=2 \ installDist -PexampleMain=MainSHELL=/bin/sh build/install/api-example/bin/api-exampleExpected output
hello from libtmuxLibrary revision: a0ecbc16da00.
The platform BOM names the version once and every other coordinate follows it, which is what stops a project mixing two releases of modules built against each other.
Start with Kotlin
import io.github.libtmux.ServerConfigimport io.github.libtmux.ServerEndpointimport io.github.libtmux.TextOutcomeimport io.github.libtmux.kotlin.*import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport kotlinx.coroutines.runBlockingimport kotlin.time.Duration.Companion.seconds
fun main() = runBlocking { val socket = "libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
withServer(config) { server -> val session = server.newSession("work") val pane = session.activeWindow?.activePane ?: error("no active pane")
pane.sendLine("printf '\\nrea%s\\n' dy") val outcome = pane.awaitText("ready", timeout = 5.seconds) check(outcome != TextOutcome.TIMED_OUT)
val lines = pane.capture() check(lines.any { it.trim() == "ready" })
val run = pane.run("echo hi && exit 3", timeout = 5.seconds) check(run.exitStatus.orNull() == 3)
println("ready; exit ${run.exitStatus.orNull()}") }}import io.github.libtmux.ServerConfigimport io.github.libtmux.ServerEndpointimport io.github.libtmux.TextOutcomeimport io.github.libtmux.kotlin.*import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport kotlinx.coroutines.runBlockingimport kotlin.time.Duration.Companion.seconds
fun main() = runBlocking { val socket = "libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
withServer(config) { server -> try { val session = server.newSession("work") val pane = session.activeWindow?.activePane ?: error("no active pane")
pane.sendLine("printf '\\nrea%s\\n' dy") val outcome = pane.awaitText("ready", timeout = 5.seconds) check(outcome != TextOutcome.TIMED_OUT)
val lines = pane.capture() check(lines.any { it.trim() == "ready" })
val run = pane.run("echo hi && exit 3", timeout = 5.seconds) check(run.exitStatus.orNull() == 3)
println("ready; exit ${run.exitStatus.orNull()}") } catch (error: Exception) { throw IllegalStateException( "tmux quickstart failed", error ) } }}import io.github.libtmux.ServerConfigimport io.github.libtmux.ServerEndpointimport io.github.libtmux.TextOutcomeimport io.github.libtmux.kotlin.*import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport kotlinx.coroutines.NonCancellableimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withContextimport kotlin.time.Duration.Companion.seconds
fun main() = runBlocking { val socket = "libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
withServer(config) { server -> try { val session = server.newSession("work") val pane = session.activeWindow?.activePane ?: error("no active pane")
pane.sendLine("printf '\\nrea%s\\n' dy") val outcome = pane.awaitText("ready", timeout = 5.seconds) check(outcome != TextOutcome.TIMED_OUT)
val lines = pane.capture() check(lines.any { it.trim() == "ready" })
val run = pane.run("echo hi && exit 3", timeout = 5.seconds) check(run.exitStatus.orNull() == 3)
println("ready; exit ${run.exitStatus.orNull()}") } finally { withContext(NonCancellable) { server.killServer() } } }}import io.github.libtmux.ServerConfigimport io.github.libtmux.ServerEndpointimport io.github.libtmux.TextOutcomeimport io.github.libtmux.kotlin.*import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport kotlinx.coroutines.NonCancellableimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withContextimport kotlin.time.Duration.Companion.seconds
fun main() = runBlocking { val socket = "libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
withServer(config) { server -> try { val session = server.newSession("work") val pane = session.activeWindow?.activePane ?: error("no active pane")
pane.sendLine("printf '\\nrea%s\\n' dy") val outcome = pane.awaitText("ready", timeout = 5.seconds) check(outcome != TextOutcome.TIMED_OUT)
val lines = pane.capture() check(lines.any { it.trim() == "ready" })
val run = pane.run("echo hi && exit 3", timeout = 5.seconds) check(run.exitStatus.orNull() == 3)
println("ready; exit ${run.exitStatus.orNull()}") } catch (error: Exception) { throw IllegalStateException( "tmux quickstart failed", error ) } finally { withContext(NonCancellable) { server.killServer() } } }}Run this example
JDK 25 and tmux on Linux/macOS; Gradle resolves Kotlin 2.4.10
Save the program above as src/main/kotlin/Main.kt in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
build.gradle.kts
plugins { kotlin("jvm") version "2.4.10" application}
repositories { mavenCentral() }
dependencies { implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }kotlin { jvmToolchain(25) }
application { mainClass.set(providers.gradleProperty("exampleMain")) }settings.gradle.kts
rootProject.name = "api-example"
includeBuild("libtmux-source")gradle.properties
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8kotlin.daemon.jvmargs=-Xmx2gorg.gradle.workers.max=2Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-java.git libtmux-sourcegit -C libtmux-source checkout --detach a0ecbc16da00140e2520462d478a2af470878272./libtmux-source/gradlew --project-dir . --console=plain --no-daemon --max-workers=2 \ installDist -PexampleMain=MainKtSHELL=/bin/sh build/install/api-example/bin/api-exampleExpected output
ready; exit 3Library revision: a0ecbc16da00.
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17")Start with Scala
import io.github.libtmux.{ServerConfig, ServerEndpoint}import io.github.libtmux.scaladsl.{config => _, *}import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport scala.util.Using
object Main { def main(args: Array[String]): Unit = { val socket = s"libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
Using.resource(Server.open(config)) { server => val session = server.newSession("work") session.newWindow("logs")
val panes = server.panes() val named = panes.filter(_.info.title().nonEmpty) val commands = named.map(_.info.currentCommand()) assert(panes.size == 2)
println(s"${session.name}: ${panes.size} panes") println(s"commands: ${commands.distinct.sorted.mkString(", ")}") } }}import io.github.libtmux.{ServerConfig, ServerEndpoint}import io.github.libtmux.scaladsl.{config => _, *}import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport scala.util.Usingimport scala.util.control.NonFatal
object Main { def main(args: Array[String]): Unit = { val socket = s"libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
Using.resource(Server.open(config)) { server => try { val session = server.newSession("work") session.newWindow("logs")
val panes = server.panes() val named = panes.filter(_.info.title().nonEmpty) val commands = named.map(_.info.currentCommand()) assert(panes.size == 2)
println(s"${session.name}: ${panes.size} panes") println(s"commands: ${commands.distinct.sorted.mkString(", ")}") } catch { case NonFatal(error) => throw new IllegalStateException( "tmux quickstart failed", error ) } } }}import io.github.libtmux.{ServerConfig, ServerEndpoint}import io.github.libtmux.scaladsl.{config => _, *}import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport scala.util.Using
object Main { def main(args: Array[String]): Unit = { val socket = s"libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
Using.resource(Server.open(config)) { server => try { val session = server.newSession("work") session.newWindow("logs")
val panes = server.panes() val named = panes.filter(_.info.title().nonEmpty) val commands = named.map(_.info.currentCommand()) assert(panes.size == 2)
println(s"${session.name}: ${panes.size} panes") println(s"commands: ${commands.distinct.sorted.mkString(", ")}") } finally { server.killServer() } } }}import io.github.libtmux.{ServerConfig, ServerEndpoint}import io.github.libtmux.scaladsl.{config => _, *}import java.nio.file.Pathimport java.time.Durationimport java.util.UUIDimport scala.util.Usingimport scala.util.control.NonFatal
object Main { def main(args: Array[String]): Unit = { val socket = s"libtmux-home-${UUID.randomUUID()}" val config = ServerConfig.builder() .endpoint(ServerEndpoint.namedSocket(socket)) .configFile(Path.of("/dev/null")) .defaultTimeout(Duration.ofSeconds(5)) .build()
Using.resource(Server.open(config)) { server => try { val session = server.newSession("work") session.newWindow("logs")
val panes = server.panes() val named = panes.filter(_.info.title().nonEmpty) val commands = named.map(_.info.currentCommand()) assert(panes.size == 2)
println(s"${session.name}: ${panes.size} panes") println(s"commands: ${commands.distinct.sorted.mkString(", ")}") } catch { case NonFatal(error) => throw new IllegalStateException( "tmux quickstart failed", error ) } finally { server.killServer() } } }}Run this example
JDK 25 and tmux on Linux/macOS; Scala 3.9.0 Direct facade
Save the program above as src/main/scala/Main.scala in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
build.gradle.kts
plugins { scala application}
repositories { mavenCentral() }
dependencies { implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT") implementation("org.scala-lang:scala3-library_3:3.9.0")}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
application { mainClass.set(providers.gradleProperty("exampleMain")) }settings.gradle.kts
rootProject.name = "api-example"
includeBuild("libtmux-source") { dependencySubstitution { substitute(module("io.github.libtmux:libtmux-scala_3")) .using(project(":libtmux-scala")) }}gradle.properties
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8kotlin.daemon.jvmargs=-Xmx2gorg.gradle.workers.max=2Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-java.git libtmux-sourcegit -C libtmux-source checkout --detach a0ecbc16da00140e2520462d478a2af470878272./libtmux-source/gradlew --project-dir . --console=plain --no-daemon --max-workers=2 \ installDist -PexampleMain=MainSHELL=/bin/sh build/install/api-example/bin/api-exampleExpected output
work: 2 panescommands: shLibrary revision: a0ecbc16da00.
libraryDependencies += "io.github.libtmux" %% "libtmux-scala" % "0.0.1-alpha.17"implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17")Start with C#
using System;using System.Linq;using System.Threading;using System.Runtime.Versioning;using LibTmux;
[assembly: UnsupportedOSPlatform("windows")]
if (OperatingSystem.IsWindows()){ throw new PlatformNotSupportedException( "This example requires tmux on Unix.");}
using var deadline = new CancellationTokenSource( TimeSpan.FromSeconds(30));var token = deadline.Token;var options = new ServerConnectionOptions{ SocketName = "quickstart-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = Environment.GetEnvironmentVariable("LIBTMUX_TMUX") ?? "tmux",};
var owned = await Server.CreateOwnedAsync(options, token);Server server = owned.Value;
Session session = await server.CreateSessionAsync( new NewSessionRequest { Name = "build", Command = "/bin/sh" }, token);
Window window = await session.CreateWindowAsync( new NewWindowRequest { Name = "tests", Command = "/bin/sh" }, token);Pane pane = (await window.GetPanesAsync(token))[0];
await pane.SendTextAsync( "printf 'hello-from-%s\\n' libtmux", enter: false, cancellationToken: token);await pane.EnterAsync(token);
var lines = await TmuxWait.UntilAsync( async cancellation => await pane.CaptureAsync( cancellationToken: cancellation), output => output.Any(line => line.TrimEnd() == "hello-from-libtmux"), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), token);
Console.WriteLine("hello-from-libtmux");using System;using System.Linq;using System.Threading;using System.Runtime.Versioning;using LibTmux;
[assembly: UnsupportedOSPlatform("windows")]
if (OperatingSystem.IsWindows()){ throw new PlatformNotSupportedException( "This example requires tmux on Unix.");}
using var deadline = new CancellationTokenSource( TimeSpan.FromSeconds(30));var token = deadline.Token;var options = new ServerConnectionOptions{ SocketName = "quickstart-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = Environment.GetEnvironmentVariable("LIBTMUX_TMUX") ?? "tmux",};
var owned = await Server.CreateOwnedAsync(options, token);Server server = owned.Value;
try{ Session session = await server.CreateSessionAsync( new NewSessionRequest { Name = "build", Command = "/bin/sh" }, token);
Window window = await session.CreateWindowAsync( new NewWindowRequest { Name = "tests", Command = "/bin/sh" }, token); Pane pane = (await window.GetPanesAsync(token))[0];
await pane.SendTextAsync( "printf 'hello-from-%s\\n' libtmux", enter: false, cancellationToken: token); await pane.EnterAsync(token);
var lines = await TmuxWait.UntilAsync( async cancellation => await pane.CaptureAsync( cancellationToken: cancellation), output => output.Any(line => line.TrimEnd() == "hello-from-libtmux"), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), token);
Console.WriteLine("hello-from-libtmux");}catch (Exception error){ throw new InvalidOperationException( "tmux quickstart failed", error);}using System;using System.Linq;using System.Threading;using System.Runtime.Versioning;using LibTmux;
[assembly: UnsupportedOSPlatform("windows")]
if (OperatingSystem.IsWindows()){ throw new PlatformNotSupportedException( "This example requires tmux on Unix.");}
using var deadline = new CancellationTokenSource( TimeSpan.FromSeconds(30));var token = deadline.Token;var options = new ServerConnectionOptions{ SocketName = "quickstart-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = Environment.GetEnvironmentVariable("LIBTMUX_TMUX") ?? "tmux",};
await using var owned = await Server.CreateOwnedAsync(options, token);Server server = owned.Value;
Session session = await server.CreateSessionAsync( new NewSessionRequest { Name = "build", Command = "/bin/sh" }, token);
Window window = await session.CreateWindowAsync( new NewWindowRequest { Name = "tests", Command = "/bin/sh" }, token);Pane pane = (await window.GetPanesAsync(token))[0];
await pane.SendTextAsync( "printf 'hello-from-%s\\n' libtmux", enter: false, cancellationToken: token);await pane.EnterAsync(token);
var lines = await TmuxWait.UntilAsync( async cancellation => await pane.CaptureAsync( cancellationToken: cancellation), output => output.Any(line => line.TrimEnd() == "hello-from-libtmux"), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), token);
Console.WriteLine("hello-from-libtmux");using System;using System.Linq;using System.Threading;using System.Runtime.Versioning;using LibTmux;
[assembly: UnsupportedOSPlatform("windows")]
if (OperatingSystem.IsWindows()){ throw new PlatformNotSupportedException( "This example requires tmux on Unix.");}
using var deadline = new CancellationTokenSource( TimeSpan.FromSeconds(30));var token = deadline.Token;var options = new ServerConnectionOptions{ SocketName = "quickstart-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = Environment.GetEnvironmentVariable("LIBTMUX_TMUX") ?? "tmux",};
await using var owned = await Server.CreateOwnedAsync(options, token);Server server = owned.Value;
try{ Session session = await server.CreateSessionAsync( new NewSessionRequest { Name = "build", Command = "/bin/sh" }, token);
Window window = await session.CreateWindowAsync( new NewWindowRequest { Name = "tests", Command = "/bin/sh" }, token); Pane pane = (await window.GetPanesAsync(token))[0];
await pane.SendTextAsync( "printf 'hello-from-%s\\n' libtmux", enter: false, cancellationToken: token); await pane.EnterAsync(token);
var lines = await TmuxWait.UntilAsync( async cancellation => await pane.CaptureAsync( cancellationToken: cancellation), output => output.Any(line => line.TrimEnd() == "hello-from-libtmux"), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), token);
Console.WriteLine("hello-from-libtmux");}catch (Exception error){ throw new InvalidOperationException( "tmux quickstart failed", error);}Run this example
.NET SDK 10.0.302, .NET 8 and 10 runtimes, tmux on Linux/macOS
Save the program above as app/Program.cs in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
app/Example.csproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFrameworks>net8.0;net10.0</TargetFrameworks> <LangVersion>12.0</LangVersion> <Nullable>enable</Nullable> <ImplicitUsings>disable</ImplicitUsings> <EnableDefaultCompileItems>false</EnableDefaultCompileItems> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <AnalysisLevel>10-recommended</AnalysisLevel> </PropertyGroup> <ItemGroup> <Compile Include="Program.cs" /> <PackageReference Include="LibTmux" Version="[0.0.0-alpha.18]" /> </ItemGroup></Project>app/global.json
{ "sdk": { "version": "10.0.302", "rollForward": "disable", "allowPrerelease": false }, "test": { "runner": "Microsoft.Testing.Platform" }}app/NuGet.config
<?xml version="1.0" encoding="utf-8"?><configuration> <packageSources> <clear /> <add key="libtmux-source" value="../libtmux-source/artifacts/api-example-packages" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> </packageSources> <packageSourceMapping> <clear /> <packageSource key="libtmux-source"> <package pattern="LibTmux" /> <package pattern="LibTmux.*" /> </packageSource> <packageSource key="nuget.org"> <package pattern="*" /> </packageSource> </packageSourceMapping></configuration>Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-dotnet.git libtmux-sourcegit -C libtmux-source checkout --detach 4f1b997b2e4aeb604c4184707e49dcd4dea44fefexport NUGET_PACKAGES="$PWD/packages"cd libtmux-sourcedotnet restore src/LibTmux.FSharp/LibTmux.FSharp.fsproj --locked-modedotnet pack src/LibTmux/LibTmux.csproj -c Release --no-restore \ -p:ContinuousIntegrationBuild=true --output artifacts/api-example-packagescd ../appdotnet restore Example.csproj --configfile NuGet.configdotnet build Example.csproj -c Release --no-restore --warnaserrorexport TMUX_TMPDIR=/tmp/libtmux-dotnet-devmkdir -p "$TMUX_TMPDIR"unset TMUX TMUX_PANEdotnet bin/Release/net8.0/Example.dlldotnet bin/Release/net10.0/Example.dllExpected output
hello-from-libtmuxLibrary revision: 4f1b997b2e4a.
$ dotnet package add LibTmux --prerelease.NET 10 SDK: run in your project directory. On .NET 8 or 9, use dotnet add package with the same arguments.
NuGet\Install-Package LibTmux -Version 0.0.0-alpha.20Run in Visual Studio on Windows, in Tools > NuGet Package Manager > Package Manager Console. Choose the target project in that console.
<PackageReference Include="LibTmux" Version="0.0.0-alpha.20" />Add this inside an ItemGroup in your .csproj or .fsproj, then run dotnet restore. For central version management, use the CPM tab.
Directory.Packages.props
Project file (.csproj or .fsproj)
<PackageReference Include="LibTmux" />Save Directory.Packages.props at the solution root, or merge these settings into the existing file. Add the versionless reference inside the project’s ItemGroup, then run dotnet restore.
$ paket add LibTmux --version 0.0.0-alpha.20 --interactiveRequires Paket and a solution initialized with paket init. The interactive prompt selects the projects that should reference the package.
#r "nuget: LibTmux, 0.0.0-alpha.20"Use in an F# .fsx script, dotnet fsi, or a Polyglot Notebook with NuGet support, on .NET 8 or later. This is not a directive for an ordinary compiled project.
#:package LibTmux@0.0.0-alpha.20C# file-based apps require the .NET 10 SDK. Place this at the top of a .cs file, before code, then run dotnet run app.cs. F# scripts use the Script & Interactive tab.
#addin nuget:?package=LibTmux&version=0.0.0-alpha.20&loaddependencies=trueUse Cake.Tool 6 or later for a .cake script on .NET 8 or later, with Cake’s in-process NuGet client enabled. This loads the library and its dependencies; it has no Cake task aliases or executable tool. Cake Frosting projects use PackageReference.
Start with F#
open Systemopen System.Threadingopen LibTmuxopen LibTmux.FSharp
let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token
let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = (Environment.GetEnvironmentVariable "LIBTMUX_TMUX" |> Option.ofObj |> Option.defaultValue "tmux") )
let! owned = LibTmux.Server.CreateOwnedAsync(options, token)
for name in [ "build"; "web"; "worker" ] do let! _ = owned.Value.CreateSessionAsync( NewSessionRequest(Name = name, Command = "/bin/sh"), token )
()
let! server = LibTmux.Server.ConnectAsync(options, token)
// List and filter: tmux narrows the listing; every row is rechecked. // atMostOne is None when nothing matches and raises when several do. let! build = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "build") |> Query.atMostOne token
let! others = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.ne "build") |> Query.list token
printfn "other sessions: %s" (String.Join(", ", [ for session in others -> session.Name ]))
match build with | None -> printfn "no build session" | Some session -> let! panes = session |> Session.panes |> Query.list token let pane = panes[0]
// Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started"
// Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3"
printfn "wait found: %b" started.Found
printfn "run: exit %d, output %A" result.ExitStatus.Value (List.ofSeq result.Output) }
runAsync().GetAwaiter().GetResult()open Systemopen System.Threadingopen LibTmuxopen LibTmux.FSharp
let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token
let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = (Environment.GetEnvironmentVariable "LIBTMUX_TMUX" |> Option.ofObj |> Option.defaultValue "tmux") )
let! owned = LibTmux.Server.CreateOwnedAsync(options, token)
for name in [ "build"; "web"; "worker" ] do let! _ = owned.Value.CreateSessionAsync( NewSessionRequest(Name = name, Command = "/bin/sh"), token )
()
let! server = LibTmux.Server.ConnectAsync(options, token)
// List and filter: tmux narrows the listing; every row is rechecked. // atMostOne is None when nothing matches and raises when several do. let! build = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "build") |> Query.atMostOne token
let! others = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.ne "build") |> Query.list token
printfn "other sessions: %s" (String.Join(", ", [ for session in others -> session.Name ]))
match build with | None -> printfn "no build session" | Some session -> let! panes = session |> Session.panes |> Query.list token let pane = panes[0]
// Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started"
// Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3"
printfn "wait found: %b" started.Found
printfn "run: exit %d, output %A" result.ExitStatus.Value (List.ofSeq result.Output) }
try runAsync().GetAwaiter().GetResult()with error -> raise (InvalidOperationException( "tmux quickstart failed", error))open Systemopen System.Threadingopen LibTmuxopen LibTmux.FSharp
let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token
let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = (Environment.GetEnvironmentVariable "LIBTMUX_TMUX" |> Option.ofObj |> Option.defaultValue "tmux") )
use! owned = LibTmux.Server.CreateOwnedAsync(options, token)
for name in [ "build"; "web"; "worker" ] do let! _ = owned.Value.CreateSessionAsync( NewSessionRequest(Name = name, Command = "/bin/sh"), token )
()
let! server = LibTmux.Server.ConnectAsync(options, token)
// List and filter: tmux narrows the listing; every row is rechecked. // atMostOne is None when nothing matches and raises when several do. let! build = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "build") |> Query.atMostOne token
let! others = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.ne "build") |> Query.list token
printfn "other sessions: %s" (String.Join(", ", [ for session in others -> session.Name ]))
match build with | None -> printfn "no build session" | Some session -> let! panes = session |> Session.panes |> Query.list token let pane = panes[0]
// Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started"
// Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3"
printfn "wait found: %b" started.Found
printfn "run: exit %d, output %A" result.ExitStatus.Value (List.ofSeq result.Output) }
runAsync().GetAwaiter().GetResult()open Systemopen System.Threadingopen LibTmuxopen LibTmux.FSharp
let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token
let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null", TmuxBinaryPath = (Environment.GetEnvironmentVariable "LIBTMUX_TMUX" |> Option.ofObj |> Option.defaultValue "tmux") )
use! owned = LibTmux.Server.CreateOwnedAsync(options, token)
for name in [ "build"; "web"; "worker" ] do let! _ = owned.Value.CreateSessionAsync( NewSessionRequest(Name = name, Command = "/bin/sh"), token )
()
let! server = LibTmux.Server.ConnectAsync(options, token)
// List and filter: tmux narrows the listing; every row is rechecked. // atMostOne is None when nothing matches and raises when several do. let! build = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "build") |> Query.atMostOne token
let! others = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.ne "build") |> Query.list token
printfn "other sessions: %s" (String.Join(", ", [ for session in others -> session.Name ]))
match build with | None -> printfn "no build session" | Some session -> let! panes = session |> Session.panes |> Query.list token let pane = panes[0]
// Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started"
// Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3"
printfn "wait found: %b" started.Found
printfn "run: exit %d, output %A" result.ExitStatus.Value (List.ofSeq result.Output) }
try runAsync().GetAwaiter().GetResult()with error -> raise (InvalidOperationException( "tmux quickstart failed", error))Run this example
.NET SDK 10.0.302, .NET 8 and 10 runtimes, tmux on Linux/macOS
Save the program above as app/Program.fs in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
app/Example.fsproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFrameworks>net8.0;net10.0</TargetFrameworks> <LangVersion>10.0</LangVersion> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference> <DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder> </PropertyGroup> <ItemGroup> <Compile Include="Program.fs" /> <PackageReference Include="FSharp.Core" Version="[10.1.302]" /> <PackageReference Include="LibTmux.FSharp" Version="[0.0.0-alpha.18]" /> </ItemGroup></Project>app/global.json
{ "sdk": { "version": "10.0.302", "rollForward": "disable", "allowPrerelease": false }, "test": { "runner": "Microsoft.Testing.Platform" }}app/NuGet.config
<?xml version="1.0" encoding="utf-8"?><configuration> <packageSources> <clear /> <add key="libtmux-source" value="../libtmux-source/artifacts/api-example-packages" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> </packageSources> <packageSourceMapping> <clear /> <packageSource key="libtmux-source"> <package pattern="LibTmux" /> <package pattern="LibTmux.*" /> </packageSource> <packageSource key="nuget.org"> <package pattern="*" /> </packageSource> </packageSourceMapping></configuration>Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-dotnet.git libtmux-sourcegit -C libtmux-source checkout --detach 4f1b997b2e4aeb604c4184707e49dcd4dea44fefexport NUGET_PACKAGES="$PWD/packages"cd libtmux-sourcedotnet restore src/LibTmux.FSharp/LibTmux.FSharp.fsproj --locked-modedotnet pack src/LibTmux/LibTmux.csproj -c Release --no-restore \ -p:ContinuousIntegrationBuild=true --output artifacts/api-example-packagesdotnet pack src/LibTmux.FSharp/LibTmux.FSharp.fsproj -c Release --no-restore \ -p:ContinuousIntegrationBuild=true --output artifacts/api-example-packagescd ../appdotnet restore Example.fsproj --configfile NuGet.configdotnet build Example.fsproj -c Release --no-restore --warnaserrorexport TMUX_TMPDIR=/tmp/libtmux-dotnet-devmkdir -p "$TMUX_TMPDIR"unset TMUX TMUX_PANEdotnet bin/Release/net8.0/Example.dlldotnet bin/Release/net10.0/Example.dllExpected output
other sessions: web, workerwait found: truerun: exit 3, output ["ok"]Library revision: 4f1b997b2e4a.
$ dotnet package add LibTmux.FSharp --version 0.0.0-alpha.18.NET 10 SDK: run in your project directory. On .NET 8 or 9, use dotnet add package with the same arguments.
NuGet\Install-Package LibTmux.FSharp -Version 0.0.0-alpha.18Run in Visual Studio on Windows, in Tools > NuGet Package Manager > Package Manager Console. Choose the target project in that console.
<PackageReference Include="LibTmux.FSharp" Version="0.0.0-alpha.18" />Add this inside an ItemGroup in your .csproj or .fsproj, then run dotnet restore. For central version management, use the CPM tab.
Directory.Packages.props
Project file (.csproj or .fsproj)
<PackageReference Include="LibTmux.FSharp" />Save Directory.Packages.props at the solution root, or merge these settings into the existing file. Add the versionless reference inside the project’s ItemGroup, then run dotnet restore.
$ paket add LibTmux.FSharp --version 0.0.0-alpha.18 --interactiveRequires Paket and a solution initialized with paket init. The interactive prompt selects the projects that should reference the package.
#r "nuget: LibTmux.FSharp, 0.0.0-alpha.18"Use in an F# .fsx script, dotnet fsi, or a Polyglot Notebook with NuGet support, on .NET 8 or later. This is not a directive for an ordinary compiled project.
#:package LibTmux.FSharp@0.0.0-alpha.18C# file-based apps require the .NET 10 SDK. Place this at the top of a .cs file, before code, then run dotnet run app.cs. F# scripts use the Script & Interactive tab.
#addin nuget:?package=LibTmux.FSharp&version=0.0.0-alpha.18&loaddependencies=trueUse Cake.Tool 6 or later for a .cake script on .NET 8 or later, with Cake’s in-process NuGet client enabled. This loads the library and its dependencies; it has no Cake task aliases or executable tool. Cake Frosting projects use PackageReference.
Start with C++
#include <chrono>#include <cstdio>#include <cstdlib>#include <filesystem>#include <stdexcept>#include <libtmux/libtmux.hpp>
int main() { char directory[] = "/tmp/ltx-cxx-home-XXXXXX"; if (!mkdtemp(directory)) { std::perror("create temporary directory"); return 1; }
const auto socket = std::filesystem::path(directory) / "tmux.sock"; const auto server = libtmux::Server::startable_at_socket_path( socket, "/dev/null", {}, {.timeout = std::chrono::seconds{5}}).value();
const auto work = [&] { const auto session = server.new_session({ .name = "work", .shell_command = "/bin/sh", }).value();
const auto editor = session.new_window({.name = "editor"}).value(); const auto logs = editor.split({ .horizontal = true, .shell_command = "/bin/sh", .percentage = 30, }).value();
logs.send_text("printf '\\nhello from %s\\n' libtmux").value(); logs.send_key("Enter").value();
const auto ready = logs.wait_for_text( "hello from libtmux", {.timeout = std::chrono::seconds{5}}).value(); if (!ready.matched) throw std::runtime_error("output did not arrive");
const auto visible = logs.capture().value(); for (const auto line : libtmux::capture_lines(visible)) { if (line == "hello from libtmux") { std::puts("hello from libtmux"); return; } } throw std::runtime_error("capture is missing the output line"); };
work();}#include <chrono>#include <cstdio>#include <cstdlib>#include <filesystem>#include <stdexcept>#include <utility>#include <libtmux/libtmux.hpp>
int main() { char directory[] = "/tmp/ltx-cxx-home-XXXXXX"; if (!mkdtemp(directory)) { std::perror("create temporary directory"); return 1; }
const auto checked = [](auto result) { if (!result) throw std::runtime_error(result.error().diagnostic); return std::move(result).value(); };
const auto socket = std::filesystem::path(directory) / "tmux.sock"; const auto server = checked(libtmux::Server::startable_at_socket_path( socket, "/dev/null", {}, {.timeout = std::chrono::seconds{5}}));
const auto work = [&] { const auto session = checked(server.new_session({ .name = "work", .shell_command = "/bin/sh", }));
const auto editor = checked(session.new_window({.name = "editor"})); const auto logs = checked(editor.split({ .horizontal = true, .shell_command = "/bin/sh", .percentage = 30, }));
checked(logs.send_text("printf '\\nhello from %s\\n' libtmux")); checked(logs.send_key("Enter"));
const auto ready = checked(logs.wait_for_text( "hello from libtmux", {.timeout = std::chrono::seconds{5}})); if (!ready.matched) throw std::runtime_error("output did not arrive");
const auto visible = checked(logs.capture()); for (const auto line : libtmux::capture_lines(visible)) { if (line == "hello from libtmux") { std::puts("hello from libtmux"); return; } } throw std::runtime_error("capture is missing the output line"); };
int status = 0; try { work(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; } return status;}#include <chrono>#include <cstdio>#include <cstdlib>#include <filesystem>#include <stdexcept>#include <utility>#include <libtmux/libtmux.hpp>
int main() { char directory[] = "/tmp/ltx-cxx-home-XXXXXX"; if (!mkdtemp(directory)) { std::perror("create temporary directory"); return 1; }
const auto socket = std::filesystem::path(directory) / "tmux.sock"; auto created = libtmux::Server::startable_at_socket_path( socket, "/dev/null", {}, {.timeout = std::chrono::seconds{5}}); if (!created) { std::fprintf(stderr, "%s\n", created.error().diagnostic.c_str()); std::filesystem::remove_all(directory); return 1; } const auto server = std::move(created).value();
const auto work = [&] { const auto session = server.new_session({ .name = "work", .shell_command = "/bin/sh", }).value();
const auto editor = session.new_window({.name = "editor"}).value(); const auto logs = editor.split({ .horizontal = true, .shell_command = "/bin/sh", .percentage = 30, }).value();
logs.send_text("printf '\\nhello from %s\\n' libtmux").value(); logs.send_key("Enter").value();
const auto ready = logs.wait_for_text( "hello from libtmux", {.timeout = std::chrono::seconds{5}}).value(); if (!ready.matched) throw std::runtime_error("output did not arrive");
const auto visible = logs.capture().value(); for (const auto line : libtmux::capture_lines(visible)) { if (line == "hello from libtmux") { std::puts("hello from libtmux"); return; } } throw std::runtime_error("capture is missing the output line"); };
int status = 0; try { work(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; }
if (std::filesystem::exists(socket)) { server.kill().value(); } std::filesystem::remove_all(directory); return status;}#include <chrono>#include <cstdio>#include <cstdlib>#include <filesystem>#include <stdexcept>#include <utility>#include <libtmux/libtmux.hpp>
int main() { char directory[] = "/tmp/ltx-cxx-home-XXXXXX"; if (!mkdtemp(directory)) { std::perror("create temporary directory"); return 1; }
const auto checked = [](auto result) { if (!result) throw std::runtime_error(result.error().diagnostic); return std::move(result).value(); };
const auto socket = std::filesystem::path(directory) / "tmux.sock"; auto created = libtmux::Server::startable_at_socket_path( socket, "/dev/null", {}, {.timeout = std::chrono::seconds{5}}); if (!created) { std::fprintf(stderr, "%s\n", created.error().diagnostic.c_str()); std::filesystem::remove_all(directory); return 1; } const auto server = std::move(created).value();
const auto work = [&] { const auto session = checked(server.new_session({ .name = "work", .shell_command = "/bin/sh", }));
const auto editor = checked(session.new_window({.name = "editor"})); const auto logs = checked(editor.split({ .horizontal = true, .shell_command = "/bin/sh", .percentage = 30, }));
checked(logs.send_text("printf '\\nhello from %s\\n' libtmux")); checked(logs.send_key("Enter"));
const auto ready = checked(logs.wait_for_text( "hello from libtmux", {.timeout = std::chrono::seconds{5}})); if (!ready.matched) throw std::runtime_error("output did not arrive");
const auto visible = checked(logs.capture()); for (const auto line : libtmux::capture_lines(visible)) { if (line == "hello from libtmux") { std::puts("hello from libtmux"); return; } } throw std::runtime_error("capture is missing the output line"); };
int status = 0; try { work(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; }
if (std::filesystem::exists(socket)) { server.kill().value(); } std::filesystem::remove_all(directory); return status;}Run this example
CMake 3.25+, Clang 18 with matching libc++ development files, C++23, tmux on Linux/macOS
Save the program above as main.cpp in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
CMakeLists.txt
cmake_minimum_required(VERSION 3.25)project(libtmux_api_example LANGUAGES CXX)
find_package(libtmux REQUIRED)
add_executable(api_example main.cpp)target_link_libraries(api_example PRIVATE libtmux::libtmux)Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-cxx.git libtmux-sourcegit -C libtmux-source checkout --detach 3f93b021cb51e8349bfb9cecc6fd4091f6e79a0fcmake -S libtmux-source -B library \ -DCMAKE_TOOLCHAIN_FILE="$PWD/libtmux-source/cmake/toolchains/clang-libcxx.cmake" \ -DCMAKE_BUILD_TYPE=Release -DLIBTMUX_BUILD_TESTS=OFF -DLIBTMUX_BUILD_EXAMPLES=OFF \ -DLIBTMUX_BUILD_TESTING_LIBRARY=OFF -DLIBTMUX_CXX_STANDARD=23 \ -DCMAKE_CXX_COMPILER=clang++cmake --build library -j 2cmake --install library --prefix "$PWD/prefix"cmake -S . -B build -DCMAKE_PREFIX_PATH="$PWD/prefix" -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_CXX_STANDARD=23cmake --build build -j 2build/api_exampleExpected output
hello from libtmuxLibrary revision: 3f93b021cb51.
Name a tag or a commit, never a moving branch. Built this way the library brings nothing else: its tests, examples and MCP server all default off when it is not the top-level project.
This repository is a vcpkg git registry, not a curated-registry port. Put this in vcpkg-configuration.json, add "libtmux" to your manifest, and run vcpkg x-update-baseline to fill the baseline.
Then add_subdirectory(third_party/libtmux) and link libtmux::libtmux.
Start with Swift
import Foundationimport LibTmux
@mainstruct Quickstart { static func main() async throws { let server = try Server( socketName: "libtmux-home-\(UUID().uuidString)", configurationFile: "/dev/null" )
try await work(server) }
static func work(_ server: Server) async throws { let session = try await server.newSession( named: "work", windowName: "editor", shell: "/bin/sh" ) let logs = try await server.newWindow( in: session, named: "logs", shell: "/bin/sh" ).window let pane = try await server.splitWindow(logs, direction: .right)
try await server.run("printf '\\nhello from %s\\n' libtmux", in: pane)
let ready = try RegexPattern("^hello from libtmux$") let result = try await server.waitForOutput( in: pane, matching: [ready], timeout: .seconds(5) ) guard result.outcome == .matched else { throw QuickstartError.outputTimedOut }
for line in result.tail where line == "hello from libtmux" { print(line) } }
enum QuickstartError: Error { case outputTimedOut }}import Foundationimport LibTmux
@mainstruct Quickstart { static func main() async throws { let server = try Server( socketName: "libtmux-home-\(UUID().uuidString)", configurationFile: "/dev/null" )
do { try await work(server) } catch { print("tmux quickstart failed: \(error)") throw error } }
static func work(_ server: Server) async throws { let session = try await server.newSession( named: "work", windowName: "editor", shell: "/bin/sh" ) let logs = try await server.newWindow( in: session, named: "logs", shell: "/bin/sh" ).window let pane = try await server.splitWindow(logs, direction: .right)
try await server.run("printf '\\nhello from %s\\n' libtmux", in: pane)
let ready = try RegexPattern("^hello from libtmux$") let result = try await server.waitForOutput( in: pane, matching: [ready], timeout: .seconds(5) ) guard result.outcome == .matched else { throw QuickstartError.outputTimedOut }
for line in result.tail where line == "hello from libtmux" { print(line) } }
enum QuickstartError: Error { case outputTimedOut }}import Foundationimport LibTmux
@mainstruct Quickstart { static func main() async throws { let server = try Server( socketName: "libtmux-home-\(UUID().uuidString)", configurationFile: "/dev/null" )
do { try await work(server) } catch { if try await server.isRunning() { try await server.killServer() } throw error } try await server.killServer() }
static func work(_ server: Server) async throws { let session = try await server.newSession( named: "work", windowName: "editor", shell: "/bin/sh" ) let logs = try await server.newWindow( in: session, named: "logs", shell: "/bin/sh" ).window let pane = try await server.splitWindow(logs, direction: .right)
try await server.run("printf '\\nhello from %s\\n' libtmux", in: pane)
let ready = try RegexPattern("^hello from libtmux$") let result = try await server.waitForOutput( in: pane, matching: [ready], timeout: .seconds(5) ) guard result.outcome == .matched else { throw QuickstartError.outputTimedOut }
for line in result.tail where line == "hello from libtmux" { print(line) } }
enum QuickstartError: Error { case outputTimedOut }}import Foundationimport LibTmux
@mainstruct Quickstart { static func main() async throws { let server = try Server( socketName: "libtmux-home-\(UUID().uuidString)", configurationFile: "/dev/null" )
do { try await work(server) } catch { print("tmux quickstart failed: \(error)") if try await server.isRunning() { try await server.killServer() } throw error } try await server.killServer() }
static func work(_ server: Server) async throws { let session = try await server.newSession( named: "work", windowName: "editor", shell: "/bin/sh" ) let logs = try await server.newWindow( in: session, named: "logs", shell: "/bin/sh" ).window let pane = try await server.splitWindow(logs, direction: .right)
try await server.run("printf '\\nhello from %s\\n' libtmux", in: pane)
let ready = try RegexPattern("^hello from libtmux$") let result = try await server.waitForOutput( in: pane, matching: [ready], timeout: .seconds(5) ) guard result.outcome == .matched else { throw QuickstartError.outputTimedOut }
for line in result.tail where line == "hello from libtmux" { print(line) } }
enum QuickstartError: Error { case outputTimedOut }}Run this example
Swift 6.2+, tmux on Linux/macOS
Save the program above as Sources/Main.swift in a new directory, along with the files below.
Cleanup is omitted; the private tmux server may remain running. Select Cleanup to stop it when the program finishes. The program stops its private tmux server when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.
Package.swift
// swift-tools-version: 6.2import PackageDescription
let package = Package( name: "ApiExample", platforms: [.macOS(.v13)], dependencies: [ .package(name: "libtmux", path: "libtmux-source") ], targets: [ .executableTarget( name: "ApiExample", dependencies: [ .product(name: "LibTmux", package: "libtmux"), ], path: "Sources" ) ])Setup and run
# Save the files above at their displayed relative paths.git clone https://github.com/libtmux/libtmux-swift.git libtmux-sourcegit -C libtmux-source checkout --detach 7993e7044347629b08020e9b02ae8bcffa0e5898swift build --jobs 2 --product ApiExample.build/debug/ApiExampleExpected output
hello from libtmuxLibrary revision: 7993e7044347.
An exact version, not a range. Every tag before 0.1.0 is a prerelease: SwiftPM keeps prereleases out of a `from: "0.1.0"` range, and `from: "0.1.0-alpha.5"` resolves forward into every prerelease after it.
Start with an agent
Copy this into any coding agent. It installs libtmux in the repository you are in, checks the install against a real tmux server, and points the agent at this site's llms.txt and docs.json so it reads the API rather than guessing it. Use the language selected above, then pick a task.
All prompts, including a plain-text URL per language and task that an agent can fetch.