# libtmux for F# > The F# library (LibTmux.FSharp). These pages include its guides, tested examples and API documentation. - [F# API reference](https://libtmux.org/en/fsharp/latest/reference/): every public symbol, generated from the source. Hosted on libtmux.org. --- # Concepts Source: https://libtmux.org/en/fsharp/latest/concepts/ > Understand F# handles, filters, transports and layout ownership. Use these concepts to reason about F# handles, selection and command execution. Each page includes complete programs with imports, project files, run commands and cleanup. For individual types and operations, open the [API reference](https://libtmux.org/en/fsharp/latest/reference/). For a first connection, start with [attaching to tmux](https://libtmux.org/en/fsharp/latest/guides/attaching-to-tmux/). - [Server, session, window, pane](https://libtmux.org/en/fsharp/latest/concepts/server-session-window-pane/): Navigate captured sessions, windows, and panes, and refresh their state. - [Filtering and queries](https://libtmux.org/en/fsharp/latest/concepts/queries/): Combine predicates, handle result counts, and match related windows. - [Commands and control mode](https://libtmux.org/en/fsharp/latest/concepts/transports/): Run bounded commands and manage a persistent control client. - [Layouts and repeated setup](https://libtmux.org/en/fsharp/latest/concepts/workspaces/): Create a split window and reuse a named window safely. --- # Server, session, window, pane Source: https://libtmux.org/en/fsharp/latest/concepts/server-session-window-pane/ > Traverse captured F# handles, retain IDs and refresh after changes. Capture a hierarchy, then read its sessions, windows and panes locally. Handles retain the state they captured; a rename does not rewrite an older handle. Use a new capture to observe later changes. [`Server`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-server/) is the F# helper module. It operates on the underlying [`LibTmux.Server`](https://libtmux.org/en/csharp/latest/reference/libtmux-server/) object. Choose a snapshot depth before traversal: sessions, windows or panes. A relation that was not captured is unavailable, rather than an empty collection. A session holds window placements; a window holds panes. A linked window can appear in several sessions, so traversal counts placements rather than necessarily distinct physical windows. Names can change; retain IDs when identifying a target. ## Setup and run Use an empty directory on Linux with Git, tmux 3.2a or newer, and .NET SDK 10.0.302. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point. ```xml title="Query.fsproj" Exe net10.0 Local ``` The launcher creates two sessions on a private socket: `work-one` with an `editor` window, and `work-two` with a `logs` window. Each pane runs `cat` so it stays alive. It stops only this server when the program finishes or fails. If shutdown fails, it reports the retained directory and exits with an error. ```sh title="run.sh" #!/bin/sh set -eu binary=$(command -v tmux) mkdir -p /tmp/libtmux-dotnet-dev directory=$(mktemp -d /tmp/libtmux-dotnet-dev/query.XXXXXX) socket="$directory/tmux.sock" cleanup() { status=$? trap - 0 HUP INT TERM if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then printf 'Cannot stop tmux; kept %s\n' "$directory" >&2 exit 1 fi rm -rf "$directory" || exit 1 exit "$status" } trap cleanup 0 trap 'exit 1' HUP INT TERM unset TMUX TMUX_PANE export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary" "$binary" -S "$socket" -f /dev/null new-session -d -s work-one -n editor /bin/cat "$binary" -S "$socket" new-session -d -s work-two -n logs /bin/cat "$@" "$binary" -S "$socket" has-session -t '=work-one' ``` Fetch the library revision used by these examples: ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 2d99ead5aba8d968e85dcac8c9a9a518452e6ba6 ``` Each run starts from the same two-session fixture. Programs do not depend on another example having run first. Their assertions fail if the observed result differs. ## Walk sessions, windows and panes Flatten the captured children and verify the fixture contains two of each. The program also prints each session and its window. ```fsharp title="Hierarchy.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Panes let sessions = captured.Sessions let windows = sessions |> Seq.collect (fun session -> session.Windows) |> Seq.toList let panes = windows |> Seq.collect (fun window -> window.Panes) |> Seq.toList if sessions.Count <> 2 || windows.Length <> 2 || panes.Length <> 2 then failwith "Unexpected hierarchy" for session in sessions do printfn "%s: %s" session.Name session.Windows[0].Name printfn "2 sessions, 2 windows, 2 panes" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Hierarchy \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Hierarchy ``` Expected program output: ```text work-one: editor work-two: logs 2 sessions, 2 windows, 2 panes ``` ## Observe a rename with a fresh read Rename the editor window. The old handle still says `editor`; the returned handle and a new server read say `renamed`. This distinction matters when a UI, another client or your own code changes tmux after a capture. ```fsharp title="Refresh.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Windows let session = captured.Sessions |> Seq.find (fun session -> session.Name = "work-one") let before = session.Windows[0] let! after = before.RenameAsync("renamed", token) if before.Name <> "editor" || after.Name <> "renamed" then failwith "Unexpected rename state" let! refreshed = server |> Server.capture token SnapshotDepth.Windows let session = refreshed.Sessions |> Seq.find (fun s -> s.Name = "work-one") let window = session.Windows[0] if window.Name <> "renamed" then failwith "Fresh read did not see the rename" printfn "%s -> %s" before.Name window.Name } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Refresh \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Refresh ``` Expected program output: ```text editor -> renamed ``` ## Select a target before changing it A successful lookup does not reserve a tmux object. Another client can remove it before your next command. Handle a command failure at the mutation, and refresh before deciding what to do next. See [filtering and queries](https://libtmux.org/en/fsharp/latest/concepts/queries/) for missing and ambiguous selections, and [layouts](https://libtmux.org/en/fsharp/latest/concepts/workspaces/) for creation. --- # Filtering and queries Source: https://libtmux.org/en/fsharp/latest/concepts/queries/ > Compose F# filters, distinguish zero and several matches, and query captured relations. Filter captured objects in F#, handle result counts explicitly, and match sessions by their related windows. Local filters read captured values; they do not subscribe to changes or issue a fresh tmux query. [`Filter`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-filter/) supplies equality, ordinal prefix matching and membership. Use [`allOf`](), [`anyOf`]() and [`negate`]() to compose conditions. [`Query.matching`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-query-matching/) materializes the matching objects in source order. Use [`Seq.filter`]() for an application predicate that does not need a portable query. ## Setup and run Use an empty directory on Linux with Git, tmux 3.2a or newer, and .NET SDK 10.0.302. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point. ```xml title="Query.fsproj" Exe net10.0 Local ``` The launcher creates two sessions on a private socket: `work-one` with an `editor` window, and `work-two` with a `logs` window. Each pane runs `cat` so it stays alive. It stops only this server when the program finishes or fails. If shutdown fails, it reports the retained directory and exits with an error. ```sh title="run.sh" #!/bin/sh set -eu binary=$(command -v tmux) mkdir -p /tmp/libtmux-dotnet-dev directory=$(mktemp -d /tmp/libtmux-dotnet-dev/query.XXXXXX) socket="$directory/tmux.sock" cleanup() { status=$? trap - 0 HUP INT TERM if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then printf 'Cannot stop tmux; kept %s\n' "$directory" >&2 exit 1 fi rm -rf "$directory" || exit 1 exit "$status" } trap cleanup 0 trap 'exit 1' HUP INT TERM unset TMUX TMUX_PANE export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary" "$binary" -S "$socket" -f /dev/null new-session -d -s work-one -n editor /bin/cat "$binary" -S "$socket" new-session -d -s work-two -n logs /bin/cat "$@" "$binary" -S "$socket" has-session -t '=work-one' ``` Fetch the library revision used by these examples: ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 2d99ead5aba8d968e85dcac8c9a9a518452e6ba6 ``` Each run starts from the same two-session fixture. Programs do not depend on another example having run first. Their assertions fail if the observed result differs. ## Match names and combine conditions Select the two names beginning with `work-`, then assert equality, AND, OR and exclusion against the same captured collection. Matching is case sensitive. The native collection predicate agrees with the typed prefix query. ```fsharp title="Local.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Sessions let prefix = Filter.startsWith "work-" SessionFields.name let matching = captured.Sessions |> Query.matching prefix let names = matching |> Seq.map (fun session -> session.Name) |> Seq.sort |> Seq.toList if names <> [ "work-one"; "work-two" ] then failwith "Unexpected sessions" printfn "%s" (String.concat ", " names) let native = captured.Sessions |> Seq.filter (fun session -> session.Name.StartsWith("work-")) if Seq.length native <> matching.Count then failwith "Local filters disagree" let onlyOne = Filter.allOf [ Filter.startsWith "work-" SessionFields.name Filter.eq "work-one" SessionFields.name ] let selected = captured.Sessions |> Query.matching onlyOne if selected.Count <> 1 || selected[0].Name <> "work-one" then failwith "Wrong AND result" let either = Filter.oneOf [ "work-one"; "work-two" ] SessionFields.name if (captured.Sessions |> Query.matching either).Count <> 2 then failwith "Wrong OR result" let excluded = Filter.eq "work-one" SessionFields.name |> Filter.negate let remaining = captured.Sessions |> Query.matching excluded if remaining.Count <> 1 || remaining[0].Name <> "work-two" then failwith "Wrong NOT result" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Local \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Local ``` Expected program output: ```text work-one, work-two ``` ## Distinguish missing and ambiguous results [`Selection.exactlyOne`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-selection-exactlyone/) returns a [`Result`](). Match [`NoMatches`]() and [`MultipleMatches`]() separately. A missing optional target and an ambiguous destructive target should not take the same path. ```fsharp title="Cardinality.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Sessions for name in [ "work-one"; "missing" ] do let matches = captured.Sessions |> Query.matching (Filter.eq name SessionFields.name) match matches |> Selection.exactlyOne with | Ok session -> printfn "%s: selected" session.Name | Error CardinalityError.NoMatches -> printfn "%s: absent" name | Error CardinalityError.MultipleMatches -> failwithf "Ambiguous session: %s" name let many = captured.Sessions |> Selection.exactlyOne match many with | Error CardinalityError.MultipleMatches -> printfn "work-: ambiguous" | _ -> failwith "Expected two sessions" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Cardinality \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Cardinality ``` Expected program output: ```text work-one: selected missing: absent work-: ambiguous ``` ## Filter through related windows Match sessions with any window named `editor`, then match sessions with none. For an empty captured relation, [`any`]() is false and [`none`]() is true; [`all`]() is true. An uncaptured relation is a different condition and must be captured before filtering. This program derives the required snapshot depth from the query document. ```fsharp title="Relations.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let editor = Filter.eq "editor" WindowFields.name let withEditor = editor |> Filter.any SessionFields.windows let withoutEditor = editor |> Filter.none SessionFields.windows let depth = (Filter.toDocument withEditor).RequiredSnapshotDepth let! captured = server |> Server.capture token depth let selected = captured.Sessions |> Query.matching withEditor let excluded = captured.Sessions |> Query.matching withoutEditor if selected.Count <> 1 || selected[0].Name <> "work-one" then failwith "Wrong editor session" if excluded.Count <> 1 || excluded[0].Name <> "work-two" then failwith "Wrong other session" printfn "editor: %s" selected[0].Name printfn "no editor: %s" excluded[0].Name } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Relations \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Relations ``` Expected program output: ```text editor: work-one no editor: work-two ``` ## Refresh before repeating a decision Reusing the same list repeats the same decision over old state. Capture again when you need to observe a rename, new window or closed pane. The [snapshot example](https://libtmux.org/en/fsharp/latest/concepts/server-session-window-pane/#observe-a-rename-with-a-fresh-read) shows the old and fresh values side by side. --- # Commands and control mode Source: https://libtmux.org/en/fsharp/latest/concepts/transports/ > Use F# command calls and persistent control connections with explicit cleanup. Choose command calls for bounded operations and a control connection when you need a persistent tmux client. Closing a client releases its resources; it does not mean the tmux server should be stopped. [`Server.capture`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-server-capture/) returns a task and takes an explicit cancellation token. The default connection executes command requests through subprocesses. [`Control.withSession`](https://libtmux.org/en/fsharp/latest/reference/libtmux-fsharp-control-withsession/) brackets a persistent control session and closes it when the callback finishes. ## Setup and run Use an empty directory on Linux with Git, tmux 3.2a or newer, and .NET SDK 10.0.302. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point. ```xml title="Query.fsproj" Exe net10.0 Local ``` The launcher creates two sessions on a private socket: `work-one` with an `editor` window, and `work-two` with a `logs` window. Each pane runs `cat` so it stays alive. It stops only this server when the program finishes or fails. If shutdown fails, it reports the retained directory and exits with an error. ```sh title="run.sh" #!/bin/sh set -eu binary=$(command -v tmux) mkdir -p /tmp/libtmux-dotnet-dev directory=$(mktemp -d /tmp/libtmux-dotnet-dev/query.XXXXXX) socket="$directory/tmux.sock" cleanup() { status=$? trap - 0 HUP INT TERM if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then printf 'Cannot stop tmux; kept %s\n' "$directory" >&2 exit 1 fi rm -rf "$directory" || exit 1 exit "$status" } trap cleanup 0 trap 'exit 1' HUP INT TERM unset TMUX TMUX_PANE export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary" "$binary" -S "$socket" -f /dev/null new-session -d -s work-one -n editor /bin/cat "$binary" -S "$socket" new-session -d -s work-two -n logs /bin/cat "$@" "$binary" -S "$socket" has-session -t '=work-one' ``` Fetch the library revision used by these examples: ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 2d99ead5aba8d968e85dcac8c9a9a518452e6ba6 ``` Each run starts from the same two-session fixture. Programs do not depend on another example having run first. Their assertions fail if the observed result differs. ## Run a bounded command and inspect its result Read the session list and filter it locally. The connection uses a five-second timeout so an unavailable server fails visibly. ```fsharp title="Local.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Sessions let prefix = Filter.startsWith "work-" SessionFields.name let matching = captured.Sessions |> Query.matching prefix let names = matching |> Seq.map (fun session -> session.Name) |> Seq.sort |> Seq.toList if names <> [ "work-one"; "work-two" ] then failwith "Unexpected sessions" printfn "%s" (String.concat ", " names) let native = captured.Sessions |> Seq.filter (fun session -> session.Name.StartsWith("work-")) if Seq.length native <> matching.Count then failwith "Local filters disagree" let onlyOne = Filter.allOf [ Filter.startsWith "work-" SessionFields.name Filter.eq "work-one" SessionFields.name ] let selected = captured.Sessions |> Query.matching onlyOne if selected.Count <> 1 || selected[0].Name <> "work-one" then failwith "Wrong AND result" let either = Filter.oneOf [ "work-one"; "work-two" ] SessionFields.name if (captured.Sessions |> Query.matching either).Count <> 2 then failwith "Wrong OR result" let excluded = Filter.eq "work-one" SessionFields.name |> Filter.negate let remaining = captured.Sessions |> Query.matching excluded if remaining.Count <> 1 || remaining[0].Name <> "work-two" then failwith "Wrong NOT result" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Local \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Local ``` Expected program output: ```text work-one, work-two ``` ## Open and close a control client Send `list-sessions` over a persistent control connection, verify the response, then close the control client. A subsequent ordinary read verifies the tmux server still has both sessions. ```fsharp title="Control.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) do! server |> Control.withSession token (fun control -> task { let command = TmuxCommand.Create("list-sessions", "-F", "#{session_name}") let! lines = control.SendAsync(command, token) let names = lines |> Seq.sort |> Seq.toList if names <> [ "work-one"; "work-two" ] then failwith "Unexpected sessions" printfn "%s" (String.concat ", " names) }) let! captured = server |> Server.capture token SnapshotDepth.Sessions if captured.Sessions.Count <> 2 then failwith "Server lost its sessions" printfn "control client closed; server still running" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Control \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Control ``` Expected program output: ```text work-one, work-two control client closed; server still running ``` ## Timeouts and ownership An operation that times out may already have reached tmux. Read the current state before retrying a mutation such as creating a window. A cancellation signal does not roll back a completed tmux command. Here the launcher owns the private server and stops it after the program exits. An application connecting to an existing server should close its own client resources and leave that server running. See [attaching to tmux](https://libtmux.org/en/fsharp/latest/guides/attaching-to-tmux/) for the connection-only example. --- # Layouts and repeated setup Source: https://libtmux.org/en/fsharp/latest/concepts/workspaces/ > Create and reuse F# tmux layouts while preserving running processes. Build a tmux layout from F# by creating a window, splitting a pane and selecting a layout. Capture again to inspect the result. The examples use the library APIs directly and give every pane a command that stays alive. ## Setup and run Use an empty directory on Linux with Git, tmux 3.2a or newer, and .NET SDK 10.0.302. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point. ```xml title="Query.fsproj" Exe net10.0 Local ``` The launcher creates two sessions on a private socket: `work-one` with an `editor` window, and `work-two` with a `logs` window. Each pane runs `cat` so it stays alive. It stops only this server when the program finishes or fails. If shutdown fails, it reports the retained directory and exits with an error. ```sh title="run.sh" #!/bin/sh set -eu binary=$(command -v tmux) mkdir -p /tmp/libtmux-dotnet-dev directory=$(mktemp -d /tmp/libtmux-dotnet-dev/query.XXXXXX) socket="$directory/tmux.sock" cleanup() { status=$? trap - 0 HUP INT TERM if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then printf 'Cannot stop tmux; kept %s\n' "$directory" >&2 exit 1 fi rm -rf "$directory" || exit 1 exit "$status" } trap cleanup 0 trap 'exit 1' HUP INT TERM unset TMUX TMUX_PANE export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary" "$binary" -S "$socket" -f /dev/null new-session -d -s work-one -n editor /bin/cat "$binary" -S "$socket" new-session -d -s work-two -n logs /bin/cat "$@" "$binary" -S "$socket" has-session -t '=work-one' ``` Fetch the library revision used by these examples: ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 2d99ead5aba8d968e85dcac8c9a9a518452e6ba6 ``` Each run starts from the same two-session fixture. Programs do not depend on another example having run first. Their assertions fail if the observed result differs. ## Create a two-pane tools window Create `tools` in `work-one`, split its pane to the right, then choose `even-horizontal`. The final read verifies two panes. The previously captured session does not automatically gain the new window. ```fsharp title="Layout.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let! captured = server |> Server.capture token SnapshotDepth.Sessions let session = captured.Sessions |> Seq.find (fun s -> s.Name = "work-one") let request = NewWindowRequest(Name = "tools", Command = "/bin/cat") let! window = session.CreateWindowAsync(request, token) let! panes = window.GetPanesAsync(token) let split = SplitPaneRequest(Direction = PaneDirection.Right, Command = "/bin/cat") let! _ = panes[0] |> Pane.split token split let! _ = window.SelectLayoutAsync(SelectLayoutRequest(Layout = "even-horizontal"), token) let! refreshed = window.GetPanesAsync(token) if refreshed.Count <> 2 then failwith "Expected two panes" printfn "tools: 2 panes" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=Layout \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=Layout ``` Expected program output: ```text tools: 2 panes ``` ## Reuse a named window Look for `tools` before creating it. Two sequential calls return the same window ID, and the session has only `editor` and `tools`. This is useful for a setup command you run repeatedly. ```fsharp title="ReuseLayout.fs" open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let run () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let token = timeout.Token let! server = LibTmux.Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), token) let ensureTools () = task { let! captured = server |> Server.capture token SnapshotDepth.Windows let session = captured.Sessions |> Seq.find (fun s -> s.Name = "work-one") match session.Windows |> Seq.tryFind (fun window -> window.Name = "tools") with | Some window -> return window | None -> return! session.CreateWindowAsync( NewWindowRequest(Name = "tools", Command = "/bin/cat"), token) } let! first = ensureTools () let! second = ensureTools () if first.Id <> second.Id then failwith "Created duplicate windows" let! captured = server |> Server.capture token SnapshotDepth.Windows let session = captured.Sessions |> Seq.find (fun s -> s.Name = "work-one") if session.Windows.Count <> 2 then failwith "Expected editor and tools windows" printfn "one tools window after two calls" } [] let main _ = try run().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ```console $ dotnet build Query.fsproj --maxcpucount:1 -p:Example=ReuseLayout \ -p:DisableImplicitLibraryPacksFolder=true -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Query.fsproj --no-build -p:Example=ReuseLayout ``` Expected program output: ```text one tools window after two calls ``` ## Decide what repeated setup means The reuse example preserves the existing window and its running processes. It does not reset its pane count, layout or commands. Choose that policy deliberately when building a reusable workspace command. This check-then-create sequence assumes one writer. Concurrent callers can both observe an absent window and create duplicates. Serialize setup in your application when several callers share a session. A later failure also leaves earlier successful mutations in place; tmux commands are not a transaction. Use [filtering and queries](https://libtmux.org/en/fsharp/latest/concepts/queries/) for stricter target selection and [captured handles](https://libtmux.org/en/fsharp/latest/concepts/server-session-window-pane/) to understand refresh behavior. --- # Examples Source: https://libtmux.org/en/fsharp/latest/examples/ > Tested F# examples against an isolated tmux server. Start with [Capture pane output](https://libtmux.org/en/fsharp/latest/examples/capture-pane-output/) for a standalone program with imports, a project file, and private-server cleanup. ## Query, send and wait This complete program creates an isolated tmux server, filters its live sessions, sends a command and waits for its output, then reads another command's exit status. The owned server closes when the task completes. Follow the [package quickstart](https://libtmux.org/en/fsharp/latest/guides/quickstart/) to create an F# project and install LibTmux.FSharp, then use this as Program.fs. ```fsharp file="examples/LibTmux.FSharp.Quickstart/Program.fs" // fsharp-snippet: Quickstart open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token // A server of its own on a private socket, without user configuration. let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token // One session whose window runs a plain shell, whatever the user's login shell is. let! session = owned.Value |> Server.newSession token (SessionSpec.running "build" "/bin/sh") let! pane = session |> Session.activePane token // Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started" printfn "wait found: %b" started.Found // Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3" match result with | PaneRun.Exited status -> printfn "run: exit %d, output %A" status (List.ofSeq result.Output) | PaneRun.Ended -> printfn "run: the shell exited first" | PaneRun.NotStarted -> printfn "run: the shell was not at a prompt" | PaneRun.TimedOut -> printfn "run: still running" // List and filter: tmux narrows the listing, then every row is rechecked. let! found = owned.Value |> Server.sessions |> Query.where (SessionFields.name |> Filter.startsWith "bu") |> Query.list token printfn "sessions: %s" (String.Join(", ", [ for listed in found -> listed.Name ])) } runAsync().GetAwaiter().GetResult() // endfsharp-snippet ``` --- # Capture pane output Source: https://libtmux.org/en/fsharp/latest/examples/capture-pane-output/ > Run a complete F# program that captures output on an isolated tmux server. This complete F# program starts a private tmux server, sends a command, and captures the line it prints. It includes imports, setup, and cleanup. You need tmux and a Unix environment; no existing session is required. ## Read what's on screen The leading newline puts the output on a fresh row. Matching the whole line avoids mistaking the echoed command for its output. ```fsharp title="Program.fs" open System open System.Collections.Generic open System.Diagnostics open System.IO open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let capture () = task { let directory = Path.Combine("/tmp/libtmux-dotnet-dev", Guid.NewGuid().ToString("N")) Directory.CreateDirectory(directory) |> ignore let errors = ResizeArray() let mutable owned: OwnedServerScope option = None let mutable stopped = false try use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10.0)) let token = timeout.Token let environment = Dictionary() for name in [ "TMUX"; "TMUX_PANE"; "ENV"; "BASH_ENV" ] do environment[name] <- null let! scope = LibTmux.Server.CreateOwnedAsync( ServerConnectionOptions( SocketPath = Path.Combine(directory, "tmux.sock"), ConfigurationFile = "/dev/null", ChildEnvironment = environment), token) owned <- Some scope let! session = scope.Value.CreateSessionAsync( NewSessionRequest(Name = "capture", Command = "/bin/sh"), token) let! panes = session.GetPanesAsync(token) let pane = panes[0] do! pane |> Pane.sendKeys token (SendKeysRequest( Text = "printf '\\nlibtmux capture ready\\n'", Literal = true, Enter = true)) let elapsed = Stopwatch.StartNew() let mutable captured = false while not captured && elapsed.Elapsed < TimeSpan.FromSeconds(5.0) do let! lines = pane |> Pane.capture token (CapturePaneRequest()) captured <- Seq.contains "libtmux capture ready" lines if not captured then do! Task.Delay(25, token) if not captured then raise (TimeoutException("Output did not arrive within five seconds")) printfn "libtmux capture ready" with error -> errors.Add(error) // Keep the endpoint available for inspection if stopping the server fails. match owned with | Some scope -> try do! scope.DisposeAsync().AsTask() stopped <- true with error -> errors.Add(error) | None -> stopped <- not (File.Exists(Path.Combine(directory, "tmux.sock"))) if stopped then try Directory.Delete(directory, true) with error -> errors.Add(error) if errors.Count > 0 then raise (AggregateException(errors)) } [] let main _ = try capture().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ## Wait for output or completion [`Pane.sendKeys`]() and [`Pane.capture`]() return tasks. The program stops its owned server after either success or failure. It reports cleanup errors alongside the original error and keeps the socket directory if stopping fails. Capture reads screen state and scrollback, so output that has scrolled away may be absent. The program prints `libtmux capture ready` when its check passes and exits unsuccessfully if an operation fails. ## Setup and run Use an empty directory and save the files using the displayed names. You need .NET SDK 10.0.302. Restore into a project-local cache from NuGet so FSharp.Core matches the library’s lockfile. Save `Capture.fsproj` beside `Program.fs`. ```xml title="Capture.fsproj" Exe net10.0 ``` The commands pin the library revision used to run this program. ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 661287848a6cfb407f37114b25e8249a29f99e3f && dotnet build Capture.fsproj --maxcpucount:1 \ -p:DisableImplicitLibraryPacksFolder=true \ -p:RestorePackagesPath="$PWD/.packages" && dotnet run --project Capture.fsproj --no-build ``` ## Where this comes from The displayed files were compiled or loaded with their native tools and run on Linux with tmux 3.2a and 3.7c. The rendering checks preserve those file bytes. --- # Guides Source: https://libtmux.org/en/fsharp/latest/guides/ > Guides for LibTmux.FSharp. Connect to tmux and work with its sessions, windows, and panes. - [Getting started](https://libtmux.org/en/fsharp/latest/guides/getting-started/): Create a project and connect to a tmux server. - [Attaching to tmux](https://libtmux.org/en/fsharp/latest/guides/attaching-to-tmux/): Connect to an existing socket and leave its server running. - [Quick start](https://libtmux.org/en/fsharp/latest/guides/quickstart/): Install the package, capture server state, and choose a read operation. - [Querying and filtering](https://libtmux.org/en/fsharp/latest/guides/queries/): List objects, handle missing matches, and filter captured relations. - [Streams and cleanup](https://libtmux.org/en/fsharp/latest/guides/streams/): Consume events with bounded lifetimes and cancellation. - [.NET interoperation](https://libtmux.org/en/fsharp/latest/guides/interop/): Use core operations alongside the F# helpers. - [Execution modes](https://libtmux.org/en/fsharp/latest/guides/modes/): Choose commands, control mode, or bounded concurrent reads. - [Query fields](https://libtmux.org/en/fsharp/latest/guides/supported-query-fields/): Browse the fields supported by typed filters. --- # Getting started Source: https://libtmux.org/en/fsharp/latest/guides/getting-started/ > Create a project and connect to a tmux server. Start with the package [quickstart](https://libtmux.org/en/fsharp/latest/guides/quickstart/#quick-start) for installation and a complete `Program.fs` that runs against an owned tmux server. This guide continues from that path. `LibTmux.FSharp` is built on [LibTmux](https://github.com/libtmux/libtmux-dotnet/). Both packages are maintained in the `libtmux` organization by the same primary author. It keeps the core handles and task-based I/O. This example creates a server on a unique socket, creates one session, lists its pane, splits it, types a line into the new pane, and reads the resulting pane IDs. Both owned scopes dispose at the end of the task. ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let inspectOwnedSessionAsync (cancellationToken: CancellationToken) = task { let options = ServerConnectionOptions( SocketName = "libtmux-fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! ownedServer = options |> Server.createOwned cancellationToken let server = ownedServer.Value use! ownedSession = server.CreateOwnedSessionAsync(NewSessionRequest(Name = "demo", Command = "/bin/sh"), cancellationToken) let! panes = ownedSession.Value |> Session.panes |> Query.list cancellationToken let! second = panes[0] |> Pane.split cancellationToken (SplitPaneRequest(Command = "/bin/sh")) do! second |> Pane.sendLine cancellationToken "printf 'ready\\n'" let! found = server |> Server.tryFindPane cancellationToken second.Id let! captured = server |> Server.capture cancellationToken SnapshotDepth.Panes return captured.Panes |> Seq.map (fun pane -> pane.Id) |> Seq.toList, found |> Option.map (fun pane -> pane.Id) } ``` [`Pane.sendLine`]() completes after tmux accepts the line; it does not wait for the shell to print. To wait for what it prints, use [`Pane.sendAndWait`]() below. [`Server.capture`]() performs I/O, then the ID projection is local. [`Server.tryFindPane`]() returns `Some pane` after a successful lookup or [`None`]() when the pane is absent. This example returns two pane IDs and [`Some`]() of the new pane's ID. Failures and cancellation still propagate. ## Send, wait, read To act on what a pane prints, wait for it instead of sleeping. This complete program types a command and waits for its output, waits for a condition over the whole screen, and runs a command to its exit status: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 20.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-send-wait-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token let! session = owned.Value |> Server.newSession token (SessionSpec.running "work" "/bin/sh") let! pane = session |> Session.activePane token // Type a line and wait for what it prints. The screen before it and // the line's own echo do not count. let! ready = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 5.) "echo server ready" "server ready" // A condition sees every visible row each time the pane changes. do! pane |> Pane.sendLine token "seq 3" let! counted = pane |> Pane.waitUntil token (TimeSpan.FromSeconds 5.) (fun rows -> rows |> Seq.exists ((=) "3")) // Running a command waits for its exit status and returns its output. let! listing = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'a\\nb\\n'; exit 4" let! screen = pane |> Pane.capture token (CapturePaneRequest()) // One case for each way a wait can end; leaving one out draws a warning. let describe wait = match wait with | PaneWait.Found -> "found" | PaneWait.Printed -> "printed" | PaneWait.Stopped pattern -> "stopped by " + pattern | PaneWait.TimedOut -> "timed out" | PaneWait.Ended -> "the pane's program ended" printfn "ready: %s" (describe ready) printfn "counted: %s" (describe counted) match listing with | PaneRun.Exited status -> printfn "run: exit %d, output %A" status (List.ofSeq listing.Output) | PaneRun.Ended -> printfn "run: the shell exited first" | PaneRun.NotStarted -> printfn "run: the shell was not at a prompt" | PaneRun.TimedOut -> printfn "run: still running" printfn "screen shows the run: %b" (screen |> Seq.exists (fun row -> row = "a")) } runAsync().GetAwaiter().GetResult() ``` It prints: ```text ready: found counted: found run: exit 4, output ["a"; "b"] screen shows the run: true ``` ### Which wait | You want to | Call | It ends when | | --- | --- | --- | | Type a line and wait for its output | [`Pane.sendAndWait`]() | A later line contains the text. The screen before the line and the line's own echo do not count. | | Send keys by request and wait by patterns | [`Pane.sendAndWaitFor`]() | A pattern or stop pattern matches later output. The echo of literal text does not count; keys sent by name are not discounted. | | Wait for output you did not type | [`Pane.waitForText`](), [`Pane.waitFor`]() | A line contains the text. Text already on screen answers at once with [`PresentAtEntry`](). | | Wait for a condition over the whole screen | [`Pane.waitUntil`]() | The condition holds over the visible rows, including what a full-screen program draws. | | Run a command to its exit status | [`Pane.run`]() | The command exits. It returns the status and the lines it printed. | | Follow output as it prints | [`Control.watchPane`](), or [`Control.watchPanes`]() for several panes on one client | You stop reading, or the panes are gone; see [streams](https://libtmux.org/en/fsharp/latest/guides/streams/). | A wait's [`PaneWaitResult`]() falls under one [`PaneWait`]() case, so a match that leaves one out draws a compiler warning. `result.Found` is the same test as `PaneWait.Found`, for code that only asks whether the text appeared: | Case | Outcomes | It means | | --- | --- | --- | | `PaneWait.Found` | [`Matched`](), [`PresentAtEntry`]() | The text or a pattern appeared, before or during the wait. | | `PaneWait.Printed` | [`AnyOutput`]() | A wait with no pattern saw the pane print something. | | `PaneWait.Stopped pattern` | [`Stopped`]() | A stop pattern matched; the case carries it. | | `PaneWait.TimedOut` | [`TimedOut`]() | The time allowed ran out. | | `PaneWait.Ended` | `PaneExited`, `AlternateScreen` | The pane's program exited, or a full-screen program took over. | Calling [`Pane.sendLine`]() and then [`Pane.waitForText`]() for text the typed line contains can end on the shell's echo before the command runs; use [`Pane.sendAndWait`]() instead. Every wait also ends early when the pane's program exits during it or a full-screen program takes over, raises [`TmuxPaneException`]() on a pane whose program had already exited, and sleeps on the pane's own output through a control client rather than polling. A wait attaches that client and reads the pane through it, which costs a few milliseconds more than reading the screen once; for a series of waits on one session, `use! _ = Session.holdWaitClient ct session` keeps the client attached so each wait skips that and takes about as long as one read. Read output already there with [`Pane.capture`](), and wait for output still to come; the [wait latency benchmark](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/benchmarks/README.md#f-wait-latency) measures each. [`Pane.run`]() needs the pane at a prompt of `sh`, `ash`, `bash`, `dash`, `zsh` or a Korn shell; fish, PowerShell and a REPL are refused. It runs the command in a subshell, so `cd` and `export` do not persist into the pane. A command still running at the timeout keeps running, and the result reports [`TimedOut`](). One that prints more than scrollback holds reports [`LinesMissed`](), and its `Output` is then only what the pane still showed; write long output to a file instead. How each call reports what can go wrong: | What happened | A wait | [`Pane.run`]() | [`Pane.readSince`]() | | --- | --- | --- | --- | | The time ran out | `PaneWait.TimedOut` | `PaneRun.TimedOut`; the command may still be running | — | | The token was cancelled | [`OperationCanceledException`]() | [`LibTmuxException`](), matched by `TmuxFailure.MayHaveRun`, once the command was sent | [`OperationCanceledException`]() | | The pane's program exits during the call | `PaneWait.Ended` | `PaneRun.Ended`, within five seconds | reads the pane as it stands | | The program had already exited | [`TmuxPaneException`]() | [`TmuxPaneException`]() | [`TmuxPaneException`]() on a read without a position | | tmux no longer has the pane | [`TmuxObjectNotFoundException`]() | [`TmuxObjectNotFoundException`]() | [`TmuxObjectNotFoundException`]() | A run cancelled once its command was sent may still be running, so it raises what `TmuxFailure.MayHaveRun` matches rather than an [`OperationCanceledException`](). A cancelled task loses that: [`Async.AwaitTask`]() and `Task.Wait` raise a bare [`TaskCanceledException`]() in its place, where a failed task keeps its exception inside an `AggregateException`, which the [`TmuxFailure`]() patterns look through. In an `async` workflow, await with [`TmuxAsync.awaitTask`](), which also keeps a tmux client's cancellation. One handler covers both: ```fsharp open System open System.Threading open LibTmux open LibTmux.FSharp let runTestsAsync (cancellationToken: CancellationToken) (pane: Pane) = task { try let! result = pane |> Pane.run cancellationToken (TimeSpan.FromMinutes 5.) "make test" match result with | PaneRun.Exited 0 -> return "passed" | PaneRun.Exited status -> return $"failed with status {status}" | PaneRun.Ended -> return "the shell exited before the tests finished" | PaneRun.NotStarted -> return "the shell was not at a prompt" | PaneRun.TimedOut -> return "still running after five minutes" with // Cancelled or lost once the command was sent: it may be running. // A tmux client cancelled mid-call matches too, even before the // command went, erring towards "may have run". That cancellation // is an OperationCanceledException, so this case comes first. | TmuxFailure.MayHaveRun _ -> return "may have run; read the pane before trying again" // Cancelled between tmux calls, before the command was sent. | :? OperationCanceledException -> return "cancelled before it was sent" } ``` ## Describe a session Describe a session as F# records, then create it in one call. [`Server.newSession`]() makes the session with its first window, adds each later window, and splits each window's panes in order. A chain adds commands tmux runs together, each acting on what the one before made, and [`Server.within`]() bounds every command a handle sends: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 20.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-build-session-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token // Describe the session, then create it in one call: the first window is // the one tmux makes with the session, and each split goes beside the // pane before it, sized in cells or as a share of the space it splits. let dev = { SessionSpec.named "dev" with Windows = [ { WindowSpec.named "editor" with Command = Some "exec sleep 60" } { WindowSpec.named "logs" with Command = Some "exec sleep 60" Splits = [ { SplitSpec.empty with Direction = Some PaneDirection.Right Size = Some(SplitSize.Percent 30) Command = Some "exec sleep 60" } { SplitSpec.empty with Size = Some(SplitSize.Cells 8) Command = Some "exec sleep 60" } ] } ] } let! session = owned.Value |> Server.newSession token dev // A chain runs in one tmux invocation; each step acts on what the one // before made. let! _ = owned.Value |> Chain.start |> Chain.newWindow session "watch" |> Chain.splitLeftRight |> Chain.sendLine "exec sleep 60" |> Chain.run token // Every command through this handle, and the handles taken from it, // gives tmux five seconds. let bounded = owned.Value |> Server.within (TimeSpan.FromSeconds 5.) let! windows = bounded |> Server.windows |> Query.list token for window in windows do let! panes = window |> Window.panes |> Query.list token printfn "%s: %d panes" window.Name panes.Count } runAsync().GetAwaiter().GetResult() ``` It prints: ```text editor: 1 panes logs: 3 panes watch: 2 panes ``` `LibTmux.Workspace` builds the same kind of session from a tmuxp workspace file, and can wait for each shell's prompt before sending it commands. Add the package and describe the session, or parse tmuxp YAML: ```console $ dotnet package add LibTmux.Workspace --prerelease ``` ```fsharp run open System.Threading open LibTmux open LibTmux.Workspace let buildWorkspaceAsync (cancellationToken: CancellationToken) (server: Server) = task { let description = WorkspaceFile( sessionName = "build", windows = [ WorkspaceWindow( windowName = "editor", panes = [ WorkspacePane([ "printf 'editing\\n'" ]); WorkspacePane() ] ) WorkspaceWindow(windowName = "logs", panes = [ WorkspacePane([ "printf 'tailing\\n'" ]) ]) ] ) // Creates the session, its windows and panes, and sends each pane its // commands once its shell is ready. let! built = WorkspaceBuilder(server).BuildAsync(description, cancellationToken) return built.Session.Name, [ for window in built.Windows -> window.Name ] } ``` `WorkspaceBuilder` creates every pane and applies each layout before typing, then waits for each pane's shell to show a prompt; pass `PaneReadiness.Never` to type at once. `BuildAsync` returns the session and windows it created. `WorkspaceFile.Parse` reads the same description from tmuxp YAML text. ## Read a snapshot For a server the caller already owns, capture once and use F# sequences: ```fsharp run open System.Threading open LibTmux open LibTmux.FSharp let readPaneCommandsAsync (cancellationToken: CancellationToken) (server: Server) = task { let! captured = server |> Server.capture cancellationToken SnapshotDepth.Panes return captured.Panes |> Seq.choose Pane.currentCommand |> Seq.toList } ``` [`Server.capture`]() performs I/O. The sequence projection only reads the captured snapshot. A null command becomes [`None`](); an uncaptured command still raises [`IncompleteSnapshotException`](). Use [portable filters](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/filters.md) when the condition must become a [`QueryDocument`](); use [`Seq.filter`]() for application-specific snapshot work. The [F# API reference](https://libtmux.org/en/fsharp/latest/reference/) is generated from compiled signatures and XML summaries. Use the core request records and entity methods for mutations. Task cancellation stops waiting; it does not undo a mutation that tmux received. The [F# example](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/examples/LibTmux.FSharp.Examples) is compiled and run against an owned tmux server. It checks that portable and native F# queries select the same panes on both target frameworks. CI repeats it against the freshly packed F# package through an isolated cache. A successful run writes `PASS F# snapshot and portable query example`. From the repository root, run that example against real tmux: ```console $ dotnet run \ --project examples/LibTmux.FSharp.Examples/LibTmux.FSharp.Examples.fsproj \ --framework net8.0 ``` --- # Attaching to tmux Source: https://libtmux.org/en/fsharp/latest/guides/attaching-to-tmux/ > Connect to an existing tmux server and find a session with F#. Connect a [`Server`]() to an explicit socket and find the existing `work` session. The program prints its name and leaves the tmux server running. It reports an error if the connection fails or the session is absent. This controls tmux from your program. To open a session in your terminal, use `tmux attach-session`; the [shared guide](https://libtmux.org/en/tmux/guides/attaching-to-tmux/) covers interactive attachment and detaching. ## Connect to an existing server Save the complete program as `Program.fs`. `LIBTMUX_SOCKET_PATH` selects the existing server. The launcher below supplies a private socket for trying the example. ```fsharp title="Program.fs" open System open System.Threading open System.Threading.Tasks open LibTmux let connect () = task { let socket = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH") if String.IsNullOrEmpty(socket) then invalidOp "Set LIBTMUX_SOCKET_PATH to an existing socket" use timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5.0)) let! server = Server.ConnectAsync( ServerConnectionOptions(SocketPath = socket), timeout.Token) let! exists = server.HasSessionAsync("work", cancellationToken = timeout.Token) if not exists then invalidOp "The work session does not exist" printfn "work" } [] let main _ = try connect().GetAwaiter().GetResult() 0 with error -> eprintfn "%O" error 1 ``` ## Setup and run Use an empty directory on Linux with Git and tmux 3.2a or newer installed. This example was checked with .NET SDK 10.0.302. Save this file beside the program using the displayed filename. ```xml title="Connect.fsproj" Exe net10.0 ``` Save the launcher as `run.sh`. It starts an isolated tmux server, runs the program, checks that the session still exists, then stops only that server. Cleanup runs after failures too. A failed shutdown keeps its socket directory and prints its location for inspection. ```sh title="run.sh" #!/bin/sh set -eu binary=$(command -v tmux) directory=$(mktemp -d "${TMPDIR:-/tmp}/libtmux-fsharp-attach.XXXXXX") socket="$directory/tmux.sock" cleanup() { status=$? trap - 0 HUP INT TERM if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then printf 'Cannot stop tmux; kept %s\n' "$directory" >&2 exit 1 fi rm -rf "$directory" || exit 1 exit "$status" } trap cleanup 0 trap 'exit 1' HUP INT TERM unset TMUX TMUX_PANE export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary" "$binary" -S "$socket" -f /dev/null new-session -d -s work /bin/cat "$@" "$binary" -S "$socket" has-session -t '=work' ``` Fetch the verified library revision, build, and run: ```console $ git clone https://github.com/libtmux/libtmux-dotnet libtmux-source && git -C libtmux-source checkout 661287848a6cfb407f37114b25e8249a29f99e3f && dotnet build Connect.fsproj --maxcpucount:1 \ -p:DisableImplicitLibraryPacksFolder=true \ -p:RestorePackagesPath="$PWD/.packages" && sh run.sh dotnet run --project Connect.fsproj --no-build ``` The program prints `work`. To use an existing server of your own, set `LIBTMUX_SOCKET_PATH` to its socket and run the program without the launcher. That launcher is responsible for the demonstration server's lifetime. ## Find or create a session The example only looks up a session. If your application creates a session after an unsuccessful lookup, another client may create the same name between those operations. Handle the creation error instead of assuming the lookup reserves the name. For a complete program that starts and owns its server, see [Capture pane output](https://libtmux.org/en/tmux/examples/capture-pane-output/). --- # Quick start Source: https://libtmux.org/en/fsharp/latest/guides/quickstart/ > Install the package, capture server state, and choose a read operation. Drive tmux from F#: send keys and wait for what a pane prints, run a command to its exit status, and list and filter sessions, windows, panes and clients with typed filters tmux evaluates itself. Every function works on the [LibTmux](https://www.nuget.org/packages/LibTmux) core objects. The [F# package](https://www.nuget.org/packages/LibTmux.FSharp) and core share a repository, release version, and primary author in the `libtmux` organization. [![build](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet.yml/badge.svg)](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet.yml) [![tmux matrix](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet-tmux.yml/badge.svg)](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet-tmux.yml) [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/LICENSE) [Run isolated tmux](https://libtmux.org/en/fsharp/latest/guides/quickstart/#quick-start) · [Connect to running tmux](https://libtmux.org/en/fsharp/latest/guides/quickstart/#existing-tmux) · [Send, wait, read](https://libtmux.org/en/fsharp/latest/guides/getting-started/#send-wait-read) · [Query tmux](https://libtmux.org/en/fsharp/latest/guides/queries/) · [Stream events](https://libtmux.org/en/fsharp/latest/guides/streams/) Find a shell, send it keys, wait for its output, and run a command: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runInShellAsync (cancellationToken: CancellationToken) (server: Server) = task { // List and filter: tmux narrows the listing, then every row is rechecked. let! shells = server |> Server.panes |> Query.where (PaneFields.currentCommand |> Filter.oneOf [ "bash"; "sh"; "zsh" ]) |> Query.list cancellationToken match shells |> Seq.tryHead with | None -> return None | Some pane -> // Type a command and wait for what it prints, not for its echo. let! ready = pane |> Pane.sendAndWait cancellationToken (TimeSpan.FromSeconds 10.) "echo ready" "ready" // Run a command to its exit status and read what it printed. let! listing = pane |> Pane.run cancellationToken (TimeSpan.FromSeconds 30.) "ls /" match listing with | PaneRun.Exited status -> return Some(ready.Found, status, listing.Output) | PaneRun.Ended | PaneRun.NotStarted | PaneRun.TimedOut -> return None } ``` Building a query reads nothing; [`Query.list`]() asks tmux, which drops panes that cannot match, and checks every row it returns. [`Pane.sendAndWait`]() types the line, then waits for a later line to contain the text; the screen before it and the line's own echo do not count. It sleeps on the pane's output instead of polling, and ends early if the program exits while it waits. [`Pane.run`]() returns the lines the command printed, and [`PaneRun`]() tells a command that exited, with its status, from one whose shell exited first, one that never started, and one that ran out of time. `server` comes from [`Server.createOwned`](), which the quick start below uses to run these steps on an isolated server. Pass a server from [`Server.connect`]() only with care: the sample types into the first shell it finds, and on a tmux already running that may be the terminal you are reading. Alpha API: pin a package version and upgrade deliberately. The walkthrough uses .NET SDK 10 and tmux 3.2a through 3.7c on Linux or macOS. The package targets `net8.0` and `net10.0`. ## Choose a call ### Connect | Need | F# call | Returns | | --- | --- | --- | | Start a server you own | `options \|> Server.createOwned ct` | [`OwnedServerScope`]() to `use!` | | Attach to a running server | `options \|> Server.connect ct` | [`Server`]() | | Bound every command's time | `Server.within timeout server` | [`Server`]() | ### Find | Need | F# call | Returns | | --- | --- | --- | | List and filter | `Server.panes server \|> Query.where filter \|> Query.list ct` | `IReadOnlyList` | | One session's or window's panes | `Session.panes session \|> Query.list ct`; [`Window.panes`]() alike | `IReadOnlyList` | | Panes showing some text | `Server.panes server \|> Query.showing search \|> Query.list ct` | `IReadOnlyList` | | Exactly one match | `Query.exactlyOne ct query`; [`Query.tryExactlyOne`]() under NativeAOT | `Result<'T, CardinalityError>`; `'T option` | | Find, or create when absent | `Query.atMostOne ct query` | `'T option`; several raise | | One object by ID | `Server.tryFindPane ct id server` | `Pane option` | | The pane a session or window shows | `Session.activePane ct session`, `Window.activePane ct window` | [`Pane`]() | ### Type, wait and run | Need | F# call | Returns | | --- | --- | --- | | Type a line, or press a key | `Pane.sendLine ct line pane`; `Pane.pressKey ct "C-c" pane` | [`Task`]() | | Type a line, wait for its output | `Pane.sendAndWait ct timeout line text pane`; [`Pane.sendAndWaitFor`]() for keys and patterns | [`PaneWaitResult`](); [`.Found`](), or match [`PaneWait`]() | | Wait for output you did not type | `Pane.waitForText ct timeout text pane`; [`Pane.waitFor`]() for patterns | [`PaneWaitResult`]() | | Wait for a screen condition | `Pane.waitUntil ct timeout condition pane` | [`PaneWaitResult`]() | | Many waits on one session | `use! _ = Session.holdWaitClient ct session` | `IAsyncDisposable`; each wait skips attaching a client, about 5 ms | | Run a command to its exit status | `Pane.run ct timeout command pane` | [`PaneRunResult`](); match [`PaneRun`]() | | Read the screen | `Pane.capture ct request pane` | `IReadOnlyList` | | What a pane printed since last time | `Pane.readSince ct position pane` | [`PaneOutputSince`](); pass its `Position` next time | | Find text on one screen | `Pane.findOnScreen ct search pane` | row `int option` | ### Build | Need | F# call | Returns | | --- | --- | --- | | Split a pane | `Pane.split ct request pane` | the new [`Pane`]() | | Rename a session or window | `Window.rename ct name window` | a handle with the new name | | Make a window or pane current | `Window.select ct window`, `Pane.select ct pane` | a handle with the state afterwards | | Kill a session, window or pane | `Pane.kill ct pane` | [`Task`]() | | Arrange, resize or move a window | `Window.selectLayout ct layout window`, `Window.resize ct request window`, `Window.move ct request window` | a handle with the state afterwards | | Title, resize, swap, respawn or clear a pane | `Pane.setTitle ct title pane`, [`Pane.resize`](), [`Pane.swap`](), [`Pane.respawn`](), [`Pane.clearHistory`]() | the handle, or [`Task`]() | | Add a window to a session | `Session.newWindow ct request session` | the new [`Window`]() | | Create a session running one command | `Server.newSession ct (SessionSpec.running name command) server` | [`Session`]() | | Create a session with windows | `Server.newSession ct spec server` | [`Session`]() | | Several commands, one tmux call | `Chain.start server \|> … \|> Chain.run ct` | [`TmuxCommandResult`]() | | Read or set a typed option | `Options.get ct key options` | the key's value type | ### Observe | Need | F# call | Returns | | --- | --- | --- | | A whole object graph | `Server.capture ct depth server` | snapshot [`Server`]() | | Live server state | `Mirror.start ct session` | [`ServerMirror`]() | | Events as they happen | `Control.withSession ct work server` | cold [`IAsyncEnumerable`]() streams | | One pane's output as it prints | `Control.watchPane pane client`; [`Control.watchPanes`]() for several | events; match [`PaneWatch`]() | | An assistant on the same tmux | the `LibTmux.Mcp` server | [MCP guide](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/mcp.md) | ### Recover | Need | F# call | Returns | | --- | --- | --- | | Tell failures apart | `TmuxFailure.NotSent`, `Ran`, `MayHaveRun` | active patterns | | Retry only unsent work | `Retry.ifNotSent ct retries operation`, or `Retry.ifNotSentAfter ct delays operation` | the operation's result | | Await a call in an `async` workflow | `TmuxAsync.awaitTask task`, `TmuxAsync.awaitUnitTask task` | [`Async`](); keeps what `MayHaveRun` matches | ### Caveats - **Queries:** tmux narrows each listing where it can, and every row is rechecked. - **Waits:** [`Pane.sendAndWait`]() ignores the line's echo. Every wait ends early when the program exits, and raises [`TmuxPaneException`]() if it already had. [Which wait](https://libtmux.org/en/fsharp/latest/guides/getting-started/#which-wait) compares them and their [`PaneWait`]() outcomes. - **Runs:** [`Pane.run`]() needs a POSIX shell prompt; it refuses fish, PowerShell and a REPL. - **Bounds:** a handle from [`Server.within`]() shares its bound with the sessions, windows and panes taken from it. - **Live state:** [`Mirror.start`]() returns a mirror to `use!`. [`Mirror.waitUntil`]() raises when no view matches in time; [`Mirror.tryWaitUntil`]() returns [`None`](). - **Events:** inside [`Control.withSession`](), [`Control.events`](), [`Control.watchPane`]() and [`Control.watchPanes`]() read the client it opens, one reader at a time. Captured sessions, windows, panes, and IDs are the core .NET types. A window linked into more than one session has contextual placements; filtering keeps their order and multiplicity. An uncaptured relationship raises [`IncompleteSnapshotException`]() rather than behaving as empty. ## Quick start Install tmux and make sure it is available on `PATH`. Create an F# console project: ```console $ dotnet new console --language F# --framework net8.0 --output tmux-demo ``` Enter it: ```console $ cd tmux-demo ``` Add [LibTmux.FSharp](https://www.nuget.org/packages/LibTmux.FSharp) from NuGet; it brings in the matching `LibTmux` core package and records the selected prerelease version in the project: ```console $ dotnet package add LibTmux.FSharp --prerelease ``` Replace `Program.fs` with this complete program: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 30.) let token = deadline.Token // A server of its own on a private socket, without user configuration. let options = ServerConnectionOptions( SocketName = "fsharp-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token // One session whose window runs a plain shell, whatever the user's login shell is. let! session = owned.Value |> Server.newSession token (SessionSpec.running "build" "/bin/sh") let! pane = session |> Session.activePane token // Type a command and wait for what it prints, not for its echo. let! started = pane |> Pane.sendAndWait token (TimeSpan.FromSeconds 10.) "echo build started" "build started" printfn "wait found: %b" started.Found // Run a command to its exit status and read what it printed. let! result = pane |> Pane.run token (TimeSpan.FromSeconds 10.) "printf 'ok\\n'; exit 3" match result with | PaneRun.Exited status -> printfn "run: exit %d, output %A" status (List.ofSeq result.Output) | PaneRun.Ended -> printfn "run: the shell exited first" | PaneRun.NotStarted -> printfn "run: the shell was not at a prompt" | PaneRun.TimedOut -> printfn "run: still running" // List and filter: tmux narrows the listing, then every row is rechecked. let! found = owned.Value |> Server.sessions |> Query.where (SessionFields.name |> Filter.startsWith "bu") |> Query.list token printfn "sessions: %s" (String.Join(", ", [ for listed in found -> listed.Name ])) } runAsync().GetAwaiter().GetResult() ``` Run it: ```console $ dotnet run ``` It prints: ```text wait found: true run: exit 3, output ["ok"] sessions: build ``` [`Server.createOwned`]() starts a server on a unique socket, with the `tmux` on `PATH`; set [`ServerConnectionOptions.TmuxBinaryPath`]() to use another. `use!` stops it when the task ends. The wait succeeds whether the line appeared before or after it began, and the run's exit status comes from the shell, not from reading the screen. The listing reaches tmux as a filter, so tmux returns only the sessions that match. The [quickstart source](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/examples/LibTmux.FSharp.Quickstart/Program.fs) is the published block. From a checkout, `dotnet run --project examples/LibTmux.FSharp.Quickstart` runs it against the source. CI instead passes `-p:UsePackageReferences=true`, restores only `LibTmux.FSharp` from freshly packed artifacts, runs the program against real tmux on both target frameworks, and compares what it prints with the block above. ## Existing tmux `options |> Server.connect ct` attaches to a server already running on the socket the options name; use it in place of [`Server.createOwned`]() for a server your program did not start. [`Server.connect`]() never starts tmux. Default options resolve the default socket; [socket selection](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/src/LibTmux/README.md#where-a-bare-connect-lands) explains the configuration order. Code running inside a tmux pane can use [`Server.FromEnvironment()`]() to locate that pane's server. ## Keep going - [Send, wait and read; split panes](https://libtmux.org/en/fsharp/latest/guides/getting-started/): owned scopes, waits, runs and live mutation. - [Query tmux](https://libtmux.org/en/fsharp/latest/guides/queries/), [filter captured objects](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/filters.md) and [supported fields](https://libtmux.org/en/fsharp/latest/guides/supported-query-fields/): every level, pushdown, screen search, relations and capture depth. - [Choose an execution mode](https://libtmux.org/en/fsharp/latest/guides/modes/) and [stream events](https://libtmux.org/en/fsharp/latest/guides/streams/): scoped clients, cold streams, pane watches, cancellation, and command chains. - [Test code that drives tmux](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/testing.md): a private server per test, waits instead of sleeps, and CI setup. - [Share one server across tasks](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/concurrency.md): what handles, waits, runs, control clients and mirrors share. - [Run tmux work in a service](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/service.md): one bounded handle, stopping, retries, one writer per pane, and telemetry. - [Call the core from F#](https://libtmux.org/en/fsharp/latest/guides/interop/) and [browse signatures](https://libtmux.org/en/fsharp/latest/reference/). - [Share tmux with an assistant](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/mcp.md): the `LibTmux.Mcp` server, a shared socket, and which F# function each tool matches. - [Read benchmark records](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/benchmarks/README.md): measured core execution modes and benchmark methods. ## Compatibility | Area | Contract and verification | | --- | --- | | .NET | Targets .NET 8 and .NET 10 and uses the matching `LibTmux` package version. | | F# | Requires FSharp.Core 8.0.100 or newer, so an application keeps its SDK's FSharp.Core. Required CI builds and runs a consumer with the .NET 8 SDK's F# compiler and implicit FSharp.Core. | | tmux | Required Linux CI runs the repository's F# integration example against tmux 3.2a, 3.3a, 3.4, 3.5, 3.6, 3.7a, 3.7b, and 3.7c on both target frameworks. The README quickstart runs against the runner's tmux in the package workflow. | | Operating systems | Linux is required CI. An advisory macOS arm64 job runs the example with Homebrew tmux on manual dispatch. Native Windows is unsupported: the core marks its tmux calls `[UnsupportedOSPlatform("windows")]`. WSL runs the Linux build, which CI does not exercise separately. | | Trimming and NativeAOT | Portable filters bind fields without reflection. A Linux consumer publishes and runs captured snapshots, a tmux query, portable filters with relations and regex, and native [`Seq`]() predicates under NativeAOT and trimming on both frameworks. | [`Selection.exactlyOne`]() and [`Query.exactlyOne`]() return FSharp.Core's [`Result`](), whose compiler-generated `ToString` formats through `printf`, which NativeAOT publication rejects. Under NativeAOT, read one row with [`Query.atMostOne`](), which still raises on several matches, or with [`Query.tryExactlyOne`]() where none and several may be treated alike. The NativeAOT consumer runs both. --- # Querying and filtering Source: https://libtmux.org/en/fsharp/latest/guides/queries/ > List objects, handle missing matches, and filter captured relations. Describe a listing with [`Server.sessions`](), [`Server.windows`](), [`Server.panes`](), [`Server.clients`](), [`Session.windows`](), [`Session.panes`]() or [`Window.panes`](), narrow it with [`Query.where`](), [`Query.showing`]() or [`Query.whereUnsafe`](), and read it with [`Query.list`](), [`Query.exactlyOne`](), [`Query.atMostOne`]() or [`Query.tryExactlyOne`](). tmux drops rows that cannot match before they are read, and every row is rechecked against the portable filter. Use [`Seq.filter`]() for application predicates over objects you already hold, and [filters](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/docs/fsharp/filters.md) to apply a portable filter to captured objects and their relations. [`Query.atMostOne`]() returns [`None`]() only when nothing matched and raises when several did, so it suits finding an object or creating it when absent; [`Query.tryExactlyOne`](), like FSharp.Core's [`Seq.tryExactlyOne`](), returns [`None`]() for both. In an application published with NativeAOT, use either of them: [`Query.exactlyOne`]() returns FSharp.Core's [`Result`](), whose compiler-generated `ToString` formats through `printf`, which NativeAOT publication rejects. These complete programs require .NET 8 or 10 and tmux on Linux or macOS. Run the commands from this repository's root. Each block can also replace `Program.fs` in a console project referencing this revision of `LibTmux.FSharp`. Each program runs the first `tmux` on `PATH`; set [`ServerConnectionOptions.TmuxBinaryPath`]() to run another. Each program creates a uniquely named server. Its `use!` bindings dispose the control clients, sessions, and server when the task finishes or fails. Your existing tmux sessions are unaffected. ## At a glance Each binding is one task; its annotation is the type the compiler checks. ```fsharp open System.Collections.Generic open System.Threading open LibTmux open LibTmux.FSharp let queryShapesAsync (ct: CancellationToken) (server: Server) (session: Session) (held: Pane list) = task { // Describing a listing reads nothing. let named = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.startsWith "bu") let! (all: IReadOnlyList) = named |> Query.list ct let! (one: Result) = named |> Query.exactlyOne ct let! (maybe: Session option) = named |> Query.tryExactlyOne ct let! (atMost: Session option) = named |> Query.atMostOne ct let! (inSession: IReadOnlyList) = session |> Session.panes |> Query.list ct let! (showingError: IReadOnlyList) = server |> Server.panes |> Query.showing (ScreenSearch.Text "ERROR:") |> Query.list ct let! (withTail: IReadOnlyList) = server |> Server.sessions |> Query.where (WindowFields.name |> Filter.eq "tail" |> Filter.any SessionFields.windows) |> Query.list ct let! (active: IReadOnlyList) = server |> Server.panes |> Query.whereUnsafe (UnsafeTmuxFilter "#{pane_active}") |> Query.list ct // Objects already in hand are filtered locally. let editors: IReadOnlyList = held |> Query.matching (PaneFields.currentCommand |> Filter.oneOf [ "nvim"; "vim" ]) return all, one, maybe, atMost, inSession, showingError, withTail, active, editors } ``` ## Query every level the same way This program creates a `build` session and a `logs` session whose pane prints an error. It filters sessions by name and by a window they contain, confines a query to one session and one window, finds the pane showing the error, and passes a raw tmux filter through. A filter that reads a relation captures only the sessions tmux keeps. ```console $ dotnet run \ --project examples/LibTmux.FSharp.Examples/LibTmux.FSharp.Examples.fsproj \ --configuration Release \ --framework net10.0 \ -p:ExampleProgram=Queries ``` ```fsharp open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 10.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-queries-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token let! build = owned.Value.CreateSessionAsync( NewSessionRequest(Name = "build", WindowName = "make", Command = "/bin/sh"), token ) let! _ = owned.Value.CreateSessionAsync( NewSessionRequest( Name = "logs", WindowName = "tail", Command = "printf 'ERROR: disk full\\n'; exec sleep 60" ), token ) let server = owned.Value let! logs = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "logs") |> Query.list token let! logPane = logs[0] |> Session.panes |> Query.list token let! logged = logPane[0] |> Pane.waitForText token (TimeSpan.FromSeconds 5.) "ERROR:" // tmux narrows each listing itself; every row is then rechecked. let! named = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.startsWith "bu") |> Query.exactlyOne token // A relation filter reads only the sessions whose windows can match. let! tailing = server |> Server.sessions |> Query.where (WindowFields.name |> Filter.eq "tail" |> Filter.any SessionFields.windows) |> Query.list token // Session and window scopes use the same functions. let! make = build |> Session.windows |> Query.where (WindowFields.name |> Filter.eq "make") |> Query.tryExactlyOne token // tryExactlyOne is None when no window, or several, matched. let! makePanes = task { match make with | Some window -> let! panes = window |> Window.panes |> Query.list token return Some panes.Count | None -> return None } // tmux searches each pane's visible rows, as find-window does. let! showingErrors = server |> Server.panes |> Query.showing (ScreenSearch.Text "ERROR:") |> Query.list token let! errorRow = showingErrors[0] |> Pane.findOnScreen token (ScreenSearch.Text "disk full") // A raw tmux filter is the escape hatch; nothing rechecks it. let! active = server |> Server.panes |> Query.whereUnsafe (UnsafeTmuxFilter "#{pane_active}") |> Query.list token // Find a session or create it: atMostOne is None only when nothing // matched, and raises when several do. let deploy = server |> Server.sessions |> Query.where (SessionFields.name |> Filter.eq "deploy") let! existing = deploy |> Query.atMostOne token if existing.IsNone then let! _ = owned.Value |> Server.newSession token (SessionSpec.running "deploy" "exec sleep 60") () let! found = deploy |> Query.atMostOne token let! several = task { try let! _ = server |> Server.sessions |> Query.atMostOne token return "one or none" with :? InvalidOperationException -> return "refused" } printfn "logged: %b" logged.Found printfn "named: %A" (named |> Result.map (fun session -> session.Name)) printfn "tailing: %A" [ for session in tailing -> session.Name ] printfn "make panes: %A" makePanes printfn "error row: %A" errorRow printfn "active panes: %d" active.Count printfn "deploy: absent %b, then found %b" existing.IsNone found.IsSome printfn "at most one of every session: %s" several } runAsync().GetAwaiter().GetResult() ``` It prints: ```text logged: true named: Ok "build" tailing: ["logs"] make panes: Some 1 error row: Some 1 active panes: 2 deploy: absent true, then found true at most one of every session: refused ``` [`Query.showing`]() and [`Pane.findOnScreen`]() search only the rows on screen, as `find-window -C` does. To search history, capture the pane with [`Pane.capture`]() and filter the lines. tmux evaluates case-sensitive string, flag, identifier and count conditions; case-insensitive and regex conditions are applied only by the recheck. ## List sessions, windows, panes, and clients This program creates two detached sessions, each with one window and pane. The four reads report those objects and an empty client list. A listing captures scalar fields; it does not populate child relations. ```console $ dotnet run \ --project examples/LibTmux.FSharp.Examples/LibTmux.FSharp.Examples.fsproj \ --configuration Release \ --framework net10.0 \ -p:ExampleProgram=ServerListings ``` ```fsharp open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 10.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-listings-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token use! _demo = owned.Value.CreateOwnedSessionAsync( NewSessionRequest(Name = "demo", WindowName = "shell", Command = "/bin/sh"), token ) use! _worker = owned.Value.CreateOwnedSessionAsync( NewSessionRequest(Name = "worker", WindowName = "jobs", Command = "/bin/sh"), token ) let server = owned.Value let! sessions = server |> Server.sessions |> Query.list token let! windows = server |> Server.windows |> Query.list token let! panes = server |> Server.panes |> Query.list token let! clients = server |> Server.clients |> Query.list token for session in sessions do printfn "Session: %s (%O)" session.Name session.Id printfn "Windows: %d; panes: %d; clients: %d" windows.Count panes.Count clients.Count } runAsync().GetAwaiter().GetResult() ``` It prints: ```text Session: demo ($0) Session: worker ($1) Windows: 2; panes: 2; clients: 0 ``` The two sessions each hold one window and one pane, and no client is attached. Window listings preserve placements: a window linked into several sessions can appear several times. ## Look up an object and handle absence Session, window, and pane lookups take typed IDs. Client lookup takes an exact, case-sensitive name. The program attaches a control client so it can exercise successful client lookup without an interactive terminal. ```console $ dotnet run \ --project examples/LibTmux.FSharp.Examples/LibTmux.FSharp.Examples.fsproj \ --configuration Release \ --framework net10.0 \ -p:ExampleProgram=ServerLookups ``` ```fsharp open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 10.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-lookups-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token use! demo = owned.Value.CreateOwnedSessionAsync( NewSessionRequest(Name = "demo", WindowName = "shell", Command = "/bin/sh"), token ) let server = owned.Value let! windows = server |> Server.windows |> Query.list token let! panes = server |> Server.panes |> Query.list token let window = windows |> Seq.exactlyOne let pane = panes |> Seq.exactlyOne use! _control = server |> Control.enter token let! clients = server |> Server.clients |> Query.list token let client = clients |> Seq.exactlyOne let! foundSession = server |> Server.tryFindSession token demo.Value.Id let! foundWindow = server |> Server.tryFindWindow token window.Id let! foundPane = server |> Server.tryFindPane token pane.Id let! foundClient = server |> Server.tryFindClient token client.Name let! missingSession = server |> Server.tryFindSession token (SessionId Int32.MaxValue) let! missingWindow = server |> Server.tryFindWindow token (WindowId Int32.MaxValue) let! missingPane = server |> Server.tryFindPane token (PaneId Int32.MaxValue) let! missingClient = server |> Server.tryFindClient token (client.Name + "-missing") printfn "session: %A" (foundSession |> Option.map (fun found -> found.Name)) printfn "window: %A" (foundWindow |> Option.map (fun found -> found.Name)) printfn "pane: %A" (foundPane |> Option.map (fun found -> found.Id = pane.Id)) printfn "client: %A" (foundClient |> Option.map (fun found -> found.Name = client.Name)) printfn "missing: %A" [ missingSession.IsSome missingWindow.IsSome missingPane.IsSome missingClient.IsSome ] } runAsync().GetAwaiter().GetResult() ``` It prints: ```text session: Some "demo" window: Some "shell" pane: Some true client: Some true missing: [false; false; false; false] ``` Each lookup finds the object it was given, and each lookup of an ID or name that does not exist returns [`None`](). [`None`]() means a successful read found no matching object. Connection failures, command failures, stale server generations, and cancellation propagate as errors. --- # Streams and cleanup Source: https://libtmux.org/en/fsharp/latest/guides/streams/ > Consume events with bounded lifetimes and cancellation. Captured snapshots are replayable local observations. Control-mode events are live, ordered, destructive observations. They are not a replayable [`seq`](), and a control client has one event stream with one consumer: a second reader started while the first is reading raises [`InvalidOperationException`](). ## Choose a wait or a stream To wait for one thing a pane prints, use [`Pane.sendAndWait`](), [`Pane.waitForText`](), [`Pane.waitUntil`]() or [`Pane.run`](); each returns a single result, and [which wait](https://libtmux.org/en/fsharp/latest/guides/getting-started/#which-wait) compares them. The waits share one control client per session while any wait on it runs, and [`Pane.run`]() learns its exit status from a private `wait-for` channel. Read a stream when the caller reacts to events as they arrive. ## Streams are cold [`Control.events`]() and [`Control.watchPane`]() return an [`IAsyncEnumerable`](). Nothing is read until a consumer enumerates it, and the consumer supplies the cancellation token. [`Control.foldWhile`]() and [`Control.iter`]() consume a stream, await one callback at a time, dispose only their enumerator, and leave the client open. Any [`IAsyncEnumerable`]() library composes the same streams; `TaskSeq<'T>` in FSharp.Control.TaskSeq is the same type. [`Control.watchPane`]() narrows a client's stream to one pane's output, and ends once the pane is confirmed gone. [`PaneWatch`]() names the six things a watch yields, so a match that leaves one out, such as a `Dropped` loss report, draws a compiler warning: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let readPaneUntilAsync (cancellationToken: CancellationToken) (marker: string) (pane: Pane) (session: IControlModeSession) = session |> Control.watchPane pane |> Control.foldWhile cancellationToken (fun output event -> task { match event with | PaneWatch.Output printed -> let output = output + printed.Data if output.Contains(marker, StringComparison.Ordinal) then return StreamStep.Stop output else return StreamStep.Continue output // Output tmux held back or the buffer dropped never arrives; // capture the pane to read what the screen shows instead. | PaneWatch.Paused _ | PaneWatch.Continued _ | PaneWatch.Dropped _ -> return StreamStep.Continue output | PaneWatch.Gone _ | PaneWatch.Exited _ -> return StreamStep.Stop output }) "" ``` A client has one event stream, so watching two panes with [`Control.watchPane`]() takes two clients. [`Control.watchPanes`]() follows several through one: each output event names its pane, each pane's end arrives as `PaneWatch.Gone` after the output buffered before it went, and the stream ends once every pane is gone. This complete program follows two panes, then their ends: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 20.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-watch-panes-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token let! session = owned.Value |> Server.newSession token (SessionSpec.running "work" "exec sleep 60") // The client buffers everything from the moment it attaches, so the // panes it should see can start afterwards. use! client = session |> Control.enterSession token let! first = session |> Session.panes |> Query.list token let start command = first[0] |> Pane.split token (SplitPaneRequest(Command = command)) let! build = start "printf 'build: ok\\n'; exec sleep 60" let! test = start "printf 'test: ok\\n'; exec sleep 60" // One client, both panes: each output event names its pane. let! printed = client |> Control.watchPanes [ build; test ] |> Control.foldWhile token (fun (printed: Map) event -> task { match event with | PaneWatch.Output output -> let pane = output.PaneId.ToString() let sofar = printed |> Map.tryFind pane |> Option.defaultValue "" let printed = printed |> Map.add pane (sofar + output.Data) if printed.Count = 2 && printed |> Map.forall (fun _ text -> text.Contains '\n') then return StreamStep.Stop printed else return StreamStep.Continue printed | _ -> return StreamStep.Continue printed }) Map.empty // Each pane's end arrives as PaneWatch.Gone, and the stream ends // once both are gone. do! build |> Pane.kill token do! test |> Pane.kill token let! ended = client |> Control.watchPanes [ build; test ] |> Control.foldWhile token (fun ended event -> task { match event with | PaneWatch.Gone pane -> return StreamStep.Continue(ended @ [ pane ]) | _ -> return StreamStep.Continue ended }) [] for pane in [ build; test ] do printfn "%s" (printed[pane.Id.ToString()].Trim()) printfn "ended in order given: %b" (ended = [ build.Id; test.Id ]) } runAsync().GetAwaiter().GetResult() ``` It prints: ```text build: ok test: ok ended in order given: true ``` The watch reads the client's only stream, so it consumes and drops events for other panes. tmux sends a control client output only from the session it is attached to, and [`Control.withSession`]() attaches to the most recently used one. Attach the client with [`Control.enterSession`]() to the session that holds the pane: a watch raises [`ArgumentException`]() for a pane elsewhere when it starts, and [`InvalidOperationException`]() if the pane's window moves to another session while it is watched. tmux discards output it has not yet sent once a pane's program exits, so the last lines of a program that exits at once may never arrive on any control client. Read final output with [`Pane.run`](), or capture a pane kept with `remain-on-exit`. ## Follow live state [`Mirror.start`]() keeps a current copy of a server's sessions, windows, panes and clients. Each change tmux announces starts a fresh capture, and a capture that finds nothing different publishes nothing, so [`Mirror.waitUntil`]() and [`Mirror.views`]() see each distinct state once. Activity times, cursor positions and history sizes change with every keystroke and do not count. tmux does not announce a pane's running command or working directory, nor a layout change in a session the mirror is not attached to; [`Mirror.startRefreshing`]() also captures whenever the mirror has been quiet for an interval: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let runAsync () = task { use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 20.) let token = deadline.Token let options = ServerConnectionOptions( SocketName = "fsharp-live-" + Guid.NewGuid().ToString("N"), ConfigurationFile = "/dev/null" ) use! owned = options |> Server.createOwned token let! session = owned.Value.CreateSessionAsync( NewSessionRequest(Name = "work", WindowName = "shell", Command = "/bin/sh"), token ) // Captures again on each change tmux announces, and every 200 ms for // changes it does not, such as the command a pane runs. use! mirror = session |> Mirror.startRefreshing token (TimeSpan.FromMilliseconds 200.) let! _ = session.CreateWindowAsync(NewWindowRequest(Name = "logs", Command = "/bin/sh"), token) let! withLogs = mirror |> Mirror.waitUntil token (TimeSpan.FromSeconds 5.) (fun view -> view.Server.Windows |> Seq.exists (fun window -> window.Name = "logs")) let! panes = session |> Session.panes |> Query.list token do! panes[0] |> Pane.sendLine token "exec sleep 30" // tryWaitUntil answers None when no view matched in time. let! sleeping = mirror |> Mirror.tryWaitUntil token (TimeSpan.FromSeconds 5.) (fun view -> view.Server.Panes |> Seq.exists (fun pane -> pane.CurrentCommand = "sleep")) printfn "windows: %s" (String.Join(", ", [ for window in withLogs.Server.Windows -> window.Name ])) match sleeping with | Some sleeping -> printfn "sleeping panes: %d" (sleeping.Server.Panes |> Seq.filter (fun pane -> pane.CurrentCommand = "sleep") |> Seq.length) printfn "newer view: %b" (sleeping.Epoch > withLogs.Epoch) | None -> printfn "no pane ran sleep within five seconds" } runAsync().GetAwaiter().GetResult() ``` It prints: ```text windows: shell, logs sleeping panes: 1 newer view: true ``` The mirror's control client receives notifications only, never pane output, and does not change window sizes. If its client ends, the mirror attaches again through the anchor session; once that session is gone, the mirror ends and [`Mirror.views`]() raises [`TmuxObjectNotFoundException`](). ## Ownership Use [`Control.withSession`]() to own a client for one task, or pass a client from [`Control.enter`]() or [`Control.enterSession`]() to [`Control.useSession`](). The [control-mode example](https://libtmux.org/en/fsharp/latest/guides/modes/#control-mode) shows both lifetimes. ## Loss, ends and failures A client buffers 512 events by default ([`ServerConnectionOptions.ControlModeEventBufferCapacity`]()). When a reader falls behind, the buffer discards the oldest output of the pane with the most output waiting, so a flooding pane loses its own output rather than a quieter pane's, and notifications about sessions, windows and layout survive it. [`TmuxEventsDroppedEvent`]() reports the loss; its [`OnlyOutput`]() is true when no notification was discarded, so state derived from notifications is still exact. The client then pauses the flooding pane in tmux until the reader catches up: [`TmuxPanePausedEvent`]() and [`TmuxPaneContinuedEvent`]() bracket output the pane printed that the reader never receives, and a client nobody reads keeps the pane paused. The pause is the client's own `refresh-client -A`, sent when its buffer fills; it does not set tmux's age-based `pause-after`, so a reader that keeps up never sees a pause. Capture the pane to read its screen after a gap. [`TmuxExitEvent`]() precedes normal stream completion. A stream fault arrives after buffered events. Cancellation stops waiting and disposes the reader; it does not undo a command tmux already received. When a callback and the enumerator's cleanup both fail, the callback's exception propagates unchanged and [`Control.cleanupFailure`]() returns the cleanup's. The callback's exception keeps its type, so a handler that matches `:? TmuxPaneException` still catches it; an `AggregateException` of both would not be caught there. --- # .NET interoperation Source: https://libtmux.org/en/fsharp/latest/guides/interop/ > Use core operations alongside the F# helpers. The companion is built on [LibTmux](https://github.com/libtmux/libtmux-dotnet/) and uses its entities, IDs, requests, exceptions, snapshots, and query documents. Pass a [`Server`](), [`Session`](), [`Window`](), or [`Pane`]() between F# and C# without conversion. `Task<'T>` remains the default asynchronous contract. Pass the cancellation token to the façade function explicitly and preserve core exceptions. Use [`option`]() only for a completed lookup that found no entity; use [`Selection.exactlyOne`]() when zero and multiple local matches need different outcomes. A task starts when called; an [`Async`]() workflow starts when run. In an `async` workflow, await with [`TmuxAsync.awaitTask`]() or [`TmuxAsync.awaitUnitTask`](). [`Async.AwaitTask`]() replaces [`TmuxOperationCanceledException`]() with [`TaskCanceledException`](), losing [`CommandMayHaveExecuted`]() and `ClientProcessId`, and wraps a failure in an `AggregateException`. [`TmuxAsync`]() raises that cancellation as itself, so `TmuxFailure.MayHaveRun` matches it, raises a failure unwrapped, and cancels the workflow for any other cancellation. [`Async.StartAsTask`]() on the way back can still make the outer task canceled or faulted, depending on continuation timing. ```fsharp run open System open LibTmux open LibTmux.FSharp let runInAsync (pane: Pane) (command: string) = async { // Cancelling the workflow cancels a call only through the token // the call was given, so pass the workflow's own. let! cancellationToken = Async.CancellationToken try let! result = pane |> Pane.run cancellationToken (TimeSpan.FromSeconds 10.) command |> TmuxAsync.awaitTask return match result with | PaneRun.Exited status -> $"exited {status}" | PaneRun.Ended -> "the shell exited first" | PaneRun.NotStarted -> "the shell was not at a prompt" | PaneRun.TimedOut -> "still running" with TmuxFailure.MayHaveRun _ -> return "may have run; read the pane before trying again" } ``` `LibTmux.Query.Json` remains optional. Add it only when a portable filter must cross a process or language boundary. It serializes the core [`QueryDocument`](); the F# package does not define a second format. ## Failures and retries Every [`LibTmuxException`]() says whether its command reached tmux. The [`TmuxFailure`]() patterns match on that. `NotSent` means tmux never saw the command, so running it again repeats nothing. `Ran` means tmux ran it and then reported an error or gave an answer that could not be used. `MayHaveRun` covers a failure or cancellation after which tmux may already have acted. [`Retry.ifNotSentAfter`]() runs an operation again only for `NotSent`, and only when no command the attempt sent before that failure reached tmux, waiting each delay in turn first: ```fsharp run open System open System.Threading open LibTmux open LibTmux.FSharp let readSessionNamesAsync (cancellationToken: CancellationToken) (server: Server) = task { try // Runs again only when tmux never received the command, after // each delay in turn, so a server still starting can answer. let! sessions = Retry.ifNotSentAfter cancellationToken [ TimeSpan.FromMilliseconds 100.; TimeSpan.FromMilliseconds 400. ] (fun token -> server.GetSessionsAsync(token)) return Ok [ for session in sessions -> session.Name ] with | TmuxFailure.Ran failure -> return Error $"tmux ran the command, then: {failure.Message}" | TmuxFailure.MayHaveRun failure -> return Error $"tmux may have acted: {failure.Message}" } ``` `NotSent` speaks for one command. An operation that creates a window and then fails to send a second command has already created the window, so [`Retry.ifNotSent`]() counts every command an attempt sends, through whatever it awaits, and repeats the attempt only if none reached tmux. A read that is safe to repeat whatever happened can use any retry policy; a command that changes tmux should be retried only this way. `Retry.ifNotSent ct retries operation` retries under the same rule without waiting, for a failure that waiting does not change. Beyond the [`LibTmuxException`]() any tmux call can raise, an F# call raises these, each documented on the function that raises it: | Exception | Raised when | | --- | --- | | [`OperationCanceledException`]() | The cancellation token fired. | | [`TmuxPaneException`]() | A pane wait or [`Pane.run`]() reaches a pane whose program had already exited, or a run reaches a pane in a mode or not at a POSIX shell. | | [`TmuxWaitTimeoutException`]() | [`Mirror.waitUntil`]() saw no matching view in time. Pane waits return [`TimedOut`]() instead. | | [`InvalidOperationException`]() | [`Query.atMostOne`]() found several matches, a mirror ended before a wait's condition held, a second reader started on a control client, or [`Server.createOwned`]() met a server already on the default socket. | | [`TmuxSessionExistsException`]() | [`Server.newSession`]() names a session that already exists. | | [`TmuxObjectNotFoundException`]() | A mirror's anchor session is gone; [`Mirror.views`]() raises the failure that ended the mirror. | | [`TmuxOptionException`]() | tmux rejected an option name or value, or reported one the key cannot read. | | [`TmuxVersionTooLowException`]() | A raw client filter ran on tmux older than 3.4. | | [`IncompleteSnapshotException`]() | A captured relation or field was read that the capture did not include. | | [`UnsupportedQueryExpressionException`]() | A [`Filter.matches`]() pattern is invalid or longer than 1024 characters. | | [`ArgumentException`]() | An argument is wrong before anything reaches tmux, such as an empty wait text or a negative retry count or delay. | ## Bound how long tmux may take Bound one call with its token: pass `(new CancellationTokenSource(timeout)).Token`. A call cancelled that way raises [`TmuxOperationCanceledException`](), whose [`CommandMayHaveExecuted`]() says whether tmux may already have run it, and which `TmuxFailure.MayHaveRun` matches. Bound every command a handle sends with `Server.within timeout server`. It returns a handle to the same server, over the same connection, whose commands, and those of every session, window and pane taken from it, give tmux that long. A command that outlasts it raises [`TmuxTransportException`]() with an [`Unknown`]() dispatch state. [`ServerConnectionOptions.CommandTimeout`]() sets the same bound for a whole connection. ## Typed options [`Options.get`]() and [`Options.set`]() take a [`TmuxOptionKey`]() that knows its value's type. The named keys, such as [`TmuxOptionKey.HistoryLimit`]() and [`TmuxOptionKey.Mouse`](), have the same type on every supported tmux; declare others with [`TmuxOptionKey.Text`](), [`Number`]() or [`Flag`](). A read returns the value tmux applies, including one inherited from a parent scope, and raises [`TmuxOptionException`]() when tmux reports none or one the key cannot read. ```fsharp run open System.Threading open LibTmux open LibTmux.FSharp let tuneAsync (cancellationToken: CancellationToken) (session: Session) (window: Window) = task { do! session.Options |> Options.set cancellationToken TmuxOptionKey.HistoryLimit 50_000 do! session.Options |> Options.set cancellationToken (TmuxOptionKey.Text "@stage") "build" let! history = session.Options |> Options.get cancellationToken TmuxOptionKey.HistoryLimit let! stage = session.Options |> Options.get cancellationToken (TmuxOptionKey.Text "@stage") // Never set on the window, so this is tmux's inherited default. let! renames = window.Options |> Options.get cancellationToken TmuxOptionKey.AutomaticRename return history, stage, renames } ``` ## Configuration, hooks and formats Core configuration and format APIs remain available on the same handles. This function takes an existing server and session, makes scoped changes, and checks what tmux reports. The [example runner](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/examples/LibTmux.FSharp.Examples/Program.fs) passes handles from an owned tmux hierarchy. ```fsharp run open System open System.Collections.Generic open System.Threading open LibTmux let inspectCoreSettingsAsync (cancellationToken: CancellationToken) (server: Server) (session: Session) = task { // A global value, overridden locally, shows through again once the // local value is unset. let! _ = session.Options.SetAsync(SetOptionRequest("status-keys", "vi", Global = true), cancellationToken) let! _ = session.Options.SetAsync(SetOptionRequest("status-keys", "emacs"), cancellationToken) do! session.Options.UnsetAsync(UnsetOptionRequest("status-keys"), cancellationToken) let! statusKeys = session.Options.GetAsync(GetOptionRequest("status-keys", IncludeInherited = true), cancellationToken) // An array option and a hook keep each entry's index. let! _ = server.Options.SetAsync( SetOptionRequest("command-alias[40]", "fsharp-window=new-window"), cancellationToken ) let! aliases = server.Options.GetAsync(GetOptionRequest("command-alias"), cancellationToken) let entries = Dictionary() entries[3] <- "display-message fsharp-hook" let! hook = server.Hooks.SetAsync(SetHooksRequest("alert-bell", entries, ClearExisting = true), cancellationToken) let! _ = session.Environment.SetAsync("LIBTMUX_FSHARP_EXAMPLE", "ready", cancellationToken = cancellationToken) let! variable = session.Environment.GetAsync("LIBTMUX_FSHARP_EXAMPLE", cancellationToken) let! rendered = server.DisplayMessageAsync( DisplayMessageRequest(Format = "fsharp-#{pid}", ReturnText = true), cancellationToken ) return {| StatusKeys = [ for option in statusKeys -> option.Value.Raw, option.Inherited ] Alias = aliases |> Seq.tryFind (fun alias -> alias.Index = Nullable 40) |> Option.map (fun alias -> alias.Value.Raw) HookIndexes = [ for value in hook.Values -> value.Index ] Variable = variable |> Option.ofObj |> Option.map (fun entry -> entry.Value) Rendered = rendered |> Option.ofObj |> Option.map List.ofSeq |} } ``` ## Core operations Call the core APIs directly for window placement, pane sizing, layouts, buffers, and copy mode. This example uses handles from the [owned tmux example runner](https://github.com/libtmux/libtmux-dotnet/blob/cf34b255c896d19a97e7d7fabea58609c916012d/examples/LibTmux.FSharp.Examples/Program.fs). `MoveAsync` returns the new window placement; the original link remains after the moved one is unlinked. Layout and resize calls return refreshed handles. The buffer belongs to the server, while copy mode belongs to the pane. ```fsharp run open System.Threading open LibTmux let exerciseCoreOperationsAsync (cancellationToken: CancellationToken) (server: Server) (session: Session) (window: Window) (pane: Pane) = task { // Link the window at index 5 as well, move that placement to 3, // then unlink it; the original placement stays. do! window.LinkAsync( LinkWindowRequest(session.Id.ToString(), TargetIndex = "5", Detach = true), cancellationToken ) let! placements = session.GetWindowsAsync(cancellationToken) let linked = placements |> Seq.find (fun item -> item.Id = window.Id && item.Index = 5) let! moved = linked.MoveAsync(MoveWindowRequest(Destination = "3", NoSelect = true), cancellationToken) do! moved.UnlinkAsync(cancellationToken = cancellationToken) let! remaining = session.GetWindowsAsync(cancellationToken) // Layout and resize return the handle they changed. let! split = window.SplitPaneAsync(cancellationToken = cancellationToken) let! laidOut = window.SelectLayoutAsync(SelectLayoutRequest(Layout = "even-horizontal"), cancellationToken) let! resized = split.ResizeAsync(ResizePaneRequest(Height = "10"), cancellationToken) do! server.Buffers.SetAsync("fsharp-ready", "fsharp-guide", cancellationToken = cancellationToken) let! contents = server.Buffers.GetAsync("fsharp-guide", cancellationToken) do! server.Buffers.DeleteAsync("fsharp-guide", cancellationToken) do! pane.EnterCopyModeAsync(cancellationToken = cancellationToken) let! copying = pane.RefreshAsync(cancellationToken) do! pane.EnterCopyModeAsync(CopyModeRequest(Cancel = true), cancellationToken) let! normal = pane.RefreshAsync(cancellationToken) return {| Moved = moved Remaining = remaining Split = split LaidOut = laidOut Resized = resized Buffer = contents InModeWhileCopying = copying.RawFormatFields["pane_in_mode"] InModeAfter = normal.RawFormatFields["pane_in_mode"] |} } ``` ## Window input Core window creation returns a handle that the F# pane helpers can use directly. A literal `"Enter"` types five characters; a key-name `"Enter"` presses the key. `Enter = true` sends a separate key command after literal text. The example checks those command shapes, sends each form to a temporary window, and kills that window. ```fsharp run open System.Threading open LibTmux open LibTmux.FSharp let exerciseWindowInputAsync (cancellationToken: CancellationToken) (session: Session) = task { let! window = session.CreateWindowAsync( NewWindowRequest(Name = "fsharp-input", Command = "/bin/cat", Attach = false), cancellationToken ) let! panes = window.GetPanesAsync(cancellationToken) let pane = panes[0] // Literal text is typed as written; a key name is pressed. Text // followed by Enter is two commands: the text, then the key. let literal = SendKeysRequest(Text = "Enter", Literal = true, Enter = false) let keyName = SendKeysRequest(Text = "Enter", Literal = false, Enter = false) let textThenEnter = SendKeysRequest(Text = "fsharp-input", Literal = true, Enter = true) do! pane |> Pane.sendKeys cancellationToken literal do! pane |> Pane.sendKeys cancellationToken keyName do! pane |> Pane.sendKeys cancellationToken textThenEnter do! window |> Window.kill cancellationToken let arguments (request: SendKeysRequest) = [ for command in request.ToCommands(pane) -> List.ofSeq (command.ToArguments()) ] return {| Window = window Panes = panes.Count Literal = arguments literal KeyName = arguments keyName TextThenEnter = arguments textThenEnter |} } ``` ```fsharp run open LibTmux open LibTmux.FSharp open LibTmux.Query.Json let encodeEditorPaneFilter () = Filter.oneOf [ "nvim"; "vim" ] PaneFields.currentCommand |> Filter.toDocument |> QueryJson.Serialize let decodeFilter json = QueryJson.Deserialize json ``` Validate and apply the decoded document with the core query APIs. A JSON round trip does not turn a portable filter into a tmux format expression. --- # Execution modes Source: https://libtmux.org/en/fsharp/latest/guides/modes/ > Choose commands, control mode, or bounded concurrent reads. The caller chooses how commands reach tmux: one tmux process per command, a control client that stays attached, or a chain that runs several commands in one tmux call. Use one-shot operations when one task describes the work. Use a control client when tmux must report work that nobody explicitly requested. Use the [`Chain`]() module when several commands should run together, each acting on what the one before made. ## Control mode [`Control.withSession`]() opens and owns a core control client. It ends the client after its task finishes. `readUntilTerminalAsync` borrows a client, so it disposes only its event enumerator. [`Control.enter`]() returns an owned client when its lifetime must extend beyond one function, and [`Control.enterSession`]() attaches one to a chosen session; pass either to [`Control.useSession`]() to transfer ownership to a task scope. ```fsharp run open System.Threading open LibTmux open LibTmux.FSharp let readUntilTerminalAsync (cancellationToken: CancellationToken) (session: IControlModeSession) = session |> Control.events |> Control.foldWhile cancellationToken (fun events event -> task { let retained = event :: events match event with | :? TmuxEventsDroppedEvent | :? TmuxExitEvent -> return StreamStep.Stop(List.rev retained) | _ -> return StreamStep.Continue retained }) [] let observeUntilTerminalAsync cancellationToken server = server |> Control.withSession cancellationToken (readUntilTerminalAsync cancellationToken) ``` [`Control.events`]() is the client's event stream; nothing is read until a consumer enumerates it. [`Control.foldWhile`]() reads and awaits one folder call at a time. It stops before reading another event when the folder returns [`StreamStep.Stop`](). It preserves unknown event types. [`TmuxEventsDroppedEvent`]() means the caller must resynchronize from a capture; when its [`OnlyOutput`]() is true, only pane output was lost and every notification arrived. The example returns it to the caller and stops. [`TmuxExitEvent`]() is a normal terminal event. A failed control stream raises after its buffered events. [`Control.iter`]() is the same borrowed-client pattern when no accumulator is needed. It completes when the stream ends or propagates the handler, stream, and cancellation errors unchanged. When a handler and the enumerator's cleanup both fail, the handler's exception propagates and [`Control.cleanupFailure`]() returns the cleanup's. ## Command chains A chain is a core [`TmuxChain`](), which F# can build directly or through the [`Chain`]() module below. Building it makes no I/O; one `ExecuteAsync` dispatches the commands in order and returns their combined output. ```fsharp run open System.Threading open LibTmux let readChainOutputAsync (cancellationToken: CancellationToken) (server: Server) = task { // Each typed request becomes one command of the chain. let print text = DisplayMessageRequest(Format = text, ReturnText = true).ToCommand(server) let chain = server.Chain().Then(print "fsharp-chain-first").Then(print "fsharp-chain-second") let! result = chain.ExecuteAsync(cancellationToken) return result.StandardOutputLines |> Seq.toList } ``` Forty request types, such as [`SendKeysRequest`](), [`SplitPaneRequest`]() and [`CapturePaneRequest`](), convert to a command with `ToCommand`, so a chain keeps their validation; [`Then(name, arguments)`]() takes any other command as text. The [`Chain`]() module builds the same chain as a pipeline, with named steps that act on what the step before made: [`Chain.newWindow`](), [`Chain.splitLeftRight`](), [`Chain.splitTopBottom`](), [`Chain.sendLine`]() and [`Chain.arrange`](), then [`Chain.run`](); [`Chain.add`]() appends a typed request's command. The [session program](https://libtmux.org/en/fsharp/latest/guides/getting-started/#describe-a-session) uses one. Use a chain for a known batch. It returns one [`TmuxCommandResult`](), not a typed object for every step. Use one-shot operations when each step needs a refreshed entity handle. ## Bounded concurrent reads [`Task.WhenAll`]() starts all inputs. Supply a bound when the input size can grow; this helper holds at most `maximumConcurrency` pane captures at once and returns results in input order. ```fsharp run open System open System.Threading open System.Threading.Tasks open LibTmux open LibTmux.FSharp let boundedMapAsync maximumConcurrency (cancellationToken: CancellationToken) (work: CancellationToken -> 'Input -> Task<'Output>) (inputs: 'Input list) = if maximumConcurrency < 1 then invalidArg "maximumConcurrency" "Maximum concurrency must be positive." task { use gate = new SemaphoreSlim(maximumConcurrency) let run index input = task { do! gate.WaitAsync(cancellationToken) try let! output = work cancellationToken input return index, output finally gate.Release() |> ignore } let! indexed = inputs |> List.mapi run |> Task.WhenAll return indexed |> Array.sortBy fst |> Array.map snd |> Array.toList } let capturePanesBoundedAsync maximumConcurrency cancellationToken (panes: seq) = panes |> Seq.toList |> boundedMapAsync maximumConcurrency cancellationToken (fun token pane -> pane |> Pane.capture token (CapturePaneRequest())) ``` Cancellation reaches waiting and active reads. The gate is released when an operation succeeds, fails, or is canceled. --- # Query fields Source: https://libtmux.org/en/fsharp/latest/guides/supported-query-fields/ > Browse the fields supported by typed filters. [`LibTmux.FSharp.Filter`]() creates version-two [`QueryDocument`]() values. These are the descriptors it exposes. [`Query.matching`]() is local and materialized; it does not send a native tmux filter. The fields below are checked against documents generated by the F# descriptors and the optional `LibTmux.Query.Json` serializer. Parent links, such as a pane's window, and placement keys remain native captured-object properties. They have no portable descriptor. ## Sessions ### `SessionFields.name` - Core property: [`Session.Name`]() - Wire name: `session_name` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Sessions`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-sessions/) - Schema version: `2` ### `SessionFields.id` - Core property: [`Session.Id`]() - Wire name: `session_id` - Value type: [`SessionId`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Sessions`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-sessions/) - Schema version: `2` ### `SessionFields.attached` - Core property: [`Session.Attached`]() - Wire name: `session_attached` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Sessions`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-sessions/) - Schema version: `2` ### `SessionFields.windowCount` - Core property: [`Session.Windows`]() - Wire name: `session_windows` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `SessionFields.windows` - Core property: [`Session.Windows`]() - Wire name: `session_windows` - Value type: `Relation` - Operators: [`any`](), [`all`](), [`none`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ## Windows ### `WindowFields.name` - Core property: [`Window.Name`]() - Wire name: `window_name` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.id` - Core property: [`Window.Id`]() - Wire name: `window_id` - Value type: [`WindowId`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.index` - Core property: [`Window.Index`]() - Wire name: `window_index` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.width` - Core property: [`Window.Width`]() - Wire name: `window_width` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.height` - Core property: [`Window.Height`]() - Wire name: `window_height` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.active` - Core property: [`Window.Active`]() - Wire name: `window_active` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.zoomed` - Core property: [`Window.Zoomed`]() - Wire name: `window_zoomed_flag` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.bellAlert` - Core property: [`Window.BellAlert`]() - Wire name: `window_bell_flag` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.activityAlert` - Core property: [`Window.ActivityAlert`]() - Wire name: `window_activity_flag` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.silenceAlert` - Core property: [`Window.SilenceAlert`]() - Wire name: `window_silence_flag` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.layout` - Core property: [`Window.Layout`]() - Wire name: `window_layout` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.flags` - Core property: [`Window.Flags`]() - Wire name: `window_flags` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Windows`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-windows/) - Schema version: `2` ### `WindowFields.paneCount` - Core property: [`Window.Panes`]() - Wire name: `window_panes` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `WindowFields.panes` - Core property: [`Window.Panes`]() - Wire name: `window_panes` - Value type: `Relation` - Operators: [`any`](), [`all`](), [`none`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ## Panes ### `PaneFields.currentCommand` - Core property: [`Pane.CurrentCommand`]() - Wire name: `pane_command` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.id` - Core property: [`Pane.Id`]() - Wire name: `pane_id` - Value type: [`PaneId`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.index` - Core property: [`Pane.Index`]() - Wire name: `pane_index` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.title` - Core property: [`Pane.Title`]() - Wire name: `pane_title` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.currentPath` - Core property: [`Pane.CurrentPath`]() - Wire name: `pane_current_path` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.width` - Core property: [`Pane.Width`]() - Wire name: `pane_width` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.height` - Core property: [`Pane.Height`]() - Wire name: `pane_height` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.left` - Core property: [`Pane.Left`]() - Wire name: `pane_left` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.top` - Core property: [`Pane.Top`]() - Wire name: `pane_top` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.atTop` - Core property: [`Pane.AtTop`]() - Wire name: `pane_at_top` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.atBottom` - Core property: [`Pane.AtBottom`]() - Wire name: `pane_at_bottom` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.atLeft` - Core property: [`Pane.AtLeft`]() - Wire name: `pane_at_left` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.atRight` - Core property: [`Pane.AtRight`]() - Wire name: `pane_at_right` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.active` - Core property: [`Pane.Active`]() - Wire name: `pane_active` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.dead` - Core property: [`Pane.Dead`]() - Wire name: `pane_dead` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.inMode` - Core property: [`Pane.InMode`]() - Wire name: `pane_in_mode` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.processId` - Core property: [`Pane.ProcessId`]() - Wire name: `pane_pid` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.synchronized` - Core property: [`Pane.Synchronized`]() - Wire name: `pane_synchronized` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.historySize` - Core property: [`Pane.HistorySize`]() - Wire name: `history_size` - Value type: [`int`]() - Operators: [`eq`](), [`ne`](), [`lt`](), [`le`](), [`gt`](), [`ge`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.deadStatus` - Core property: [`Pane.DeadStatus`]() - Wire name: `pane_dead_status` - Value type: `int option` - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.tty` - Core property: [`Pane.Tty`]() - Wire name: `pane_tty` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ### `PaneFields.startCommand` - Core property: [`Pane.StartCommand`]() - Wire name: `pane_start_command` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Panes`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-panes/) - Schema version: `2` ## Clients ### `ClientFields.name` - Core property: [`Client.Name`]() - Wire name: `client_name` - Value type: [`string`]() - Operators: [`eq`](), [`ne`](), [`eqIgnoreCase`](), [`isNull`](), [`startsWith`](), [`startsWithIgnoreCase`](), [`endsWith`](), [`endsWithIgnoreCase`](), [`contains`](), [`containsIgnoreCase`](), [`matches`](), [`matchesIgnoreCase`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Sessions`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-sessions/) - Schema version: `2` ### `ClientFields.controlMode` - Core property: [`Client.IsControlClient`]() - Wire name: `client_control_mode` - Value type: [`bool`]() - Operators: [`eq`](), [`ne`](), [`oneOf`](), [`notOneOf`]() - Required depth: [`Sessions`](https://libtmux.org/en/csharp/latest/reference/libtmux-snapshotdepth-sessions/) - Schema version: `2` ## Limits The core wire catalog also reserves `client_id`. The core client model has no typed client ID, so the F# façade does not invent one. `session_windows` and `window_panes` each appear twice: as counts, [`SessionFields.windowCount`]() and [`WindowFields.paneCount`](), and as the relations [`SessionFields.windows`]() and [`WindowFields.panes`]() that [`Filter.any`]() and [`Filter.all`]() walk. A tmux format outside this catalog, such as `session_created` or `pane_dead_signal`, filters through [`Query.whereUnsafe`](), which hands tmux a raw filter such as `UnsafeTmuxFilter "#{e|>|:#{session_created},1700000000}"`. tmux evaluates it and nothing rechecks it, so it has none of the typed fields' guarantees, and a malformed or unknown token keeps no rows rather than raising. `allOf []` matches everything, and `anyOf []`, `oneOf []` match nothing; `notOneOf []` matches everything. Each is a Boolean constant predicate, which serializes and travels like any other document.