npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libtmux/workspace

v0.1.0-alpha.12

Published

Declarative tmux workspace builder, built on libtmux.

Downloads

831

Readme

@libtmux/workspace

Describe a tmux session as data; apply it. Applying twice converges rather than duplicating.

npm downloads typescript tmux license

Part of libtmux for Bun and TypeScript. Built on libtmux.

[!WARNING] Alpha. Prerelease software: the config shape and the exported functions can change between alpha releases without a deprecation cycle. Pin an exact version.

Install

$ bun add --exact @libtmux/[email protected] [email protected]
$ npm i --save-exact @libtmux/[email protected] [email protected]
$ pnpm add --save-exact @libtmux/[email protected] [email protected]
$ yarn add --exact @libtmux/[email protected] [email protected]

libtmux is a peer of this package in practice: you pass it the Server. Requires Node 22+ or Bun 1.3.14+, and tmux 3.2a or newer.

This package and @libtmux/workspace-cli are two implementations, not one product with two faces. They differ in what they do with a session that already exists — this one converges it, the CLI compares it against the document and refuses when they disagree — and in what a document may say: the schema here is strict, so environment, before_script, window_index, suppress_history, x- keys and the CLI's other fields are rejected. A file written for the CLI is not necessarily one this accepts.

Linux is the only supported host for real tmux control. The macOS CI lane checks package artifacts without exercising tmux; macOS runtime behavior is unproven. WSL is untested.

Use it

import { Server } from "libtmux";
import { applyWorkspace } from "@libtmux/workspace";

const server = new Server();

await applyWorkspace(server, {
  session_name: "api",
  windows: [
    { window_name: "editor", panes: ["vim", "git status"] },
    {
      window_name: "server",
      layout: "even-horizontal",
      panes: [{ shell_command: "bun dev", focus: true }, "bun test --watch"],
    },
  ],
});

applyWorkspace resolves to the Session it built or converged.

Commands use create-only policy by default: shell_command and shell_command_before run only in panes this apply created. Pass { commands: "always" } only for commands that are safe to repeat. This package converges tmux topology; it does not monitor or restart processes.

Options named in the workspace are set on every apply. Removing an option from the file does not unset it because tmux does not record which current value the workspace owns. @libtmux-workspace is reserved for ownership bookkeeping and is rejected in workspace options. Option names are literal; tmux format syntax in a key is not expanded.

If tmux rejects a later operation, applyWorkspace throws WorkspaceApplyError. Its frozen completed milestones name the high-level work that finished, failed names the stage that stopped, and cause retains the original error. requiresReplan is always true: callers must rediscover tmux structure before deciding what to do next. It does not report whether pane commands ran; mutations are not transactions, and transport failure may leave delivery indeterminate.

applyWorkspace validates every window layout before claiming a session or changing options. Names accept unique abbreviations supported by the running daemon; classic layouts require a valid checksum. Classic and JSON layouts require a nonempty tree and enough pane cells; JSON requires tmux 3.8 or newer. A window is rebalanced between splits, because halving one pane in turn runs out of room by the fifth at a default 80x24, so a window that names no layout — or names the empty string — comes up tiled. A declared layout is applied last and has the final say. tmux remains responsible for geometry and pruning. The library limits custom layouts to 8192 characters and classic layouts to 256 nested groups. JSON layouts share tmux's limit of 200 nested objects.

The shape

Config is tmuxp-shaped, so the field names are the ones people already know — session_name, windows, panes, shell_command, shell_command_before, start_directory, layout, focus, options.

| Field | On | Effect | | ---------------------- | ----------------------- | ----------------------------------------------- | | session_name | workspace | The session to build or converge | | start_directory | workspace, window, pane | Working directory, inherited downward | | options | workspace, window | String, finite number, or boolean tmux values | | window_name | window | Renamed if it differs | | layout | window | Applied once the pane count settles | | panes | window | A string is shorthand for { shell_command } | | shell_command | pane | One command or a list, sent in order | | shell_command_before | window | Run in every pane of the window, before its own | | focus | window, pane | Selected last, so the final one wins |

Because the config is data, it can come from a YAML file, an HTTP request, or a literal. parseWorkspace validates one you already parsed; parseWorkspaceYaml is a convenience that needs Bun's YAML parser and says so when it is missing.

start_directory resolves pane, then window, then workspace. The same order applies to the first pane tmux creates with the session.

Converging, not just creating

Two details that are easy to get wrong and are handled here:

No stray leading window. tmux gives every new session a window and every new window a pane, so the first of each is adopted rather than created. Creating them anyway is the classic workspace-builder bug.

Position, not index. tmux window indexes are not positions — base-index shifts them and a killed window leaves a gap — so a window is matched by ordinal.

It prunes what it built, not what it found. A session name is a lookup, not a claim: dev in this file and dev you started by hand are the same name. So a session this package creates is stamped with a tmux user option, and surplus windows and panes are removed only from a session carrying that stamp. Against one it merely found, applying converges additively and leaves the rest alone.

Pruning follows tmux's sharing model. An independently linked surplus window is unlinked only from this session. A grouped session shares one window list, so its surplus windows are retained. Surplus panes are also retained when their window has another placement because killing a pane changes every placement.

Ask before applying, which is the question a converging tool cannot answer afterwards:

import { planWorkspace } from "@libtmux/workspace";

const desired = { session_name: "api", windows: [{ panes: ["vim"] }] };
const plan = await planWorkspace(server, desired);
plan.owned; // false for a session this workspace did not create
plan.removesWindows; // ID-keyed kill or unlink actions
plan.removesPanes; // exact pane IDs, grouped by their window placement
plan.renamesWindows; // an array, so duplicate current names stay distinct
plan.retains; // surplus plus the policy or sharing reason

Plan entries are frozen data carrying tmux IDs, indexes, positions, and names. The plan covers session, window, and pane membership, not options, layouts, focus, or pane commands. It is advisory: acquire a fresh one after any delay or failed apply because tmux structure may have changed. Planning accepts prune; command policy is apply-only.

prune decides the rest. "owned" is the default above; "never" removes nothing; "always" authorizes this one plan or apply to remove surplus from a session the workspace did not create:

await applyWorkspace(
  server,
  { session_name: "api", windows: [{ panes: ["vim"] }] },
  { prune: "always" },
);

"always" does not claim the session or stamp persistent ownership. A later apply using the default "owned" policy leaves new surplus alone.

Two applyWorkspace calls racing on the same session name are not coordinated: tmux offers no lock on a session that does not exist yet, so either can observe no session and both attempt to create one, or one apply's converge can run against a session mid-build by the other. Serialize calls that target the same name.

A worked example

examples/workspace/workspace.ts calls applyWorkspace with a declared layout, inspects the resulting session, and tears it down while treating "already gone" as an answer. The integration suite runs it against a real tmux server.

License

MIT