From bd2e91d62a1f5fdcd6efce34c53c27b13675ca71 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 29 Sep 2026 23:17:05 -0500 Subject: [PATCH 1/3] docs(examples) Keep root workspace about tmux why: The shared workspace page repeated incomplete language excerpts and long source inventories beside existing complete port examples. what: - Show a complete tmux command file and isolated shell program at the root - Keep compact port links and the existing source-section anchors - Redirect legacy port example paths to their owned workspace pages - Regenerate API mentions and remove the three unused source excerpts - Protect native program bytes and forward/reverse port routing verification: Sixteen native cases pass on tmux 3.2a and 3.7c, including parse errors, partial creation, shutdown errors and control isolation. Foreign-language and missing-alias controls fail as expected. The outer local gate passes in 47.89 seconds. --- .../docs/examples/workspace-from-file.md | 299 +++++------------- .../ports/cxx/workspace/internals/examples.md | 1 + .../dotnet/workspace/internals/examples.md | 1 + .../ports/go/workspace/internals/examples.md | 1 + .../java/workspace/internals/examples.md | 1 + .../docs/ports/py/workspace/examples.md | 1 + .../ports/rs/workspace/internals/examples.md | 1 + .../swift/workspace/internals/examples.md | 1 + .../ports/ts/workspace/internals/examples.md | 1 + site/src/data/example-sources.json | 18 -- site/src/data/mentions.json | 126 -------- site/test/complete-examples.test.ts | 38 ++- site/test/example-sources.test.ts | 5 +- site/test/fixtures/product-examples.json | 32 +- 14 files changed, 155 insertions(+), 371 deletions(-) diff --git a/site/src/content/docs/examples/workspace-from-file.md b/site/src/content/docs/examples/workspace-from-file.md index 5d00c163..a2908803 100644 --- a/site/src/content/docs/examples/workspace-from-file.md +++ b/site/src/content/docs/examples/workspace-from-file.md @@ -1,7 +1,7 @@ --- -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 @@ -9,242 +9,95 @@ sidebar: 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. - -For Python, use [tmuxp](https://tmuxp.git-pull.com/), a separate application -built on libtmux's `Server`, `Session`, `Window`, and `Pane` APIs. - +## Define the layout -```typescript file="examples/workspace/workspace.ts" -``` - - -TypeScript's `applyWorkspace` reuses existing objects when the same -configuration is applied again. - +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' ``` - -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. - - -```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' +) ``` - -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. - - -```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 -``` - - -Java's `read` and `parse` validate the configuration and return a `Workspace` -value. Only `build` changes tmux state. - - -```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: - -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. - - - -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. - - -```swift file="Examples/Sources/ExampleCode/Workspaces.swift" +```console +$ sh workspace.sh ``` - -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. - - -## Where this comes from - - - -### Python - - -**Source:** Not listed. - -**In this page:** no fence; the README says tmuxp is a separate project by -design - -**Checked by:** n/a - - - - -### TypeScript - - -**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` - - - - -### Go - - -**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 - - - - -### Rust - - -**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 - - - - -### Java - - -**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` - - - - -### .NET - - -**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 - - - - -### C++ - - -**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 - +`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. - - -### Swift - +## 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 +[Python CLI](/py/latest/workspace/examples/) · +[TypeScript](/ts/latest/workspace/internals/examples/) · +[Go](/go/latest/workspace/internals/examples/) · +[Rust](/rs/latest/workspace/internals/examples/) · +[Java](/java/latest/workspace/internals/examples/) · +[.NET](/dotnet/latest/workspace/internals/examples/) · +[C++](/cxx/latest/workspace/internals/examples/) · +[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` - +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 + + -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`. diff --git a/site/src/content/docs/ports/cxx/workspace/internals/examples.md b/site/src/content/docs/ports/cxx/workspace/internals/examples.md index 1045ce21..2cf415bb 100644 --- a/site/src/content/docs/ports/cxx/workspace/internals/examples.md +++ b/site/src/content/docs/ports/cxx/workspace/internals/examples.md @@ -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 diff --git a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md index 09be21b8..cbecd8e5 100644 --- a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md +++ b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: ".NET workspace builder examples" description: "Internal examples for building and inspecting workspaces through the .NET API." port: dotnet product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/content/docs/ports/go/workspace/internals/examples.md b/site/src/content/docs/ports/go/workspace/internals/examples.md index 8026f5cd..c3da073f 100644 --- a/site/src/content/docs/ports/go/workspace/internals/examples.md +++ b/site/src/content/docs/ports/go/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: Go workspace builder examples description: Build and inspect a workspace from YAML on a private tmux server with Go. port: go product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/content/docs/ports/java/workspace/internals/examples.md b/site/src/content/docs/ports/java/workspace/internals/examples.md index 012962c6..6faf3e80 100644 --- a/site/src/content/docs/ports/java/workspace/internals/examples.md +++ b/site/src/content/docs/ports/java/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: "Java workspace builder examples" description: "Internal examples for building and inspecting workspaces through the Java API." port: java product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/content/docs/ports/py/workspace/examples.md b/site/src/content/docs/ports/py/workspace/examples.md index e0afad60..4da1cc73 100644 --- a/site/src/content/docs/ports/py/workspace/examples.md +++ b/site/src/content/docs/ports/py/workspace/examples.md @@ -3,6 +3,7 @@ title: "Python workspace examples" description: "Load tmux workspaces from YAML or JSON with the tmuxp CLI." port: py product: workspace +aliases: [examples/workspace-from-file] sidebar: label: Examples order: 3 diff --git a/site/src/content/docs/ports/rs/workspace/internals/examples.md b/site/src/content/docs/ports/rs/workspace/internals/examples.md index b592cbda..3c0c57ab 100644 --- a/site/src/content/docs/ports/rs/workspace/internals/examples.md +++ b/site/src/content/docs/ports/rs/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: Rust workspace builder examples description: Build, inspect, and capture a workspace on a private tmux server with Rust. port: rs product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/content/docs/ports/swift/workspace/internals/examples.md b/site/src/content/docs/ports/swift/workspace/internals/examples.md index e7f7a7bc..256088fb 100644 --- a/site/src/content/docs/ports/swift/workspace/internals/examples.md +++ b/site/src/content/docs/ports/swift/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: Swift workspace builder examples description: Build and inspect a private workspace with a complete Swift program. port: swift product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/content/docs/ports/ts/workspace/internals/examples.md b/site/src/content/docs/ports/ts/workspace/internals/examples.md index 0537fe50..bbd78b50 100644 --- a/site/src/content/docs/ports/ts/workspace/internals/examples.md +++ b/site/src/content/docs/ports/ts/workspace/internals/examples.md @@ -3,6 +3,7 @@ title: TypeScript workspace builder examples description: Build and inspect a workspace on a private tmux server with TypeScript. port: ts product: workspace +aliases: [examples/workspace-from-file] sidebar: group: Internals label: Examples diff --git a/site/src/data/example-sources.json b/site/src/data/example-sources.json index ef4582d9..1509e414 100644 --- a/site/src/data/example-sources.json +++ b/site/src/data/example-sources.json @@ -17,12 +17,6 @@ "sha256": "28725dec488dc4b80ef9812d328f1df2fce947d975ac6fc406e8bea85670545a", "content": "// Command quickstart demonstrates a complete session, window, and pane lifecycle.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/libtmux/libtmux-go/tmux\"\n)\n\nfunc main() {\n\tif err := start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// start owns cleanup because log.Fatal skips deferred calls in main.\nfunc start() error {\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tserver, err := tmux.NewServer(tmux.ServerOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"configure tmux server: %w\", err)\n\t}\n\treturn run(ctx, server)\n}\n\n// run accepts injected server state so tests can isolate the example.\nfunc run(ctx context.Context, server tmux.Server) (err error) {\n\t// docs:quickstart given:ctx context.Context; server tmux.Server\n\tsession, err := server.NewSession(ctx, tmux.NewSessionRequest{\n\t\tName: \"libtmux-go-quickstart\", WindowName: \"start\",\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create session: %w\", err)\n\t}\n\tdefer func() {\n\t\tcleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)\n\t\tdefer cleanupCancel()\n\t\terr = errors.Join(err, session.Kill(cleanupCtx))\n\t}()\n\n\twindow, err := session.NewWindow(ctx, tmux.NewWindowRequest{Name: new(\"work\")})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create window: %w\", err)\n\t}\n\tpane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{\n\t\tDirection: tmux.PaneDirectionRight, Command: \"sh\",\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"split window: %w\", err)\n\t}\n\toutput, err := pane.OpenObservation(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"watch pane: %w\", err)\n\t}\n\tdefer func() { err = errors.Join(err, output.Close()) }()\n\tif _, err := fmt.Fprintln(pane.Writer(ctx), \"printf 'libtmux ready\\\\n'\"); err != nil {\n\t\treturn fmt.Errorf(\"send command: %w\", err)\n\t}\n\t// docs:end\n\n\tscanner := bufio.NewScanner(output.Reader(ctx))\n\tfor scanner.Scan() {\n\t\tif scanner.Text() == \"libtmux ready\" {\n\t\t\tfmt.Println(\"libtmux ready\")\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"read pane: %w\", scanner.Err())\n}\n" }, - "go:workspace/example_test.go": { - "repository": "libtmux/libtmux-go", - "revision": "bb06e26e116e941813ca40bf45e7e3a47d38f52a", - "sha256": "2eaaf9c21bb471139af18b70585554b61ca0463b16486984e5aa4ecbc8368e8b", - "content": "package workspace_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/libtmux/libtmux-go/tmux\"\n\t\"github.com/libtmux/libtmux-go/workspace\"\n)\n\n// Load a tmuxp-style document and build the session it describes.\nfunc Example() {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\tdefer cancel()\n\tserver, err := tmux.NewServer(tmux.ServerOptions{\n\t\tSocketName: \"libtmux-go-example-workspace\",\n\t})\n\tif err != nil {\n\t\tfmt.Println(\"server:\", err)\n\t\treturn\n\t}\n\tdefer killExampleServer(server)\n\n\tdocument := []byte(`\nsession_name: review\nwindows:\n - window_name: editor\n panes:\n - shell_command: printf 'ready\\n'\n - window_name: tests\n panes:\n - shell_command: printf 'ready\\n'\n - shell_command: printf 'ready\\n'\n`)\n\tparsed, err := workspace.Parse(document)\n\tif err != nil {\n\t\tfmt.Println(\"parse:\", err)\n\t\treturn\n\t}\n\n\tsession, err := workspace.Build(ctx, server, parsed)\n\tif err != nil {\n\t\tfmt.Println(\"build:\", err)\n\t\treturn\n\t}\n\n\tname, _ := session.Name()\n\twindows, err := session.SearchWindows(ctx, nil)\n\tif err != nil {\n\t\tfmt.Println(\"search windows:\", err)\n\t\treturn\n\t}\n\tfmt.Println(name, len(windows))\n\t// Output: review 2\n}\n\n// Create the initial session, prefer a retained connection where tmux supports\n// one, then populate the rest of the workspace through that session.\nfunc ExampleBuildInto() {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\tdefer cancel()\n\tserver, err := tmux.NewServer(tmux.ServerOptions{\n\t\tSocketName: \"libtmux-go-example-workspace-continue\",\n\t})\n\tif err != nil {\n\t\tfmt.Println(\"server:\", err)\n\t\treturn\n\t}\n\tdefer killExampleServer(server)\n\n\tdescribed := workspace.Workspace{\n\t\tSessionName: \"review\",\n\t\tWindows: []workspace.Window{\n\t\t\t{Name: \"editor\", Panes: []workspace.Pane{{Shell: \"sleep 60\"}}},\n\t\t\t{Name: \"tests\", Panes: []workspace.Pane{{Shell: \"sleep 60\"}}},\n\t\t},\n\t}\n\trequest, err := described.InitialSessionRequest()\n\tif err != nil {\n\t\tfmt.Println(\"request:\", err)\n\t\treturn\n\t}\n\t_, connection, err := server.NewSessionConnection(\n\t\tctx,\n\t\trequest,\n\t\ttmux.ConnectionOptions{},\n\t)\n\tif err != nil {\n\t\tfmt.Println(\"create:\", err)\n\t\treturn\n\t}\n\tdefer func() { _ = connection.Close() }()\n\tsession := connection.Session()\n\tif err := workspace.BuildInto(ctx, session, described); err != nil {\n\t\tfmt.Println(\"build:\", err)\n\t\treturn\n\t}\n\twindows, err := session.SearchWindows(ctx, nil)\n\tif err != nil {\n\t\tfmt.Println(\"search windows:\", err)\n\t\treturn\n\t}\n\tfmt.Println(len(windows))\n\t// Output: 2\n}\n\n// A misspelled key fails the parse rather than being dropped, so a workspace\n// that does not do what its author meant says so before anything is built.\nfunc ExampleParse_unknownField() {\n\t_, err := workspace.Parse([]byte(\"session_name: review\\nwindow:\\n - {}\\n\"))\n\tfmt.Println(err != nil)\n\t// Output: true\n}\n\n// killExampleServer stops an example's server on a context of its own, since an\n// example's own context may already be spent by the time it returns.\nfunc killExampleServer(server tmux.Server) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\t_ = server.Kill(ctx)\n}\n" - }, "java:examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java": { "repository": "libtmux/libtmux-java", "revision": "842228310449e879ebcaa3f910597757c9dbffd6", @@ -89,22 +83,10 @@ "sha256": "b51fb2fce7c3df5571e8d596c3ca2d636cb38b2e13a8f9d6d278d1d64010f4a7", "content": "// The examples in the README's \"Change what is there\" section.\n\nimport LibTmux\n\npublic func buildASessionByHand(_ server: Server) async throws -> Pane {\n let session = try await server.newSession(named: \"work\", windowName: \"editor\")\n _ = try await server.setOption(\"@purpose\", to: \"development\", scope: .session(session))\n let logs = try await server.newWindow(in: session, named: \"logs\").window\n let pane = try await server.splitWindow(logs, direction: .right)\n try await server.run(\"tail -f /tmp/build.log\", in: pane)\n return pane\n}\n\npublic func readBackWhatAPanePrinted(_ server: Server, _ pane: Pane) async throws -> [String] {\n let lines = try await server.capture(pane)\n print(lines.suffix(5).joined(separator: \"\\n\"))\n return lines\n}\n\npublic func spendOneProcessOnAllOfIt(_ server: Server) async throws {\n var plan = TmuxCommandList()\n for name in [\"edit\", \"test\", \"logs\"] {\n plan = plan.then(\"new-window\", [\"-d\", \"-n\", name])\n }\n _ = try await server.run(plan)\n}\n" }, - "swift:Examples/Sources/ExampleCode/Workspaces.swift": { - "repository": "libtmux/libtmux-swift", - "revision": "254f8b2be7eb60cacc3ffcb3ea8e456784f582df", - "sha256": "a721e1f287c7b492a598edee125a8905caf8b10a886e53d53291ffd5b99432ac", - "content": "// The examples in the README's `WorkspaceBuilder` section.\n\nimport Foundation\nimport LibTmux\nimport TmuxWorkspace\n\npublic func describeAWorkspaceInSwift() -> Workspace {\n let workspace = Workspace(\n sessionName: \"work\",\n windows: [\n WindowPlan(\n windowName: \"editor\",\n layout: \"even-horizontal\",\n panes: [PanePlan(), PanePlan()]\n ),\n WindowPlan(\n windowName: \"logs\",\n panes: [PanePlan(shellCommands: [\"tail -f /tmp/build.log\"])]\n ),\n ]\n )\n return workspace\n}\n\npublic func buildItOnAServer(_ server: Server, _ workspace: Workspace) async throws -> Session {\n let session = try await WorkspaceBuilder.build(workspace, on: server)\n print(session.name, session.windowCount)\n return session\n}\n\npublic func readAWorkspaceWrittenAsJSON(_ json: Data) throws -> Workspace {\n try Workspace.decode(json: json)\n}\n\n// Deliberately NOT wrapped in `#if YAMLWorkspaces`. A trait defines its\n// compilation condition only inside the package that declares it, so that guard\n// in a consumer package is always false and deletes the example — loudly if a\n// test calls it, silently if nothing does. The symbol is nonetheless here,\n// because the dependency was resolved with the trait on.\npublic func readATmuxpFile(_ text: String) throws -> Workspace {\n try Workspace.decode(yaml: text)\n}\n" - }, "ts:examples/quickstart/quickstart.ts": { "repository": "libtmux/libtmux-ts", "revision": "3fe1ca654b81b8cbf4a13b777a001a3298c87a6f", "sha256": "4058cfefdbd2a54c69f9da93a3d57dfe2f49abd20af8256a42456fd7947c186b", "content": "import { Server, TmuxCommandError, type ServerSnapshot } from \"libtmux\";\n\n/**\n * A runnable tour of the API, driven by the tests so it cannot rot.\n *\n * Every step here appears in README.md.\n */\nexport async function quickstart(server: Server): Promise {\n // Nothing is read until you ask. `snapshot()` is the only step that talks to\n // tmux; everything reachable from it resolves locally.\n const session = await server.newSession({ name: \"quickstart\" });\n const editor = await session.newWindow({ name: \"editor\" });\n await editor.split();\n\n const snapshot = await server.snapshot();\n\n // Declarative filtering, serializable and stable on the wire.\n const found = snapshot.windows.where({ name: \"editor\" }).one();\n\n // Relations are plain properties: no await, no tmux command.\n const paneCount = found.panes.length;\n if (paneCount !== 2) throw new Error(`expected two panes, saw ${String(paneCount)}`);\n\n // A criterion is spelled like the handle accessor it filters.\n if (snapshot.panes.count({ currentCommand: { contains: \"\" } }) === 0) {\n throw new Error(\"expected panes to report a current command\");\n }\n const first = found.panes.at(0);\n if (first === undefined) throw new Error(\"expected a pane\");\n await first.sendKeys(\"echo hello-from-libtmux\", { literal: true });\n\n // Failures carry their parts rather than a formatted sentence.\n try {\n await server.setOption(\"not-a-real-option\", \"1\");\n } catch (error) {\n if (!(error instanceof TmuxCommandError)) throw error;\n if (error.args[0] !== \"set-option\") throw error;\n }\n\n return snapshot;\n}\n" - }, - "ts:examples/workspace/workspace.ts": { - "repository": "libtmux/libtmux-ts", - "revision": "3fe1ca654b81b8cbf4a13b777a001a3298c87a6f", - "sha256": "35ffadc734aad94422f0e7c939bd448db97e13130d1c79047ba9c343a0672cf2", - "content": "import type { Server } from \"libtmux/server\";\nimport type { Session } from \"libtmux/session\";\nimport { applyWorkspace } from \"@libtmux/workspace\";\nimport type { WorkspaceInput } from \"@libtmux/workspace/config\";\n\n/**\n * Build the shape most people reach for tmux to get: one session, a window\n * per concern, each pane already running the thing it is there for.\n *\n * `server.batch` plans several windows and resolves them from one final\n * snapshot. `buildWorkspace`, below, delegates declared topology to the\n * workspace package instead of maintaining another reconciler here.\n */\nexport async function buildSimpleWorkspace(server: Server): Promise {\n const built = await server.newSession({\n name: \"work\",\n shellCommand: \"sleep 30\",\n windowName: \"editor\",\n });\n\n const [logs, shell] = await server.batch([\n built.plan.newWindow({ name: \"logs\", shellCommand: \"tail -f /dev/null\" }),\n built.plan.newWindow({ name: \"shell\" }),\n ]);\n\n await logs.selectLayout(\"even-horizontal\");\n void shell.name; // \"shell\"\n\n return built;\n}\n\nexport const DEVELOPMENT_WORKSPACE = {\n session_name: \"workspace-example\",\n windows: [\n { panes: [\"sleep 30\", \"sleep 30\"], window_name: \"editor\" },\n { panes: [\"sleep 30\"], window_name: \"server\" },\n { window_name: \"logs\" },\n ],\n} satisfies WorkspaceInput;\n\n/** Apply the package's declarative workspace to a server. */\nexport function buildWorkspace(server: Server): Promise {\n return applyWorkspace(server, DEVELOPMENT_WORKSPACE);\n}\n\n/**\n * Tear a workspace down without caring whether it is there.\n *\n * Killing a session that has already gone is not a failure worth propagating,\n * which is the one case worth handling separately from every other tmux error.\n */\nexport async function removeWorkspace(server: Server, name: string): Promise {\n const found = (await server.snapshot()).sessions.first({ name });\n if (found === undefined) return false;\n await found.kill();\n return true;\n}\n" } } diff --git a/site/src/data/mentions.json b/site/src/data/mentions.json index a291d5b2..68c1b55a 100644 --- a/site/src/data/mentions.json +++ b/site/src/data/mentions.json @@ -1267,13 +1267,6 @@ "title": ".NET workspace builder API", "section": "ports" }, - { - "port": "dotnet", - "symbol": "LibTmux.Workspace.PaneReadiness.Always", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "dotnet", "symbol": "LibTmux.Workspace.PaneReadiness.Auto", @@ -1288,13 +1281,6 @@ "title": ".NET workspace builder API", "section": "ports" }, - { - "port": "dotnet", - "symbol": "LibTmux.Workspace.PaneReadiness.Auto", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "dotnet", "symbol": "LibTmux.Workspace.PaneReadiness.Never", @@ -1309,13 +1295,6 @@ "title": ".NET workspace builder API", "section": "ports" }, - { - "port": "dotnet", - "symbol": "LibTmux.Workspace.PaneReadiness.Never", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "dotnet", "symbol": "LibTmux.Workspace.WorkspaceBuilder", @@ -1358,13 +1337,6 @@ "title": ".NET workspace builder API", "section": "ports" }, - { - "port": "dotnet", - "symbol": "LibTmux.Workspace.WorkspaceBuilder.BuildAsync", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "dotnet", "symbol": "LibTmux.Workspace.WorkspaceBuildException", @@ -1400,13 +1372,6 @@ "title": ".NET workspace builder API", "section": "ports" }, - { - "port": "dotnet", - "symbol": "LibTmux.Workspace.WorkspaceBuildException.PartialResult", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "dotnet", "symbol": "LibTmux.Workspace.WorkspaceFile", @@ -2093,13 +2058,6 @@ "title": "Go workspace builder API", "section": "ports" }, - { - "port": "go", - "symbol": "workspace.Build", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "go", "symbol": "workspace.Build", @@ -2198,13 +2156,6 @@ "title": "Go workspace builder API", "section": "ports" }, - { - "port": "go", - "symbol": "workspace.Parse", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "go", "symbol": "workspace.Parse", @@ -2779,13 +2730,6 @@ "title": "Java workspace builder API", "section": "ports" }, - { - "port": "java", - "symbol": "io.github.libtmux.workspace.Workspace.Workspace", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "java", "symbol": "io.github.libtmux.workspace.Workspace.Workspace", @@ -4207,13 +4151,6 @@ "title": "Architecture", "section": "topics" }, - { - "port": "py", - "symbol": "libtmux.pane.Pane", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "py", "symbol": "libtmux.pane.Pane", @@ -4326,13 +4263,6 @@ "title": "Testing with libtmux", "section": "guides" }, - { - "port": "py", - "symbol": "libtmux.server.Server", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "py", "symbol": "libtmux.server.Server", @@ -4466,13 +4396,6 @@ "title": "Workspaces", "section": "concepts" }, - { - "port": "py", - "symbol": "libtmux.session.Session", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "py", "symbol": "libtmux.session.Session", @@ -4557,13 +4480,6 @@ "title": "Workspaces", "section": "concepts" }, - { - "port": "py", - "symbol": "libtmux.window.Window", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "py", "symbol": "libtmux.window.Window", @@ -5026,13 +4942,6 @@ "title": "Options and hooks", "section": "topics" }, - { - "port": "rs", - "symbol": "freeze.freeze", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "rs", "symbol": "freeze.freeze", @@ -6692,13 +6601,6 @@ "title": "Swift workspace builder API", "section": "ports" }, - { - "port": "swift", - "symbol": "Workspace.decode(json:)", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "swift", "symbol": "Workspace.decode(json:)", @@ -6720,13 +6622,6 @@ "title": "Swift workspace builder API", "section": "ports" }, - { - "port": "swift", - "symbol": "Workspace.decode(yaml:)", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "swift", "symbol": "Workspace.decode(yaml:)", @@ -6769,13 +6664,6 @@ "title": "Swift workspace builder API", "section": "ports" }, - { - "port": "swift", - "symbol": "WorkspaceBuilder.build(_:on:)", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "swift", "symbol": "WorkspaceBuilder.build(_:on:)", @@ -6832,13 +6720,6 @@ "title": "Workspaces", "section": "concepts" }, - { - "port": "ts", - "symbol": "builder.applyWorkspace", - "page": "/examples/workspace-from-file/", - "title": "Build a workspace from a file", - "section": "examples" - }, { "port": "ts", "symbol": "builder.applyWorkspace", @@ -7513,13 +7394,6 @@ "line": 152, "why": "not defined in the stated port" }, - { - "port": "swift", - "text": "YAMLWorkspaces", - "page": "/examples/workspace-from-file/", - "line": 120, - "why": "not defined in the stated port" - }, { "port": "dotnet", "text": "IEnumerable.Matching(expression)", diff --git a/site/test/complete-examples.test.ts b/site/test/complete-examples.test.ts index 56bcddbd..8ea70dcf 100644 --- a/site/test/complete-examples.test.ts +++ b/site/test/complete-examples.test.ts @@ -8,7 +8,7 @@ import products from './fixtures/product-examples.json' import { remarkPortCode, resolvePortCode } from '../src/plugins/remark-port-code.mjs' import { rehypeCodeTabs } from '../src/plugins/rehype-code-tabs.mjs' import { docsEntryAvailable, pagePortLinks } from '../src/lib/page-port-links' -import { docsRoutePath } from '../src/lib/docs-paths' +import { docsPath, docsRedirects, docsRoutePath } from '../src/lib/docs-paths' const readPage = (page: string) => readFileSync(new URL(`../src/content/docs/${page}.md`, import.meta.url), 'utf8') const parsePage = (page: string) => parseFrontmatter(readPage(page), { frontmatter: 'remove' }) @@ -96,4 +96,40 @@ describe('verified complete programs', () => { expect(links.filter((link) => link.links.length).map((link) => link.port).sort()) .toEqual(receipt.examples.map((example) => example.port).sort()) }) + + it('keeps the root workspace runnable and redirects legacy port pages to their owned examples', () => { + const example = products.rootWorkspace + const { content, frontmatter } = parsePage(example.page) + expect(frontmatter.supportedPorts).toEqual([]) + const blocks = fences(content) + expect(blocks.every((block) => ['sh', 'text', 'console'].includes(block.language))).toBe(true) + for (const file of example.files) { + const matching = blocks.filter((block) => block.title === file.name) + expect(matching).toHaveLength(1) + expect(sha256(matching[0].code)).toBe(file.sha256) + } + expect(blocks.filter((block) => block.language === 'console') + .map((block) => block.code.replace(/^\$ /gm, '').trim())).toEqual(example.shellRecipe) + const docs = [{ id: example.page, data: frontmatter }, ...example.ports.map((port) => { + const id = `ports/${port}/workspace/${port === 'py' ? '' : 'internals/'}examples` + const { frontmatter: data } = parsePage(id) + expect(data.port).toBe(port) + expect(data.aliases).toContain(example.page) + expect(content).toContain(`/${port}/latest/${docsPath({ id, data })}/`) + return { id, data } + })] + const links = pagePortLinks({ pagePath: example.page, version: 'latest', defaults: {}, docs }) + expect(links.filter((link) => link.links.length).map((link) => link.port).sort()) + .toEqual([...example.ports].sort()) + for (const port of example.ports) { + const available = docs.filter((doc) => docsEntryAvailable(doc, port)) + expect(available).toHaveLength(1) + expect(docsRedirects(available, port)).toEqual([ + { path: example.page, target: docsPath(available[0]) }, + ]) + const reverse = pagePortLinks({ pagePath: docsPath(available[0]), portSlug: port, version: 'latest', defaults: {}, docs }) + expect(reverse.filter((link) => link.links.length).map((link) => link.port).sort()) + .toEqual([...example.ports].sort()) + } + }) }) diff --git a/site/test/example-sources.test.ts b/site/test/example-sources.test.ts index a857b246..b6d87a89 100644 --- a/site/test/example-sources.test.ts +++ b/site/test/example-sources.test.ts @@ -27,8 +27,9 @@ const CONTENT = join(dirname(fileURLToPath(import.meta.url)), '../src/content/do * Raise this as examples are converted; it exists to stop the number going * the other way. It measures source inclusion, not execution coverage. */ -// The standalone Go workspace program is covered by complete-examples.test.ts. -const SOURCED_FLOOR = 13 +// Complete workspace programs replace their shared source excerpts; native +// receipts and rendering checks live in complete-examples.test.ts. +const SOURCED_FLOOR = 10 interface Fence { file: string diff --git a/site/test/fixtures/product-examples.json b/site/test/fixtures/product-examples.json index 05a6c7a5..e26683aa 100644 --- a/site/test/fixtures/product-examples.json +++ b/site/test/fixtures/product-examples.json @@ -271,5 +271,35 @@ "cargo run --quiet" ] } - ] + ], + "rootWorkspace": { + "page": "examples/workspace-from-file", + "files": [ + { + "name": "workspace.conf", + "sha256": "0d166ecbe8a973649cb10498e69430bca86c647a398a830a8590d8d80f17b1a4" + }, + { + "name": "workspace.sh", + "sha256": "3c3f550610e34f4b43378700915b189a4a5a37d0987352a94312674a004df411" + } + ], + "tmux": [ + "3.2a", + "3.7c" + ], + "shellRecipe": [ + "sh workspace.sh" + ], + "ports": [ + "py", + "ts", + "go", + "rs", + "java", + "dotnet", + "cxx", + "swift" + ] + } } From 321fe02c63cbed18a379c5daec5f44b26efd2678 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 29 Sep 2026 23:27:11 -0500 Subject: [PATCH 2/3] fix(docs) Preserve fragments in static redirects why: A static meta refresh discards an incoming section fragment and query string. Existing workspace example citations stopped at the top of the new page after moving to the complete port examples. what: - Share a redirect page between guide aliases and port-home fallbacks - Carry the query string and fragment to the canonical destination - Keep a noindex canonical page and JavaScript-disabled meta fallback - Retain legacy source-section anchors on the eight workspace examples - Exercise section links and the no-JavaScript path in the browser gate verification: The built old Go alias drops its source-section fragment. The fresh-source browser gate keeps the fragment and query, finds the legacy anchor, and reaches the page with JavaScript disabled. The full outer local gate passes in 47.51 seconds. --- site/scripts/check-navigation.mjs | 16 ++++++++++ site/src/components/docs/RedirectPage.astro | 31 +++++++++++++++++++ .../ports/cxx/workspace/internals/examples.md | 3 ++ .../dotnet/workspace/internals/examples.md | 3 ++ .../ports/go/workspace/internals/examples.md | 3 ++ .../java/workspace/internals/examples.md | 3 ++ .../docs/ports/py/workspace/examples.md | 3 ++ .../ports/rs/workspace/internals/examples.md | 3 ++ .../swift/workspace/internals/examples.md | 3 ++ .../ports/ts/workspace/internals/examples.md | 3 ++ site/src/pages/[...slug].astro | 5 +-- site/src/pages/[port]/index.astro | 20 +++--------- 12 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 site/src/components/docs/RedirectPage.astro diff --git a/site/scripts/check-navigation.mjs b/site/scripts/check-navigation.mjs index 653f797c..59520bef 100644 --- a/site/scripts/check-navigation.mjs +++ b/site/scripts/check-navigation.mjs @@ -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') } diff --git a/site/src/components/docs/RedirectPage.astro b/site/src/components/docs/RedirectPage.astro new file mode 100644 index 00000000..cd7a8dd2 --- /dev/null +++ b/site/src/components/docs/RedirectPage.astro @@ -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 +--- + + + + + + {title} + + + + + + + Continue to {title} + + diff --git a/site/src/content/docs/ports/cxx/workspace/internals/examples.md b/site/src/content/docs/ports/cxx/workspace/internals/examples.md index 2cf415bb..8ee1cc5f 100644 --- a/site/src/content/docs/ports/cxx/workspace/internals/examples.md +++ b/site/src/content/docs/ports/cxx/workspace/internals/examples.md @@ -116,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. + + + [Workspace builder source](https://github.com/libtmux/libtmux-cxx/blob/393d4b0ad666f18a6581f1eb281741a75a7503f0/examples/workspace/include/libtmux_consumers/workspace.hpp) diff --git a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md index cbecd8e5..bcfac208 100644 --- a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md +++ b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md @@ -111,4 +111,7 @@ owned-scope cleanup uses its own lifetime. That does not establish completion of programs running in the panes. A build failure can leave partial results; the owned server scope removes them here. + + + [Workspace API source](https://github.com/libtmux/libtmux-dotnet/tree/320dc64f4b8b7815842471327a5e6b84a1499bf8/src/LibTmux.Workspace) diff --git a/site/src/content/docs/ports/go/workspace/internals/examples.md b/site/src/content/docs/ports/go/workspace/internals/examples.md index c3da073f..df5219ba 100644 --- a/site/src/content/docs/ports/go/workspace/internals/examples.md +++ b/site/src/content/docs/ports/go/workspace/internals/examples.md @@ -186,5 +186,8 @@ error. For a workspace that stays open, let your application retain its explicitly selected server and choose when to stop it. + + + [Builder source](https://github.com/libtmux/libtmux-go/blob/bb06e26e116e941813ca40bf45e7e3a47d38f52a/workspace/builder.go); [configuration source](https://github.com/libtmux/libtmux-go/blob/bb06e26e116e941813ca40bf45e7e3a47d38f52a/workspace/workspace.go). diff --git a/site/src/content/docs/ports/java/workspace/internals/examples.md b/site/src/content/docs/ports/java/workspace/internals/examples.md index 6faf3e80..abb62ab1 100644 --- a/site/src/content/docs/ports/java/workspace/internals/examples.md +++ b/site/src/content/docs/ports/java/workspace/internals/examples.md @@ -125,4 +125,7 @@ Building creates the layout and sends the pane commands. It does not wait for those programs to finish. Keep the session running instead of calling `killServer` when adapting this example into an application launcher. + + + [Workspace API source](https://github.com/libtmux/libtmux-java/tree/842228310449e879ebcaa3f910597757c9dbffd6/libtmux-workspace/src/main/java/io/github/libtmux/workspace) diff --git a/site/src/content/docs/ports/py/workspace/examples.md b/site/src/content/docs/ports/py/workspace/examples.md index 4da1cc73..5094cf91 100644 --- a/site/src/content/docs/ports/py/workspace/examples.md +++ b/site/src/content/docs/ports/py/workspace/examples.md @@ -84,4 +84,7 @@ selection, attachment, and existing sessions. For contributors studying how a file becomes a session, see the [internal builder example](../internals/examples/). + + + [Two-pane YAML source](https://github.com/tmux-python/tmuxp/blob/618b398acc05506d3c682906c36cdeb29dcfa1ff/examples/2-pane-vertical.yaml) diff --git a/site/src/content/docs/ports/rs/workspace/internals/examples.md b/site/src/content/docs/ports/rs/workspace/internals/examples.md index 3c0c57ab..0ca1a15b 100644 --- a/site/src/content/docs/ports/rs/workspace/internals/examples.md +++ b/site/src/content/docs/ports/rs/workspace/internals/examples.md @@ -143,5 +143,8 @@ shutdown both fail, the error includes both failures. For a persistent workspace, let your application retain an explicitly selected server and choose its own shutdown point. + + + [Builder source](https://github.com/libtmux/libtmux-rs/blob/d4e08b4eaab62ef4eeedab79b47973ae9a1de310/crates/tmux-workspace/src/lib.rs); [capture source](https://github.com/libtmux/libtmux-rs/blob/d4e08b4eaab62ef4eeedab79b47973ae9a1de310/crates/tmux-workspace/src/freeze.rs). diff --git a/site/src/content/docs/ports/swift/workspace/internals/examples.md b/site/src/content/docs/ports/swift/workspace/internals/examples.md index 256088fb..f52be278 100644 --- a/site/src/content/docs/ports/swift/workspace/internals/examples.md +++ b/site/src/content/docs/ports/swift/workspace/internals/examples.md @@ -167,4 +167,7 @@ Describe panes with Swift values or decode a configuration with `Workspace.decode(json:)`. The [guide](../guides/) explains ownership, configuration input and failed-build cleanup. + + + [Library source](https://github.com/libtmux/libtmux-swift/tree/254f8b2be7eb60cacc3ffcb3ea8e456784f582df/Sources/TmuxWorkspace). diff --git a/site/src/content/docs/ports/ts/workspace/internals/examples.md b/site/src/content/docs/ports/ts/workspace/internals/examples.md index bbd78b50..f31e698d 100644 --- a/site/src/content/docs/ports/ts/workspace/internals/examples.md +++ b/site/src/content/docs/ports/ts/workspace/internals/examples.md @@ -140,5 +140,8 @@ For a workspace that stays open, let the application retain its explicitly selected server and choose when to stop it. A failed build can leave partial work; the example's server cleanup removes it. + + + [Example source](https://github.com/libtmux/libtmux-ts/blob/3fe1ca654b81b8cbf4a13b777a001a3298c87a6f/examples/workspace/workspace.ts); [Workspace builder source](https://github.com/libtmux/libtmux-ts/blob/3fe1ca654b81b8cbf4a13b777a001a3298c87a6f/packages/workspace/src/builder.ts). diff --git a/site/src/pages/[...slug].astro b/site/src/pages/[...slug].astro index a4bb2a7e..e799f38f 100644 --- a/site/src/pages/[...slug].astro +++ b/site/src/pages/[...slug].astro @@ -1,6 +1,7 @@ --- import { getCollection, type CollectionEntry } from 'astro:content' import DocPage from '../components/docs/DocPage.astro' +import RedirectPage from '../components/docs/RedirectPage.astro' import McpOverview from '../components/mcp/Overview.astro' import McpTools from '../components/mcp/Tools.astro' import ProductApiPage from '../components/api/ProductApiPage.astro' @@ -70,7 +71,7 @@ interface Props { toolName?: string } const { entry, placeholderLocale, globalPage, version, model, symbol, mcpPort, toolName, redirect } = Astro.props -if (redirect) return Astro.redirect(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/${redirect}/`, 301) +const destination = redirect ? `${import.meta.env.BASE_URL.replace(/\/$/, '')}/${redirect}/` : undefined --- -{globalPage === 'mcp' ? : globalPage === 'tools' ? : mcpPort ? : model && symbol ? : entry && } +{destination ? : globalPage === 'mcp' ? : globalPage === 'tools' ? : mcpPort ? : model && symbol ? : entry && } diff --git a/site/src/pages/[port]/index.astro b/site/src/pages/[port]/index.astro index 25c6c665..75753b53 100644 --- a/site/src/pages/[port]/index.astro +++ b/site/src/pages/[port]/index.astro @@ -4,14 +4,15 @@ import { IS_ROOT_BUILD } from '../../lib/site-root' import { defaultVersionFor } from '../../lib/versions' import { DEFAULT_LOCALE } from '../../i18n/locales' import { buildLocale } from '../../i18n/resolve' +import RedirectPage from '../../components/docs/RedirectPage.astro' /** * `//`: a redirect to the port's default version. * * The edge function answers this URL with a 302 from its KeyValueStore * (infra/cloudfront-function.js rule 1), so readers reach this page only - * when that lookup misses. It takes the shape of Astro's own static redirect - * (`redirectTemplate`): meta refresh, `noindex`, canonical, visible link. + * when that lookup misses. The static fallback preserves query strings and + * section links, with a meta refresh for browsers without JavaScript. */ export function getStaticPaths() { // Root build only: every port+version build renders this route too. @@ -31,19 +32,6 @@ interface Props { const { port } = Astro.props as Props const destination = portHomeUrl(port, defaultVersionFor(port.slug)) -const canonical = new URL(destination, Astro.site ?? 'https://libtmux.org').href --- - - - - - {port.name} documentation - - - - - - Continue to the {port.name} documentation - - + From 4f9559e34e130bf7ffe4664a3cd08f575b13d7b4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 30 Sep 2026 00:32:00 -0500 Subject: [PATCH 3/3] test(docs) Check workspace task counterparts why: The Python workspace example now shares its task alias with the native builder examples. The publication test still expected only the old same-section routes and rejected the valid seven new links. what: - Check each canonical counterpart, including the native Internals pages - Keep same-section expectations for other product pages and versions - Require every expected counterpart destination to exist Verification: Reproduced the CI assertion locally, then passed the focused assembled-output check and the full local gate in 51.28 seconds. --- site/test/product-docs.test.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/site/test/product-docs.test.ts b/site/test/product-docs.test.ts index a1794d4a..2f0ec413 100644 --- a/site/test/product-docs.test.ts +++ b/site/test/product-docs.test.ts @@ -396,14 +396,24 @@ describe.skipIf(!SITE_BUILT)('assembled MCP and Workspace Manager docs', () => { const defaults = manifest().defaultVersion for (const page of pages()) await inspect(page.path, (document) => { const links = [...document.querySelectorAll('[data-page-port-switcher] a[href]')] - const available = PORTS.filter((port) => !port.parentLibrary && sectionsFor(port.slug, page.product).includes(page.section)) - expect(links.length, `${page.path} port counterparts`).toBe(available.length) - for (const port of PORTS) { + const counterparts = new Map(PORTS.flatMap((port) => { + // The Python CLI example shares the root task alias with each + // native builder example, whose canonical page is under Internals. + const section = page.port === 'py' && page.product === 'workspace' && page.section === 'examples' + && ['ts', 'rs', 'go', 'java', 'dotnet', 'cxx', 'swift'].includes(port.slug) + ? 'internals/examples' : page.section + if (port.parentLibrary || !sectionsFor(port.slug, page.product).includes(section)) return [] const version = port.slug === page.port ? page.version : defaults[port.slug] - const expected = urlFor(`${port.slug}/${version}/${page.product}/${page.section ? `${page.section}/` : ''}`).pathname - expect(links.some((link) => new URL(link.getAttribute('href')!, urlFor(page.path)).pathname === expected), - `${page.path} counterpart ${expected}`).toBe(available.includes(port)) - if (!available.includes(port)) { + return [[port.slug, urlFor(`${port.slug}/${version}/${page.product}/${section ? `${section}/` : ''}`).pathname]] + })) + expect(links.length, `${page.path} port counterparts`).toBe(counterparts.size) + for (const port of PORTS) { + const expected = counterparts.get(port.slug) + if (expected) { + expect(links.some((link) => new URL(link.getAttribute('href')!, urlFor(page.path)).pathname === expected), + `${page.path} counterpart ${expected}`).toBe(true) + expect(resolves(expected), `${page.path} counterpart exists at ${expected}`).toBe(true) + } else { const unavailable = [...document.querySelectorAll('[data-page-port-switcher] [aria-disabled="true"]')] expect(unavailable.some((entry) => entry.textContent.includes(port.name)), `${page.path} unavailable ${port.name}`).toBe(true) }