Rand Stats

LibTmux

zef:libtmux
libtmux for Raku

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.

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.