Core LibraryalphaGuides

Choose documentation 3

v0.1.0.alpha.3

Current version

v0.1.0.alpha.3
English

Prerelease This site documents an alpha of libtmux. Its structure, URLs and APIs are subject to change.

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:

Terminal window
$ gem install libtmux-async --pre

Open 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.wait
end

It prints:

captured panes: 2
all 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.wait
end

It prints:

request: LibTmux::Cancelled
delivery: possibly_sent
  • run returns the same binary CommandResult as the core.
  • Cancelled carries delivery as :not_sent or :possibly_sent. A deadline cannot undo tmux effects.
  • Interactive terminal attachment stays on the blocking core and raises UnsupportedFeatureError here.

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 limitDefault
Active subprocess clients4
Admitted subprocess requests, including unconsumed results32
Queued request payload4 MiB
Retained process and map output payload8 MiB
Per-command stdout / stderr1 MiB / 256 KiB
Control connections4
Ordinary command deadline5 seconds
  • Independent requests may complete out of order, and the result is still in input order.
  • map caps 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.wait
end

It prints:

reply: ready
event: %window-renamed @0 renamed
  • exchange answers a GuardedReply with :boundary_window attribution. It makes no claim that the command finished.
  • Events arrive through events.next or an explicit subscribe. 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_output and resume_output report 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. attach stays on the blocking core and raises UnsupportedFeatureError on 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_control refuses an older daemon before a control client attaches.
  • Async 2.46. The gem depends on async ~> 2.46.0 and the same version of libtmux.
  • 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

ReferenceThe Async classes, one page each
Async guideRunning concurrent commands within an owned scope
Execution modesBlocking calls, Async tasks and control subscriptions
RecipesComplete programs with checked excerpts
Public behaviorScheduling, ownership and limits of a scope
Esc

Type to search.

Reference preview Open reference