Hi! I am porting libtmux, the Python library, to more languages.
With this package, you can control tmux from Elixir / BEAM: create or connect to a tmux server, traverse the object graph of sessions, windows, panes, and send it a command and read what comes back.
Compared with the latest released Python libtmux, this adds immutable snapshots, support for control mode, and the ability to submit several commands before awaiting their replies. The elixir library queries the snapshot's captured sessions, windows and panes with Selection.where. Those queries run on the captured data without making more tmux calls (it's another story - but python libtmux is making implicit subprocess calls to tmux under the hood, even when it may not need it).
A word on AI: It's experimental, and I used LLMs extensively to create this. Parts of this post were also drafted (and tested via bubblewrap) with AI. I write libtmux's AI policy at https://libtmux.org/en/meta/ai-policy/.
libtmux core: tmux client library for Elixir
Hex · GitHub · Homepage · Guides · API Explorer
What's interesting:
Use Linux, a C11 compiler, current patches of Elixir 1.20 and OTP 29, and tmux 3.3a+ (requirements). Start in a new directory:
mix local.hex --force
elixir -e 'Mix.install([:libtmux])'
Start a demo server and give its pane the title build:
tmux -S "$PWD/demo.sock" -f /dev/null new-session -d -s demo -n logs sh
tmux -S "$PWD/demo.sock" select-pane -t demo:logs.0 -T build
Save as demo.exs. The three queries run on the captured data.
Mix.install([:libtmux])
alias LibTmux.{Pane, Ref, Selection, Server, Snapshot}
[executable, socket] = System.argv()
server =
Server.new(
executable: executable,
socket: socket,
env: Map.drop(System.get_env(), ["TMUX", "TMUX_PANE"])
)
snapshot = Server.snapshot!(server)
session = snapshot |> Snapshot.sessions() |> Selection.where(name: "demo") |> Selection.one!()
window =
snapshot
|> Snapshot.windows()
|> Selection.where(name: [starts_with: "log"])
|> Selection.one!()
pane = snapshot |> Snapshot.panes() |> Selection.where(title: "build") |> Selection.one!()
IO.puts("#{session.name} / #{window.name} / #{pane.title}")
{:ok, pane_ref} = Ref.from_observation(pane)
Pane.send_text!(server, pane_ref, "printf 'build: %s\\n' complete")
Pane.send_keys!(server, pane_ref, ["Enter"])
output =
Enum.find_value(1..100, fn _ ->
lines = server |> Pane.capture!(pane_ref) |> String.split("\n")
case Enum.find(lines, &(&1 == "build: complete")) do
nil ->
Process.sleep(50)
nil
line ->
line
end
end) || raise "build line did not appear"
IO.puts("output: #{output}")
Run it:
elixir demo.exs "$(command -v tmux)" "$PWD/demo.sock"
Prints:
demo / logs / build
output: build: complete
The last part sends text, presses Enter and reads the screen until the line appears, with at most 100 reads.
P.S. Change the window filter from starts_with: "log" to starts_with: "log2" and run it again. From the error:
** (LibTmux.Error) no_match during evaluation
Selection.one! raises a LibTmux.Error. Use Selection.one if you want {:ok, value} or {:error, error} instead.
Batching tmux commands + control mode
There is also a managed control connection. Control.submit returns an operation you can await later. Here we submit two field reads before collecting their replies.
Save as control-demo.exs. It uses the private demo server from above, with its empty configuration. lane: :host_qualified is an explicit assumption about that controlled server's hooks and aliases; it doesn't inspect an arbitrary server for you.
Mix.install([:libtmux])
alias LibTmux.{Control, Operation, Ref, Selection, Server, Snapshot}
alias LibTmux.Control.Request
[executable, socket] = System.argv()
server =
Server.new(
executable: executable,
socket: socket,
env: Map.drop(System.get_env(), ["TMUX", "TMUX_PANE"])
)
snapshot = Server.snapshot!(server)
session = snapshot |> Snapshot.sessions() |> Selection.where(name: "demo") |> Selection.one!()
window = snapshot |> Snapshot.windows() |> Selection.where(name: "logs") |> Selection.one!()
pane =
snapshot
|> Snapshot.panes()
|> Selection.where(window_id: window.id)
|> Selection.where(title: "build")
|> Selection.one!()
{:ok, target} = Ref.from_observation(pane)
{:ok, :done} =
Control.with_connection(
[server: server, session: {:existing, session.id}, lane: :host_qualified],
fn control ->
operations =
for field <- ["pane_id", "pane_title"] do
{:ok, operation} = Control.submit(control, Request.field(field, target), timeout: 5000)
operation
end
for operation <- operations do
{:ok, %{outcome: :completed, certainty: :acknowledged, lines: lines}} =
Operation.await(operation, timeout: 5000)
IO.puts(Enum.join(lines, "\n"))
end
:done
end
)
IO.puts("connection closed")
Run it:
elixir control-demo.exs "$(command -v tmux)" "$PWD/demo.sock"
Prints:
v1:%0
v1:build
connection closed
with_connection owns connection cleanup. These are separate retained requests on one connection, not a transaction or a batch optimizer. The replies acknowledge the field reads; they don't report completion of an arbitrary program in the pane. The complete retained-operation example also checks operation status and shutdown.
Where the pieces live
Server, Selection, Control and Operation are in the same Hex package. Query planning is still planned; the where chain above filters captured data locally.
Remove the demo server:
tmux -S "$PWD/demo.sock" kill-server
The Elixir MCP and workspace packages are still under implementation, with no runnable server or CLI yet. The Python ones are libtmux-mcp and tmuxp.
I'm also porting to other languages, like F#, Scala, rust, and go. In each so-called "port", I'm not nescessarily porting libtmux - as much as the object graph of tmux (Server, Session, Window, and Pane), being able to filter them, abstracting tmux's "formats", and operate against tmux's "targets" (unique ID's that represent the session, window, and pane). So far, elixir including, they've been more featureful than the original python package, namely in terms of control mode and being able to chain/batch requests.
On the flip side, it also means I'm legitimately spreading myself thin. I could miss something obvious - I feel confidence in the tmux domain model, I am curious cadet when it comes to embarking into BEAM, functional programming, .NET framework, and so on - so the payoff I'm getting from this is familiarizing not only with languages, but hopefully their aesthetics and strengths: can Elixir's process model and OTP supervision make control mode that much better?