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.1requirement 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
- htopFreeze 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,blankor 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: falseon a command holds for the commands after it in that pane, so the next one is typed onto the same line. A pane’senter: falsecovers itsshell_command_beforecommands, which are typed first.sleep_beforeandsleep_afterhold the same way, and are waited for in tmux: each is alibtmux::plan::Pausebetween the commands it separates.~and$NAMEor${NAME}expand from the loading process’s environment in names, start directories, andenvironmentand option values. An unset variable stays as written, and there is no escape: a frozen name holding$HOMEreads back as the home directory.- A window’s relative
start_directoryjoins the session’s. One starting with.starts from the directory it would inherit, else from the file’s. tmuxp’sstart-directory.yamlnames 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
layoutis tiled (an even grid). tmuxp stacks it instead, halving each split from the last. With no explicitfocus, the last pane a window builds stays active, as tmuxp leaves it. window_shellis 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 ownshelloverrides it for that pane.- A pane’s own
environmentreplaces its window’s, rather than adding to it, as tmuxp’s builder does. shell_command_beforetrickles down through the workspace, the window and the pane, in that order, ahead of the pane’s ownshell_command.<<: *anchorand<<: [*a, *b]merge keys resolve at every level, asPyYAML’s safe loader does.- A missing or null
windowskey 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_shellor a pane’s ownshellnames 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. ~namein 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-featuresCargo.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-workspacecommand’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§
- Pane
Config - One pane.
- Shell
Command - One command typed into a pane, with tmuxp’s per-command settings.
- Window
Config - One window and the panes it should contain.
- Workspace
- One workspace: a session and the windows it should contain.
- Workspace
Builder - Creates tmux sessions from workspace configurations.
Enums§
- Build
Error - A failure while building a workspace.
- Config
Error - A workspace configuration failure.
Functions§
- freeze
- Describe a live session as a workspace.