# libtmux for Perl

Source: https://libtmux.org/en/perl/latest/

> libtmux for Perl, version latest.

![](https://libtmux.org/en/brand/perl/library/logo.svg)

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

Core API Workspace Manager

```perl
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;
    }
);
```

```perl
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;
}
```

```perl
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;
    }
);
```

```perl
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

\+ Error handling + Cleanup

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

```perl
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

```bash
perl examples/quickstart.pl
```

### Expected output

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

[Core Library documentation →](https://libtmux.org/en/perl/latest/core/)[More Perl examples →](https://libtmux.org/en/perl/latest/examples/)

### Install `LibTmux`

```console
$ 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.

```console
$ cpm install --global ./packages/workspace
```

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

* [Workspace Manager documentation →](https://libtmux.org/en/perl/latest/workspace/)

Build the workspace from a program

examples/workspace.pl

```perl
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`](https://github.com/libtmux/libtmux-perl/blob/352715a4fa66a96eed7763b90daaa536aa2f121d/examples/workspace.pl), which `perl scripts/run-examples.pl --installed "$prefix"` runs in the repository's CI (`.github/workflows/scaffold.yml`).

### [Topics](https://libtmux.org/en/perl/latest/topics/)

[Subject-by-subject, with Perl in every code block.](https://libtmux.org/en/perl/latest/topics/)

### [Guides](https://libtmux.org/en/perl/latest/guides/)

[Task guides for Perl.](https://libtmux.org/en/perl/latest/guides/)

### [Examples](https://libtmux.org/en/perl/latest/examples/)

[Complete programs, sourced from the code each port tests.](https://libtmux.org/en/perl/latest/examples/)

### [Concepts](https://libtmux.org/en/perl/latest/concepts/)

[The domain model every port shares.](https://libtmux.org/en/perl/latest/concepts/)

## Packages and integrations

### [Async](https://libtmux.org/en/perl/latest/guides/async/)

[Optional Future execution with a caller-owned IO::Async loop.](https://libtmux.org/en/perl/latest/guides/async/)

### [Workspace Manager](https://libtmux.org/en/perl/latest/workspace/)

[Validate, plan, and load versioned JSON workspaces through public core APIs.](https://libtmux.org/en/perl/latest/workspace/)

## API references

### [Official reference](https://libtmux.org/en/perl/latest/reference/)

[Perl API signatures and documentation, with links to related guides and APIs in other languages.](https://libtmux.org/en/perl/latest/reference/)

[All Perl versions and install instructions](https://libtmux.org/en/perl/)
