Skip to content

Repository files navigation

libtmux for Elixir

libtmux for Elixir

Hex.pm · API reference · HexDocs · Documentation

Core checks libtmux libtmux.org license

Drive tmux from Elixir: capture sessions, windows and panes as an immutable snapshot, filter it with plain data, then rename, split and type through domain actions.

Contents — Installation · Try the examples · Find things · Rename · Control · Errors · Guides · Support · Leaves out

Installation

Add the package to your Mix dependencies:

{:libtmux, "~> 0.1"}

Fetch dependencies:

$ mix deps.get

It needs:

  • Elixir 1.20 or newer, on OTP 29 or newer
  • tmux 3.3 or newer
  • Linux and a C compiler, which builds a small helper
  • no third-party runtime packages and no NIF

The exact floors and tested releases are in Packages and support.

Try the examples

The snippets below form one executable program. From this checkout, run it on a private tmux server:

$ python3 examples/owned_server.py \
    --run "$(command -v tmux)" \
    mix run examples/readme.exs
  • The runner starts the server, passes in the executable, socket and session ID, and stops the server afterward.
  • Your own tmux sessions are untouched. The runner needs Python 3.9+.
  • Starting and stopping the server is the host's job; the library never starts one.

No checkout? The standalone demo installs the package from Hex. To read an existing server, use the capture-only program.

LibTmux.Server.new/1 picks the socket in this order:

  • :socket (a path) or :socket_name
  • LIBTMUX_SOCKET_PATH, then LIBTMUX_SOCKET
  • neither: tmux's choice, which is the server TMUX names inside tmux and the default socket outside it

A Server with a chosen socket removes TMUX.

Find sessions, windows and panes

Server.snapshot!/2: what tmux has now

One call captures every session, window and pane as an immutable graph. Snapshot.sessions/1, Snapshot.windows/1 and Snapshot.panes/1 pick a type, and Selection.where/2 filters it. None of them calls tmux again.

alias LibTmux.{Selection, Server, Snapshot}

[executable, socket, session_id] = System.argv()

server =
  Server.new(
    executable: executable,
    socket: socket,
    env: Map.drop(System.get_env(), ["TMUX", "TMUX_PANE"])
  )

snapshot = Server.snapshot!(server)

active_ids =
  snapshot
  |> Snapshot.panes()
  |> Selection.where(active: true)
  |> Enum.map(& &1.id)

active_ids is ["%0"]: the demo server starts with one pane. Capture again when you need fresh state.

More: Captured state.

Selection.where/2: filter by data

Criteria are keywords or maps, and can test related panes. Filter.from_json/3 reads the same criteria from JSON, so a filter can come from a file or a request. Selection.filter/2 takes an Elixir function instead.

alias LibTmux.{Filter, Pane, Selection, Snapshot}

windows =
  snapshot
  |> Snapshot.windows()
  |> Selection.where(panes: [some: [active: true, current_command: "cat"]])
  |> Enum.map(& &1.id)

json = """
{"version":1,"entity":"pane",
 "where":{"active":true,"currentCommand":{"in":["cat","nvim"]}}}
"""

{:ok, filter} = Filter.from_json(Pane, json)

pane_ids =
  snapshot
  |> Snapshot.panes()
  |> Selection.where(filter)
  |> Enum.map(& &1.id)

windows is ["@0"] and pane_ids is ["%0"]. One pane must meet both conditions inside some.

More: Filtering.

Rename a window

Window.rename!/4: change a window's name

Continue with the same snapshot and server. Selection.one!/2 needs exactly one match, Session.active_window/2 reads the captured link, and Ref.from_observation/1 turns an observation into an action target.

alias LibTmux.{Ref, Selection, Session, Snapshot, Window}

session = snapshot |> Snapshot.sessions() |> Selection.one!(id: session_id)
window = Session.active_window(snapshot, session)
{:ok, session_ref} = Ref.from_observation(session)
{:ok, window_ref} = Ref.from_observation(window)

Window.rename!(server, window_ref, "editor", session: session_ref)

It returns :ok once tmux acknowledges the command. session: names the session whose link to the window to use. The old snapshot keeps the old name.

More: One-shot actions.

Split a pane, send input and read the screen

Control.with_connection/2: one connection for many commands

It attaches a control client to an existing session, runs your callback, and closes the client. Pane.split!/3 returns the new pane's ID and a bound reference. Pane.send_text!/4 sends literal text and Pane.send_keys!/4 sends named keys. After the scope closes, Pane.capture!/3 reads the new pane's screen through the server.

alias LibTmux.{Control, Pane, Ref, Selection, Snapshot}

pane = snapshot |> Snapshot.panes() |> Selection.one!(window_id: window.id, active: true)
{:ok, pane_ref} = Ref.from_observation(pane)

{:ok, {created_id, created}} =
  Control.with_connection(
    [server: server, session: {:existing, session.id}, lane: :host_qualified],
    fn control ->
      %{id: id, ref: created} =
        Pane.split!(control, pane_ref, axis: :horizontal, command: {:argv, ["/bin/cat"]})

      Pane.send_text!(control, created, "hello")
      Pane.send_keys!(control, created, ["Enter"])
      {id, created}
    end
  )

screen = Pane.capture!(server, created, max_bytes: 4096)

The program prints:

{"window":"editor","active_panes":["%0"],"created_pane":"%1","connection_retired":true}
  • {:argv, ["/bin/cat"]} runs a program directly; {:shell, "command"} goes through a shell.
  • An acknowledgement does not mean the program has read its input, so screen may not show hello yet.
  • Pane.capture!/3 runs one command through the server; a control connection refuses it.
  • lane: :host_qualified says you control this server's hooks and aliases. The demo runner does. Read the managed-control guide before using another host.

More: Managed control.

Errors and recovery

Action.CompletionError: what went wrong

Actions return {:ok, result} only when the command completes; the bang forms raise instead. A command that times out may still have taken effect, so inspect it before you retry.

More: Recovery, Mental model.

Guides

Support and status

  • Alpha. Releases are numbered 0.1.N. Any release may change or remove exported functions; the changelog marks each break. Pin an exact version to avoid one.
  • Linux. The helper that owns tmux processes needs Linux.
  • tmux. 3.3 or newer.

What it leaves out

  • Server lifecycle. It never starts a server and has no stop call; send kill-server through Server.run/3.
  • A query planner. Selections run locally over a snapshot; Query, Batch and Plan are planned.
  • Change notifications. Observation and Tracker are planned.

AI policy

Use these packages with AI tools as you like. When contributing, check what you submit and wait for a maintainer's invitation before preparing a pull request. See the AI policy, also kept as AI_POLICY.md.

Contributing

See Contributing for development setup and checks, and Writing for documentation conventions.

Related projects: libtmux for Python, libtmux ports, and tmux. Artwork. Licensed under the MIT License.

Releases

Packages

Contributors

Languages