Core LibraryGuides

Choose documentation 1

latest

Current version

latest
English

Prerelease This site documents an alpha of libtmux. Its structure, URLs and APIs are subject to change.

Quick start

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 core objects. The F# package and core share a repository, release version, and primary author in the libtmux organization.

build tmux matrix license

Run isolated tmux · Connect to running tmux · Send, wait, read · Query tmux · Stream events

Find a shell, send it keys, wait for its output, and run a command:

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

NeedF# callReturns
Start a server you ownoptions |> Server.createOwned ctOwnedServerScope to use!
Attach to a running serveroptions |> Server.connect ctServer
Bound every command’s timeServer.within timeout serverServer

Find

NeedF# callReturns
List and filterServer.panes server |> Query.where filter |> Query.list ctIReadOnlyList<Pane>
One session’s or window’s panesSession.panes session |> Query.list ct; Window.panes alikeIReadOnlyList<Pane>
Panes showing some textServer.panes server |> Query.showing search |> Query.list ctIReadOnlyList<Pane>
Exactly one matchQuery.exactlyOne ct query; Query.tryExactlyOne under NativeAOTResult<'T, CardinalityError>; 'T option
Find, or create when absentQuery.atMostOne ct query'T option; several raise
One object by IDServer.tryFindPane ct id serverPane option
The pane a session or window showsSession.activePane ct session, Window.activePane ct windowPane

Type, wait and run

NeedF# callReturns
Type a line, or press a keyPane.sendLine ct line pane; Pane.pressKey ct "C-c" paneTask
Type a line, wait for its outputPane.sendAndWait ct timeout line text pane; Pane.sendAndWaitFor for keys and patternsPaneWaitResult; .Found, or match PaneWait
Wait for output you did not typePane.waitForText ct timeout text pane; Pane.waitFor for patternsPaneWaitResult
Wait for a screen conditionPane.waitUntil ct timeout condition panePaneWaitResult
Many waits on one sessionuse! _ = Session.holdWaitClient ct sessionIAsyncDisposable; each wait skips attaching a client, about 5 ms
Run a command to its exit statusPane.run ct timeout command panePaneRunResult; match PaneRun
Read the screenPane.capture ct request paneIReadOnlyList<string>
What a pane printed since last timePane.readSince ct position panePaneOutputSince; pass its Position next time
Find text on one screenPane.findOnScreen ct search panerow int option

Build

NeedF# callReturns
Split a panePane.split ct request panethe new Pane
Rename a session or windowWindow.rename ct name windowa handle with the new name
Make a window or pane currentWindow.select ct window, Pane.select ct panea handle with the state afterwards
Kill a session, window or panePane.kill ct paneTask
Arrange, resize or move a windowWindow.selectLayout ct layout window, Window.resize ct request window, Window.move ct request windowa handle with the state afterwards
Title, resize, swap, respawn or clear a panePane.setTitle ct title pane, Pane.resize, Pane.swap, Pane.respawn, Pane.clearHistorythe handle, or Task
Add a window to a sessionSession.newWindow ct request sessionthe new Window
Create a session running one commandServer.newSession ct (SessionSpec.running name command) serverSession
Create a session with windowsServer.newSession ct spec serverSession
Several commands, one tmux callChain.start server |> … |> Chain.run ctTmuxCommandResult
Read or set a typed optionOptions.get ct key optionsthe key’s value type

Observe

NeedF# callReturns
A whole object graphServer.capture ct depth serversnapshot Server
Live server stateMirror.start ct sessionServerMirror
Events as they happenControl.withSession ct work servercold IAsyncEnumerable streams
One pane’s output as it printsControl.watchPane pane client; Control.watchPanes for severalevents; match PaneWatch
An assistant on the same tmuxthe LibTmux.Mcp serverMCP guide

Recover

NeedF# callReturns
Tell failures apartTmuxFailure.NotSent, Ran, MayHaveRunactive patterns
Retry only unsent workRetry.ifNotSent ct retries operation, or Retry.ifNotSentAfter ct delays operationthe operation’s result
Await a call in an async workflowTmuxAsync.awaitTask task, TmuxAsync.awaitUnitTask taskAsync; keeps what MayHaveRun matches

Caveats

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:

Terminal window
$ dotnet new console --language F# --framework net8.0 --output tmux-demo

Enter it:

Terminal window
$ cd tmux-demo

Add LibTmux.FSharp from NuGet; it brings in the matching LibTmux core package and records the selected prerelease version in the project:

Terminal window
$ dotnet package add LibTmux.FSharp --prerelease

Replace Program.fs with this complete program:

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:

Terminal window
$ dotnet run

It prints:

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 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 explains the configuration order. Code running inside a tmux pane can use Server.FromEnvironment() to locate that pane’s server.

Keep going

Compatibility

AreaContract and verification
.NETTargets .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.
tmuxRequired 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 systemsLinux 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 NativeAOTPortable 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.

Esc

Type to search.