PerlHome

Choose documentation 2

latest

Current version

latest
English

Unreleased LibTmux has not been released. Its API may change during development. This site's structure and URLs may change too.

libtmux for Perl

Perl-based tmux client

tmux client library for Perl. Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, capture pane contents

Product
use v5.44;
use Config;
use FindBin;
use lib "$FindBin::Bin/../scripts/lib";
use LibTmuxFixture;
# This complete demo owns a Linux child-reaping scope, including tmux panes.
BEGIN {
die 'Requires Linux x86_64 LP64'
unless $^O eq 'linux'
&& $Config::Config{archname} =~ /x86_64/
&& $Config::Config{ptrsize} == 8 && $Config::Config{ivsize} == 8;
syscall(157, 36, 1, 0, 0, 0) == 0 or die "subreaper: $!";
}
LibTmuxFixture->run(
sub ($server, $root) {
my $session = $server->new_session(
name => 'demo', command => ['/bin/cat']
);
my $editor = $session->window_links->one->window;
$server->rename_window(window => $editor, name => 'editor');
my $right = $server->split_pane(
pane => $editor->panes->one, horizontal => true,
command => ['/bin/cat']
);
my $logs = $server->new_window(
session => $session, name => 'logs', command => ['/bin/cat']
);
$server->select_pane(pane => $right);
my $snapshot = $server->snapshot;
my $link = $snapshot->window_links->one({
window => {-is => {id => $logs->id}}
});
$server->select_window(link => $link);
$snapshot = $server->snapshot;
for my $entry ($snapshot->sessions->@*) {
say 'session ', $entry->name;
for my $link ($entry->window_links->@*) {
my $window = $link->window;
my $ids = join ' ', map { $_->id } $window->panes->@*;
my $label = $link->index . ':' . $window->name;
say ' window ', $label, ': ', $ids;
}
}
my $panes = $snapshot->windows->one({name => 'editor'})->panes
->where({width => {-gte => 1}})
->grep(sub ($pane) { $pane->height >= 1 });
my $selected = $panes->one({active => true});
die 'unexpected pane membership' unless $panes->count == 2;
my $name = $selected->window->name;
say $selected->id, ' is in window ', $name;
}
);
use v5.44;
use Config;
use FindBin;
use lib "$FindBin::Bin/../scripts/lib";
use LibTmuxFixture;
# This complete demo owns a Linux child-reaping scope, including tmux panes.
BEGIN {
die 'Requires Linux x86_64 LP64'
unless $^O eq 'linux'
&& $Config::Config{archname} =~ /x86_64/
&& $Config::Config{ptrsize} == 8 && $Config::Config{ivsize} == 8;
syscall(157, 36, 1, 0, 0, 0) == 0 or die "subreaper: $!";
}
try {
LibTmuxFixture->run(
sub ($server, $root) {
my $session = $server->new_session(
name => 'demo', command => ['/bin/cat']
);
my $editor = $session->window_links->one->window;
$server->rename_window(window => $editor, name => 'editor');
my $right = $server->split_pane(
pane => $editor->panes->one, horizontal => true,
command => ['/bin/cat']
);
my $logs = $server->new_window(
session => $session, name => 'logs', command => ['/bin/cat']
);
$server->select_pane(pane => $right);
my $snapshot = $server->snapshot;
my $link = $snapshot->window_links->one({
window => {-is => {id => $logs->id}}
});
$server->select_window(link => $link);
$snapshot = $server->snapshot;
for my $entry ($snapshot->sessions->@*) {
say 'session ', $entry->name;
for my $link ($entry->window_links->@*) {
my $window = $link->window;
my $ids = join ' ', map { $_->id } $window->panes->@*;
my $label = $link->index . ':' . $window->name;
say ' window ', $label, ': ', $ids;
}
}
my $panes = $snapshot->windows->one({name => 'editor'})->panes
->where({width => {-gte => 1}})
->grep(sub ($pane) { $pane->height >= 1 });
my $selected = $panes->one({active => true});
die 'unexpected pane membership' unless $panes->count == 2;
my $name = $selected->window->name;
say $selected->id, ' is in window ', $name;
}
);
}
catch ($error) {
warn "tmux quickstart failed: $error\n";
die $error;
}
use v5.44;
use Config;
use FindBin;
use lib "$FindBin::Bin/../scripts/lib";
use LibTmuxFixture;
# This complete demo owns a Linux child-reaping scope, including tmux panes.
BEGIN {
die 'Requires Linux x86_64 LP64'
unless $^O eq 'linux'
&& $Config::Config{archname} =~ /x86_64/
&& $Config::Config{ptrsize} == 8 && $Config::Config{ivsize} == 8;
syscall(157, 36, 1, 0, 0, 0) == 0 or die "subreaper: $!";
}
LibTmuxFixture->run(
sub ($server, $root) {
my $session = $server->new_session(
name => 'demo', command => ['/bin/cat']
);
my $failure;
try {
my $editor = $session->window_links->one->window;
$server->rename_window(window => $editor, name => 'editor');
my $right = $server->split_pane(
pane => $editor->panes->one, horizontal => true,
command => ['/bin/cat']
);
my $logs = $server->new_window(
session => $session, name => 'logs', command => ['/bin/cat']
);
$server->select_pane(pane => $right);
my $snapshot = $server->snapshot;
my $link = $snapshot->window_links->one({
window => {-is => {id => $logs->id}}
});
$server->select_window(link => $link);
$snapshot = $server->snapshot;
for my $entry ($snapshot->sessions->@*) {
say 'session ', $entry->name;
for my $link ($entry->window_links->@*) {
my $window = $link->window;
my $ids = join ' ', map { $_->id } $window->panes->@*;
my $label = $link->index . ':' . $window->name;
say ' window ', $label, ': ', $ids;
}
}
my $panes = $snapshot->windows->one({name => 'editor'})->panes
->where({width => {-gte => 1}})
->grep(sub ($pane) { $pane->height >= 1 });
my $selected = $panes->one({active => true});
die 'unexpected pane membership' unless $panes->count == 2;
my $name = $selected->window->name;
say $selected->id, ' is in window ', $name;
}
catch ($error) { $failure = $error }
$server->kill_session(session => $session);
die $failure if defined $failure;
}
);
use v5.44;
use Config;
use FindBin;
use lib "$FindBin::Bin/../scripts/lib";
use LibTmuxFixture;
# This complete demo owns a Linux child-reaping scope, including tmux panes.
BEGIN {
die 'Requires Linux x86_64 LP64'
unless $^O eq 'linux'
&& $Config::Config{archname} =~ /x86_64/
&& $Config::Config{ptrsize} == 8
&& $Config::Config{ivsize} == 8;
syscall(157, 36, 1, 0, 0, 0) == 0 or die "subreaper: $!";
}
try {
LibTmuxFixture->run(
sub ($server, $root) {
my $session = $server->new_session(
name => 'demo',
command => ['/bin/cat']
);
my $failure;
try {
my $editor = $session->window_links->one->window;
$server->rename_window(window => $editor, name => 'editor');
my $right = $server->split_pane(
pane => $editor->panes->one,
horizontal => true,
command => ['/bin/cat']
);
my $logs = $server->new_window(
session => $session,
name => 'logs',
command => ['/bin/cat']
);
$server->select_pane(pane => $right);
my $snapshot = $server->snapshot;
my $link = $snapshot->window_links->one(
{
window => {-is => {id => $logs->id}}
}
);
$server->select_window(link => $link);
$snapshot = $server->snapshot;
for my $entry ($snapshot->sessions->@*) {
say 'session ', $entry->name;
for my $link ($entry->window_links->@*) {
my $window = $link->window;
my $ids = join ' ', map { $_->id } $window->panes->@*;
my $label = $link->index . ':' . $window->name;
say ' window ', $label, ': ', $ids;
}
}
my $panes =
$snapshot->windows->one({name => 'editor'})
->panes->where({width => {-gte => 1}})
->grep(sub ($pane) { $pane->height >= 1 });
my $selected = $panes->one({active => true});
die 'unexpected pane membership' unless $panes->count == 2;
my $name = $selected->window->name;
say $selected->id, ' is in window ', $name;
}
catch ($error) { $failure = $error }
$server->kill_session(session => $session);
die $failure if defined $failure;
}
);
}
catch ($error) {
warn "tmux quickstart failed: $error\n";
die $error;
}
Example details
Run this example

Linux x86_64 LP64, Perl 5.44.0, tmux 3.3a or newer, and the LibTmux core distribution installed from source. Save both files with the displayed directories. No CPAN release is claimed.

Save the program above as examples/quickstart.pl in a new directory, along with the files below.

The included standard-library fixture owns its private socket and verifies child exit. Cleanup also tears down the created session explicitly. 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.

scripts/lib/LibTmuxFixture.pm
package LibTmuxFixture;
use v5.44;
use File::Temp qw(tempdir);
use File::Path qw(remove_tree);
use LibTmux::Server;
use Errno qw(ECHILD EINTR);
sub _children ($pid) {
open my $in, '<', "/proc/$pid/task/$pid/children" or return ();
my $line = do { local $/; <$in> };
close $in;
return grep { length } split /\s+/, $line;
}
sub _stamp ($pid) {
open my $in, '<', "/proc/$pid/stat" or return undef;
my $line = do { local $/; <$in> };
close $in;
return undef unless $line =~ /\A[0-9]+ \(.*\) (.*)\z/s;
my @fields = split /\s+/, $1;
return $fields[19];
}
sub _tree ($pid, $owned, $order) {
return if exists $owned->{$pid};
my $stamp = _stamp($pid);
return unless defined $stamp;
$owned->{$pid} = $stamp;
push @$order, $pid;
_tree($_, $owned, $order) for _children($pid);
}
sub run ($class, $code, %args) {
my %allowed = map { $_ => true } qw(executable timeout max_output env);
die 'fixture selectors/configuration cannot be overridden'
if grep { !$allowed{$_} } keys %args;
my %baseline = map { $_ => true } _children($$);
my $root = tempdir('libtmux-perl-fixture-XXXX', TMPDIR => 1, CLEANUP => 0);
my $server = LibTmuxFixture::Server->new(
socket_path => "$root/s",
config_file => '/dev/null',
executable => $ENV{LIBTMUX_TEST_TMUX} // 'tmux',
timeout => 0.8,
%args
);
my ($result, $failure);
local $SIG{TERM} = sub { die 'fixture interrupted by TERM' };
local $SIG{INT} = sub { die 'fixture interrupted by INT' };
try { $result = $code->($server, $root) } catch ($error) {
$failure = $error
}
my $cleanup_error;
{
local $SIG{TERM} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
try {
my (%owned, @order);
my $pid = $server->{fixture_pid};
if (!$pid && -S "$root/s") {
try {
$pid = 0 +
$server->cmd('display-message', '-p', '#{pid}')->stdout
}
catch ($error) {
die $error
unless ref($error)
&& $error->isa('LibTmux::X::NoServer')
}
}
_tree($pid, \%owned, \@order) if $pid;
if (-S "$root/s") {
try { $server->kill_server }
catch ($error) {
die $error
unless ref($error)
&& $error->isa('LibTmux::X::NoServer')
}
}
# This author fixture runs in an isolated test process. Preserve children
# that existed before its scope, including nested/borrowed fixtures.
_tree($_, \%owned, \@order)
for grep { !$baseline{$_} } _children($$);
for my $child (@order) {
my $stamp = _stamp($child);
kill 'KILL', $child
if defined($stamp) && $stamp eq $owned{$child};
}
local $SIG{ALRM} = sub { die 'owned fixture child did not exit' };
alarm 1;
for my $child (@order) {
my $waited;
do { $waited = waitpid($child, 0) }
while ($waited < 0 && $! == EINTR);
my $wait_error = 0 + $!;
my $stamp = _stamp($child);
die 'owned fixture process remained after teardown'
if defined($stamp) && $stamp eq $owned{$child};
die 'fixture child exit was not observed'
if $waited < 0 && $wait_error != ECHILD;
}
alarm 0;
}
catch ($error) { alarm 0; $cleanup_error = $error }
}
remove_tree($root);
if (defined $failure) {
if (ref($failure) && $failure isa LibTmux::X::Creation) {
my $cause = $failure->cause;
warn "fixture acquisition cause: $cause\n" if defined $cause;
}
warn "fixture cleanup also failed: $cleanup_error"
if defined $cleanup_error;
die $failure;
}
die $cleanup_error if defined $cleanup_error;
return $result;
}
package LibTmuxFixture::Server;
use parent 'LibTmux::Server';
sub new_session ($self, %args) {
my $session = $self->SUPER::new_session(%args);
$self->{fixture_pid} =
0 + $self->cmd('display-message', '-p', '#{pid}')->stdout
unless $self->{fixture_pid};
return $session;
}
1;

Setup and run

perl examples/quickstart.pl

Expected output

session demo
window 0:editor: %0 %1
window 1:logs: %2
%1 is in window editor

Install LibTmux

$ git clone https://github.com/libtmux/libtmux-perl.git && \
  cd libtmux-perl && \
  cpm install --global ./packages/core

Unpublished development distribution. Requires Perl 5.44.0, Linux x86_64 LP64, and tmux 3.3a or newer.

Workspace Manager

Install LibTmux-Workspace

The workspace package is separate from the core library.

$ cpm install --global ./packages/workspace

From the source checkout after core installation. No CPAN release.

Build the workspace from a program
examples/workspace.pl
my $workspace = LibTmux::Workspace->new(
config => {
schema => 'libtmux.workspace/v1',
sessions => [
{
name => 'workspace-example',
windows => [
{
name => 'api',
cwd => $root,
layout => 'even-horizontal',
panes => [
{command => ['/bin/sleep', '120']},
{
command => ['/bin/sleep', '120'],
horizontal => true
},
],
}
],
}
],
}
);
die 'plan mutated state' if $workspace->plan->{mutates};
my $loaded = $workspace->load(server => $server);
die 'load failed' unless $loaded->{status} eq 'loaded';
die 'panes missing' unless $server->snapshot->panes->count == 2;
die 'repeat did not skip'
unless $workspace->load(server => $server, existing => 'skip')
->{skipped}->@* == 1;
say 'workspace load and repeat: ok';

Excerpt of examples/workspace.pl, which perl scripts/run-examples.pl --installed "$prefix" runs in the repository's CI (.github/workflows/scaffold.yml).

Packages and integrations

API references

All Perl versions and install instructions

Esc

Type to search.

Reference previewOpen reference