Getting started
Start a private tmux server, split panes, send input, find panes with plain Raku predicates, and react to terminal output as it arrives.
Install¶
Needs Raku 6.d or newer, tmux 3.3a or newer and Linux. Install from Raku Land with its Fez authority:
$ zef install 'LibTmux:auth<zef:libtmux>'To install a checkout instead:
$ zef install .The toolchain guide covers Raku and zef setup. The changelog lists each release.
Create sessions and panes¶
Server.start and PaneRef.split: create and arrange panes¶
Start a private server, split its first pane and title both panes. Each runs
cat, and LEAVE closes the server on any exit.
Server.new-session and
SessionRef.new-window add sessions and windows.
use LibTmux;
my $server = Server.start(:name<work>, :shell-command('exec cat'));LEAVE $server.close if $server.defined;
my $before = $server.snapshot;my $left = $before.panes.single.ref;my $right = $left.split(:horizontal, :shell-command('exec cat'));$left.set-title('editor');$right.set-title('logs');
my $after = $server.snapshot;my $editor = $after.panes.filter(*.title eq 'editor').single;say "{$editor.title}: {$after.panes.elems} panes";It prints editor: 2 panes. The earlier snapshot still has one pane: .ref
commands change tmux, while a snapshot keeps what it captured.
Save it as quickstart.raku and run it:
$ raku quickstart.rakuFrom a checkout:
$ raku -I. examples/quickstart.rakuMore: the native API guide.
Resize and remove panes¶
PaneRef.resize and PaneRef.kill: size and remove panes¶
Split a pane, size the window and the first pane, then kill the second pane.
WindowRef.resize sizes a window. A window, a session
and the daemon go with WindowRef.kill,
SessionRef.kill and
Server.kill-server.
use v6.d;use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<work>, :width(120), :height(30), :shell-command('exec cat')); LEAVE $server.close if $server.defined;
my $before = $server.snapshot; my $window = $before.windows.single.ref; my $editor = $before.panes.single.ref; my $logs = $editor.split(:horizontal, :shell-command('exec cat'));
$window.resize(:width(100), :height(20)); $editor.resize(:width(30)); my $sized = $server.snapshot; say "window: {$sized.windows.single.width}x{$sized.windows.single.height}"; say "editor: {$sized.panes.where(%(id => $editor.id)).single.width} columns";
$logs.kill; say "panes: {$server.snapshot.panes.elems}";}It prints window: 100x20, editor: 30 columns and panes: 1.
resizetakes:widthand:height, or:directionwith:amount. It refuses a mix of the two.
More: the native API guide.
Query captured panes¶
PaneSelection.where: select panes from a snapshot¶
Select wide panes with query data, then find editor with a Raku predicate
through filter. Native .grep, .map and
.sort list the rest.
Server.query-panes asks tmux for a fresh selection
instead of reading a snapshot.
use v6.d;use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<work>, :width(160), :height(40), :shell-command('exec cat')); LEAVE $server.close if $server.defined;
my $editor = $server.snapshot.panes.single.ref; my $tests = $editor.split(:horizontal, :shell-command('exec cat')); my $logs = $tests.split(:shell-command('exec cat')); $editor.set-title('editor'); $tests.set-title('tests'); $logs.set-title('logs');
my $snapshot = $server.snapshot; my @wide := $snapshot.panes.where(%(width => %(gte => 60))); my $selected = @wide.filter(*.title eq 'editor').single; say "Wide pane: {$selected.title}";
my @others = $snapshot.panes.grep(*.id ne $selected.id).map(*.title).sort; say 'Other panes: ' ~ @others.join(', ');}It prints Wide pane: editor and Other panes: logs, tests.
- Bind with
:=to keep a typed selection;=makes a plain array. wherechecks fields and operators;filterreturns the same selection type..singlewants exactly one match,.single-or-nilallows none..onestays Raku’s junction.
More: the native API guide covers relations and portable filters.
Choose where a query runs¶
Server.explain-panes: what tmux filters and what Raku filters¶
Server.query-panes takes :pushdown:
auto(the default) has tmux apply the restrictions it can match exactly, then rechecks every result in Raku.neverlists first and filters in Raku.requirethrows when any part would stay local.
use v6.d;use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<work>, :shell-command('exec cat')); LEAVE $server.close if $server.defined; $server.snapshot.panes.single.ref.set-title('editor');
my %wide = width => %(gte => 60); my %titled = AND => [%wide, %(title => %(contains => 'edit'))];
say 'width: fully pushed = ' ~ $server.explain-panes(:where(%wide)).fully-pushed; my $plan = $server.explain-panes(:where(%titled)); say 'width and title: fully pushed = ' ~ $plan.fully-pushed; say 'tmux filter: ' ~ $plan.source-filter;
say 'Matches: ' ~ $server.query-panes(:where(%titled)).map(*.title).join(', '); try { $server.query-panes(:where(%titled), :pushdown<require>); CATCH { when X::LibTmux::Unsupported { say 'require: refused, title stays local' } } }}It prints:
width: fully pushed = Truewidth and title: fully pushed = Falsetmux filter: #{e|>=:#{pane_width},60}Matches: editorrequire: refused, title stays localMore: the native API guide.
Adapt to the running daemon¶
Capabilities.supports: choose a supported operation¶
Create an empty status pane: floating when the daemon supports it, tiled
otherwise. Snapshot.capabilities holds what the
daemon, not the client binary, can do.
Options.describe gives an option’s type, scopes and
first release; Server.options and
Server.hooks read and set options and hooks.
use v6.d;use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<work>, :width(120), :height(36), :shell-command('exec cat')); LEAVE $server.close if $server.defined;
my $snapshot = $server.snapshot; my $main = $snapshot.panes.single.ref; my $floating = $snapshot.capabilities.supports('pane-floating'); my $auxiliary = $floating ?? $main.new-floating(:width(40), :height(12), :x(2), :y(2), :empty) !! $main.split(:horizontal, :empty); $auxiliary.set-title('status');
my $after = $server.snapshot; die 'The auxiliary pane was not captured' unless $after.panes.elems == 2; say 'Status pane: ' ~ ($floating ?? 'floating' !! 'tiled'); say $after.panes.where(%(id => $auxiliary.id)).single.title;}It prints Status pane: floating or Status pane: tiled, then status.
More: the compatibility policy and the field, option, hook and capability reference.
Watch pane output¶
ControlConnection.events: receive output through a Supply¶
Server.open-control-async (or
Server.open-control) opens a persistent control
connection. Its events are one shared Supply, so react and whenever read
them. This program subscribes, sends text with
PaneRef.send-text and checks the screen with
PaneRef.capture.
use v6.d;use LibTmux;use LibTmux::Control;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<output>, :shell-command('exec cat')); LEAVE $server.close if $server.defined; my $snapshot = await $server.snapshot-async; my $pane = $snapshot.panes.single.ref; my $connection = await $server.open-control-async(:session-id($snapshot.sessions.single.id)); LEAVE $connection.close if $connection.defined;
my $message = 'hello from Raku'; my $received = ''; react { whenever $connection.events.grep(PaneOutput) -> $event { if $event.pane-id eq $pane.id { $received ~= $event.data.decode('utf8-c8'); die 'Pane output exceeded its bound' if $received.chars > 65536; done if $received.contains($message); } } whenever start { $pane.send-text($message); $pane.send-keys('Enter') } { } whenever Promise.in(2) { die 'Pane output timed out' } }
die 'The captured screen did not contain the message' unless $pane.capture.decode('utf8').contains($message); say $message; say 'Screen capture confirmed';}It prints hello from Raku and Screen capture confirmed.
send-textsends literal text;send-keys('Enter')submits it.- Output arrives as bytes, so split or invalid UTF-8 survives.
More: the longer session workflow program also queries and explains.
Run commands in a batch¶
Server.run-batch: run commands in order, keep every outcome¶
Build a Batch of Operation values. Each one
reports Complete, Failed or Skipped. Here the second fails, the third is
skipped, and the program removes the first effect itself.
use v6.d;use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') { my $server = Server.start(:binary($tmux), :name<recovery>, :shell-command('exec cat')); LEAVE $server.close if $server.defined; my $before = $server.snapshot; my $batch = Batch.new(:operations([ Operation.new(:name<set-option>, :acknowledgement-only, :command(Invocation.new(:argv(['set-option', '-s', '@before-failure', 'set'])))), Operation.new(:name<invalid-command>, :acknowledgement-only, :command(Invocation.new(:argv(['libtmux-example-unknown-command='])))), Operation.new(:name<skipped-option>, :acknowledgement-only, :command(Invocation.new(:argv(['set-option', '-s', '@after-failure', 'set'])))), ])); my $report = $server.run-batch($batch); die 'Batch attribution was wrong' unless $report.operations.map(*.outcome).List eqv (Complete, Failed, Skipped); my $options = $server.options; die 'The successful first effect disappeared' unless $options.get('@before-failure') eq 'set'; die 'A skipped operation ran' if $options.entries(:name<@after-failure>).elems; say .name ~ ': ' ~ .outcome ~ ' (scope: ' ~ .completion-scope ~ ')' for $report.operations;
$options.unset('@before-failure'); die 'Explicit recovery failed' if $options.entries(:name<@before-failure>).elems; say 'Recovery: removed the first effect explicitly'; my $pid = $before.incarnation.pid; my $directory = $server.endpoint.socket-path.IO.parent; $server.close; die 'Owned daemon or socket directory survived cleanup' if "/proc/$pid".IO.e || $directory.e; say 'Owned daemon and socket cleaned up';}It prints:
set-option: Complete (scope: command)invalid-command: Failed (scope: command)skipped-option: Skipped (scope: command)Recovery: removed the first effect explicitlyOwned daemon and socket cleaned upA failed batch does not roll back earlier commands.
More: the examples guide.
Close what you open¶
Server.close: stop a server you started¶
Server.startowns a private server;closestops it.Server.newborrows a server;closeleaves it running.- Control connections and attachments have their own lifetimes. Close them too.
- A reference refuses a replacement server that reuses its ids.
What it leaves out¶
- Live objects. A snapshot is what tmux said when you asked. It does not change when tmux does.
- Rollback. A failed batch does not undo commands that already ran.
- Callbacks in tmux. Raku predicates always run locally.
Support and status¶
- Experimental. The API may change between
0.xreleases. The changelog marks each break. - tmux 3.3a or newer. A release the library does not list yet is refused.
- Linux. Other platforms are unverified.
Documentation¶
Headings above link to the native declarations and their RakuDoc comments.
- Native API guide: the object graph, queries, capabilities, async execution, control events and recovery.
- Examples: complete Raku programs with checked results.
- Contributing: compiler setup and validation.
- API documentation authoring and writing conventions.
- MIT license and artwork provenance.