libtmux for Raku
LibTmux controls tmux from Raku: create sessions and panes, find them through
typed snapshots, send input, and react to terminal output. Use ordinary Raku
predicates and collections alongside validated query data and explicit commands.
Install
Requires Linux, Raku 6.d and tmux 3.3a through 3.8. Install from this checkout;
there is no published Raku distribution yet. The
toolchain guide covers Raku and zef setup.
$ zef install .
Create an observatory
Start a private tmux server, split its first pane, and label the instruments.
This is the complete quickstart program. LEAVE
closes the owned server when the program exits, including on an exception.
use LibTmux;
my $server = Server.start(:name<observatory>, :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('Starlight telescope');
$right.set-title('Orion weather');
my $after = $server.snapshot;
my $telescope = $after.panes.filter(*.title.starts-with('Starlight')).single;
say "{$telescope.title}: {$after.panes.elems} panes";
It prints Starlight telescope: 2 panes. The earlier snapshot still contains one
pane: commands through .ref change tmux, while snapshots retain the state they
captured. Reading a snapshot or following its relationships makes no further
tmux calls.
Run it from the checkout with a supported tmux executable on PATH:
$ raku -I. examples/quickstart.raku
Find the right panes
Build a three-instrument display, select wide panes with query data, then use a
Raku predicate to find the telescope. The complete
query program also uses native .grep, .map and
.sort to list the other instruments.
use v6.d;
use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') {
my $server = Server.start(:binary($tmux), :name<instruments>,
:width(160), :height(40), :shell-command('exec cat'));
LEAVE $server.close if $server.defined;
my $telescope = $server.snapshot.panes.single.ref;
my $weather = $telescope.split(:horizontal, :shell-command('exec cat'));
my $recorder = $weather.split(:shell-command('exec cat'));
$telescope.set-title('Starlight telescope');
$weather.set-title('Orion weather');
$recorder.set-title('Spectrum recorder');
my $snapshot = $server.snapshot;
my @wide := $snapshot.panes.where(%(width => %(gte => 60)));
my $selected = @wide.filter(*.title.ends-with('telescope')).single;
say "Wide telescope: {$selected.title}";
my @others = $snapshot.panes.grep(*.id ne $selected.id).map(*.title).sort;
say 'Other instruments: ' ~ @others.join(', ');
}
It prints Wide telescope: Starlight telescope and
Other instruments: Orion weather, Spectrum recorder.
Binding with := retains the typed selection. .where validates query fields
and operators; .filter returns another selection of the same entity type.
Native .grep returns a Seq, and .map can produce ordinary strings.
.single requires exactly one result; .single-or-nil permits no result.
Raku's .one keeps its junction meaning.
For live acquisition, query-panes(:where(%criteria)) returns a fresh selection.
explain-panes shows which restrictions run in tmux and which remain local.
See the native query guide for
relations, portable filters and source execution.
Adapt to the running daemon
Give Aurora telemetry a floating pane when the daemon supports it, with a tiled
split as the fallback. The capability example
asks the captured daemon capabilities before choosing an operation.
use v6.d;
use LibTmux;
sub MAIN(Str:D :$tmux = 'tmux') {
my $server = Server.start(:binary($tmux), :name<flight-deck>,
: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('Aurora telemetry');
my $after = $server.snapshot;
die 'The auxiliary pane was not captured' unless $after.panes.elems == 2;
say 'Telemetry placement: ' ~ ($floating ?? 'floating' !! 'tiled');
say $after.panes.where(%(id => $auxiliary.id)).single.title;
}
It prints Telemetry placement: floating or Telemetry placement: tiled,
followed by Aurora telemetry. A newer client executable does not upgrade an
older daemon. Unknown releases are refused conservatively. The
compatibility policy describes support;
the field and capability reference lists
feature requirements and unavailable values.
Watch a downlink
Asynchronous operations return Promise values. A control connection exposes
events through a shared Supply; native react and whenever consume them.
This complete downlink program subscribes before
sending text, waits for the received bytes, then checks the captured screen.
use v6.d;
use LibTmux;
use LibTmux::Control;
sub MAIN(Str:D :$tmux = 'tmux') {
my $server = Server.start(:binary($tmux), :name<downlink>, :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 = 'Starlight downlink: 42 stars catalogued';
my $received = '';
react {
whenever $connection.events.grep(PaneOutput) -> $event {
if $event.pane-id eq $pane.id {
$received ~= $event.data.decode('utf8-c8');
die 'Downlink 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 'Downlink timed out' }
}
die 'The captured screen did not contain the downlink'
unless $pane.capture.decode('utf8').contains($message);
say $message;
say 'Screen capture confirmed';
}
It prints Starlight downlink: 42 stars catalogued and Screen capture confirmed.
The deadline bounds the wait; output events supply the completion evidence.
send-text sends literal text, and send-keys('Enter') submits it separately.
Pane output arrives as bytes so fragmented or invalid UTF-8 remains available.
The longer observatory program combines pane
creation, output observation, capture, local queries and an explained source
query in one lifecycle.
Recover after a batch fails
A batch records each operation's outcome. The
recovery program produces Complete, Failed
and Skipped, verifies that the first effect remains, then removes that effect
explicitly. Command acknowledgement alone does not establish application
completion, and a failed batch does not roll back earlier commands.
$ raku -I. examples/batch-recovery.raku
The examples guide gives the commands, expected results and
cleanup behavior for every complete program.
Ownership and documentation
Server.start creates and owns a daemon on a private socket. Server.new
configures a borrowed endpoint; closing it leaves that daemon running. Close
both explicitly. Control connections and terminal attachments have their own
lifetimes and also need closing. References are scoped to the observed daemon
incarnation and refuse a replacement daemon.