Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions site/scripts/check-navigation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ export async function checkNavigation(page, base) {
assert.deepEqual(await page.evaluate(() => window.__navigationProbe), { loads: 3, blank: false, wrongTheme: false })
assert.equal(await page.locator('html').getAttribute('data-mcp-install-cooldown-enabled'), '1')
console.log('Navigation: document retained, Back, search, code tabs, menu and saved theme/cooldown pass')

const legacy = `${base}/go/latest/examples/workspace-from-file/`
const current = `${base}/go/latest/workspace/internals/examples/`
await page.goto(`${legacy}?from=legacy#where-this-comes-from`, { waitUntil: 'load' })
await page.waitForURL(`${current}?from=legacy#where-this-comes-from`)
assert.equal(await page.locator('#where-this-comes-from').count(), 1, 'Legacy section still exists')
const fallback = await page.context().browser().newContext({ javaScriptEnabled: false })
try {
const reader = await fallback.newPage()
await reader.goto(legacy, { waitUntil: 'load' })
await reader.waitForURL(current)
assert.match(await reader.locator('h1').textContent(), /Go workspace/)
} finally {
await fallback.close()
}
console.log('Redirects: legacy query/section links and the JavaScript-disabled fallback pass')
}
31 changes: 31 additions & 0 deletions site/src/components/docs/RedirectPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
import { DEFAULT_LOCALE } from '../../i18n/locales'

interface Props {
destination: string
title: string
}

const { destination, title } = Astro.props
const canonical = new URL(destination, Astro.site ?? 'https://libtmux.org').href
---

<!doctype html>
<html lang={DEFAULT_LOCALE}>
<head>
<meta charset="utf-8" />
<title>{title}</title>
<meta name="robots" content="noindex" />
<link rel="canonical" href={canonical} />
<script is:inline define:vars={{ destination }}>
const target = new URL(destination, location.href)
target.search = location.search
target.hash = location.hash
location.replace(target.href)
</script>
<noscript><meta http-equiv="refresh" content={`0;url=${destination}`} /></noscript>
</head>
<body>
<a href={destination}>Continue to {title}</a>
</body>
</html>
299 changes: 76 additions & 223 deletions site/src/content/docs/examples/workspace-from-file.md
Original file line number Diff line number Diff line change
@@ -1,250 +1,103 @@
---
supportedPorts: [py, ts, rs, go, java, dotnet, cxx, swift]
supportedPorts: []
title: Build a workspace from a file
description: Describe a session in configuration, validate it, and build its windows and panes.
description: Create tmux windows and panes from a command file on a private server.
sidebar:
label: Build a workspace from a file
group: Examples
order: 4
tableOfContents: true
---

[tmuxp](https://tmuxp.git-pull.com/) describes sessions, windows, panes, and
shell commands in configuration files. A workspace builder turns that
configuration into tmux objects. [Source details](#where-this-comes-from) identify the example
files and their checks.
A tmux command file can create a session, arrange its windows, and split its
panes. This example builds two windows with three panes, prints their names
and pane counts, then removes its private server. It requires tmux 3.2 or
newer and a POSIX shell.

<!-- port:py -->
For Python, use [tmuxp](https://tmuxp.git-pull.com/), a separate application
built on libtmux's `Server`, `Session`, `Window`, and `Pane` APIs.
<!-- /port -->
## Define the layout

```typescript file="examples/workspace/workspace.ts"
```

<!-- port:ts -->
TypeScript's `applyWorkspace` reuses existing objects when the same
configuration is applied again.
<!-- /port -->
Save this command file:

```go file="workspace/example_test.go"
```text title="workspace.conf"
new-session -d -s dev -n editor -x 100 -y 30 'sh'
split-window -h -t '=dev:editor' 'sh'
new-window -t '=dev' -n logs 'sh'
select-window -t '=dev:editor'
select-pane -t '=dev:editor.0'
```

<!-- port:go -->
Go's `Example()`, in `workspace/example_test.go`, is a Go `Example`
function: `go test` runs it and checks its output against the
`// Output:` comment at the end, so this is executed on every test run
rather than merely present in a README. `Parse` rejects a field it doesn't
recognize rather than dropping it silently, and reports every problem it
finds at once with the line it's on. `Build` is not atomic: tmux has no
transaction, so a failure partway through leaves whatever was already
created in place, identified by the session `Build` still returns.
<!-- /port -->

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

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);
The `editor` window has two panes; `logs` has one. Each pane starts `sh`.
Explicit targets keep each command tied to the intended session and window.

## Build and inspect it

Save this complete program as `workspace.sh` beside the configuration file:

```sh title="workspace.sh"
(
set -eu
directory=$(mktemp -d "${TMPDIR:-/tmp}/libtmux-workspace.XXXXXX")
socket="$directory/tmux.sock"

cleanup() {
status=$?
trap - EXIT
if [ -S "$socket" ] && ! tmux -S "$socket" kill-server; then
printf '%s\n' "Cannot stop tmux; kept $directory." >&2
exit 1
fi
rm -rf "$directory" || status=$?
exit "$status"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

tmux -S "$socket" -f /dev/null start-server \; \
set-option -s exit-empty off
tmux -S "$socket" source-file ./workspace.conf
tmux -S "$socket" list-windows -t '=dev' \
-F '#{window_name}: #{window_panes} panes'
)
```

<!-- port:rs -->
Rust's `freeze(&session).await?` exports an existing session to the workspace
format. It recovers windows, panes, and working directories, but cannot recover
the shell command originally typed to start a process.
<!-- /port -->

```java
Workspace workspace = WorkspaceBuilder.parse("""
session_name: built
windows:
- window_name: editor
layout: even-horizontal
panes:
- shell_command: echo one
- shell_command: echo two
- window_name: server
panes:
- echo three
""");

Session session = WorkspaceBuilder.build(server, workspace);

session.name(); // → built
session.windows().size(); // → 2
session.windows().get(0).panes().size(); // → 2
```

<!-- port:java -->
Java's `read` and `parse` validate the configuration and return a `Workspace`
value. Only `build` changes tmux state.
<!-- /port -->

```csharp
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
""");

WorkspaceResult result = await new WorkspaceBuilder(server).BuildAsync(workspace, ct);
Console.WriteLine($"{result.Session.Name}: {result.Windows.Count} windows");
```
Run it from that directory:

<!-- port:dotnet -->
The .NET builder can wait for shell readiness before sending commands; [Sending
keys](/guides/sending-keys/#the-race-you-cant-see-from-the-call-site) explains
the startup race. It polls `pane_current_command`, `cursor_x`, and `cursor_y`
for up to ten seconds by default. `PaneReadiness.Auto` waits for zsh, `Always`
waits for every pane running the session's default shell, and `Never` sends
immediately. If `BuildAsync` fails partway through,
`WorkspaceBuildException.PartialResult` identifies what was created.
<!-- /port -->

<!-- port:cxx -->
C++'s `examples/workspace/` implements a consumer of the core API with its own
`workspace.hpp` and `tmuxp.hpp` types. Those types are part of the example, not
the library package. See [the example's
README](https://github.com/libtmux/libtmux-cxx/tree/main/examples/workspace) to
adapt it.
<!-- /port -->

```swift file="Examples/Sources/ExampleCode/Workspaces.swift"
```console
$ sh workspace.sh
```

<!-- port:swift -->
Swift's `WorkspaceBuilder.build` rejects an existing session with the requested
name. `Workspace.decode(yaml:)` reads tmuxp YAML when the `YAMLWorkspaces` trait
is enabled. `Workspace.decode(json:)` needs no additional trait.
<!-- /port -->

## Where this comes from

<!-- port:py -->
<!-- port:root -->
### Python
<!-- /port -->

**Source:** Not listed.

**In this page:** no fence; the README says tmuxp is a separate project by
design

**Checked by:** n/a
<!-- /port -->

<!-- port:ts -->
<!-- port:root -->
### TypeScript
<!-- /port -->

**Source:** `examples/workspace/workspace.ts` (`@libtmux/workspace`)

**In this page:** read whole from the file

**Checked by:** run against real tmux by `bun test examples/workspace`
<!-- /port -->

<!-- port:go -->
<!-- port:root -->
### Go
<!-- /port -->

**Source:** `workspace/example_test.go` (`workspace.Parse` / `workspace.Build`)

**In this page:** read whole from the file
The result is:

**Checked by:** `Example()` and its siblings run under `go test` and are checked
against their own `// Output:` comments
<!-- /port -->

<!-- port:rs -->
<!-- port:root -->
### Rust
<!-- /port -->

**Source:** `crates/tmux-workspace/README.md`, "Build it"

**In this page:** hand-quoted

**Checked by:** the crate's own `crates/tmux-workspace/src/lib.rs` includes the
README as a doc comment (`#![doc = include_str!("../README.md")]`), so `cargo
test --doc` runs this exact block
<!-- /port -->

<!-- port:java -->
<!-- port:root -->
### Java
<!-- /port -->

**Source:** `libtmux-workspace/README.md`, "What you get back"

**In this page:** hand-quoted

**Checked by:** every Java fence in the module's README is compiled and run
against real tmux by `docs-tests`
<!-- /port -->

<!-- port:dotnet -->
<!-- port:root -->
### .NET
<!-- /port -->

**Source:** `src/LibTmux.Workspace/README.md`

**In this page:** hand-quoted

**Checked by:** one of the READMEs and docs `ReadmeExampleTests` compiles and
runs against real tmux
<!-- /port -->

<!-- port:cxx -->
<!-- port:root -->
### C++
<!-- /port -->

**Source:** `examples/workspace/` (a consumer, not a library API)

**In this page:** prose only
```text
editor: 2 panes
logs: 1 panes
```

**Checked by:** `examples/workspace/tests/` runs the consumer suite against
real tmux; `ctest -R consumer.workspace` selects it. It exercises the
example's own types, not a published `libtmux` API
<!-- /port -->
`source-file` executes the configuration on the private server. Setting
`exit-empty` to `off` keeps that server available for cleanup even when a
configuration error prevents session creation. Errors remain visible and
make the program fail. Cleanup runs after partial creation too; if stopping
tmux fails, the script keeps the socket directory and reports its location.

<!-- port:swift -->
<!-- port:root -->
### Swift
<!-- /port -->
## Use a workspace manager

**Source:** `Examples/Sources/ExampleCode/Workspaces.swift`
For YAML or JSON configuration, validation, and language APIs, choose a port:

**In this page:** read whole from the file
<a id="python"></a>[Python CLI](/py/latest/workspace/examples/) ·
<a id="typescript"></a>[TypeScript](/ts/latest/workspace/internals/examples/) ·
<a id="go"></a>[Go](/go/latest/workspace/internals/examples/) ·
<a id="rust"></a>[Rust](/rs/latest/workspace/internals/examples/) ·
<a id="java"></a>[Java](/java/latest/workspace/internals/examples/) ·
<a id="net"></a>[.NET](/dotnet/latest/workspace/internals/examples/) ·
<a id="c"></a>[C++](/cxx/latest/workspace/internals/examples/) ·
<a id="swift"></a>[Swift](/swift/latest/workspace/internals/examples/)

**Checked by:** matched against the README's "Workspaces, from a file or from
Swift" section by `Scripts/check_examples.py`; compiled and run by `swift test
--package-path Examples`
<!-- /port -->
The [workspace concept guide](/concepts/workspaces/) explains configuration
and ownership. The [capture example](/examples/capture-pane-output/) shows
how to wait for pane output after sending a command.

### Source inclusion
<a id="where-this-comes-from"></a>
<a id="source-inclusion"></a>

Some blocks are excerpts from tested README examples. The source
details above identify those files and their checks.
The [tmux manual source](https://github.com/tmux/tmux/blob/94796f6b1182507efac8a272fc309a79e22e58a5/tmux.1)
describes `source-file`, window targets, and `exit-empty`.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: "C++ workspace builder examples"
description: "Internal examples for building and inspecting workspaces through the C++ API."
port: cxx
product: workspace
aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
Expand Down Expand Up @@ -115,4 +116,7 @@ This header belongs to the source consumer. Installing the core package alone
does not supply the workspace include directory. For YAML input, the separate
consumer build also needs its parser and yaml-cpp dependency.

<a id="where-this-comes-from"></a>
<a id="source-inclusion"></a>

[Workspace builder source](https://github.com/libtmux/libtmux-cxx/blob/393d4b0ad666f18a6581f1eb281741a75a7503f0/examples/workspace/include/libtmux_consumers/workspace.hpp)
Loading
Loading