LibTmux.Workspace
0.1.1
Prefix Reserved
dotnet add package LibTmux.Workspace --version 0.1.1
NuGet\Install-Package LibTmux.Workspace -Version 0.1.1
<PackageReference Include="LibTmux.Workspace" Version="0.1.1" />
<PackageVersion Include="LibTmux.Workspace" Version="0.1.1" />
<PackageReference Include="LibTmux.Workspace" />
paket add LibTmux.Workspace --version 0.1.1
#r "nuget: LibTmux.Workspace, 0.1.1"
#:package LibTmux.Workspace@0.1.1
#addin nuget:?package=LibTmux.Workspace&version=0.1.1
#tool nuget:?package=LibTmux.Workspace&version=0.1.1
LibTmux.Workspace
Build tmux sessions from tmuxp YAML or JSON workspace files, on top of LibTmux.
Parse a file, review the plan of what it will do, and apply it or build in one call. It suits launchers, devcontainer entrypoints, and internal CLIs that already describe sessions in tmuxp YAML and want typed results.
Add it to your project:
$ dotnet add package LibTmux.Workspace
Releases. Releases are numbered
0.1.N, and each release incrementsN. The API is not settled: any release may change or remove exported identifiers, and the changelog marks each break. Pin an exact version to avoid one.
Contents — Quick start · Plan and apply · Read a file · Options and commands · Startup · Failures · Library or command · Leaves out · Support
Quick start
Build a two-window session from a YAML declaration. The block uses your
configured LibTmux Server and cancellation token; the
LibTmux quick start
covers choosing a socket:
WorkspaceFile workspace = WorkspaceFile.Parse("""
session_name: api
windows:
- window_name: editor
panes:
- shell_command: echo editing
- window_name: server
panes:
- shell_command: echo serving
""");
WorkspaceResult result = await new WorkspaceBuilder(server).BuildAsync(workspace, ct);
if (result.Session.Name != "api" || result.Windows.Count != 2)
{
throw new InvalidOperationException("Expected the session api with two windows.");
}
It runs against a tmux server of its own and prints nothing. The session api
has windows editor and server, and the block throws otherwise.
Every csharp run block on this page runs the same way, with server,
session, and ct already in hand.
Plan and apply a workspace
WorkspaceBuilder.PlanAsync: review before anything runs
A plan lists what will happen.
ApplyAsync executes it, and
Validate checks a declaration
locally before any tmux endpoint is chosen:
WorkspaceFile workspace = WorkspaceFile.Parse("""
session_name: api
start_directory: /tmp
windows:
- window_name: editor
panes:
- shell_command: echo editing
- window_name: server
panes:
- shell_command: echo serving
""");
WorkspaceBuilder.Validate(workspace);
WorkspaceBuilder builder = new(server);
WorkspacePlan plan = await builder.PlanAsync(workspace, cancellationToken: ct);
foreach (WorkspaceAction action in plan.Actions)
Console.WriteLine(action);
WorkspaceResult result = await builder.ApplyAsync(plan, ct);
Console.WriteLine($"{result.Session.Name}: {result.Windows.Count} windows");
It prints one line per action, then api: 2 windows.
- Planning observes the endpoint and creates nothing. Displaying or enumerating the plan runs no tmux command.
ApplyAsyncrechecks the daemon and session it observed before it executes.- From the third pane in a window, the plan adds an
ArrangePanesaction that tiles the existing panes to make room. It can resize programs already running.
WorkspaceBuilder.BuildAsync: plan and apply in one call
It uses the default policies, as the quick start does. It sends input immediately; it does not wait for shells to be ready or for commands to finish. Startup covers waiting.
Read a workspace file
WorkspaceFile.Parse: YAML or JSON into a declaration
It reads the text without contacting tmux. Unknown keys, wrong value shapes,
multiple YAML documents, and inputs over 1 MiB throw
WorkspaceFormatException, naming
the property path, line, and column.
WorkspaceFile.Resolve: absolute directories
Resolve directories relative to the file when reading from disk:
string source = Path.GetFullPath("session.yaml");
WorkspaceFile fromDisk = WorkspaceFile.Parse(File.ReadAllText(source))
.Resolve(Path.GetDirectoryName(source)!);
- A window inherits the session directory, and a pane inherits its window's. Neither call contacts tmux or checks that a directory exists.
$NAMEand${NAME}expand from thevariablesargument. A leading~needs an absoluteHOMEvalue there, and$$is a literal dollar sign. Unknown directory variables fail.- The resolver does not read process environment variables.
WorkspaceFile.FromSnapshot: a captured session as a declaration
Capture through SnapshotDepth.Panes first; a
missing relation throws
IncompleteSnapshotException:
Server captured = await server.CaptureSnapshotAsync(SnapshotDepth.Panes, ct);
Session source = captured.Sessions.Single(value => value.Id == session.Id);
WorkspaceFile frozen = WorkspaceFile.FromSnapshot(source);
WorkspaceFile resolved = frozen.Resolve("/tmp");
Console.WriteLine($"{resolved.SessionName}: {resolved.Windows.Count} windows");
It prints the session name and its window count.
The declaration keeps window and pane order, window indexes, names, layouts, pane directories, and the selected window and pane. It omits commands, environment, options, terminal text, entity IDs, and pane indices.
Set options, environment, and commands
Options: when each one applies
session_name: workers
global_options:
default-shell: /bin/sh
windows:
- window_name: workers
layout: main-horizontal
options:
main-pane-height: 5
options_after:
synchronize-panes: on
panes:
- shell_command: echo left
- shell_command: echo right
- Session
optionsandglobal_optionsapply before the windows are created; windowoptionsapply before startup input and further splits;options_afterapply after all pane input and the final layout. - Global options change defaults on the selected daemon. Plans list them as
SetOptionRequestactions withGlobal = true;Appendapplies them andReuseleaves them untouched. WorkspaceFile.GlobalOptionsandWorkspaceWindow.OptionsAfterexpose the same read-only maps in C#.
Commands: enter
windows:
- panes:
- enter: false
shell_command_before:
- "echo "
shell_command:
- cmd: ready
enter: true
environmentmerges at session, window, and pane level; a child wins on the same key.- Commands are sent in session-before, window-before, pane-before, then pane order.
- With
enter: false, tmux types the text without pressing Enter, and later text joins the pending line until an Enter is requested.WorkspacePane.Enterholds the pane default. - A non-Boolean
entervalue is rejected with its path and location. Other tmuxp command modifiers are unsupported.
Choose startup and existing-session behavior
WorkspacePlanOptions: what the plan does about conflicts and readiness
| Policy | Behavior |
|---|---|
ExistingSession = Error (default) |
Refuse a conflicting session. |
ExistingSession = Reuse |
Return the inspected session without changing it. |
ExistingSession = Append |
Add windows while preserving existing children and local session options; apply declared global options. |
ExistingSession = Replace |
Replace the inspected session while preserving its daemon. |
Readiness = Immediate (default) |
Send each command as literal input, pressing Enter according to its effective inherited state. |
Readiness = Cooperative |
Wait for startup to signal its per-pane channel before sending commands. |
- The default
ServerStartup = CreateOrJoinmay start a daemon or join one that appeared after planning, and grants no ownership of it.RequireExistingneeds a running daemon. - With
Cooperative, each pane gets a freshLIBTMUX_WORKSPACE_READYvalue, and startup signals it withtmux wait-for -S "$LIBTMUX_WORKSPACE_READY". A signal sent before the wait is kept.
This receiver signals before it starts /bin/cat, which accepts queued input:
string tmux = "'" + server.ConnectionOptions.TmuxBinaryPath
.Replace("'", "'\"'\"'", StringComparison.Ordinal) + "'";
WorkspaceFile receiver = new("receiver",
options: new Dictionary<string, string>
{
["default-command"] = $"{tmux} wait-for -S \"$LIBTMUX_WORKSPACE_READY\"; exec /bin/cat",
},
windows: [new WorkspaceWindow("input", panes: [new WorkspacePane(["first line"])])]);
WorkspaceBuilder builder = new(server);
WorkspacePlan plan = await builder.PlanAsync(receiver, new WorkspacePlanOptions
{
Readiness = WorkspaceReadiness.Cooperative,
ReadinessTimeout = TimeSpan.FromSeconds(5),
}, ct);
WorkspaceResult result = await builder.ApplyAsync(plan, ct);
Console.WriteLine(result.Session.Name);
It prints the session name. A timeout stops input to that pane.
Handle results and failures
WorkspaceBuildException: what happened when application fails
BuildAsyncreturns the session and windows it created. ItsUnsupportedlist names the final layouts tmux rejected; those windows stay usable.Journalrecords every action, including failures and actions never started.PartialResultholds the state that exists, or is null when no session could be read.- Cancelling during application throws
WorkspaceOperationCanceledExceptionwith the same partial state. It does not roll anything back. CompensateOnFailure = truecleans up resources proven to be created by this application, withinCleanupTimeout. It does not reverse shell commands, host effects, or global option changes.- A missing session name or an empty window list throws
WorkspaceFormatExceptionbefore anything is created.
Set window_index in YAML, or WorkspaceWindow(windowIndex: 5) in C#, to ask
for an index. Duplicate requests fail local validation.
Library or command line
tmux-workspace, the command-line tool, has its own builder rather
than calling WorkspaceBuilder. Read a
file against the side that will build it:
| This library | tmux-workspace |
|
|---|---|---|
| Document language | a closed tmuxp subset | the tmuxp language, plus x- passthrough |
| Layout validation | syntax and version checked before actions; runtime rejection recorded in Unsupported |
refuses the load |
| Shell readiness | immediate by default; explicit cooperative readiness fails on timeout | cursor heuristic; sends the command after timeout |
| A failure partway | throws with an action journal and optional receipt-proven compensation | removes a session it created |
| Session size | tmux's default, 80x24 | the invoking terminal |
A file the CLI loads can raise
WorkspaceFormatException here.
Both build the same topology for the subset they share.
Related packages
| Package | Adds |
|---|---|
| LibTmux | The client. Required. |
| LibTmux.Query.Json | JSON for query documents |
| LibTmux.Mcp | A Model Context Protocol server, as a .NET tool |
What it leaves out
- A tmuxp runtime. Plugins, lifecycle hooks, and tmuxp's configuration search path are rejected. Run tmuxp for those.
- The rest of tmuxp's language. It reads a closed subset: session name,
start directory, options,
global_options,options_after, windows, window indexes, panes, layouts, focus, environment, andshell_commandvalues. - Host scripts by default.
before_scriptruns only when the plan enablesAllowHostScripts. It is/bin/sh -ctext, bounded byHostScriptTimeoutandMaxHostOutputBytes. - Process checkpoints. A captured declaration cannot recover the shell command a foreground program started from.
Support and status
- Releases. Releases are numbered
0.1.N, and each release incrementsN. The API is not settled: any release may change or remove exported identifiers, and the changelog marks each break. Pin an exact version to avoid one. - Dependencies. LibTmux, and YamlDotNet for the files.
- tmux and .NET. The tmux versions LibTmux supports, on net8.0 and net10.0.
- Systems. Linux, including WSL. macOS is best effort, without CI coverage. CI builds on Windows x64.
Documentation
- The full contracts: validation, resolving, option stages, commands, readiness, failures, and conversion
WorkspaceBuilderand the workspace reference on libtmux.org- Source:
src/LibTmux.Workspace; issues and history on GitHub
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- LibTmux (>= 0.1.1)
- YamlDotNet (>= 18.1.0)
-
net8.0
- LibTmux (>= 0.1.1)
- YamlDotNet (>= 18.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.1 | 232 | 11/1/2026 |
| 0.1.0 | 296 | 10/25/2026 |
| 0.0.0-alpha.24 | 97 | 10/25/2026 |
| 0.0.0-alpha.23 | 117 | 10/24/2026 |
| 0.0.0-alpha.22 | 131 | 10/19/2026 |
| 0.0.0-alpha.21 | 66 | 10/11/2026 |
| 0.0.0-alpha.20 | 417 | 10/5/2026 |
| 0.0.0-alpha.19 | 65 | 10/4/2026 |
| 0.0.0-alpha.18 | 68 | 10/3/2026 |
| 0.0.0-alpha.17 | 77 | 9/27/2026 |
| 0.0.0-alpha.16 | 69 | 9/26/2026 |
| 0.0.0-alpha.15 | 77 | 9/20/2026 |
| 0.0.0-alpha.14 | 69 | 9/20/2026 |
| 0.0.0-alpha.12 | 71 | 9/12/2026 |
| 0.0.0-alpha.10 | 88 | 8/30/2026 |
| 0.0.0-alpha.9 | 84 | 8/22/2026 |
| 0.0.0-alpha.8 | 89 | 8/22/2026 |
| 0.0.0-alpha.7 | 99 | 8/16/2026 |
| 0.0.0-alpha.6 | 84 | 8/16/2026 |
| 0.0.0-alpha.5 | 79 | 8/16/2026 |
LibTmux, LibTmux.FSharp, LibTmux.Testing, LibTmux.Query.Json, LibTmux.Extensions.DependencyInjection, LibTmux.Workspace, LibTmux.Workspace.Cli and LibTmux.Mcp share one version, so this release covers them together. What changed, per package and per release: https://github.com/libtmux/libtmux-dotnet/blob/master/CHANGELOG.md The public API is not settled and any release may change it; pin an exact version.