Async
Run libtmux operations inside an application-owned Async task: a subprocess facade, ordered mapping, control replies and event subscriptions.
API reference · Guide · RubyGems
Part of libtmux for Ruby. Built on libtmux.
Imports start no scheduler, tmux server or background task. A scope borrows a
core Server and leaves its daemon running when the scope exits.
Contents — Quick start · Cancel · Map · Control · Leaves out · Support · Documentation
Quick start¶
Install the alpha. It needs Ruby 3.3 or newer and tmux 3.3a or newer on
PATH:
$ gem install libtmux-async --preOpen a scope over a core server with LibTmux::Async.open, then
capture every pane concurrently with Scope#map. The outer
Server.start block only supplies a server for the scope to
borrow:
require "libtmux"require "libtmux/async"
LibTmux::Server.start do |server| server.new_session(name: "work", command: ["/bin/cat"]) server.new_session(name: "logs", command: ["/bin/cat"]) Async do |parent| LibTmux::Async.open(parent: parent, server: server) do |scope| refs = scope.server.list_panes.map(&:ref) screens = scope.map(refs, concurrency: 2) { |ref| scope.server.pane(ref).capture } puts "captured panes: #{screens.length}" puts "all succeeded: #{screens.all?(&:success?)}" end end.waitendIt prints:
captured panes: 2all succeeded: true- Leave out
parent:to use the current Async task. - A scope rejects use from another thread, process or scheduler, so create one scope for each scheduler thread.
- Scope exit retires its clients and joins its owned tasks.
Cancel a request without stalling the others¶
Async::Server#run: cancel a request that is waiting¶
scope.server.run waits for a tmux command in a task. Cancel that task and
the scope retires its client, while sibling requests keep running.
wait_for returns once the request has signalled
that it is running, so the cancel arrives while it waits:
require "libtmux"require "libtmux/async"
LibTmux::Server.start do |server| server.new_session(name: "work", command: ["/bin/cat"]) Async do |parent| LibTmux::Async.open(parent: parent, server: server) do |scope| waiting = parent.async do scope.server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 5) rescue LibTmux::Cancelled => error error end scope.server.wait_for("ready", timeout: 5) waiting.cancel failure = waiting.wait puts "request: #{failure.class}" puts "delivery: #{failure.delivery}" end end.waitendIt prints:
request: LibTmux::Cancelleddelivery: possibly_sentrunreturns the same binaryCommandResultas the core.Cancelledcarriesdeliveryas:not_sentor:possibly_sent. A deadline cannot undo tmux effects.- Interactive terminal attachment stays on the blocking core and raises
UnsupportedFeatureErrorhere.
The complete program also keeps capturing while the request waits, and checks that the cancelled client is reaped. The contract is in Async scope.
Keep results ordered and bounded¶
Scope#map: input order, capped payloads¶
map runs a block for each item, up to concurrency: at a time, and returns a
frozen Array in input order. The quick start is a map. These limits apply to
every scope:
| Scope limit | Default |
|---|---|
| Active subprocess clients¶ | 4 |
| Admitted subprocess requests, including unconsumed results¶ | 32 |
| Queued request payload¶ | 4 MiB |
| Retained process and map output payload¶ | 8 MiB |
| Per-command stdout / stderr¶ | 1 MiB / 256 KiB |
| Control connections¶ | 4 |
| Ordinary command deadline¶ | 5 seconds |
- Independent requests may complete out of order, and the result is still in input order.
mapcaps retained items and counts payload bytes of strings, primitive values,CommandResult, Arrays and Hashes. Cycles and deep nesting are refused.- An application-defined result needs a
result_bytes:callable that returns a nonnegative Integer. - Keep measured values unchanged until the map returns. The counts cover payload bytes and do not measure the Ruby heap.
The contract is in Async map.
Read control events in a scope¶
Async::Server#open_control: replies and events¶
A control connection has its own request, reply and subscriber limits.
exchange runs one command, and
events yields what tmux reports outside a
reply:
require "libtmux"require "libtmux/async"
LibTmux::Server.start do |server| session = server.new_session(name: "work", command: ["/bin/cat"]) window = session.list_windows.fetch(0) Async do |parent| LibTmux::Async.open(parent: parent, server: server) do |scope| scope.server.open_control(session: session.ref) do |control| reply = control.exchange("display-message -p ready", timeout: 5) puts "reply: #{reply.blocks.last.body.strip}" events = control.events window.rename("renamed") event = events.next(timeout: 5) until event&.raw&.start_with?("%window-renamed") puts "event: #{event.raw.strip}" end end end.waitendIt prints:
reply: readyevent: %window-renamed @0 renamedexchangeanswers aGuardedReplywith:boundary_windowattribution. It makes no claim that the command finished.- Events arrive through
events.nextor an explicitsubscribe. Consumer callbacks run outside the parser. - A reliable subscription raises on overflow, and a tail subscription reports the dropped range.
- Cancelling a dispatched exchange that is not drained closes the connection.
pause_outputandresume_outputreport possible loss through gap events. A gap does not show that the action took effect.- To reconnect, close the connection and pass it as
reconnect:. The replacement stays owned by the scope, has a new generation, and starts with a gap of unknown loss. Nothing is replayed.
The contract is in Control connections.
What it leaves out¶
- Terminal attachment.
attachstays on the blocking core and raisesUnsupportedFeatureErroron this facade. - Replay. Reconnect and resume never replay requests or missed output. See Control connections.
- A scheduler. Imports start no scheduler or background task, and your application owns the Async root task. See Execution modes.
- Cross-thread scopes. A scope belongs to the thread, process and scheduler that opened it. See Async scope.
Support and status¶
Alpha. APIs may change between releases.
- Ruby 3.3 or newer and tmux 3.3a or newer. The facade dispatches through a version-checked core server, so it carries the same daemon release check.
- Control connections need tmux 3.7 or newer.
open_controlrefuses an older daemon before a control client attaches. - Async 2.46. The gem depends on
async~> 2.46.0 and the same version oflibtmux. - Linux, including WSL, and macOS. The compatibility workflow runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and retains per-revision results. macOS is supported on a best-effort basis, and CI coverage for it is highly limited.
Documentation¶
| Reference¶ | The Async classes, one page each |
| Async guide¶ | Running concurrent commands within an owned scope |
| Execution modes¶ | Blocking calls, Async tasks and control subscriptions |
| Recipes¶ | Complete programs with checked excerpts |
| Public behavior¶ | Scheduling, ownership and limits of a scope |