# 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.
[](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet.yml)
[](https://github.com/libtmux/libtmux-dotnet/actions/workflows/dotnet-tmux.yml)
[](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.