# 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`]().