Core Library0.xGuides

Choose documentation 1

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.

Getting started

LibTmux license

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:

Terminal window
$ zef install 'LibTmux:auth<zef:libtmux>'

To install a checkout instead:

Terminal window
$ 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:

Terminal window
$ raku quickstart.raku

From a checkout:

Terminal window
$ raku -I. examples/quickstart.raku

More: 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.

  • resize takes :width and :height, or :direction with :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.
  • where checks fields and operators; filter returns the same selection type.
  • .single wants exactly one match, .single-or-nil allows none. .one stays 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.
  • never lists first and filters in Raku.
  • require throws 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 = True
width and title: fully pushed = False
tmux filter: #{e|>=:#{pane_width},60}
Matches: editor
require: refused, title stays local

More: 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.

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 explicitly
Owned daemon and socket cleaned up

A failed batch does not roll back earlier commands.

More: the examples guide.

Close what you open

Server.close: stop a server you started

  • Server.start owns a private server; close stops it.
  • Server.new borrows a server; close leaves 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.x releases. 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.

Esc

Type to search.

Reference previewOpen reference