# libtmux for Kotlin
> The Kotlin library (io.github.libtmux:libtmux-kotlin). These pages include its guides, tested examples and API documentation.
- [Kotlin API reference](https://libtmux.org/en/kotlin/latest/reference/): every public symbol, generated from the source. Hosted on libtmux.org.
---
# Concepts
Source: https://libtmux.org/en/kotlin/latest/concepts/
> Understand Kotlin handles, filters, transports and layout ownership.
Use these concepts to reason about Kotlin 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/kotlin/latest/reference/). For a first connection, start with [attaching to tmux](https://libtmux.org/en/kotlin/latest/guides/attaching-to-tmux/).
- [Server, session, window, pane](https://libtmux.org/en/kotlin/latest/concepts/server-session-window-pane/): Navigate captured sessions, windows, and panes, and refresh their state.
- [Filtering and queries](https://libtmux.org/en/kotlin/latest/concepts/queries/): Combine predicates, handle result counts, and match related windows.
- [Commands and control mode](https://libtmux.org/en/kotlin/latest/concepts/transports/): Run bounded commands and manage a persistent control client.
- [Layouts and repeated setup](https://libtmux.org/en/kotlin/latest/concepts/workspaces/): Create a split window and reuse a named window safely.
---
# Server, session, window, pane
Source: https://libtmux.org/en/kotlin/latest/concepts/server-session-window-pane/
> Traverse captured Kotlin 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/kotlin/latest/reference/io-github-libtmux-kotlin-server/) selects the socket and owns client resources. [`Session`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-session/), [`Window`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-window/) and [`Pane`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-pane/) hold captured state and send mutations through that server. The session list captures the hierarchy; walking its children reads that capture.
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 JDK 25. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point.
The pinned source checkout supplies the Gradle wrapper and the library dependency.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source")
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("${example}.kt") }
}
application { mainClass.set("${example}Kt") }
```
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
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-java-dev
directory=$(mktemp -d /tmp/libtmux-java-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-java libtmux-source &&
git -C libtmux-source checkout be1d62fbaa1aa634c687e1c50bceeb09ef0b8a15
```
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.
```kotlin title="Hierarchy.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val sessions = server.sessions()
val windows = sessions.flatMap { it.windows }
val panes = windows.flatMap { it.panes }
check(sessions.size == 2 && windows.size == 2 && panes.size == 2)
for (session in sessions) {
println("${session.name}: ${session.windows.single().name}")
}
println("2 sessions, 2 windows, 2 panes")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Hierarchy --console=plain --max-workers=2
```
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.
```kotlin title="Refresh.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val session = server.sessions().single { it.name == "work-one" }
val before = session.windows.single()
val after = before.rename("renamed")
check(before.name == "editor")
check(after.name == "renamed")
val readAgain = server.sessions().single { it.name == "work-one" }.windows.single()
check(readAgain.name == "renamed")
println("${before.name} -> ${readAgain.name}")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Refresh --console=plain --max-workers=2
```
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/kotlin/latest/concepts/queries/) for missing and ambiguous selections, and [layouts](https://libtmux.org/en/kotlin/latest/concepts/workspaces/) for creation.
---
# Filtering and queries
Source: https://libtmux.org/en/kotlin/latest/concepts/queries/
> Compose Kotlin filters, distinguish zero and several matches, and query captured relations.
Filter captured objects in Kotlin, 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.
[`TextField`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-query-textfield/) builds typed expressions with `eq`, `ne`, [`startsWith`](), [`endsWith`](), [`contains`](), [`oneOf`]() and [`matches`](). Compose them with [`.and()`](), [`.or()`]() and `!`. Ordinary Kotlin predicates remain useful for application-specific conditions.
## Setup and run
Use an empty directory on Linux with Git, tmux 3.2a or newer, and JDK 25. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point.
The pinned source checkout supplies the Gradle wrapper and the library dependency.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source")
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("${example}.kt") }
}
application { mainClass.set("${example}Kt") }
```
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
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-java-dev
directory=$(mktemp -d /tmp/libtmux-java-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-java libtmux-source &&
git -C libtmux-source checkout be1d62fbaa1aa634c687e1c50bceeb09ef0b8a15
```
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.
```kotlin title="Local.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val sessions = server.sessions()
val matching = sessions.filter(Session.name startsWith "work-")
val names = matching.map { it.name }.sorted()
check(names == listOf("work-one", "work-two"))
println(names.joinToString(", "))
// Filtering the captured list makes no new tmux calls.
check(sessions.filter { it.name.startsWith("work-") } == matching)
val onlyOne = (Session.name startsWith "work-").and(Session.name endsWith "one")
check(sessions.filter(onlyOne).single().name == "work-one")
val either = (Session.name eq "work-one").or(Session.name eq "work-two")
check(sessions.filter(either).size == 2)
check(sessions.filter(!(Session.name eq "work-one")).single().name == "work-two")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Local --console=plain --max-workers=2
```
Expected program output:
```text
work-one, work-two
```
## Distinguish missing and ambiguous results
The program distinguishes zero and one local result. The checked server lookup throws [`CardinalityException.MultipleMatches`]() for two matches. Do not use [`singleOrNull()`]() when zero and several matches need different handling.
```kotlin title="Cardinality.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val sessions = server.sessions()
for (name in listOf("work-one", "missing")) {
val matches = sessions.filter(Session.name eq name)
when (matches.size) {
0 -> println("$name: absent")
1 -> println("$name: selected")
else -> error("More than one session named $name")
}
}
val ambiguous = sessions.filter(Session.name startsWith "work-")
check(ambiguous.size == 2)
// singleOrNull() alone cannot distinguish zero from several matches.
try {
server.session(Session.name startsWith "work-")
error("Expected an ambiguous selection")
} catch (error: io.github.libtmux.exception.CardinalityException.MultipleMatches) {
println("work-: ambiguous")
}
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Cardinality --console=plain --max-workers=2
```
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.
```kotlin title="Relations.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val sessions = server.sessions()
val editorWindow = Window.name eq "editor"
val withEditor = sessions.filter(Session.windows any editorWindow)
val withoutEditor = sessions.filter(Session.windows none editorWindow)
check(withEditor.map { it.name } == listOf("work-one"))
check(withoutEditor.map { it.name } == listOf("work-two"))
println("editor: ${withEditor.single().name}")
println("no editor: ${withoutEditor.single().name}")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Relations --console=plain --max-workers=2
```
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/kotlin/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/kotlin/latest/concepts/transports/
> Use Kotlin 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.
[`withServer`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-withserver/) closes the borrowed client scope. Suspended command calls use the configured execution policy. A suspend function is not itself a persistent control connection; open one explicitly with [`withControl`](https://libtmux.org/en/kotlin/latest/reference/io-github-libtmux-kotlin-withcontrol/).
## Setup and run
Use an empty directory on Linux with Git, tmux 3.2a or newer, and JDK 25. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point.
The pinned source checkout supplies the Gradle wrapper and the library dependency.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source")
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("${example}.kt") }
}
application { mainClass.set("${example}Kt") }
```
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
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-java-dev
directory=$(mktemp -d /tmp/libtmux-java-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-java libtmux-source &&
git -C libtmux-source checkout be1d62fbaa1aa634c687e1c50bceeb09ef0b8a15
```
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.
```kotlin title="Local.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val sessions = server.sessions()
val matching = sessions.filter(Session.name startsWith "work-")
val names = matching.map { it.name }.sorted()
check(names == listOf("work-one", "work-two"))
println(names.joinToString(", "))
// Filtering the captured list makes no new tmux calls.
check(sessions.filter { it.name.startsWith("work-") } == matching)
val onlyOne = (Session.name startsWith "work-").and(Session.name endsWith "one")
check(sessions.filter(onlyOne).single().name == "work-one")
val either = (Session.name eq "work-one").or(Session.name eq "work-two")
check(sessions.filter(either).size == 2)
check(sessions.filter(!(Session.name eq "work-one")).single().name == "work-two")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Local --console=plain --max-workers=2
```
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.
```kotlin title="Control.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val session = server.sessions().single { it.name == "work-one" }
withControl(server, session) { control ->
val reply = control.send("list-sessions", "-F", "#{session_name}")
check(reply.succeeded()) { "tmux rejected list-sessions: ${reply.outcome()}" }
val names = reply.lines().sorted()
check(names == listOf("work-one", "work-two"))
println(names.joinToString(", "))
}
check(server.sessions().size == 2)
println("control client closed; server still running")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Control --console=plain --max-workers=2
```
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/kotlin/latest/guides/attaching-to-tmux/) for the connection-only example.
---
# Layouts and repeated setup
Source: https://libtmux.org/en/kotlin/latest/concepts/workspaces/
> Create and reuse Kotlin tmux layouts while preserving running processes.
Build a tmux layout from Kotlin 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 JDK 25. Save the project files and launcher below, then save any complete program on this page. Each program has its own imports and entry point.
The pinned source checkout supplies the Gradle wrapper and the library dependency.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source")
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("${example}.kt") }
}
application { mainClass.set("${example}Kt") }
```
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
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-java-dev
directory=$(mktemp -d /tmp/libtmux-java-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-java libtmux-source &&
git -C libtmux-source checkout be1d62fbaa1aa634c687e1c50bceeb09ef0b8a15
```
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.
```kotlin title="Layout.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val session = server.sessions().single { it.name == "work-one" }
val window = session.newWindow(io.github.libtmux.WindowSpec.builder()
.named("tools").running("/bin/cat").build())
val original = window.panes.single()
original.split(io.github.libtmux.SplitSpec.builder().toRight().running("/bin/cat").build())
window.selectLayout(io.github.libtmux.Layout.EVEN_HORIZONTAL)
val refreshed = server.sessions().single { it.name == "work-one" }
val tools = refreshed.windows.single { it.name == "tools" }
check(tools.panes.size == 2)
println("tools: 2 panes")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=Layout --console=plain --max-workers=2
```
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.
```kotlin title="ReuseLayout.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.*
import io.github.libtmux.kotlin.query.*
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
suspend fun ensureTools(): Window {
val session = server.sessions().single { it.name == "work-one" }
val existing = session.windows.singleOrNull { it.name == "tools" }
if (existing != null) return existing
return session.newWindow(io.github.libtmux.WindowSpec.builder()
.named("tools").running("/bin/cat").build())
}
val first = ensureTools()
val second = ensureTools()
check(first.id == second.id)
check(server.sessions().single { it.name == "work-one" }.windows.size == 2)
println("one tools window after two calls")
}
}
```
```console
$ sh run.sh ./libtmux-source/gradlew --project-dir . run \
-Pexample=ReuseLayout --console=plain --max-workers=2
```
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/kotlin/latest/concepts/queries/) for stricter target selection and [captured handles](https://libtmux.org/en/kotlin/latest/concepts/server-session-window-pane/) to understand refresh behavior.
---
# Examples
Source: https://libtmux.org/en/kotlin/latest/examples/
> Tested Kotlin examples against an isolated tmux server.
Start with [Capture pane output](https://libtmux.org/en/kotlin/latest/examples/capture-pane-output/) for a standalone program with imports, Gradle files, and private-server cleanup.
## Read a Flow
Read pushed pane output as a coroutine Flow and cancel a pending wait. The complete program belongs to the Java repository's examples module; that module also supplies `WatchPaneOutput`, and its test starts an isolated tmux server before calling this program's [`main`](). Run `./gradlew :examples:test` from that repository.
```kotlin file="examples/src/main/kotlin/io/github/libtmux/examples/WatchWithFlow.kt"
package io.github.libtmux.examples
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.control.Delivery
import io.github.libtmux.kotlin.Server
import io.github.libtmux.kotlin.await
import io.github.libtmux.kotlin.send
import io.github.libtmux.kotlin.sessions
import io.github.libtmux.kotlin.withControl
import io.github.libtmux.kotlin.withServer
import java.nio.file.Path
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
/**
* Reads a pane's output as a `Flow`, then gives up on a wait by cancelling it.
*
* ```
* java -cp ... io.github.libtmux.examples.WatchWithFlowKt /tmp/libtmux-java-dev/demo/s
* ```
*/
fun main(args: Array) {
val socket = Path.of(args.getOrElse(0) { "/tmp/libtmux-java-dev/demo/s" })
runBlocking { println(watchWithFlow(socket, 10.seconds)) }
}
/**
* Collects output until the line `echo` printed arrives, then starts a channel wait that nothing
* will signal and cancels it after a moment. Cancelling ends the coroutine at once and is not a
* timeout; the tmux client behind the wait is stopped with it.
*
* @return one line for each: whether the echo arrived, and whether the wait was cancelled
*/
suspend fun watchWithFlow(socket: Path, deadline: Duration): String {
val config = ServerConfig.builder().endpoint(ServerEndpoint.socketPath(socket)).build()
return withServer(config) { server: Server ->
val session = server.sessions().first()
withControl(server, session) { control ->
// control.output(...) is a cold Flow: the subscription it opens on first collection
// closes when collection ends, is cancelled, or throws — nothing to close by hand. The
// command goes in onSubscribed: sent before collecting, its output could arrive before
// there was a subscription to hold it.
val seen = StringBuilder()
val echoed =
withTimeoutOrNull(deadline) {
control.output(capacity = 32) {
control.send("send-keys", "-t", session.name, "echo flowed", "Enter")
}.map { Delivery.kept(it).data }.first { chunk ->
seen.append(chunk)
WatchPaneOutput.printedLine(seen.toString(), "flowed")
}
} != null
val cancelled =
withTimeoutOrNull(200.milliseconds) { server.channel("never-signalled").await(30.seconds) } == null
"echoed=$echoed\ncancelled=$cancelled"
}
}
}
```
---
# Capture pane output
Source: https://libtmux.org/en/kotlin/latest/examples/capture-pane-output/
> Run a complete Kotlin program that captures output on an isolated tmux server.
This complete Kotlin 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.
```kotlin title="Capture.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.capture
import io.github.libtmux.kotlin.killServer
import io.github.libtmux.kotlin.newSession
import io.github.libtmux.kotlin.sendLine
import io.github.libtmux.kotlin.withServer
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
fun main() = runBlocking {
val root = Files.createDirectories(Path.of("/tmp/libtmux-java-dev"))
val directory = Files.createTempDirectory(root, "capture-")
val socket = directory.resolve("tmux.sock")
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(socket))
.configFile(Path.of("/dev/null"))
.defaultTimeout(Duration.ofSeconds(1))
.build()
var failure: Throwable? = null
try {
withServer(config) { server ->
try {
val session = server.newSession {
name = "capture"
running("/bin/sh")
env("ENV", "/dev/null")
}
val pane = session.windows.first().panes.first()
pane.sendLine("printf '\\nlibtmux capture ready\\n'")
withTimeout(5_000) {
while (!pane.capture().contains("libtmux capture ready")) {
delay(25)
}
}
println("libtmux capture ready")
} catch (error: Throwable) {
failure = error
throw error
} finally {
withContext(NonCancellable) {
try {
if (Files.exists(socket)) server.killServer()
Files.deleteIfExists(socket)
Files.delete(directory)
} catch (cleanup: Throwable) {
val original = failure
if (original == null) throw cleanup
original.addSuppressed(cleanup)
}
}
}
}
} finally {
// Opening a client can fail before the cleanup block is entered.
if (!Files.exists(socket)) Files.deleteIfExists(directory)
}
}
```
## Wait for output or completion
Capture and input calls suspend. `withTimeout` bounds the polling loop, and `delay` yields between captures. [`withServer`]() closes the client; the [`finally`]() block also stops the tmux server this program created. Cleanup errors stay visible, and a server that cannot be stopped keeps its socket.
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
JDK 25. The pinned repository supplies Gradle; the project files select Kotlin 2.4.10 and include the library build.
Save `settings.gradle.kts` beside `Capture.kt`.
```kotlin title="settings.gradle.kts"
rootProject.name = "capture"
includeBuild("libtmux-source")
```
Save `build.gradle.kts` beside `Capture.kt`.
```kotlin title="build.gradle.kts"
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("Capture.kt") }
}
application { mainClass.set("CaptureKt") }
```
Save `gradle.properties` beside `Capture.kt`.
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
The commands pin the library revision used to run this program.
```console
$ git clone https://github.com/libtmux/libtmux-java libtmux-source &&
git -C libtmux-source checkout 85ebf6955e56703c5be74e2afd34a18309044741 &&
./libtmux-source/gradlew --project-dir . run --console=plain --max-workers=2
```
## 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/kotlin/latest/guides/
> Guides for io.github.libtmux:libtmux-kotlin.
Connect to tmux and work with its sessions, windows, and panes.
- [Getting started](https://libtmux.org/en/kotlin/latest/guides/getting-started/): Read about libtmux-kotlin.
- [Attaching to tmux](https://libtmux.org/en/kotlin/latest/guides/attaching-to-tmux/): Connect to an existing socket and leave its server running.
- [Coroutines and flows](https://libtmux.org/en/kotlin/latest/guides/coroutines/): Read about kotlin.
---
# Getting started
Source: https://libtmux.org/en/kotlin/latest/guides/getting-started/
> Getting started: io.github.libtmux:libtmux-kotlin documentation.
**Coroutine wrapper classes, a session/window/split DSL, and [`Flow`]()/[`StateFlow`]()
bridges over the Java API.**
`io.github.libtmux:libtmux-kotlin` — [on Maven Central](https://central.sonatype.com/artifact/io.github.libtmux/libtmux-kotlin).
> **Alpha.** The API will change without notice.
Needs Kotlin 2.1 or later. The module is compiled for Kotlin 2.2 metadata and
the 2.2 standard library, the level kotlinx-coroutines 1.11 is compiled for, and
a Kotlin compiler reads metadata up to one minor version newer than itself.
`ConsumerBaselineTest` fails if a build raises that level.
Every Kotlin example below is executed against a real tmux server by
[`ReadmeExamplesTest`](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/ReadmeExamplesTest.kt),
one test per section.
## Install
```kotlin
dependencies {
implementation(platform("io.github.libtmux:libtmux-bom:0.0.1-alpha.17"))
implementation("io.github.libtmux:libtmux-kotlin")
}
```
## Wrapper classes, not the Java types directly
[`Server`](), [`Session`](), [`Window`](), [`Pane`](), [`Client`](), and [`ControlClient`]() here are
Kotlin classes over the matching Java handle, which each answers as `asJava`;
[`Server.fromJava`]() wraps a server opened in Java. Every operation that reaches
tmux is [`suspend`](); captured state is a plain property. A tmux subsystem an
accessor answers — [`Pane.options()`](), [`Server.hooks()`](), [`Server.keys()`](), and
the rest — is wrapped the same way, so calling into it never blocks the
caller's thread outside [`ExecutionPolicy`]().
[`withServer`]() opens one and closes it even if the block throws or is
cancelled:
```kotlin
// Given: config: ServerConfig
withServer(config) { server ->
val session = server.newSession("build")
session.name // → build
server.admissionBound > 0 // → true
}
```
## Declare a session, its windows, and their splits with the DSL
`@DslMarker` keeps a nested block from reaching an outer block's receiver by
accident: a `split { }` inside a `window { }` cannot set the *session's*
`directory` without writing `this@newSession.directory` to say so. tmux
always gives a new session one window; the first `window { }` block renames
that one, and each later block is a genuine new window — two blocks make two
windows, not three.
```kotlin
// Given: config: ServerConfig
withServer(config) { server ->
val session = server.newSession {
name = "editors"
window { name = "left" }
window {
name = "right"
split { toRight(); percent(30) }
}
}
session.windows.size // → 2
}
```
## Send keys, capture output, run a command
```kotlin
// Given: config: ServerConfig
import io.github.libtmux.kotlin.orNull
import kotlin.time.Duration.Companion.seconds
withServer(config) { server ->
val session = server.newSession("keys-demo")
val pane = session.activeWindow?.activePane ?: error("no active pane")
pane.sendLine("echo ready")
pane.awaitText("ready", timeout = 5.seconds)
val lines = pane.capture()
val run = pane.run("echo hi && exit 3", timeout = 5.seconds)
run.exitStatus.orNull() // → 3
lines.isNotEmpty() // → true
}
```
## Query with typed fields on the companion
Fields live on the handle type's companion — [`Pane.command`](), not a static
[`Pane_.command()`]() — generated from the same `field-catalog.tsv` that
generates the Java metamodel, so the two can never disagree.
```kotlin
// Given: config: ServerConfig
import io.github.libtmux.kotlin.query.active
import io.github.libtmux.kotlin.query.command
withServer(config) { server ->
server.newSession("query-demo")
val editors = server.panes(Pane.command startsWith "nvim")
val activePanes = server.panes(Pane.active.isTrue())
editors.size >= 0 // → true
activePanes.isNotEmpty() // → true
}
```
[`server.session(expr)`]() throws [`CardinalityException`]() on no match or more than
one; [`server.sessionOrNull(expr)`]() is null on no match and still throws on
more than one — Kotlin's own collection convention (`single`/`singleOrNull`),
applied to a tmux lookup.
## Exhaustive `when` over the sealed failure tree
```kotlin
import io.github.libtmux.exception.CardinalityException
import io.github.libtmux.exception.CommandRejectedException
import io.github.libtmux.exception.ControlEndedException
import io.github.libtmux.exception.DispatchException
import io.github.libtmux.exception.LibTmuxException
import io.github.libtmux.exception.MalformedResponseException
import io.github.libtmux.exception.ServerUnavailableException
import io.github.libtmux.exception.TargetGoneException
import io.github.libtmux.exception.UnencodableTextException
import io.github.libtmux.exception.UnsupportedFeatureException
fun nextStep(failure: LibTmuxException): String =
when (failure) { // exhaustive, no else
is TargetGoneException -> "look it up again"
is ServerUnavailableException -> "start a server"
is CommandRejectedException -> "change the request"
is DispatchException -> if (failure.safeToRetry()) "send it again" else "read state first"
is ControlEndedException -> "attach again"
is UnsupportedFeatureException -> "do without"
is UnencodableTextException -> "use a UTF-8 locale"
is MalformedResponseException -> "report it"
is CardinalityException.NoMatch -> "nothing matched"
is CardinalityException.MultipleMatches -> "ambiguous"
}
nextStep(CardinalityException.MultipleMatches("many", 3)) // → ambiguous
```
A new leaf breaks every such `when` at the branch that omits it:
`ExhaustivenessCompileTest` compiles a `when` one branch short and asserts the
compiler rejects it.
## A subscription as a cold `Flow`
```kotlin
// Given: config: ServerConfig
import io.github.libtmux.control.Delivery
import kotlinx.coroutines.flow.first
withServer(config) { server ->
val session = server.newSession("flow-demo")
withControl(server, session) { control ->
val step = control.output(capacity = 64) {
control.send("send-keys", "-t", session.name, "echo flowed", "Enter")
}.first()
val outcome = when (step) { // exhaustive, no else
is Delivery.Event -> "kept"
is Delivery.Gap -> "lost ${step.missed}"
}
outcome // → kept
}
}
```
[`output`]()/[`events`]() open a *fresh* Java subscription per [`collect()`]() — over
[`EventSubscription.poll`]()/[`onReady`](), never a parked thread — and close it when
collection ends, is cancelled, or throws. Output tmux sends before that
subscription opens is not delivered, so the command whose output you want goes
in the trailing `onSubscribed` block, which runs once the subscription exists
and before the first read. A [`Delivery.Gap`]() is an element like any other,
ahead of the events that survived a full buffer; nothing is silently dropped.
## The live server as a `StateFlow`
```kotlin
// Given: config: ServerConfig
import kotlinx.coroutines.flow.first
withServer(config) { server ->
val session = server.newSession("live-demo")
server.withLiveState(session) { live ->
val view = live.first()
view.epoch >= 0L // → true
}
}
```
Wraps Java's [`ServerMirror`]() — resnapshot on notification, on gap, and on
reconnect happen once, inside it — rather than patching state in Kotlin.
Named [`liveState`](), not [`ServerMirror`](), so it does not collide with the Java
type it wraps. [`liveState`]()'s own background pump runs until its scope ends,
by design — a caller collecting for a program's whole life passes its own
long-lived scope — so [`withLiveState`]() is the scoped form for everything
shorter: it cancels the pump once its block returns, the same shape
[`withServer`]()/[`withControl`]() already have for the resources they open.
## Retry using the safe-to-retry predicate
```kotlin
// Given: config: ServerConfig
withServer(config) { server ->
val version = retryIfSafe(times = 3) { server.version() }
version.major >= 3 // → true
}
```
[`retryIfSafe`]() catches only [`DispatchException`]() and checks
`failure.safeToRetry()`, computed by the operation that threw it from its own
catalogued idempotence — no retry executor, and no catalog lookup at the call
site.
## Why nothing in Java may depend on this
The build fails if it does. Per the JSpecify specification a class carrying
`@kotlin.Metadata` is **not** null-marked, because the Kotlin compiler does not
yet emit full nullness into binaries
([KT-47417](https://youtrack.jetbrains.com/projects/KT/issues/KT-47417/Emit-jspecify-annotations-for-types-in-Kotlin-binaries)).
A Kotlin-authored API would therefore be worse for a Java caller and invisible to
NullAway. The dependency runs one way only.
## Next
- [Kotlin guide](https://libtmux.org/en/kotlin/latest/guides/coroutines/)
- [`libtmux`](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux/) — the API this wraps
- [Root README](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/README.md)
---
# Attaching to tmux
Source: https://libtmux.org/en/kotlin/latest/guides/attaching-to-tmux/
> Connect to an existing tmux server and find a session with Kotlin.
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 `Connect.kt`. `LIBTMUX_SOCKET_PATH` selects
the existing server. The launcher below supplies a private socket for trying
the example.
```kotlin title="Connect.kt"
import io.github.libtmux.ServerConfig
import io.github.libtmux.ServerEndpoint
import io.github.libtmux.kotlin.sessions
import io.github.libtmux.kotlin.withServer
import java.nio.file.Path
import java.time.Duration
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val socket = requireNotNull(System.getenv("LIBTMUX_SOCKET_PATH")) {
"Set LIBTMUX_SOCKET_PATH to an existing socket"
}
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
withServer(config) { server ->
val session = server.sessions().single { it.name == "work" }
println(session.name)
}
}
```
## Setup and run
Use an empty directory on Linux with Git and tmux 3.2a or newer installed.
This example was checked with JDK 25.0.3, Kotlin 2.4.10.
Save this file beside the program using the displayed filename.
```kotlin title="build.gradle.kts"
plugins {
application
kotlin("jvm") version "2.4.10"
}
repositories { mavenCentral() }
dependencies {
implementation("io.github.libtmux:libtmux-kotlin:0.0.1-alpha.17-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("."); kotlin.include("Connect.kt") }
}
application { mainClass.set("ConnectKt") }
```
Save this file beside the program using the displayed filename.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source")
```
Save this file beside the program using the displayed filename.
```properties title="gradle.properties"
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2
```
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-kotlin-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-java libtmux-source &&
git -C libtmux-source checkout 85ebf6955e56703c5be74e2afd34a18309044741 &&
sh run.sh ./libtmux-source/gradlew --project-dir . run --console=plain --max-workers=2
```
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/).
---
# Coroutines and flows
Source: https://libtmux.org/en/kotlin/latest/guides/coroutines/
> Coroutines and flows: io.github.libtmux:libtmux-kotlin documentation.
## Wrapper classes over the Java handles
[`Server`](), [`Session`](), [`Window`](), [`Pane`](), [`Client`](), and [`ControlClient`]() in
[`io.github.libtmux.kotlin`]() are hand-written Kotlin classes, not the Java types
directly and not extensions on them. A same-named extension is silently
shadowed by a Java member with only a compiler warning, so a member on a real
wrapper class is what makes a generated suspend mirror safe to add later
without an accidental collision.
Every operation that may contact tmux is [`suspend`](). Captured state — an id, a
name, a size — is a plain, non-suspending property. Query fields live on the
handle type's companion ([`Pane.command`]()), never as an instance member, so
[`Pane.command`]() (the field) and [`pane.currentCommand`]() (the captured value)
never collide.
A tmux subsystem an accessor answers — [`Pane.options()`](), [`Server.hooks()`](),
[`Server.shell()`](), [`Server.commands()`](), [`Server.buffers()`](),
[`Session.environment()`](), [`Server.messageLog()`](), [`Server.prompt()`](),
[`Server.keys()`](), [`Server.batch()`](), [`Server.chain()`]() — is wrapped the same way:
a small Kotlin class over the Java handle, whose own operations are [`suspend`]()
in turn.
```kotlin
// Given: config: ServerConfig
withServer(config) { server ->
val session = server.newSession("guide-demo")
session.name // → guide-demo
}
```
Each wrapper answers the Java handle it holds as `asJava`, the same object
rather than a copy, for a Java API that takes one. [`Server.fromJava`]() goes the
other way, over a server opened in Java, such as a test fixture's; closing the
wrapper closes that server. The two sets of classes share simple names, so a
file that needs both imports one under an alias:
`import io.github.libtmux.Server as JavaServer`.
## `ExecutionPolicy`: two independently sized pools
[`ExecutionPolicy.commands`]() backs every [`suspend`]() operation this module
generates or hand-writes, sized from [`ServerConfig.maxConcurrentCommands()`]() —
the transport's real admission bound, not a guessed constant.
[`ExecutionPolicy.streamReads`]() backs only what still genuinely blocks a
thread: [`Server.liveState`]()'s background pump. [`ControlClient.output`]()/[`events`]()
hold no thread from either pool; they are built on the non-blocking
[`EventSubscription.poll`]()/[`onReady`]() path, not a parked [`next()`]().
[`ExecutionPolicy.default(config)`]() sizes both pools; pass a differently-sized
one to [`Server.open`]() or [`withServer`]() when a program opens more than the
default 16 concurrent [`liveState`]() watches.
## The cold `Flow` bridge, and why it is not `callbackFlow`
[`ControlClient.output`]()/[`events`]() return a plain `flow {}` that calls
[`EventSubscription.poll()`]() from the collector itself, suspending on the
subscription's one-shot [`onReady`]() callback via `suspendCancellableCoroutine`
when nothing is buffered, and [`clearReady()`]() on cancellation. A fresh Java
subscription opens per [`collect()`](), so two concurrent collections never share
one subscription's buffer or gap accounting.
A `callbackFlow` over the same subscription would have its readiness callback
drain [`poll()`]() straight into the flow's channel, where a full channel's
`trySend` failing discards the polled item with no [`Gap`]() recorded. `flow {}`
calls [`poll()`]() only from the collector itself, so an overflow can only happen
inside the subscription's own buffer, which is exactly what turns into a
[`Gap`]().
## Blocking calls from a coroutine
Every libtmux call blocks its thread until tmux answers. This module already
wraps each one in [`runInterruptible`](), dispatched on [`ExecutionPolicy.commands`]()
or [`.streamReads`](), so cancelling the coroutine interrupts the wait rather than
leaving it running past the point nothing is listening for its result.
- An ordinary suspend call — [`newSession`](), [`capture`](), `sendLine` — already
runs on `policy.commands`, sized from the server's own admission bound.
- [`Server.liveState`]()'s background pump runs on [`policy.streamReads`]().
- [`ControlClient.output`]()/[`events`]() hold no thread at all; only collecting the
returned [`Flow`]() reads.
## The DSL, and the compile error the first draft had
`@DslMarker` marks [`SessionBuilder`](), [`WindowBuilder`](), and [`SplitBuilder`]() so an
inner block cannot reach an outer block's receiver by accident. Directory is
settable at all three levels for exactly this reason: writing `directory = x`
inside a nested `split { }` resolves to the innermost builder, and reaching
the outer one on purpose needs `this@newSession.directory = x` written out.
```kotlin
// Given: config: ServerConfig
withServer(config) { server ->
val session = server.newSession {
name = "layout-demo"
window { name = "editor" }
window {
name = "shell"
split { toRight(); percent(30) }
}
}
session.windows.size // → 2
}
```
tmux's [`SplitSpec.Builder`]() spells direction and size as method calls —
[`below()`]()/[`above()`]()/[`toRight()`]()/[`toLeft()`](), [`cells(n)`]()/[`percent(n)`]() — not an
enum. The DSL forwards to that real vocabulary directly rather than inventing
a parallel [`SplitDirection`]()/[`PaneSize`]() type.
`env(name, value)` is on [`SessionBuilder`](), [`WindowBuilder`](), and [`SplitBuilder`]()
alike, matching the Java [`SessionSpec.Builder`]()/[`WindowSpec.Builder`]()/
[`SplitSpec.Builder`]() each has, and sets a variable in the environment the new
session, window, or pane's process starts with.
## Why nothing in Java may depend on this
Nothing written in Java may depend on `libtmux-kotlin`, and the build fails if
it does. Per the JSpecify specification a class carrying `@kotlin.Metadata` is
*not* null-marked, because the Kotlin compiler does not yet emit full
nullness into binaries
([KT-47417](https://youtrack.jetbrains.com/projects/KT/issues/KT-47417/Emit-jspecify-annotations-for-types-in-Kotlin-binaries)).
A Kotlin-authored API would therefore be worse for a Java caller and invisible
to NullAway. The dependency runs one way only.
See the [module README](https://libtmux.org/en/kotlin/latest/guides/getting-started/) for the full call-site
tour, including the query DSL, the exhaustive `when` over sealed failures, and
[`StateFlow`]().