libtmux Reference MCP Search

Context managers

Edit this page on GitHub

Creating a tmux object through libtmux normally hands you something that lives until you kill it yourself. A context manager hands that cleanup back to the language instead: scope the object to a block, and it’s gone the moment you leave — whether you exit cleanly or an exception unwinds the stack partway through. Workspaces already flags this as worth checking per port rather than assuming; this page is that check, done.

The idea is universal. How far each port takes it is not — and the honest answer, verified against each port’s own source, is that only two of the eight extend it past their own test helpers:

PortServerSessionWindowPane
Pythonyesyesyesyes
.NETyesyesyes
Javacloses conn.
Rusttest-only
C++test-only
TypeScript
Go
Swift

“test-only” means the mechanism exists to give a test a private, disposable tmux server — not to scope an ordinary session, window, or pane your program creates. Java’s “closes conn.” is its own, weaker case: Server implements AutoCloseable, but closing it releases this process’s own transport rather than tearing down the tmux server underneath — the distinction the Java section below spells out, because a table cell alone would read as the same guarantee Python’s yes gives. A dash means the port has no built-in scoping for that object at all: you call kill() yourself, in your own try/finally, defer, or RAII type, the same as you would for any resource the language doesn’t manage for you.

Python: every level, including nestedLink to section

Server, Session, Window, and Pane are all context managers. Entering one is just holding the object; exiting kills it — including when the block raises:

Cleanup runs pane, then window, then session, then server — the reverse of creation order — which keeps tmux’s own bookkeeping consistent as each layer disappears out from under the one below it.

.NET: an explicit ownership type, stopping at WindowLink to section

.NET doesn’t make Session or Window themselves disposable. Instead, creating one with ownership in mind returns a separate scope type — OwnedSessionScope, OwnedWindowScope — that wraps the object and implements IAsyncDisposable:

The ladder stops at Window — there is no OwnedPaneScope for a pane you split off one, verified by its absence rather than assumed. LibTmux.Testing adds one more shape for tests specifically: TemporaryHierarchyScope, from TmuxTestFactory.CreateHierarchyAsync(), wraps an entire private server-session-window-pane stack in one IAsyncDisposable that kills the server on exit — the test equivalent of Python’s nested example above, minus the manual nesting.

Java: Server is closeable, but closing one doesn’t kill itLink to section

Server implements AutoCloseable, which is what makes the flagship example’s try (Server server = Server.open(config)) { ... } block compile — but read close()’s own doc comment before assuming it matches Python’s with Server(): it “releases an owned transport” and is explicitly “never kills tmux.” Exiting the block drops the client connection this process opened; the tmux server and every session on it are still there afterward. Session, Window, and Pane don’t implement AutoCloseable at all — killing one of those is always an explicit .kill()-shaped call inside the block, and so, despite appearances, is killing the server itself.

ServerConfig config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(socket))
.build();
try (Server server = Server.open(config)) {
Session session = server.newSession("demo");
Window window = session.newWindow("build");
Pane pane = window.split();
pane.sendLine("echo hello from libtmux");
// session, window, and pane all outlive this block — only the
// connection this `server` handle held is released on the way out.
}

Rust: no async Drop, so cleanup is explicit or best-effortLink to section

Rust’s Drop::drop is a synchronous method, and killing a tmux object is an async tmux command — the two don’t compose, so Rust has no RAII type that awaits a kill on scope exit the way Python’s with or .NET’s await using do. Two things stand in for it instead:

  • kill(self) consumes the handle. Session, window, and pane kill methods take self by value, so the compiler refuses to let you call anything on a handle you’ve already killed — not scope-exit cleanup, but a real, verified guardrail against use-after-kill.
  • libtmux::test::TestServer is test-only, and asymmetric on purpose. Its preferred teardown is an explicit guard.shutdown().await?, which can report a real error if cleanup failed. Its impl Drop is a fallback for the path where you didn’t call that — force_cleanup(), synchronous and best-effort, because drop() has no await to give it.

C++: RAII exists, but only for a private test serverLink to section

Session, Window, and Pane are non-owning value types in C++ — there is no destructor to hook, because there is no resource these objects own, and killing the tmux-side entity is always an explicit .kill() call. The one RAII type in the project, libtmux::test::ScopedTmuxServer, lives in a separate testing CMake component and scopes a whole private tmux server (its own TMUX_TMPDIR, a redacted environment) rather than any single session, window, or pane:

TypeScript, Go, Swift: no built-in scoping at allLink to section

None of these three give Session, Window, or Pane a disposal hook, and none has a test-only exception the way Rust and C++ do:

  • TypeScript does define [Symbol.asyncDispose], which is what makes await using events = server.watch() and await using live = await server.connect() work — but those are the control-mode connection/notification-stream handles from Control mode vs one-shot, not the session or pane the connection is watching. Killing a session or pane you created is still an explicit await session.kill(), written by hand in a finally:

  • Go follows the same split: ControlClient, PaneObservation, and NotificationStream all implement io.Closer, so defer conn.Close() is the idiomatic way to release those — but Session, Window, and Pane have no Close(), only Kill(ctx), called explicitly or via your own defer:

  • Swift has neither: Session, Window, and Pane are plain value types holding an ID and a few fields, nothing that owns a resource to release. Cleanup is always try await server.kill(session) (or window, or pane), called when you mean it, wrapped in your own defer if you want scope-exit behavior.

What this means in practiceLink to section

Outside Python and, up to Window, .NET, reaching for “clean this up automatically” in a test or a short-lived script means writing the try/finally, defer, or RAII type yourself — or, in Rust’s and C++‘s cases, reaching for the port’s own test-only server guard if what you actually need is an entire disposable server rather than one object inside a shared one. None of that is a gap so much as a difference worth knowing about before you assume a with-shaped block exists where it doesn’t.

Esc

Type to search.