Core LibraryunreleasedHome

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.

Edit this page on GitHub

tmux client library for Perl

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

Install

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

At a glance

Server → Session → LibTmux::WindowLink → Window → Pane. Each level of the tmux hierarchy has its own entry in the Perl API, with the tmux object it wraps.

TypeWraps
LibTmux::Server A tmux server
LibTmux::Session A session, ID $n
LibTmux::Window A physical window linked into sessions, ID @n
LibTmux::Pane A pane in a window, ID %n
LibTmux::WindowLink A session-window association, with its index and active state

What it does

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

Typed API

What you get over the tmux CLI

The core uses stable Perl 5.44 features and standard-library runtime modules. The optional LibTmux-Async distribution returns Future values and requires a caller-owned IO::Async::Loop::Ppoll loop. Its transport reference explains admission, cancellation, and cleanup. Linux x86_64 LP64 is qualified; other platforms and ABIs remain unsupported.

The distributions are available from source and have no CPAN release.

Esc

Type to search.

Reference previewOpen reference