RubyHome

Choose documentation 3

latest

Current version

latest
English

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

libtmux for Ruby

Ruby-based tmux client

tmux client library for Ruby. Control tmux, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents

Product
Capability: Navigate tmux via a typed object graph
require "libtmux"
# A private server: its own socket and an empty config, so this neither
# touches nor is changed by a tmux you already use.
snapshot, ref = LibTmux::Server.start do |server|
session = server.new_session(name: "demo", command: ["/bin/cat"])
# Lay out the session: name its window, split it, add a second window.
editor = server.window(session.snapshot.current_window.ref)
editor.rename("editor")
left = server.pane(editor.snapshot.active_pane.ref)
right = left.split(direction: :horizontal, command: ["/bin/sh"])
logs = session.new_window(name: "logs", command: ["/bin/cat"])
# Type a command into the new pane and wait for what it prints.
right.send_text("printf 'hello from %s\\n' tmux")
right.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
until right.capture.stdout.include?("hello from tmux")
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "Output did not arrive within five seconds"
end
sleep 0.05
end
# Move the focus an attached client sees: the pane, then the window.
right.select
session.list_window_links.find { |link| link.window == logs }.select
[server.snapshot, right.ref]
end
# Walk the objects: down from the server, then up from one pane.
snapshot.sessions.each do |entry|
puts "session #{entry.name}"
entry.window_links.each do |link|
pane_ids = link.window.panes.map(&:id).join(" ")
puts " window #{link.index}:#{link.window.name}: #{pane_ids}"
end
end
pane = snapshot.resolve(ref)
window = pane.window
owner = window.window_links.first.session
puts "#{pane.id} is in window #{window.name} of session #{owner.name}"
require "libtmux"
# A private server: its own socket and an empty config, so this neither
# touches nor is changed by a tmux you already use.
begin
snapshot, ref = LibTmux::Server.start do |server|
session = server.new_session(name: "demo", command: ["/bin/cat"])
# Lay out the session: name its window, split it, add a second window.
editor = server.window(session.snapshot.current_window.ref)
editor.rename("editor")
left = server.pane(editor.snapshot.active_pane.ref)
right = left.split(direction: :horizontal, command: ["/bin/sh"])
logs = session.new_window(name: "logs", command: ["/bin/cat"])
# Type a command into the new pane and wait for what it prints.
right.send_text("printf 'hello from %s\\n' tmux")
right.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
until right.capture.stdout.include?("hello from tmux")
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "Output did not arrive within five seconds"
end
sleep 0.05
end
# Move the focus an attached client sees: the pane, then the window.
right.select
session.list_window_links.find { |link| link.window == logs }.select
[server.snapshot, right.ref]
end
# Walk the objects: down from the server, then up from one pane.
snapshot.sessions.each do |entry|
puts "session #{entry.name}"
entry.window_links.each do |link|
pane_ids = link.window.panes.map(&:id).join(" ")
puts " window #{link.index}:#{link.window.name}: #{pane_ids}"
end
end
pane = snapshot.resolve(ref)
window = pane.window
owner = window.window_links.first.session
puts "#{pane.id} is in window #{window.name} of session #{owner.name}"
rescue StandardError => error
warn "tmux quickstart failed: #{error.message}"
raise
end
require "libtmux"
# A private server: its own socket and an empty config, so this neither
# touches nor is changed by a tmux you already use.
server = LibTmux::Server.start
begin
session = server.new_session(name: "demo", command: ["/bin/cat"])
# Lay out the session: name its window, split it, add a second window.
editor = server.window(session.snapshot.current_window.ref)
editor.rename("editor")
left = server.pane(editor.snapshot.active_pane.ref)
right = left.split(direction: :horizontal, command: ["/bin/sh"])
logs = session.new_window(name: "logs", command: ["/bin/cat"])
# Type a command into the new pane and wait for what it prints.
right.send_text("printf 'hello from %s\\n' tmux")
right.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
until right.capture.stdout.include?("hello from tmux")
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "Output did not arrive within five seconds"
end
sleep 0.05
end
# Move the focus an attached client sees: the pane, then the window.
right.select
session.list_window_links.find { |link| link.window == logs }.select
# Walk the objects: down from the server, then up from one pane.
snapshot = server.snapshot
snapshot.sessions.each do |entry|
puts "session #{entry.name}"
entry.window_links.each do |link|
pane_ids = link.window.panes.map(&:id).join(" ")
puts " window #{link.index}:#{link.window.name}: #{pane_ids}"
end
end
pane = snapshot.resolve(right.ref)
window = pane.window
owner = window.window_links.first.session
puts "#{pane.id} is in window #{window.name} of session #{owner.name}"
ensure
server.close
end
require "libtmux"
# A private server: its own socket and an empty config, so this neither
# touches nor is changed by a tmux you already use.
server = LibTmux::Server.start
begin
session = server.new_session(name: "demo", command: ["/bin/cat"])
# Lay out the session: name its window, split it, add a second window.
editor = server.window(session.snapshot.current_window.ref)
editor.rename("editor")
left = server.pane(editor.snapshot.active_pane.ref)
right = left.split(direction: :horizontal, command: ["/bin/sh"])
logs = session.new_window(name: "logs", command: ["/bin/cat"])
# Type a command into the new pane and wait for what it prints.
right.send_text("printf 'hello from %s\\n' tmux")
right.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
until right.capture.stdout.include?("hello from tmux")
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "Output did not arrive within five seconds"
end
sleep 0.05
end
# Move the focus an attached client sees: the pane, then the window.
right.select
session.list_window_links.find { |link| link.window == logs }.select
# Walk the objects: down from the server, then up from one pane.
snapshot = server.snapshot
snapshot.sessions.each do |entry|
puts "session #{entry.name}"
entry.window_links.each do |link|
pane_ids = link.window.panes.map(&:id).join(" ")
puts " window #{link.index}:#{link.window.name}: #{pane_ids}"
end
end
pane = snapshot.resolve(right.ref)
window = pane.window
owner = window.window_links.first.session
puts "#{pane.id} is in window #{window.name} of session #{owner.name}"
rescue StandardError => error
warn "tmux quickstart failed: #{error.message}"
raise
ensure
server.close
end
Example details
No tmux server yet

tmux before the first call: no server is running.

No tmux server yet

tmux after Server.start: no server is running; the call sent no command.

%0 cat
[demo]

tmux after Server#new_session: added session demo.

%0 cat
[demo]

tmux after Server#window: unchanged.

%0 cat
[demo]

tmux after Window#rename: renamed window 0:env to 0:editor.

%0 cat
[demo]

tmux after Server#pane: unchanged.

%0 cat
%1 sh
[demo]

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

%0 cat
%1 sh
[demo]

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

%0 cat
%1 sh
[demo]

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

%0 cat
%1 sh
[demo]

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

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

tmux after Pane#capture: read pane %1.

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

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

%2 cat
[demo]

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

%2 cat
[demo]

tmux after Server#snapshot: unchanged.

%2 cat
[demo]

tmux after Snapshot#resolve: unchanged, the call sent no command.

%2 cat
[demo]

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

%2 cat
[demo]

tmux after WindowSnapshot#window_links: unchanged, the call sent no command.

Before the first call

No tmux server is running yet.

Server.start returned LibTmux::Server

{
"owned": true,
"tmuxVersion": "3.8a"
}

The call sent no tmux command.

Server#new_session returned LibTmux::Session

{
"id": "$0"
}
1 tmux command, 1 that changes tmux
  1. new-session -d -P -F #{n:session_id}:#{session_id} -s demo -- /usr/bin/env -- /bin/cat 2:$0

Server#window returned LibTmux::Window

{
"id": "@0"
}
3 tmux commands
  1. list-sessions -F #{n:session_id}:#{q:session_id}#{n:session_name}:#{q:session_name}#{n:session_created}:#{q:session_created}#{n:session_attached}:#{q:session_attached}#{n:session_windows}:#{q:session_windows} 2:\$04:demo10:17938869301:01:1
  2. list-windows -a -F #{n:window_id}:#{q:window_id}#{n:window_name}:#{q:window_name}#{n:window_width}:#{q:window_width}#{n:window_height}:#{q:window_height}#{n:window_panes}:#{q:window_panes}#{n:window_layout}:#{q:window_layout}#{n:session_id}:#{q:session_id}#{n:window_id}:#{q:window_id}#{n:window_index}:#{q:window_index}#{n:window_active}:#{q:window_active} 2:@03:env2:802:241:171:\{\"V\":2,\"L\":\{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"\%0\"\}\}2:\$02:@01:01:1
  3. list-panes -a -F #{n:pane_id}:#{q:pane_id}#{n:window_id}:#{q:window_id}#{n:pane_index}:#{q:pane_index}#{n:pane_pid}:#{q:pane_pid}#{n:pane_current_command}:#{q:pane_current_command}#{n:pane_current_path}:#{q:pane_current_path}#{n:pane_title}:#{q:pane_title}#{n:pane_active}:#{q:pane_active}#{n:pane_dead}:#{q:pane_dead}#{n:pane_dead_status}:#{q:pane_dead_status}#{n:pane_width}:#{q:pane_width}#{n:pane… 2:\%02:@01:07:29776673:cat31:/tmp/marked1:d1:11:00:2:802:24

Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).

Window#rename returned LibTmux::CommandResult

{
"success": true
}
1 tmux command, 1 that changes tmux
  1. rename-window -t @0 -- editor no output

Server#pane returned LibTmux::Pane

{
"id": "%0"
}
3 tmux commands
  1. list-sessions -F #{n:session_id}:#{q:session_id}#{n:session_name}:#{q:session_name}#{n:session_created}:#{q:session_created}#{n:session_attached}:#{q:session_attached}#{n:session_windows}:#{q:session_windows} 2:\$04:demo10:17938869301:01:1
  2. list-windows -a -F #{n:window_id}:#{q:window_id}#{n:window_name}:#{q:window_name}#{n:window_width}:#{q:window_width}#{n:window_height}:#{q:window_height}#{n:window_panes}:#{q:window_panes}#{n:window_layout}:#{q:window_layout}#{n:session_id}:#{q:session_id}#{n:window_id}:#{q:window_id}#{n:window_index}:#{q:window_index}#{n:window_active}:#{q:window_active} 2:@06:editor2:802:241:171:\{\"V\":2,\"L\":\{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"\%0\"\}\}2:\$02:@01:01:1
  3. list-panes -a -F #{n:pane_id}:#{q:pane_id}#{n:window_id}:#{q:window_id}#{n:pane_index}:#{q:pane_index}#{n:pane_pid}:#{q:pane_pid}#{n:pane_current_command}:#{q:pane_current_command}#{n:pane_current_path}:#{q:pane_current_path}#{n:pane_title}:#{q:pane_title}#{n:pane_active}:#{q:pane_active}#{n:pane_dead}:#{q:pane_dead}#{n:pane_dead_status}:#{q:pane_dead_status}#{n:pane_width}:#{q:pane_width}#{n:pane… 2:\%02:@01:07:29776673:cat31:/tmp/marked1:d1:11:00:2:802:24

Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).

Pane#split returned LibTmux::Pane

{
"id": "%1"
}
1 tmux command, 1 that changes tmux
  1. split-window -d -h -P -F #{n:pane_id}:#{pane_id} -t %0 -- /usr/bin/env -- /bin/sh 2:%1

Session#new_window returned LibTmux::Window

{
"id": "@1"
}
1 tmux command, 1 that changes tmux
  1. new-window -d -P -F #{n:window_id}:#{window_id} -t $0 -n logs -- /usr/bin/env -- /bin/cat 2:@1

Pane#send_text returned LibTmux::CommandResult

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

Pane#send_keys returned LibTmux::CommandResult

{
"success": true
}
1 tmux command, 1 that changes tmux
  1. send-keys -t %1 -- Enter no output

Pane#capture returned LibTmux::CommandResult

{
"lines": [
"$ printf 'hello from %s\\n' tmux",
"hello from tmux",
"$"
]
}
1 tmux command
  1. capture-pane -p -t %1 $ printf 'hello from %s\n' tmux hello from tmux $

Pane#select returned LibTmux::CommandResult

{
"success": true
}
1 tmux command, 1 that changes tmux
  1. select-pane -t %1 no output

WindowLink#select returned LibTmux::CommandResult

{
"success": true
}
4 tmux commands, 1 that changes tmux
  1. list-windows -t $0 -F #{n:session_id}:#{q:session_id}#{n:window_index}:#{q:window_index}#{n:window_id}:#{q:window_id} 2:\$01:02:@0 2:\$01:12:@1
  2. show-options -s -- command-alias command-alias[0] split-pane=split-window command-alias[1] splitp=split-window command-alias[2] "server-info=show-messages -JT" command-alias[3] "info=show-messages -JT" command-alias[4] "choose-window=choose-tree -w" command-alias[5] "choose-session=choose-tree -s"
  3. list-commands -F #{n:command_list_name}:#{q:command_list_name}#{n:command_list_alias}:#{q:command_list_alias} 14:attach-session6:attach 8:bind-key4:bind 10:break-pane6:breakp 12:capture-pane8:capturep 13:choose-buffer0: 13:choose-client0: 11:choose-tree0: 13:clear-history9:clearhist 20:clear-prompt-history10:clearphist 10:clock-mode0: 14:command-prompt0: 14:confirm-before7:confirm 9:copy-mode0: 14:customize-mode0: 13:delete-buffer7:deleteb 13:detach-client6:detach 12:display-menu4:menu 15:display-message7…
  4. select-window -t $0:1 no output

Server#snapshot returned LibTmux::Snapshot

{
"sessions": 1,
"windows": 2,
"panes": 3
}
3 tmux commands
  1. list-sessions -F #{n:session_id}:#{q:session_id}#{n:session_name}:#{q:session_name}#{n:session_created}:#{q:session_created}#{n:session_attached}:#{q:session_attached}#{n:session_windows}:#{q:session_windows} 2:\$04:demo10:17938869301:01:2
  2. list-windows -a -F #{n:window_id}:#{q:window_id}#{n:window_name}:#{q:window_name}#{n:window_width}:#{q:window_width}#{n:window_height}:#{q:window_height}#{n:window_panes}:#{q:window_panes}#{n:window_layout}:#{q:window_layout}#{n:session_id}:#{q:session_id}#{n:window_id}:#{q:window_id}#{n:window_index}:#{q:window_index}#{n:window_active}:#{q:window_active} 2:@06:editor2:802:241:2171:\{\"V\":2,\"L\":\{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":\[\{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"l\":0,\"i\":0,\"I\":\"\%0\"\},\{\"t\":\"p\",\"w\":39,\"h\":24,\"x\":41,\"y\":0,\"a\":true,\"i\":1,\"I\":\"\%1\"\}]\}\}2:\$02:@01:01:0 2:@14:logs2:802:241:171:\{\"V\":2,\"L\":\{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"\%…
  3. list-panes -a -F #{n:pane_id}:#{q:pane_id}#{n:window_id}:#{q:window_id}#{n:pane_index}:#{q:pane_index}#{n:pane_pid}:#{q:pane_pid}#{n:pane_current_command}:#{q:pane_current_command}#{n:pane_current_path}:#{q:pane_current_path}#{n:pane_title}:#{q:pane_title}#{n:pane_active}:#{q:pane_active}#{n:pane_dead}:#{q:pane_dead}#{n:pane_dead_status}:#{q:pane_dead_status}#{n:pane_width}:#{q:pane_width}#{n:pane… 2:\%02:@01:07:29776673:cat31:/tmp/marked1:d1:01:00:2:402:24 2:\%12:@01:17:29778492:sh31:/tmp/marked1:d1:11:00:2:392:24 2:\%22:@11:07:29778723:cat31:/tmp/marked1:d1:11:00:2:802:24

Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).

Snapshot#resolve returned LibTmux::PaneSnapshot

{
"id": "%1"
}

The call sent no tmux command.

PaneSnapshot#window returned LibTmux::WindowSnapshot

{
"id": "@0",
"name": "editor"
}

The call sent no tmux command.

WindowSnapshot#window_links returned LibTmux::SessionSnapshot

{
"id": "$0",
"name": "demo"
}

The call sent no tmux command.

0/16
Run this example

POSIX shell, tmux 3.3a or newer, Ruby 4.0.7, Bundler 4.0.20, a C build toolchain for gems and access to rubygems.org.

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 and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.

Gemfile
source "https://rubygems.org"
gem "libtmux", "~> 0.1"
Gemfile.lock
GEM
remote: https://rubygems.org/
specs:
fiddle (1.1.8)
fileutils (1.8.1)
io-console (0.9.5)
json (3.0.2)
libtmux (0.1.2)
fiddle (~> 1.1)
io-console (~> 0.8)
json (~> 3.0.2)
securerandom (~> 0.4)
tmpdir (~> 0.3)
securerandom (0.4.1)
tmpdir (0.3.2)
fileutils
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
libtmux (~> 0.1)
CHECKSUMS
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
fileutils (1.8.1) sha256=2d541a1ee34a9ed5c4f054be8fb819a26b5d654488a31dc8806917cf448b9cc3
io-console (0.9.5) sha256=83dd9bbf4c4680416629245b39cd4212eb5f4d9a4ea91804870c1f6b846414d5
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
libtmux (0.1.2) sha256=340498b8e45b5b5d6511668909d9c3104fe7b17e65daaa9cfa50661ce299356a
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
tmpdir (0.3.2) sha256=e276ef386182169f8d417d9e90f12dba82a54e81646df0caf21de4a7987a0c27
BUNDLED WITH
4.0.20

Setup and run

bundle config set --local path vendor/bundle
bundle install --jobs 2
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
bundle exec ruby quickstart.rb

Expected output

session demo
window 0:editor: %0 %1
window 1:logs: %2
%1 is in window editor of session demo
# frozen_string_literal: true
begin
# docs:begin main
require "libtmux"
LibTmux::Server.start do |server|
created = server.new_session(
name: "capture", command: ["/bin/sh"],
environment: {"ENV" => "/dev/null"}, receipt: true
)
pane = created.pane
pane.send_text("printf '\\nlibtmux capture ready\\n'")
pane.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
captured = pane.capture.stdout.lines(chomp: true)
until captured.include?("libtmux capture ready")
raise "Output did not arrive within five seconds" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
sleep 0.05
captured = pane.capture.stdout.lines(chomp: true)
end
raise "Expected output is absent from the pane" unless captured.include?("libtmux capture ready")
puts "libtmux capture ready"
end
# docs:end main
rescue StandardError => error
warn "#{error.class}: #{error.message}"
exit 1
end
No tmux server yet

tmux before the first call: no server is running.

No tmux server yet

tmux after Server.start: no server is running; the call sent no command.

%0 sh
[capture]

tmux after Server#new_session: added session capture.

%0 sh
[capture]

tmux after Pane#send_text: renamed window 0:env to 0:sh; sent keys to pane %0.

%0 sh
[capture]

tmux after Pane#send_keys: sent keys to pane %0.

%0 sh
$ printf '\nlibtmux capture ready\n'

libtmux capture ready
$
[capture]

tmux after Pane#capture: read pane %0.

Before the first call

No tmux server is running yet.

Server.start returned LibTmux::Server

{
"owned": true,
"tmuxVersion": "3.8a"
}

The call sent no tmux command.

Server#new_session returned LibTmux::CreationReceipt

{
"session": "$0",
"window": "@0",
"pane": "%0"
}
1 tmux command, 1 that changes tmux
  1. new-session -d -P -F #{n:session_id}:#{session_id}#{n:window_id}:#{window_id}#{n:pane_id}:#{pane_id} -s capture -e ENV=/dev/null -- /usr/bin/env -- /bin/sh 2:$02:@02:%0

Pane#send_text returned LibTmux::CommandResult

{
"success": true
}
1 tmux command, 1 that changes tmux
  1. send-keys -l -t %0 -- 'printf '\''\nlibtmux capture ready\n'\''' no output

Pane#send_keys returned LibTmux::CommandResult

{
"success": true
}
1 tmux command, 1 that changes tmux
  1. send-keys -t %0 -- Enter no output

Pane#capture returned LibTmux::CommandResult

{
"lines": [
"$ printf '\\nlibtmux capture ready\\n'",
"libtmux capture ready",
"$"
]
}
1 tmux command
  1. capture-pane -p -t %0 $ printf '\nlibtmux capture ready\n' libtmux capture ready $
0/5
# frozen_string_literal: true
begin
# docs:begin main
require "libtmux"
LibTmux::Server.start do |server|
session = server.new_session(name: "work", command: ["/bin/cat"])
window = session.new_window(name: "logs", command: ["/bin/cat"])
puts "window: #{window.snapshot.name}"
puts "session windows: #{session.list_windows.length}"
end
# docs:end main
rescue StandardError => error
warn "#{error.class}: #{error.message}"
exit 1
end
No tmux server yet

tmux before the first call: no server is running.

No tmux server yet

tmux after Server.start: no server is running; the call sent no command.

%0 cat
[work]

tmux after Server#new_session: added session work.

%0 cat
[work]

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

%0 cat
[work]

tmux after Window#snapshot: unchanged.

%0 cat
[work]

tmux after Session#list_windows: unchanged.

Before the first call

No tmux server is running yet.

Server.start returned LibTmux::Server

{
"owned": true,
"tmuxVersion": "3.8a"
}

The call sent no tmux command.

Server#new_session returned LibTmux::Session

{
"id": "$0"
}
1 tmux command, 1 that changes tmux
  1. new-session -d -P -F #{n:session_id}:#{session_id} -s work -- /usr/bin/env -- /bin/cat 2:$0

Session#new_window returned LibTmux::Window

{
"id": "@1"
}
1 tmux command, 1 that changes tmux
  1. new-window -d -P -F #{n:window_id}:#{window_id} -t $0 -n logs -- /usr/bin/env -- /bin/cat 2:@1

Window#snapshot returned LibTmux::WindowSnapshot

{
"id": "@1",
"name": "logs"
}
3 tmux commands
  1. list-sessions -F #{n:session_id}:#{q:session_id}#{n:session_name}:#{q:session_name}#{n:session_created}:#{q:session_created}#{n:session_attached}:#{q:session_attached}#{n:session_windows}:#{q:session_windows} 2:\$04:work10:17938754101:01:2
  2. list-windows -a -F #{n:window_id}:#{q:window_id}#{n:window_name}:#{q:window_name}#{n:window_width}:#{q:window_width}#{n:window_height}:#{q:window_height}#{n:window_panes}:#{q:window_panes}#{n:window_layout}:#{q:window_layout}#{n:session_id}:#{q:session_id}#{n:window_id}:#{q:window_id}#{n:window_index}:#{q:window_index}#{n:window_active}:#{q:window_active} 2:@03:env2:802:241:171:\{\"V\":2,\"L\":\{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"\%0\"\}\}2:\$02:@01:01:1 2:@14:logs2:802:241:171:\{\"V\":2,\"L\":\{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"\%1\"\}\}2:\$02:@11:11:0
  3. list-panes -a -F #{n:pane_id}:#{q:pane_id}#{n:window_id}:#{q:window_id}#{n:pane_index}:#{q:pane_index}#{n:pane_pid}:#{q:pane_pid}#{n:pane_current_command}:#{q:pane_current_command}#{n:pane_current_path}:#{q:pane_current_path}#{n:pane_title}:#{q:pane_title}#{n:pane_active}:#{q:pane_active}#{n:pane_dead}:#{q:pane_dead}#{n:pane_dead_status}:#{q:pane_dead_status}#{n:pane_width}:#{q:pane_width}#{n:pane… 2:\%02:@01:07:25770323:cat31:/tmp/marked1:d1:11:00:2:802:24 2:\%12:@11:07:25770553:cat31:/tmp/marked1:d1:11:00:2:802:24

Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).

Session#list_windows returned Array<LibTmux::Window>

{
"windows": [
"@0",
"@1"
]
}
1 tmux command
  1. list-windows -t $0 -F #{n:window_id}:#{window_id} 2:@0 2:@1
0/5

Install libtmux

$ gem install libtmux
gem "libtmux"

MCP

tmux through MCP

Replay Hover a call to preview the tmux state after it. Click to pin it.
tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux before the first call.

tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux after tmux_capabilities: unchanged, the call only reads.

tmux window of 1 pane
%1 sh
[checks] 0:sh*

tmux after tmux_create.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_create.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_snapshot: unchanged, the call only reads.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_send.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_wait.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_send.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_wait.

tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux after tmux_close.

you

Create a tmux session called checks, split its window into two panes, run pytest -q in one and show checks.log in the other, show me the results, then remove the session.

agentAsk the server which tools and limits are enabled.

Result
{
"ok": true,
"data": {
"endpoint": "docs",
"server_identity": {
"generation": "3f9a1c",
"pid": 1119217,
"start_time": 1794050582,
"tmux_version": "3.8a"
},
"enabled_tools": [
"tmux_capabilities",
"tmux_snapshot",
"tmux_send",
"tmux_create",
"… 4 more"
],
"criteria_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:libtmux-ruby:where:1",
"title": "Ruby libtmux criteria version 1",
"description": "The Ruby decoder additionally enforces byte, depth and node limits, rejects duplicate keys and floating-point tokens for integer fields.",
"oneOf": [
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
"… 1 more"
],
"$defs": {
"session.id": {
"anyOf": "…"
},
"session.name": {
"anyOf": "…"
},
"session.created": {
"anyOf": "…"
},
"session.attached": {
"anyOf": "…"
},
"session.window_count": {
"anyOf": "…"
},
"session": {
"type": "object",
"additionalProperties": false,
"properties": "…"
},
"window.id": {
"anyOf": "…"
},
"window.name": {
"anyOf": "…"
},
"window.width": {
"anyOf": "…"
},
"window.height": {
"anyOf": "…"
},
"window.pane_count": {
"anyOf": "…"
},
"window.layout": {
"anyOf": "…"
},
"…": "30 more"
}
},
"limits": {
"max_captures": 16,
"max_capture_bytes": 8388608,
"capture_ttl_seconds": 30,
"request_timeout_seconds": 30,
"max_response_bytes": 1048576,
"max_mutation_bytes": 65536,
"max_observers": 16,
"max_enrollments": 8,
"default_enrollment_seconds": 60,
"max_enrollment_seconds": 300,
"max_script_bytes": 65536,
"default_run_output_bytes": 65536,
"…": "2 more"
},
"owns_daemon": false,
"resource_subscriptions": false,
"observation": {
"screen": "bounded_rows",
"history_continuity": "unknown",
"process_cursor": "conditional",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 3 more"
],
"wait_conditions": [
"screen_contains",
"process_exit"
]
},
"authored_run": {
"availability": "conditional",
"shell_profile": "zsh-5.9-zle",
"enrollment": "explicit_source",
"authorization": "exact_generation_at_queue_grant",
"stdin": "closed",
"persistent_shell_changes": false,
"descendant_termination": "unobserved",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 5 more"
]
}
}
}

agentCreate the session.

tmux_create {"kind":"session","name":"checks","argv":["sh"]}
Result
{
"ok": true,
"data": {
"entity": {
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
"created": [
{
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
{
"generation": "3f9a1c",
"kind": "window",
"id": "@1"
},
{
"generation": "3f9a1c",
"kind": "pane",
"id": "%1"
}
],
"delivery": "observed",
"program_completion": "unobserved"
}
}

agentSplit the pane the create result returned (%1) in two.

tmux_create {"kind":"pane","parent":{"kind":"pane","id":"%1","generation":"3f9a1c"},"direction":"horizontal","argv":["sh"]}
Result
{
"ok": true,
"data": {
"entity": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
},
"created": [
{
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
}
],
"delivery": "observed",
"program_completion": "unobserved"
}
}

agentRead the window's layout.

tmux_snapshot {"entity":"window"}
Result
{
"ok": true,
"data": {
"capture_id": "a13829fbcf68c4d13192d49431427978",
"server_identity": {
"generation": "3f9a1c",
"pid": 1119217,
"start_time": 1794050582,
"tmux_version": "3.8a"
},
"entity": "window",
"coverage": {
"session": "complete",
"window": "complete",
"pane": "complete",
"window_link": "complete",
"client": "complete"
},
"interval": {
"clock": "monotonic_seconds",
"started": 6950.386331011,
"finished": 6950.398742721,
"reads": 5
},
"items": [
{
"kind": "window",
"fields": {
"id": "@0",
"name": "cat",
"width": 80,
"height": 24,
"paneCount": 1,
"layout": "{\"V\":2,\"L\":{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%0\"}}"
},
"ref": {
"generation": "3f9a1c",
"kind": "window",
"id": "@0"
}
},
{
"kind": "window",
"fields": {
"id": "@1",
"name": "sh",
"width": 80,
"height": 24,
"paneCount": 2,
"layout": "{\"V\":2,\"L\":{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":[{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%1\"},{\"t\":\"p\",\"w\":39,\"h\":24,\"x\":41,\"y\":0,\"i\":1,\"I\":\"%2\"}]}}"
},
"ref": {
"generation": "3f9a1c",
"kind": "window",
"id": "@1"
}
}
],
"truncated": false
}
}

agentType the command into the pane the create result returned (%1).

tmux_send {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"input":{"type":"text","text":"pytest -q\n"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%1"
},
"delivery": "observed",
"client_exit_status": 0,
"completion": "dispatch_only"
}
}

agentWait for the test summary in the pane the create result returned (%1).

tmux_wait {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"2 passed"},"timeout":10}
Result
{
"ok": true,
"data": {
"target": {
"kind": "pane",
"id": "%1",
"generation": "3f9a1c"
},
"condition": "screen_contains",
"capture": {
"mode": "snapshot",
"target": {
"kind": "pane",
"id": "%1",
"generation": "3f9a1c"
},
"capture_id": "7614cc6ece6c7b3bbcb5119bb6291b2f",
"process_generation": "23fb4e239db0000be47a27a2b54059d5",
"rows": [
"$ pytest -q\n",
".. [100%]\n",
"2 passed in 0.01s\n",
"$ \n",
"… 20 more"
],
"encoding": "utf-8",
"row_count": 24,
"bytes": 93,
"truncated": false,
"history_continuity": "unknown",
"scope": {
"max_lines": 200,
"max_bytes": 65536,
"history_lines": 0
},
"interval": {
"clock": "monotonic_seconds",
"started": 6953.983863023,
"finished": 6954.004227945
}
}
}
}

agentType the command into the new pane the split returned (%2).

tmux_send {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"input":{"type":"text","text":"cat checks.log\n"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
},
"delivery": "observed",
"client_exit_status": 0,
"completion": "dispatch_only"
}
}

agentWait for the last line of the log in the pane the split returned (%2).

tmux_wait {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"checks finished"},"timeout":10}
Result
{
"ok": true,
"data": {
"target": {
"kind": "pane",
"id": "%2",
"generation": "3f9a1c"
},
"condition": "screen_contains",
"capture": {
"mode": "snapshot",
"target": {
"kind": "pane",
"id": "%2",
"generation": "3f9a1c"
},
"capture_id": "14373ab34e565f19369f7e1bc9e0b210",
"process_generation": "0dd4defa031cdb8636cd70dc67eede49",
"rows": [
"$ cat checks.log\n",
"2026-01-01 checks started\n",
"2026-01-01 checks finished\n",
"$ \n",
"… 20 more"
],
"encoding": "utf-8",
"row_count": 24,
"bytes": 93,
"truncated": false,
"history_continuity": "unknown",
"scope": {
"max_lines": 200,
"max_bytes": 65536,
"history_lines": 0
},
"interval": {
"clock": "monotonic_seconds",
"started": 6957.556795452,
"finished": 6957.578571397
}
}
}
}

agentRemove the session by the id the create result returned ($1).

tmux_close {"target":{"kind":"session","id":"$1","generation":"3f9a1c"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
"delivery": "observed",
"client_exit_status": 0
}
}
0/9

Install libtmux-mcp

Choose your client and configuration scope, then run the command or paste the configuration.

With RubyGems installed:

$ claude mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With RubyGems installed:

$ claude mcp add tmux --scope user -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With RubyGems installed:

$ claude mcp add tmux --scope project -- libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With Bundler installed:

$ claude mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With Bundler installed:

$ claude mcp add tmux --scope user -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With Bundler installed:

$ claude mcp add tmux --scope project -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With RubyGems installed:

$ codex mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With RubyGems installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "libtmux-mcp"
args = ["--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With Bundler installed:

$ codex mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With Bundler installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "bundle"
args = ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With RubyGems installed:

$ gemini mcp add --scope user tmux libtmux-mcp -- --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With RubyGems installed:

$ gemini mcp add --scope project tmux libtmux-mcp -- \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With Bundler installed:

$ gemini mcp add --scope user tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With Bundler installed:

$ gemini mcp add --scope project tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With RubyGems installed:

$ grok mcp add --scope user tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With RubyGems installed:

$ grok mcp add --scope project tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With Bundler installed:

$ grok mcp add --scope user tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With Bundler installed:

$ grok mcp add --scope project tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With RubyGems installed:

$ opencode mcp add tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

With Bundler installed:

$ opencode mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

Embed the server in a program
examples/mcp_protocol.rb
application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example")
sdk = application.sdk_server
input, client_input = IO.pipe
client_output, output = IO.pipe
transport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output)
runner = parent.async { transport.run }

Excerpt of examples/mcp_protocol.rb, which bundle exec scripts/matrix runs in the repository's CI (.github/workflows/compatibility.yml).

Workspace Manager

Tiled layout

The tiled layout arranges the panes in a grid.

session_name: work
environment:
PROJECT_MODE: development
windows:
- window_name: editor
window_index: 1
layout: tiled
panes:
- shell_command: printf 'editor ready\n'
- {}
{
"session_name": "work",
"environment": {
"PROJECT_MODE": "development"
},
"windows": [
{
"window_name": "editor",
"window_index": 1,
"layout": "tiled",
"panes": [
{
"shell_command": "printf 'editor ready\\n'"
},
{}
]
}
]
}
Window 1, editor: 2 panes, tiled❯ printf 'editor ready\n'
layout tiled

Save the file as workspace.yaml and load it with:

libtmux-workspace load workspace.yaml

Workspace file: examples/workspace.yaml. Tested by test_installed_imports_use_declared_dependencies_without_starting_resources (test/packaging/package_test.rb; a test runs the command).

Short-hand panes

A pane can be one command written as a string, and a command list can be one string.

session_name: shorthands
windows:
- window_name: long form
panes:
- shell_command:
- echo 'did you know'
- echo 'you can inline'
- shell_command: echo 'single commands'
- echo 'for panes'
{
"session_name": "shorthands",
"windows": [
{
"window_name": "long form",
"panes": [
{
"shell_command": [
"echo 'did you know'",
"echo 'you can inline'"
]
},
{
"shell_command": "echo 'single commands'"
},
"echo 'for panes'"
]
}
]
}
Window 0, long form: 3 panes, even-vertical❯ echo 'did you know'❯ echo 'you can inline'❯ echo 'single commands'❯ echo 'for panes'
no layout set

Workspace file: test/fixtures/tmuxp/shorthands.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Two panes

A window of two panes.

session_name: 2-pane-vertical
windows:
- window_name: my test window
panes:
- echo hello
- echo hello
{
"session_name": "2-pane-vertical",
"windows": [
{
"window_name": "my test window",
"panes": [
"echo hello",
"echo hello"
]
}
]
}
Window 0, my test window: 2 panes, even-vertical❯ echo hello❯ echo hello
no layout set

Workspace file: test/fixtures/tmuxp/2-pane-vertical.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Three panes

A window of three panes.

session_name: 3-panes
windows:
- window_name: dev window
layout: main-vertical
shell_command_before:
- cd ~/
panes:
- shell_command:
- cd /var/log
- ls -al | grep \.log
- echo hello
- echo hello
{
"session_name": "3-panes",
"windows": [
{
"window_name": "dev window",
"layout": "main-vertical",
"shell_command_before": [
"cd ~/"
],
"panes": [
{
"shell_command": [
"cd /var/log",
"ls -al | grep \\.log"
]
},
"echo hello",
"echo hello"
]
}
]
}
Window 0, dev window: 3 panes, main-vertical❯ cd /var/log❯ ls -al | grep \.log❯ echo hello❯ echo hello
layout main-vertical

Workspace file: test/fixtures/tmuxp/3-pane.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Four panes

A window of four panes.

session_name: 4-pane-split
windows:
- window_name: dev window
layout: tiled
shell_command_before:
- cd ~/
panes:
- shell_command:
- cd /var/log
- ls -al | grep \.log
- echo hello
- echo hello
- echo hello
{
"session_name": "4-pane-split",
"windows": [
{
"window_name": "dev window",
"layout": "tiled",
"shell_command_before": [
"cd ~/"
],
"panes": [
{
"shell_command": [
"cd /var/log",
"ls -al | grep \\.log"
]
},
"echo hello",
"echo hello",
"echo hello"
]
}
]
}
Window 0, dev window: 4 panes, tiled❯ cd /var/log❯ ls -al | grep \.log❯ echo hello❯ echo hello❯ echo hello
layout tiled

Workspace file: test/fixtures/tmuxp/4-pane.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Environment

environment sets variables for a session, a window or a pane.

session_name: Environment variables test
environment:
EDITOR: /usr/bin/vim
DJANGO_SETTINGS_MODULE: my_app.settings.local
SERVER_PORT: "8009"
windows:
- window_name: Django project
panes:
- ./manage.py runserver 0.0.0.0:${SERVER_PORT}
- window_name: Another Django project
environment:
DJANGO_SETTINGS_MODULE: my_app.settings.local
SERVER_PORT: "8010"
panes:
- ./manage.py runserver 0.0.0.0:${SERVER_PORT}
- environment:
DJANGO_SETTINGS_MODULE: my_app.settings.local-testing
SERVER_PORT: "8011"
shell_command: ./manage.py runserver 0.0.0.0:${SERVER_PORT}
{
"session_name": "Environment variables test",
"environment": {
"EDITOR": "/usr/bin/vim",
"DJANGO_SETTINGS_MODULE": "my_app.settings.local",
"SERVER_PORT": "8009"
},
"windows": [
{
"window_name": "Django project",
"panes": [
"./manage.py runserver 0.0.0.0:${SERVER_PORT}"
]
},
{
"window_name": "Another Django project",
"environment": {
"DJANGO_SETTINGS_MODULE": "my_app.settings.local",
"SERVER_PORT": "8010"
},
"panes": [
"./manage.py runserver 0.0.0.0:${SERVER_PORT}",
{
"environment": {
"DJANGO_SETTINGS_MODULE": "my_app.settings.local-testing",
"SERVER_PORT": "8011"
},
"shell_command": "./manage.py runserver 0.0.0.0:${SERVER_PORT}"
}
]
}
]
}
Window 0, Django project: 1 pane, even-vertical❯ ./manage.py runserver 0.0.0.0:${SERVER_PORT}
no layout set
Window 1, Another Django project: 2 panes, even-vertical❯ ./manage.py runserver 0.0.0.0:${SERVER_PORT}❯ ./manage.py runserver 0.0.0.0:${SERVER_PORT}
no layout set

Workspace file: test/fixtures/tmuxp/session-environment.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Window index

window_index places a window at an index; a window without one takes the lowest free index.

session_name: Window index example
windows:
- window_name: zero
panes:
- echo "this window's index will be zero"
- window_name: five
panes:
- echo "this window's index will be five"
window_index: 5
- window_name: one
panes:
- echo "this window's index will be one"
{
"session_name": "Window index example",
"windows": [
{
"window_name": "zero",
"panes": [
"echo \"this window's index will be zero\""
]
},
{
"window_name": "five",
"panes": [
"echo \"this window's index will be five\""
],
"window_index": 5
},
{
"window_name": "one",
"panes": [
"echo \"this window's index will be one\""
]
}
]
}
Window 0, zero: 1 pane, even-vertical❯ echo "this window's index will be zero"
no layout set
Window 1, one: 1 pane, even-vertical❯ echo "this window's index will be one"
no layout set
Window 5, five: 1 pane, even-vertical❯ echo "this window's index will be five"
no layout set

Workspace file: test/fixtures/tmuxp/window-index.yaml. Tested by test_tmuxp_examples_load_through_workspace_load (test/unit/workspace_test.rb; loaded by the library in a test; the command is not run).

Install libtmux-workspace

The workspace package is separate from the core library.

$ gem install libtmux-workspace
gem "libtmux-workspace"
Build the workspace from a program
examples/workspace_apply.rb
workspace = LibTmux::Workspace.load(File.join(__dir__, "workspace.yaml"))
plan = workspace.plan(snapshot: server.snapshot)
Example.check(server.list_sessions.size == 1, "planning changed tmux")
result = plan.apply(server: server)
Example.check(result.success?, "workspace apply failed")

Excerpt of examples/workspace_apply.rb, which bundle exec scripts/matrix runs in the repository's CI (.github/workflows/compatibility.yml).

Packages and integrations

API references

Example programs, each run by the port's CI:

Esc

Type to search.

Reference previewOpen reference