# libtmux for Scala
> The Scala library (io.github.libtmux:libtmux-scala_3). These pages include its guides, tested examples and API documentation.
- [Scala API reference](https://libtmux.org/en/scala/latest/reference/): every public symbol, generated from the source. Hosted on libtmux.org.
---
# Concepts
Source: https://libtmux.org/en/scala/latest/concepts/
> Understand Scala handles, filters, transports and layout ownership.
Use these concepts to reason about Scala 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/scala/latest/reference/). For a first connection, start with [attaching to tmux](https://libtmux.org/en/scala/latest/guides/attaching-to-tmux/).
- [Server, session, window, pane](https://libtmux.org/en/scala/latest/concepts/server-session-window-pane/): Navigate captured sessions, windows, and panes, and refresh their state.
- [Filtering and queries](https://libtmux.org/en/scala/latest/concepts/queries/): Combine predicates, handle result counts, and match related windows.
- [Commands and control mode](https://libtmux.org/en/scala/latest/concepts/transports/): Run bounded commands and manage a persistent control client.
- [Layouts and repeated setup](https://libtmux.org/en/scala/latest/concepts/workspaces/): Create a split window and reuse a named window safely.
---
# Server, session, window, pane
Source: https://libtmux.org/en/scala/latest/concepts/server-session-window-pane/
> Traverse captured Scala 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/scala/latest/reference/io-github-libtmux-scaladsl-server/) selects the socket and owns client resources. [`Session`](https://libtmux.org/en/scala/latest/reference/io-github-libtmux-scaladsl-session/), [`Window`](https://libtmux.org/en/scala/latest/reference/io-github-libtmux-scaladsl-window/) and [`Pane`](https://libtmux.org/en/scala/latest/reference/io-github-libtmux-scaladsl-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") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("${example}.scala") }
application { mainClass.set(example) }
```
```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.
```scala title="Hierarchy.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Hierarchy {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val sessions = server.sessions()
val windows = sessions.flatMap(_.windows)
val panes = windows.flatMap(_.panes)
assert(sessions.size == 2 && windows.size == 2 && panes.size == 2)
for (session <- sessions) {
println(s"${session.name}: ${session.windows.head.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.
```scala title="Refresh.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Refresh {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val session = server.sessions().find(_.name == "work-one").get
val before = session.windows.head
val after = before.rename("renamed")
assert(before.name == "editor")
assert(after.name == "renamed")
val readAgain = server.sessions().find(_.name == "work-one").get.windows.head
assert(readAgain.name == "renamed")
println(s"${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/scala/latest/concepts/queries/) for missing and ambiguous selections, and [layouts](https://libtmux.org/en/scala/latest/concepts/workspaces/) for creation.
---
# Filtering and queries
Source: https://libtmux.org/en/scala/latest/concepts/queries/
> Compose Scala filters, distinguish zero and several matches, and query captured relations.
Filter captured objects in Scala, 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.
[`Expr`](https://libtmux.org/en/scala/latest/reference/io-github-libtmux-scaladsl-query-expr-zfkn/) supports `&&`, `||` and `!`. The field companions expose equality, prefix, suffix, substring and pattern matching. Use [`.matching(expression)`]() for a typed query or `.filter(predicate)` for an ordinary Scala condition.
## 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") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("${example}.scala") }
application { mainClass.set(example) }
```
```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.
```scala title="Local.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Local {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val sessions = server.sessions()
val matching = sessions.matching(Session.name.startsWith("work-"))
val names = matching.map(_.name).sorted
assert(names == Vector("work-one", "work-two"))
println(names.mkString(", "))
// Filtering the captured vector makes no new tmux calls.
assert(sessions.filter(_.name.startsWith("work-")) == matching)
val onlyOne = Session.name.startsWith("work-") && Session.name.endsWith("one")
assert(sessions.matching(onlyOne).head.name == "work-one")
val either = Session.name.is("work-one") || Session.name.is("work-two")
assert(sessions.matching(either).size == 2)
assert(sessions.matching(!Session.name.is("work-one")).head.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
An exactly-one selection returns an [`Either`](): one handle on success, [`CardinalityError.NoMatch`]() for zero, or [`MultipleMatches`]() for several. An at-most-one selection also rejects ambiguity; it does not pick an arbitrary first result.
```scala title="Cardinality.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Cardinality {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val sessions = server.sessions()
for (name <- Vector("work-one", "missing")) {
sessions.matching(Session.name.is(name)).exactlyOne match {
case Right(session) => println(s"${session.name}: selected")
case Left(CardinalityError.NoMatch) => println(s"$name: absent")
case Left(CardinalityError.MultipleMatches(count)) =>
throw new IllegalStateException(s"At least $count sessions named $name")
}
}
val many = sessions.matching(Session.name.startsWith("work-")).atMostOne
assert(many == Left(CardinalityError.MultipleMatches(2)))
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.
```scala title="Relations.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Relations {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val sessions = server.sessions()
val editorWindow = Window.name.is("editor")
val withEditor = sessions.matching(Session.windows.any(editorWindow))
val withoutEditor = sessions.matching(Session.windows.none(editorWindow))
assert(withEditor.map(_.name) == Vector("work-one"))
assert(withoutEditor.map(_.name) == Vector("work-two"))
println(s"editor: ${withEditor.head.name}")
println(s"no editor: ${withoutEditor.head.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/scala/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/scala/latest/concepts/transports/
> Use Scala 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.
The direct [`Server`](https://libtmux.org/en/scala/latest/reference/io-github-libtmux-scaladsl-server/) API blocks until the operation completes. [`Using.resource`]() closes its client resources. For effectful applications, the [execution guide](https://libtmux.org/en/scala/latest/guides/execution/) covers the Cats Effect and Ox packages.
## 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") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("${example}.scala") }
application { mainClass.set(example) }
```
```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.
```scala title="Local.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Local {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val sessions = server.sessions()
val matching = sessions.matching(Session.name.startsWith("work-"))
val names = matching.map(_.name).sorted
assert(names == Vector("work-one", "work-two"))
println(names.mkString(", "))
// Filtering the captured vector makes no new tmux calls.
assert(sessions.filter(_.name.startsWith("work-")) == matching)
val onlyOne = Session.name.startsWith("work-") && Session.name.endsWith("one")
assert(sessions.matching(onlyOne).head.name == "work-one")
val either = Session.name.is("work-one") || Session.name.is("work-two")
assert(sessions.matching(either).size == 2)
assert(sessions.matching(!Session.name.is("work-one")).head.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.
```scala title="Control.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Control {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val session = server.sessions().find(_.name == "work-one").get
Using.resource(server.control(session)) { control =>
val reply = control.send("list-sessions", "-F", "#{session_name}")
require(reply.succeeded(), s"tmux rejected list-sessions: ${reply.outcome()}")
val names = reply.lines().asScala.toVector.sorted
assert(names == Vector("work-one", "work-two"))
println(names.mkString(", "))
}
assert(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/scala/latest/guides/attaching-to-tmux/) for the connection-only example.
---
# Layouts and repeated setup
Source: https://libtmux.org/en/scala/latest/concepts/workspaces/
> Create and reuse Scala tmux layouts while preserving running processes.
Build a tmux layout from Scala 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") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
```kotlin title="build.gradle.kts"
val example = providers.gradleProperty("example").getOrElse("Local")
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("${example}.scala") }
application { mainClass.set(example) }
```
```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.
```scala title="Layout.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object Layout {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val session = server.sessions().find(_.name == "work-one").get
val window = session.newWindow(io.github.libtmux.WindowSpec.builder()
.named("tools").running("/bin/cat").build())
window.panes.head.split(io.github.libtmux.SplitSpec.builder()
.toRight().running("/bin/cat").build())
window.selectLayout(io.github.libtmux.Layout.EVEN_HORIZONTAL)
val refreshed = server.sessions().find(_.name == "work-one").get
val tools = refreshed.windows.find(_.name == "tools").get
assert(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.
```scala title="ReuseLayout.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import io.github.libtmux.scaladsl.query.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
import scala.jdk.CollectionConverters.*
object ReuseLayout {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
def ensureTools(): Window = {
val session = server.sessions().find(_.name == "work-one").get
session.windows.find(_.name == "tools").getOrElse {
session.newWindow(io.github.libtmux.WindowSpec.builder()
.named("tools").running("/bin/cat").build())
}
}
val first = ensureTools()
val second = ensureTools()
assert(first.id == second.id)
assert(server.sessions().find(_.name == "work-one").get.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/scala/latest/concepts/queries/) for stricter target selection and [captured handles](https://libtmux.org/en/scala/latest/concepts/server-session-window-pane/) to understand refresh behavior.
---
# Examples
Source: https://libtmux.org/en/scala/latest/examples/
> Tested Scala examples against an isolated tmux server.
Start with [Capture pane output](https://libtmux.org/en/scala/latest/examples/capture-pane-output/) for a standalone program with imports, Gradle files, and private-server cleanup.
For an existing server, use [Attaching to tmux](https://libtmux.org/en/scala/latest/guides/attaching-to-tmux/).
That program includes the imports, build files, and launcher. It reads a session
without stopping the server that owns it.
---
# Capture pane output
Source: https://libtmux.org/en/scala/latest/examples/capture-pane-output/
> Run a complete Scala program that captures output on an isolated tmux server.
This complete Scala 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.
```scala title="Capture.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint, SessionSpec}
import io.github.libtmux.scaladsl.*
import java.nio.file.{Files, Path}
import java.time.Duration
import scala.util.Using
object Capture {
def main(args: Array[String]): Unit = {
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: Option[Throwable] = None
try {
Using.resource(Server.open(config)) { server =>
try {
val session = server.newSession(SessionSpec.builder()
.named("capture").running("/bin/sh").env("ENV", "/dev/null").build())
val pane = session.windows.head.panes.head
pane.sendLine("printf '\\nlibtmux capture ready\\n'")
val deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos
var captured = false
while (!captured && System.nanoTime() < deadline) {
captured = pane.capture().contains("libtmux capture ready")
if (!captured) Thread.sleep(25)
}
if (!captured)
throw new IllegalStateException("Output did not arrive within five seconds")
println("libtmux capture ready")
} catch {
case error: Throwable =>
failure = Some(error)
throw error
} finally {
try {
if (Files.exists(socket)) server.killServer()
Files.deleteIfExists(socket)
Files.delete(directory)
} catch {
case cleanup: Throwable => failure match {
case Some(original) => original.addSuppressed(cleanup)
case None => throw 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
The Scala facade uses blocking calls. The loop checks complete captured lines against a monotonic deadline. [`Using.resource`](https://www.scala-lang.org/api/3.x/scala/util/Using$.html) 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 Scala 3.9.0 and include the library build.
Save `settings.gradle.kts` beside `Capture.scala`.
```kotlin title="settings.gradle.kts"
rootProject.name = "capture"
includeBuild("libtmux-source") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
Save `build.gradle.kts` beside `Capture.scala`.
```kotlin title="build.gradle.kts"
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("Capture.scala") }
application { mainClass.set("Capture") }
```
Save `gradle.properties` beside `Capture.scala`.
```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/scala/latest/guides/
> Guides for io.github.libtmux:libtmux-scala_3.
Connect to tmux and work with its sessions, windows, and panes.
- [Getting started](https://libtmux.org/en/scala/latest/guides/getting-started/): Read about getting started.
- [Attaching to tmux](https://libtmux.org/en/scala/latest/guides/attaching-to-tmux/): Connect to an existing socket and leave its server running.
- [Installation and requirements](https://libtmux.org/en/scala/latest/guides/overview/): Read about libtmux-scala.
- [Queries](https://libtmux.org/en/scala/latest/guides/query/): Read about queries.
- [Ownership](https://libtmux.org/en/scala/latest/guides/ownership/): Read about ownership.
- [Execution](https://libtmux.org/en/scala/latest/guides/execution/): Read about execution.
- [Streaming](https://libtmux.org/en/scala/latest/guides/streaming/): Read about streaming.
- [Compatibility](https://libtmux.org/en/scala/latest/guides/compatibility/): Read about compatibility.
---
# Getting started
Source: https://libtmux.org/en/scala/latest/guides/getting-started/
> Getting started: io.github.libtmux:libtmux-scala_3 documentation.
The Scala facades need Scala 3.9 and JDK 25 or newer, and tmux 3.2a through
3.7c on the machine they drive.
## Install
Three artifacts, all in group `io.github.libtmux` and all at the same version
as `libtmux` itself. `%%` adds the `_3` suffix that marks a Scala 3 artifact.
They are on Maven Central and release with the Java artifacts.
- **`libtmux-scala_3`** — direct-style handles, collections and the typed query
DSL.
- **`libtmux-scala-cats_3`** — Cats Effect resources and FS2 observations.
- **`libtmux-scala-ox_3`** — an Ox [`Flow`]() over subscriptions and live views.
```sbt
libraryDependencies += "io.github.libtmux" %% "libtmux-scala" % ""
```
For Cats Effect and FS2, or for Ox, add the matching module:
```sbt
libraryDependencies += "io.github.libtmux" %% "libtmux-scala-cats" % ""
```
```sbt
libraryDependencies += "io.github.libtmux" %% "libtmux-scala-ox" % ""
```
From Gradle or Maven, name the suffixed artifact directly:
`io.github.libtmux:libtmux-scala_3:`. The core facade depends on
`libtmux` and the Scala 3 library, and on nothing else.
## A first client
The function below takes three caller-supplied values and constructs a Java
[`ServerConfig`]() before opening the Scala client:
| Parameter | Value to supply |
| --- | --- |
| `binary` | Absolute path to your tmux executable |
| `socket` | An isolated socket you own under `/tmp/libtmux-java-dev/` |
| `configFile` | Your tmux configuration file, or `/dev/null` for none |
For example, choose `/tmp/libtmux-java-dev/scala-start/socket`; the function
creates its parent directory. Opening the client does not create tmux;
[`newSession`]() does. The operation creates a session, splits its window, verifies
the resulting panes and kills that session. Closing the client is separate
from session cleanup. An existing server's other sessions remain running;
tmux normally exits when its final session closes.
Call `firstClient` with your three values from your application.
The final call in this tested snippet takes those inputs from the owned test
fixture's `config`; the function builds and uses its own configuration.
```scala
import io.github.libtmux.{
Layout, ServerConfig, ServerEndpoint, SessionSpec, SplitSpec
}
import io.github.libtmux.scaladsl.{config => _, *}
import java.nio.file.{Files, Path}
import java.time.Duration
import scala.util.Using
def firstClient(binary: String, socket: Path, configFile: Path): Unit = {
require(Path.of(binary).isAbsolute, "supply an absolute tmux executable")
val ownedSocket = socket.toAbsolutePath.normalize()
require(
ownedSocket.startsWith(Path.of("/tmp/libtmux-java-dev")) ||
ownedSocket.startsWith(Path.of("/tmp/libtmux-java-test")),
"choose a socket under an owned libtmux Java directory"
)
Files.createDirectories(ownedSocket.getParent)
val selected = ServerConfig.builder()
.binary(binary)
.endpoint(ServerEndpoint.socketPath(ownedSocket))
.configFile(configFile)
.defaultTimeout(Duration.ofMillis(800))
.build()
Using.resource(Server.open(selected)) { server =>
val session = server.newSession(
SessionSpec.builder().named("scala-start").running("cat", "-").build()
)
try {
val window = session.windows.head
val second = window.split(
SplitSpec.builder().running("cat", "-").build()
)
window.selectLayout(Layout.EVEN_HORIZONTAL)
second.select()
// window.panes is CAPTURED: it answers from window's own frozen capture, taken before the
// split, so this refreshes first rather than reading stale data.
val panes = window.refresh().panes
assert(panes.size == 2)
assert(panes.exists(_.info.id().value() == second.info.id().value()))
} finally session.kill()
}
}
config.endpoint() match {
case endpoint: ServerEndpoint.SocketPath =>
firstClient(
config.binaryPath(),
endpoint.path(),
config.configFile().orElse(Path.of("/dev/null"))
)
case _ => throw new IllegalArgumentException("an explicit socket is required")
}
```
Timeouts are [`scala.concurrent.duration.FiniteDuration`]() at every public entry
point, converted once at the boundary: `pane.awaitText("$", 5.seconds)`
never surfaces [`java.time.Duration`]() to the caller.
Follow with [queries](https://libtmux.org/en/scala/latest/guides/query/), [ownership](https://libtmux.org/en/scala/latest/guides/ownership/), then
[execution](https://libtmux.org/en/scala/latest/guides/execution/). For immediate access without the facade, use the
[direct Java guide](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/docs/guide/scala.md).
## Build from source
The facades build with the rest of the repository, from its root:
```console
$ ./gradlew :libtmux-scala:check :libtmux-scala-cats:check :libtmux-scala-ox:check
```
The real-tmux suites run with the Java ones in `integration-tests`:
```console
$ ./gradlew :integration-tests:test --tests 'io.github.libtmux.scaladsl.*'
```
Format Scala sources before committing:
```console
$ ./gradlew spotlessApply
```
The API documentation, with a browser for every source it documents, generated
operations included, starts at `libtmux-scala/build/docs/scaladoc/index.html`:
```console
$ ./gradlew :libtmux-scala:scaladocSite
```
Begin with [direct-style `Server`][server] or [Cats `Server`][cats-server].
[server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala/src/main/scala/io/github/libtmux/scaladsl/Server.scala
[cats-server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Server.scala
---
# Attaching to tmux
Source: https://libtmux.org/en/scala/latest/guides/attaching-to-tmux/
> Connect to an existing tmux server and find a session with Scala.
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.scala`. `LIBTMUX_SOCKET_PATH` selects
the existing server. The launcher below supplies a private socket for trying
the example.
```scala title="Connect.scala"
import io.github.libtmux.{ServerConfig, ServerEndpoint}
import io.github.libtmux.scaladsl.*
import java.nio.file.Path
import java.time.Duration
import scala.util.Using
object Connect {
def main(args: Array[String]): Unit = {
val socket = sys.env.getOrElse("LIBTMUX_SOCKET_PATH",
throw new IllegalArgumentException("Set LIBTMUX_SOCKET_PATH to an existing socket"))
val config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(Path.of(socket)))
.defaultTimeout(Duration.ofSeconds(5))
.build()
Using.resource(Server.open(config)) { server =>
val session = server.sessions().find(_.name == "work").getOrElse(
throw new IllegalStateException("The work session does not exist"))
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, Scala 3.9.0.
Save this file beside the program using the displayed filename.
```kotlin title="build.gradle.kts"
plugins {
application
scala
}
repositories { mavenCentral() }
dependencies {
implementation("org.scala-lang:scala3-library_3:3.9.0")
implementation("io.github.libtmux:libtmux-scala_3:0.0.1-alpha.17-SNAPSHOT")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }
sourceSets.main { scala.srcDir("."); scala.include("Connect.scala") }
application { mainClass.set("Connect") }
```
Save this file beside the program using the displayed filename.
```kotlin title="settings.gradle.kts"
rootProject.name = "connect"
includeBuild("libtmux-source") {
dependencySubstitution {
substitute(module("io.github.libtmux:libtmux-scala_3"))
.using(project(":libtmux-scala"))
}
}
```
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-scala-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/).
---
# Installation and requirements
Source: https://libtmux.org/en/scala/latest/guides/overview/
> Installation and requirements: io.github.libtmux:libtmux-scala_3 documentation.
**Scala 3 collections and opaque handles over libtmux for Java.**
Use Scala collections and explicit effects to inspect and operate tmux through
libtmux for Java. The direct-style facade's handles are opaque aliases of the
Java ones — lossless by construction, never a hand-copied parallel type — and
return immutable [`Vector`]() and [`Option`]() values. The
[separate Cats module](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/README.md) supplies scoped
effects and FS2 observations. Both retain the original Java handles, targeting
rules and command engine.
**This project is alpha.** Releases carry an `-alpha` prerelease tag. The API is
not settled, and any release may change or remove exported identifiers without a
deprecation period. Pin an exact version rather than a range. Not recommended
for production.
## Requirements
Scala 3.9 and JDK 25 or newer; there is no Scala 2.13 build. tmux 3.2a through
3.7c, which the Java library supports and every tmux lane in CI runs these
suites against. See [Compatibility](https://libtmux.org/en/scala/latest/guides/compatibility/).
## Installation
```sbt
libraryDependencies += "io.github.libtmux" %% "libtmux-scala" % ""
```
From Gradle or Maven the coordinate is `io.github.libtmux:libtmux-scala_3`.
The version is always `libtmux`'s own: the Scala artifacts are on Maven Central
and release with the Java ones. Core depends on `libtmux` and the Scala 3 library only; Cats, FS2
and Ox arrive through [`libtmux-scala-cats`](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/) and
[`libtmux-scala-ox`](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-ox/).
## Inspect captured panes
Start with [a first client](https://libtmux.org/en/scala/latest/guides/getting-started/#a-first-client) to construct
[`ServerConfig`]() from your tmux executable, owned socket and configuration file.
Here `config` selects an existing server. Opening the client does not create a
session. This operation closes its owned client while leaving the daemon
running.
```scala
import io.github.libtmux.scaladsl.{config => _, *}
import scala.util.Using
Using.resource(Server.open(config)) { server =>
val panes = server.panes()
val named = panes.filter(_.info.title().nonEmpty)
val commands = named.map(_.info.currentCommand())
assert(commands.size <= panes.size)
assert(panes.forall(p => p.window.info.context().equals(p.info.context())))
}
```
[`server.panes()`]() acquires state. Reading `info`, traversing `window`, or
filtering the captured vector performs no further tmux commands. `refresh()`
returns a new capture. A failed acquisition raises the original Java error;
it does not become an empty vector.
## Documentation
- [Getting started](https://libtmux.org/en/scala/latest/guides/getting-started/): installation, a
first owned client, and building from source.
- [Queries](https://libtmux.org/en/scala/latest/guides/query/): native collections, the typed field
DSL and strict cardinality.
- [Ownership](https://libtmux.org/en/scala/latest/guides/ownership/): captured handles, borrowing and
cleanup.
- [Execution](https://libtmux.org/en/scala/latest/guides/execution/): direct-style calls, bounded
effects and command outcomes.
- [Streaming](https://libtmux.org/en/scala/latest/guides/streaming/): subscriptions, cancellation and
visible loss.
- [Compatibility](https://libtmux.org/en/scala/latest/guides/compatibility/): compilers, runtimes and
what CI runs.
- [Examples](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/examples/): runnable programs, each executed against a real tmux.
The [Java guide](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/docs/guide/scala.md) shows direct Java use without the
facade. Source contracts live beside [direct-style operations][server]
and [Cats resources][cats-server]. For changes to existing APIs, see the
repository's [migration notes](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/MIGRATION.md).
[server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala/src/main/scala/io/github/libtmux/scaladsl/Server.scala
[cats-server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Server.scala
---
# Queries
Source: https://libtmux.org/en/scala/latest/guides/query/
> Queries: io.github.libtmux:libtmux-scala_3 documentation.
Acquire once, then use Scala collections. A handle's captured fields (`.info`,
returning the real Java [`PaneState`]()/[`WindowState`]()/[`SessionState`]()/[`ClientState`]()
record) describe state as of that capture. A `filter`, `find`, `collect`, sort
or comprehension over a [`Vector`]() of handles does not acquire another
snapshot. A custom predicate can still perform whatever work its author puts
inside it.
## Native predicates
`config` identifies the server. Keep the capture while deriving several views
of the same data. A view defers local computation; it does not refresh tmux.
```scala
import io.github.libtmux.scaladsl.{config => _, *}
import scala.util.Using
Using.resource(Server.open(config)) { server =>
val panes = server.panes()
val named = panes.filter(_.info.title().nonEmpty)
val paths = panes.collect {
case pane if pane.info.pid().isPresent => pane.info.currentPath()
}
val firstActive = panes.find(_.info.active())
val deferred = panes.view.filter(_.info.title().nonEmpty)
assert(deferred.toVector == named)
assert(paths.size <= panes.size)
assert(firstActive.forall(_.info.active()))
}
```
Missing metadata answers through the record's own Java accessor ([`Optional`](),
[`OptionalLong`]()): a missing PID is different from PID zero, and unavailable
floating-pane metadata is different from [`Optional.of(false)`]().
## The typed field DSL and strict cardinality
Fields hang on the handle type's own companion ([`Pane.command`](), [`Pane.active`](),
...), generated one-line forwards to Java's own field metamodel
([`Pane_.command()`]()). [`.matching`]() filters a captured [`Vector`]() locally; `&&`,
`||` and `!` compose expressions, alongside their named forms [`.and`](), [`.or`]()
and [`.not`](). [`.exactlyOne`]()/[`.atMostOne`]() give the strict cardinality `headOption`
and `find` do not: both return `Either[CardinalityError, ...]`, mirroring
Java's own [`CardinalityException`]() leaves losslessly, including
[`MultipleMatches`]()'s [`atLeast`]() count.
```scala
import io.github.libtmux.scaladsl.query.*
assert(Vector.empty[Int].atMostOne == Right(None))
assert(Vector("selected").exactlyOne == Right("selected"))
val ambiguous = Vector(1, 2).atMostOne
assert(ambiguous == Left(CardinalityError.MultipleMatches(2)))
```
## Symbolic and named operators
```scala
import io.github.libtmux.scaladsl.{config => _, *}
import io.github.libtmux.scaladsl.query._
import scala.util.Using
Using.resource(Server.open(config)) { server =>
val panes = server.panes()
val expression = Pane.command.is("cat") && Pane.width.atLeast(1)
val selected = panes.matching(expression)
val native = panes.filter(p => p.info.currentCommand() == "cat" && p.info.size().width() >= 1)
assert(selected == native)
}
```
The Cats module reads the same expressions over a captured [`Vector`](), inside
the effect's own `map`: `server.panes.map(_.filter(...))` (`Vector[Pane[F]]`
does not itself carry [`.matching`]() — apply the predicate to each pane's own
`.info` field instead, or to `.underlying.asJava` for a Java expression). This
is still local evaluation over a captured vector; it does not run a Java
command.
An invalid field/operator combination must fail at compilation:
```scala
import io.github.libtmux.scaladsl.Pane
Pane.active.contains("yes")
```
## Relationships and identity
Java's [`Session_`](), [`Window_`](), [`Pane_`]() and [`Client_`]() expose the canonical
fields; the Scala field companions are one-line forwards to them, never a
second, hand-copied metamodel. Use their existing [`any`](), `all`, [`none`]() and
to-one `is` relations, exposed through [`Fields.ToManyRef`]()/[`Fields.ToOneRef`]().
`all` over an empty relation is true. Applying [`any`]() to a conjunction requires
one related object satisfying both conditions; conjoining two separate [`any`]()
expressions allows two different related objects.
Window equality includes its session and index placement. Pane equality is
physical, while traversal retains each occurrence's window context. A vector
can therefore contain equal pane handles reached through different links.
Deduplicate only when that is the intended question, retaining server identity
when mixing endpoints. See [ownership](https://libtmux.org/en/scala/latest/guides/ownership/).
## Serialization and regular expressions
Opted-in serialization uses the separate Java `libtmux-jackson` artifact and
its `libtmux.filter/1` schema, over the raw Java [`FilterExpr`]() an `Expr[T]`
wraps (`.asJava`). Keep the canonical Java field and relation objects;
rebuilding an accessor with the same name does not establish model authority.
Unknown schemas, fields, operators and relations remain errors. Arbitrary
Scala closures are not serialized.
Java regex matching uses [`java.util.regex.Pattern`]() and substring search through
[`Matcher.find`](). Supply a Scala regex's `.pattern` explicitly. Neither the
expression adapter nor serialization implies tmux `-f` compilation or an MCP
query parameter.
---
# Ownership
Source: https://libtmux.org/en/scala/latest/guides/ownership/
> Ownership: io.github.libtmux:libtmux-scala_3 documentation.
Opening a Scala server owns a Java client. Closing that client releases its
transport and local workers; it does not kill the tmux daemon. [`killServer`]() is
an explicit, separate operation. A borrowed facade leaves the original Java
client under its existing owner's control.
## Direct-style scopes
[`Server.open`][server] returns an opaque [`Server`](), bounded by [`AutoCloseable`](),
for [`scala.util.Using.resource`]() or an explicit `try`/`finally` scope — the
bound is the type's own, so [`close()`]() needs no forwarding of its own. Its
[`close`]() is idempotent. [`Server.fromJava`]() borrows a client instead of owning
one; closing that facade closes the Java client too, so a caller who only
means to borrow keeps the original owner's [`close()`]() as the one that matters.
An opaque handle carries no Scala-side scope state of its own — no
owned/closed/parent bookkeeping to keep in sync with Java's. Whether a call
through it fails after the owning [`Server`]() closes is exactly Java's own
use-after-close contract ([`ServerClosedException`]()), not a second policy this
facade adds.
## Effect scopes
[`cats.Server.resource`][cats-server] acquires the client when its [`Resource`]()
runs. [`cats.Server.fromJava`]() also returns a [`Resource`](), with borrowing
semantics. Both scope the operations submitted through the returned facade.
Release rejects new work, cancels queued and running operations, waits for their
finalizers, and then closes the owned client if there is one.
Returning a handle or an unevaluated effect from the resource body does not
extend its lifetime. The handle's captured data remains available; executing
its effect after release fails. Release cancels operations still running; an
operation can finish before that cancellation reaches it. A caller that masks
cancellation can receive a scope-closed failure instead of waiting indefinitely
for a canceled child operation.
Partial acquisition failure and failure in the resource body still release
what the resource acquired. Cancellation also waits for cleanup. Borrowing
changes which client is closed, not the requirement to release the facade's
own work. If an external owner closes a borrowed transport while a request is
dispatched, that request can fail with Java's `UNKNOWN` dispatch outcome. The
facade preserves that failure; it does not turn owner closure into a successful
empty result or an automatic retry.
Here `config` selects an existing server with a session. The outer resource
owns Java. The inner Scala scope borrows it, and the original client still
answers after that scope ends:
```scala
import _root_.cats.effect.{IO, Resource}
import io.github.libtmux.{Server => JavaServer}
import io.github.libtmux.scaladsl.cats.{config => _, *}
import io.github.libtmux.scaladsl.cats.{Server => ScalaServer}
Resource
.make(IO.blocking(JavaServer.open(config)))(java => IO.blocking(java.close()))
.use { java =>
ScalaServer.fromJava[IO](java).use { server =>
server.sessions().flatMap(values => IO(assert(values.nonEmpty)))
}.flatMap(_ => IO.blocking(assert(java.isAlive())))
}
```
## Java identity and one escape hatch
Every direct-style handle is opaque over its Java one — the same object, not a
copy — so equality and hash code are Java's own. A pane's identity is
physical; a window's identity includes its captured session and window
index. Borrowing does not replace that identity by reacquiring the same
textual ID. Refresh follows the Java contract and can return a pane through a
different window occurrence.
`.asJava` is the one escape hatch, on every handle, always public: opaque
wrapping is free, so it is a zero-cost coercion, never a copy and never a
second, more "unsafe" name to reach for. Code using the raw Java handle must
still follow its ownership and threading contract. Reaching `.asJava` from an
owned scope does not keep that client open past the Scala facade's own
[`close()`](); reaching it from a borrowed scope does not make the Scala facade
its owner.
## Control attachments
[`Control.attach`][control] on a captured session owns a separate process
attachment to that capture's process, started by the session's transport.
[`Control.attachUnfenced`]() on a config and session id does not check the process.
Releasing either stops its admitted requests before closing the attachment and
preserves the daemon.
Acquire an output or event subscription inside the attachment's resource and
before starting its producer. Release the subscription before the attachment.
An [observation][observation] allows one active stream consumer; canceled
consumption releases that slot. Releasing the observation closes its Java
subscription and discards buffered events. Keep command and observation
attachments separate when canceling a command must not end observation.
See [execution](https://libtmux.org/en/scala/latest/guides/execution/) for admission bounds, cancellation uncertainty,
and the difference between a control acknowledgement and command completion.
[server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala/src/main/scala/io/github/libtmux/scaladsl/Server.scala
[cats-server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Server.scala
[control]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Control.scala
[observation]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Observation.scala
---
# Execution
Source: https://libtmux.org/en/scala/latest/guides/execution/
> Execution: io.github.libtmux:libtmux-scala_3 documentation.
The direct-style facade runs operations immediately. The Cats facade
constructs lazy effects: creating a read or mutation effect performs no tmux
I/O, and evaluating it again repeats the operation. Both delegate commands and
transport behavior to Java. Choose the [direct-style server][server] for
ordinary synchronous code or the [Cats server][cats-server] inside a managed
effect scope.
Here `config` is a Java [`ServerConfig`]() for an existing tmux server. The same
read effect observes the value present at each evaluation:
```scala
import _root_.cats.effect.IO
import io.github.libtmux.scaladsl.cats.{config => _, *}
import io.github.libtmux.scaladsl.cats.{Server => ScalaServer}
ScalaServer.resource[IO](config).use { server =>
val name = "scala-guide-session"
val read = server.hasSession(name)
for {
before <- read
_ <- server.newSession(name)
after <- read
_ <- IO {
assert(!before)
assert(after)
}
} yield ()
}
```
Captured `info`, session windows, window panes, and client attachments are pure
reads: generated as plain values in both layers, never wrapped in `F` on the
Cats side, since the catalog marks them [`CAPTURED`](). A captured accessor that
answers a tmux subsystem instead — [`Server.hooks`](), [`Pane.options`](),
[`Server.shell`](), and the rest — is CAPTURED for the same reason, its own
construction touches no tmux state, but on the Cats side it answers that
subsystem's own wrapper class rather than the raw Java handle direct style
still returns; the subsystem's own reads and mutations then run through `F`
exactly as every other operation does. Filtering their immutable collections
does not refresh them. Listing, refresh, format expansion, pane mode
inspection, and mutation perform I/O. [`Pane.awaitText`]() polls captured text.
## Admission and blocking work
Cats operations run through an [interruptible blocking boundary][execution],
including a server or control client's own acquisition step and every
subsystem operation reached through [`Server.hooks`](), [`Pane.options`](), and the
rest. An owned server
accepts `maxConcurrentCalls` from one through four. A borrowed server accepts
a positive bound, but that bound covers only calls through that facade. Its
owner must account for other users and the underlying transport's capacity.
[`Pane.awaitText`]() and [`Pane.await`]() reserve one facade call for work that can
release the wait (`Execution.waiting`), and require capacity of at least two.
[`Pane.run`]() uses ordinary admission: its private completion channel is normally
signalled by the pane's shell.
This bounds admitted calls; it does not make Java I/O thread-free. Waiting
operations occupy blocking workers, and process transport uses its own workers
to service child processes. The limit does not bound the number of fibers a
caller may queue. Use bounded effect traversal when submitting a large
collection. Ordered traversal preserves result order, while concurrent tmux
mutations can still reach the daemon in a different order.
Prompt cancellation depends on a borrowed transport honoring interruption and
completing its cleanup.
Command timeouts start when Java receives the operation, after Scala admission.
An effect timeout can bound admission and execution together, but cancellation
still waits for the operation's cleanup. A timeout is not a promise that a
dispatched mutation had no effect.
## Failures and cancellation
Ordinary failures preserve Java's exception and dispatch certainty: the sealed
[`LibTmuxException`]() tree matches directly from Scala (see
[errors](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/docs/guide/scala.md)), and `DispatchException#safeToRetry`
answers the question a caller usually has without a catalog lookup. A
[`NOT_DISPATCHED`]() transport failure differs from `UNKNOWN`: the latter may have
changed tmux. Raw [`Server.cmd`]() returns Java's own [`transport.CommandResult`](), so
a nonzero exit remains data with its stdout and stderr.
Cats cancellation interrupts local work, waits for owned cleanup, and ends in
`Outcome.Canceled` whether or not the command reached tmux. Cancellation carries
no error, so the facade does not turn it into one. To learn whether a canceled
command was dispatched, set an [`OperationObserver`]() on the Java [`ServerConfig`]():
the process transport reports `UNKNOWN` for a command it interrupted after
starting. Treat a canceled mutation as possibly dispatched; the facade does not
retry it, roll it back, or switch transports. A daemon-side shell job can
continue after its requesting client is canceled. Closing a borrowed transport
from its owner can instead produce an ordinary Java `UNKNOWN` failure.
## Control replies
[`Control.acknowledge`]() reports a [protocol reply][control]. [`accepted`]() means a
successful reply frame arrived; deferred tmux work can still be running. Use a
separate completion signal for that work. Canceling an active control request
can close its attachment and affect queued requests and observations, so use
separate attachments when their lifetimes must be independent.
[`Control.isAlive`]() reports whether that process is still running.
[`Control.standardError`]() is the text that process wrote to its error stream,
at most 4096 bytes. [`standardErrorTruncated`]() says the stream continued past
that bound. [`Control.watch`]() asks tmux to push a format when its value
changes, and [`unwatch`]() removes that name. A target that is not a pane or
window id watches the attached session.
[server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala/src/main/scala/io/github/libtmux/scaladsl/Server.scala
[cats-server]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Server.scala
[execution]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Execution.scala
[control]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Control.scala
---
# Streaming
Source: https://libtmux.org/en/scala/latest/guides/streaming/
> Streaming: io.github.libtmux:libtmux-scala_3 documentation.
The Cats module adapts Java control subscriptions to FS2. Acquire an attachment,
then an observation, then start the producer. An observation has one active
stream consumer; use FS2's explicit broadcast operations if several consumers
need the same values. Starting another reader on the same observation fails.
## Observe a state change
Here `config` selects an existing server with a session. The subscription is
registered before the rename. Evaluating the returned [`IO`]() performs the work;
constructing it alone does not.
```scala
import _root_.cats.effect.IO
import io.github.libtmux.control.Notification
import io.github.libtmux.scaladsl.cats.{config => _, *}
import scala.concurrent.duration._
Server.resource[IO](config).use { server =>
server.sessions().flatMap { sessions =>
val session = sessions.head
Control.attach[IO](session).use { control =>
control.events(8).use { observation =>
for {
renamed <- session.rename("scala-streamed")
event <- observation.stream
.map(Observation.value)
.unNone
.map(_.notification())
.collect { case value: Notification.SessionRenamed => value }
.filter(_.name() == renamed.info.name())
.take(1)
.compile.lastOrError.timeout(5.seconds)
drops <- observation.droppedCount
_ <- IO {
assert(event.session().value() == session.info.id().value())
assert(drops == 0L)
}
} yield ()
}
}
}
}
```
[`events`]() retains Java's typed notifications and its unknown notification
variant. [`output`]() yields pane-attributed [`PaneOutput`]() values. Their `data` is
decoded terminal text, not exact bytes or a captured screen. A marker may span
several values; retain the necessary suffix while matching it.
## Loss and state reconciliation
Each subscription has a bounded queue. Overflow drops the oldest buffered
value. The stream then emits a [`Delivery.Gap`]() ahead of what survived.
[`Observation.value`]() drops that gap, so a pipeline that keeps only values
cannot see where the loss sat. [`Observation.kept`]() fails the read instead.
[`droppedCount`]() is the cumulative total.
Closing a subscription discards queued values without counting them as
overflow.
FS2 demand does not make tmux obey backpressure, and the facade never mutes
pane output to imitate it. Read the counter when completeness matters. If it
increases, reacquire a snapshot before making decisions about current object
state. A snapshot cannot reconstruct the dropped terminal output. The
[ObserveChanges example](https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/examples/src/main/scala/io/github/libtmux/scaladsl/examples/ObserveChanges.scala) demonstrates the live view's own
reconciliation over a real rename.
## Cancellation and closure
The Cats stream suspends the *fiber*, not a platform thread, while idle: it
polls first — nothing suspends when a step is already buffered — and only
arms the subscription's one-shot readiness callback ([`onReady`]()) when nothing
is, disarming it ([`clearReady`]()) if the fiber is cancelled first. No
[`ExecutionContext`]() sized for blocking stream reads is needed, and no thread is
parked per subscription. Canceling a reader releases its consumer slot.
Releasing the observation closes its subscription and disarms any pending
wakeup.
Deliberate Scala observation or attachment closure ends the stream. If the
control client ends the subscription, the stream fails with that cause.
[`Observation.UnknownCause`]() is only the remaining case: the subscription
ended, this side did not close it, and Java recorded no cause. Do not label
every unexpected end as a timeout or a server crash.
The direct-style module reads the same subscription through its own
`Observation`, blocking in Java's [`next()`]() per read and guarding against a
second, overlapping [`read`]() on the same instance with a scoped CAS — the
direct-style analogue of the Cats module's stream ownership.
Raw [`Control.acknowledge`]() calls use bounded, supervised admission. Their
timeout begins after Scala admission. Canceling a genuinely dispatched request
can close the attachment and affect queued requests and observations. Use a
separate attachment when an observation must survive command cancellation.
An accepted reply is an acknowledgement, not proof that deferred tmux work
finished. See [execution](https://libtmux.org/en/scala/latest/guides/execution/) for completion signals and uncertainty.
The [Control source][control] and [Observation source][observation] specify the
resource and stream boundaries.
[control]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Control.scala
[observation]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux-scala-cats/src/main/scala/io/github/libtmux/scaladsl/cats/Observation.scala
---
# Compatibility
Source: https://libtmux.org/en/scala/latest/guides/compatibility/
> Compatibility: io.github.libtmux:libtmux-scala_3 documentation.
The Scala facades build, test and publish with the rest of the repository, so
they meet the same gates the Java library does.
## Compilers and runtimes
Scala 3.9 only — there is no 2.13 build — compiled to JDK 25 bytecode. CI runs
the facades' unit suites, their real-tmux suites, the documented Scala examples
and the runnable programs on JDK 25 and 27 on Linux, and on macOS.
Every lane of the [tmux matrix][tmux-matrix] runs the real-tmux suites against
its own tmux: `3.2a`, `3.3`, `3.3a`, `3.4`, `3.5`, `3.6`, `3.7`, `3.7a`, `3.7b`
and `3.7c`. The Scala fixture takes the lane's tmux the same way the Java one
does.
## Artifacts
- **`libtmux-scala_3`** — direct-style operations and the typed query DSL.
- **`libtmux-scala-cats_3`** — Cats Effect resources and FS2 observations.
- **`libtmux-scala-ox_3`** — an Ox [`Flow`]() over subscriptions and live views.
Core's runtime dependencies are the Scala 3 library and `libtmux`. Cats Effect
and FS2 belong to `libtmux-scala-cats`, Ox to `libtmux-scala-ox`. None depends
on Jackson, JUnit, Kotlin, MCP or the workspace packages.
The Scala artifacts release with the Java ones, at the same version, in the
same Central deployment, signed and attested alike. `libtmux-bom` manages them
too, so one BOM version selects a matching Java and Scala set.
## The generated and handwritten surface
Every direct-style extension method and Cats per-operation forward that reaches
tmux is generated from the operation catalog `libtmux` ships, by
[`ScalaOperationGenerator`][generator] — never hand-copied per method, so it
cannot drift from what Java exposes. [`WAIT`](), [`STREAM`]() and [`LIFECYCLE`]()
operations are written by hand, for their cancellation or resource scoping:
[`Server.open`]()/`fromJava`/[`within`]()/[`control`](), [`Pane.awaitText`]()/`run`/`await`,
[`LiveView`](), [`LiveServer`](), and both modules' `Observation`.
Both facades generate from the same catalog through the same owner-and-kind
branching, and the [generator's tests][generator-tests] hold the two to it: a
captured operation forwards purely on both, and a mutation is effect-wrapped on
the Cats side only. Nothing Scala has shipped yet, so there is no earlier
release for MiMa or `tasty-mima` to compare against.
## Inherited feature boundaries
The facade preserves Java's version guards. [Named buffer deletion][buffers]
rejects tmux before 3.4 because those versions can delete the wrong buffer.
[`Shell.capturing`][java-shell] rejects tmux 3.3a and 3.4, which lose the
requested output. Java's tmux matrix asserts these unsupported results rather
than skip the contract or substitute an empty successful result.
Capture and buffer reads retain Java's normalized text, including its handling
of trailing empty lines. Observations retain decoded text chunks. These APIs
do not promise arbitrary-byte round trips. Sparse hook listings preserve
command order without inventing their original indices. Basic copy-mode entry,
inspection, and exit are wrapped; advanced commands use explicit raw access.
See [execution](https://libtmux.org/en/scala/latest/guides/execution/) for control acknowledgement limits, and
[ownership](https://libtmux.org/en/scala/latest/guides/ownership/) for resource lifetimes.
[generator]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/build-logic/codegen/src/main/kotlin/io/github/libtmux/codegen/scala/ScalaOperationGenerator.kt
[generator-tests]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/build-logic/codegen/src/test/kotlin/io/github/libtmux/codegen/scala/ScalaOperationGeneratorTest.kt
[tmux-matrix]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/build-logic/conventions/src/main/kotlin/libtmux.tmux-matrix.gradle.kts
[java-options]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux/src/main/java/io/github/libtmux/Options.java
[buffers]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux/src/main/java/io/github/libtmux/Buffers.java
[java-shell]: https://github.com/libtmux/libtmux-java/blob/f56392b5d9bc7f1f9c1631842333fb90f3d82d40/libtmux/src/main/java/io/github/libtmux/Shell.java