Skip to main content

Crate tmux_workspace

Crate tmux_workspace 

Source
Expand description

Build tmux workspaces from tmuxp-style YAML, using libtmux.

This is the library. The package also ships the tmux-workspace command, a native tmuxp: cargo install tmux-workspace --version 0.1.0-alpha.16, and see its README and its reference. The library reads tmuxp’s own example files and, by default, records rather than acts on a handful of tmuxp’s keys; Reading a tmuxp file names them and says where it still departs from tmuxp.

Alpha. The API changes between releases, including in ways that will not be called out as breaking, because nothing here is stable yet. Cargo will not resolve a prerelease unless the requirement names one, so a plain 0.1 requirement does not pick this up: depend on the exact version, and expect to edit it.

Describe the workspace:

session_name: dev
windows:
  - window_name: editor
    panes:
      - vim
      - htop

Freeze a session someone built by hand back into one:

use libtmux::test::TestServer;
use tmux_workspace::{freeze, Workspace};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Runs for real, against an isolated tmux under `/tmp/libtmux-rs-test/`.
    // Your own code reaches a session someone left running through
    // `libtmux::Server::new()?`.
    let guard = TestServer::new().await?;
    let session = guard.server().new_session("dev").await?;

    let workspace = freeze(&session).await?;
    let yaml = workspace.to_yaml();

    // Keep it wherever the project keeps them:
    //   std::fs::write("dev.yaml", &yaml)?;
    assert_eq!(Workspace::from_yaml(&yaml)?, workspace);

    guard.shutdown().await?;
    Ok(())
}

What freezing recovers is the shape – windows, panes, working directories, which is focused. What it cannot is history: tmux remembers what a pane is running, not the command someone typed to start it.

Build it:

use libtmux::test::TestServer;
use tmux_workspace::{Workspace, WorkspaceBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Runs for real, against an isolated tmux under `/tmp/libtmux-rs-test/`.
    // Your own code reads the file, so a `./` start directory is the file's,
    // and uses `libtmux::Server::new()?`:
    //   let workspace = Workspace::from_file("dev.yaml")?;
    let source = "
session_name: dev
windows:
  - window_name: editor
    panes: [/bin/sh, /bin/sh]
";
    let workspace = Workspace::from_yaml(source)?;

    let guard = TestServer::new().await?;
    let session = WorkspaceBuilder::new(guard.server()).build(&workspace).await?;

    assert_eq!(session.name().to_string_lossy(), "dev");
    assert_eq!(session.windows().await?.len(), 1);

    guard.shutdown().await?;
    Ok(())
}

Builds validate every configured layout before sessions change. Unique name abbreviations use the running daemon’s version; a cold endpoint uses the selected client. Custom layouts need a checksum, a nonempty tree and enough pane cells, with at most 256 nested groups. Geometry correction and pruning remain tmux’s responsibility. An empty layout string leaves the default arrangement in place.

cargo run --example build does all of this end to end: parse, list the plan’s steps, build, and freeze the result back to YAML.

§See what it would do first

plan returns the work without doing any of it, so a caller can print it, count it, or decide against it:

use libtmux::test::TestServer;
use tmux_workspace::{Workspace, WorkspaceBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let workspace = Workspace::from_yaml(
        "
session_name: dev
windows:
  - window_name: editor
    panes: [/bin/sh, /bin/sh]
",
    )?;

    let guard = TestServer::new().await?;
    let plan = WorkspaceBuilder::new(guard.server()).plan(&workspace);

    // Nothing has reached tmux, but every command is already known.
    let commands: Vec<_> = plan
        .preview()
        .into_iter()
        .flatten()
        .map(|command| command.summary().to_string())
        .collect();

    assert!(commands[0].contains("new-session"));
    assert_eq!(guard.server().sessions().await?.len(), 0, "nothing ran");

    guard.shutdown().await?;
    Ok(())
}

§Reading a tmuxp file

A file written for tmuxp builds the session tmuxp would build, including where tmuxp’s behaviour is surprising:

  • A lone pane, blank or empty - is a pane with no command. Among other commands the word is typed.
  • Commands are typed after a space, which keeps them out of history in a shell set to ignore such lines, unless suppress_history: false. The space reaches whatever the pane runs, a program as much as a shell.
  • enter: false on a command holds for the commands after it in that pane, so the next one is typed onto the same line. A pane’s enter: false covers its shell_command_before commands, which are typed first.
  • sleep_before and sleep_after hold the same way, and are waited for in tmux: each is a libtmux::plan::Pause between the commands it separates.
  • ~ and $NAME or ${NAME} expand from the loading process’s environment in names, start directories, and environment and option values. An unset variable stays as written, and there is no escape: a frozen name holding $HOME reads back as the home directory.
  • A window’s relative start_directory joins the session’s. One starting with . starts from the directory it would inherit, else from the file’s. tmuxp’s start-directory.yaml names a window for the file’s directory that tmuxp’s loader, and this crate, put in the session’s. A pane’s other relative path is not joined to its window’s: it starts from the current directory, as tmux would start it.
  • A window that names no layout is tiled (an even grid). tmuxp stacks it instead, halving each split from the last. With no explicit focus, the last pane a window builds stays active, as tmuxp leaves it.
  • window_shell is the default shell for every pane in the window, not only the one it comes with, as tmuxp’s own builder does (get_pane_shell); a pane’s own shell overrides it for that pane.
  • A pane’s own environment replaces its window’s, rather than adding to it, as tmuxp’s builder does.
  • shell_command_before trickles down through the workspace, the window and the pane, in that order, ahead of the pane’s own shell_command.
  • <<: *anchor and <<: [*a, *b] merge keys resolve at every level, as PyYAML’s safe loader does.
  • A missing or null windows key is refused, as tmuxp’s own schema validation requires; an empty list is not, and keeps the window tmux made.

It differs where following tmuxp would be unsafe or impossible:

  • Commands are typed as written, for the pane’s shell to expand, and the loader’s value of each variable a command, window_shell or a pane’s own shell names is added to that pane’s environment unless the document sets it. tmuxp pastes the value into the command instead, which reads a variable’s value as shell code.
  • ~name in a start directory is refused, not looked up; elsewhere it stays as written.
  • A . path with nothing to inherit, and a null among commands, crash tmuxp. Here the first starts from the file’s directory and the second is an error naming its line.

Three of tmuxp’s keys are not acted on, and are listed in unsupported_keys along with any key tmuxp does not have: before_script, plugins, and a window’s options_after.

Workspace::from_yaml and Workspace::from_file record a key they do not act on in unsupported_keys rather than refusing it, so a richer tmuxp file still loads: the three above, plus workspace_builder, workspace_builder_options, config and socket_name on the workspace. Workspace::from_yaml_strict and Workspace::from_file_strict refuse any other unrecognized key instead, unless it starts with x-, which is the tmux-workspace command’s own policy. cargo run --example strict reads one document both ways.

The tmux-workspace command has its own parser and builder but reads a document the same way, tmuxp’s; the last section of its reference lists where it differs.

§Install

The command is a default feature; a library dependent leaves it, and clap with it, out:

$ cargo add tmux-workspace@0.1.0-alpha.16 --no-default-features
Cargo.toml
[dependencies]
tmux-workspace = { version = "0.1.0-alpha.16", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

§Why this crate exists

It exists to drive the libtmux public API from outside the crate that defines it. Tests written inside libtmux can reach pub(crate) items and can be written around whatever shape the internals happen to have; a separate crate cannot. So this one builds something real — sessions, windows, panes, splits, and the layout a workspace file describes — using only what a published consumer can see. An API that is awkward to use from here is awkward for everyone, and that shows up as a compile error rather than as a review comment.

Being published is part of that rather than beside it. A crate that is only ever built inside its own workspace never proves its dependency requirements resolve, and this one could not have been published at all until libtmux shipped the plan feature it asks for — which is exactly the kind of thing that is invisible from inside the tree.

Modules§

command
The tmux-workspace command’s reference, rendered with the library’s documentation so it is readable on docs.rs and a link to it is checked when the documentation builds. Documentation only; it holds no items.

Structs§

PaneConfig
One pane.
ShellCommand
One command typed into a pane, with tmuxp’s per-command settings.
WindowConfig
One window and the panes it should contain.
Workspace
One workspace: a session and the windows it should contain.
WorkspaceBuilder
Creates tmux sessions from workspace configurations.

Enums§

BuildError
A failure while building a workspace.
ConfigError
A workspace configuration failure.

Functions§

freeze
Describe a live session as a workspace.