libtmux Reference MCP Search

Waiting and retrying

Edit this page on GitHub

A command that changes tmux’s state — new window, split, send a keystroke — answers as soon as tmux has accepted it, not once whatever it triggered has actually happened. Pane interaction covers the specific, common case of that: waiting for text to show up in a pane. This page is the more general version — waiting for any condition to become true — and the one mechanism that isn’t polling at all: tmux’s own wait-for signal channel, which every port reaches in some form.

Polling a conditionLink to section

The shape is always the same: call a check, and if it isn’t true yet, wait a short interval and call it again, until it’s true or a deadline passes. Some ports ship this as a reusable helper; most of the ones that do restrict it to their test-support surface, because it’s a fixture’s problem far more often than a production program’s:

PortHelperWhere it lives
Pythonlibtmux.test.retry_until(fn, seconds=, interval=)libtmux.test — ships in the same package, but under the test-support module; raises WaitTimeout
TypeScriptconnectedServer.waitFor(matches, options)the public library itself — but only on a control-mode connection (Control mode vs one-shot), and the condition is a predicate over a whole ServerSnapshot, not a boolean thunk
Gotmuxtest.WaitFor(ctx, interval, condition)tmuxtest, a separate test-support package from tmux
Rustlibtmux::test::retry_until(within, condition)libtmux::test, gated behind the test-support Cargo feature — an explicit opt-in, not just a namespace
Javanot found in the shipped library; a package-private Await.until(...) exists only inside the integration-tests module, which downstream code cannot depend on
.NETLibTmux.Testing.TmuxWait.UntilAsync(probe, timeout, interval)LibTmux.Testing, part of the same shipped LibTmux package
C++no generic condition-poll helper found in the public library; a wait_until exists only in the private testing component, for waiting on a spawned child process, not on tmux state
Swifta waitUntil helper exists only inside the test target’s own support code, not shipped

Reading down that table: five ports ship something, and four of those five mark it as test-support rather than production API — Python, Rust, and Go by namespace or feature flag, .NET by putting it under LibTmux.Testing in the same package. TypeScript’s waitFor is the one genuine exception: it’s part of the ordinary public surface, not a test helper, because it answers a different question — “has the server reached this state” rather than “has this boolean become true” — and it needs a live connection to do it safely (subscribe first, then read, so a change landing in between isn’t missed).

const live = await server.connect();
await live.waitFor((snapshot) => snapshot.windows.exists({ name: "build" }));

Java, C++, and Swift have no shipped equivalent to reach for here: the pattern is the same one written by hand — a loop, a check, a sleep, a deadline — that Pane interaction already shows Python writing out before retry_until covered the generic case.

tmux’s own wait-for channelLink to section

Polling asks tmux the same question over and over. wait-for asks tmux to tell you once, by having one command signal a named channel and another block until it’s signalled — no repeated round trips, and no interval to tune. It’s tmux’s own primitive (wait-for -S <channel> to signal, wait-for <channel> to block), not something any port invented, and every one of the eight reaches it in some form:

PortSignalWait
Pythonserver.wait_for(channel, set_flag=True)server.wait_for(channel)
TypeScriptnot exposed as public API — used only inside the test-server’s own startup handshake
Goserver.WaitFor(ctx, tmux.WaitForRequest{Channel: name, Mode: tmux.WaitForModeSignal})tmux.WaitForRequest{Channel: name} (the zero-value Mode waits)
Rustserver.signal_channel(name).await?server.wait_for_channel(name, timeout).await?ChannelWait::Signalled or TimedOut
Javaserver.channel(name).signal()server.channel(name).await(timeout) → a WakeReason, never silently “success”
.NETserver.OpenWaitChannel(name) returns a TmuxWaitChannel; signalling is the same request with a different modeawait using the channel, then WaitAsync(budget)
C++server.signal(channel)server.wait_for(channel, timeout)
Swifttry await server.signal(channel)try await server.wait(for: channel)

A signal sent before anyone is waiting is not lost — tmux latches it, so the next wait on that channel returns immediately. This is why the pattern shown above (signal, then wait) is safe even though it looks backwards: the whole point of a channel is that the two calls don’t need to race each other. Whoever is waiting wants the pattern the other way round — start the wait, then run the command that signals it — but that’s the same latch working in your favor either way, not something you need to sequence carefully.

Two ports go further and turn the naive version of this into a real guardrail rather than leaving it as a footgun. tmux’s own wait-for exits successfully both when the channel was genuinely signalled and when the server itself went away out from under the waiter — a dead server makes tmux’s client exit zero, indistinguishable from a real signal, if nothing checks further. Java’s WakeReason and Swift’s wait(for:) (which compares the server’s process ID before and after) both refuse to conflate the two; plain wait-for output alone cannot tell them apart.

Java’s Channel documents a second trap the others don’t call out as directly: a signal sent when nobody is waiting is remembered and satisfies the next wait, possibly from an unrelated later run — so a channel’s history isn’t yours alone unless you drain() it first to start from a known state, or otherwise pick a name specific enough that two unrelated pieces of work never collide on it.

Esc

Type to search.