tmux client library for Perl
Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, capture pane contents.
Installation and guides
Install the native distributions and work with captured tmux state.
Filtering example
Run a complete snapshot, criteria, callback, and native-array workflow.
API reference
Native package signatures and interlinked POD for core and optional async.
Install
git clone https://github.com/libtmux/libtmux-perl.git && \ cd libtmux-perl && \ cpm install --global ./packages/coreAt 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.
| Type | Wraps |
|---|---|
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;}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.plExpected output
session demo window 0:editor: %0 %1 window 1:logs: %2%1 is in window editorTyped API
- Typed objects instead of parsed format strings:
LibTmux::Pane - Addressing by tmux ID:
LibTmux::Pane::id - Filtering and queries over tmux objects:
LibTmux::Selection::where - Typed errors instead of exit codes and stderr:
LibTmux::X::InvalidFilter
What you get over the tmux CLI
- Connect to or start a tmux server:
LibTmux::Server::new - Traversal from a server down to its panes:
LibTmux::Server::snapshot - Send keys to a pane:
LibTmux::Server::send_keys - Capture pane contents:
LibTmux::Server::capture_pane - Arguments passed as a list, with no shell quoting:
LibTmux::Server::cmd - A typed error when the running tmux lacks a feature:
LibTmux::X::Unsupported - Async operations:
LibTmux::Async
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.