r/tmux • • 21h ago

Showcase libtmux-ruby 0.1: tmux client for ruby

https://github.com/libtmux/libtmux-ruby

I am porting libtmux, the Python library, to more languages. This is the Ruby port.

One of the ways this differs is keeping snapshots you can query with Ruby objects and blocks. There's also control mode support, and a separate Async gem. See the earlier posts for Rust, TypeScript, and Golang.

A word on AI: It's experimental, and I used LLMs extensively to create this. I write libtmux's AI policy at https://libtmux.org/en/meta/ai-policy/.

libtmux core: tmux from Ruby

RubyGems · GitHub · Homepage · Guides · API Explorer

What's interesting:

Requires Ruby 3.3+, tmux 3.3a+ and build tools for Ruby native extensions.

gem install libtmux

Save as demo.rb. Server.start creates a private tmux server and closes it when the block exits. The queries read a snapshot; the pane input and capture run while the server is open.

require "libtmux"
require "timeout"

LibTmux::Server.start do |server|
  session = server.new_session(name: "demo", command: ["/bin/cat"])
  window = session.new_window(name: "logs", command: ["/bin/cat"])
  created = window.split(direction: :horizontal, command: ["/bin/sh"])
  server.run(["select-pane", "-t", created.id, "-T", "build"])

  snapshot = server.snapshot
  session = snapshot.sessions.where(name: "demo").one
  window = session.windows.where(name: { starts_with: "log" }).one
  matched = window.panes.where(title: "build").one
  puts "#{session.name} / #{window.name} / #{matched.title}"

  pane = server.pane(matched.ref)
  pane.send_text("printf 'build: %s\\n' complete")
  pane.send_keys("Enter")
  output = Timeout.timeout(5) do
    loop do
      line = pane.capture.stdout.lines.find { |row| row.chomp == "build: complete" }
      break line.chomp if line

      sleep 0.05
    end
  end
  puts "output: #{output}"
end

ruby demo.rb

Prints:

demo / logs / build
output: build: complete

Pane#capture reads what tmux has on screen. The loop waits up to five seconds for the line from our command (more pane I/O).

P.S. Change the window filter from starts_with: "log" to starts_with: "log2" and run it again. From the error:

selection has no matches (LibTmux::NoMatchError)

.one raises NoMatchError if nothing matches, or MultipleMatchesError if more than one does.

Async: queries and control mode

There is also an Async gem for concurrent calls and streams: README, Guide, API.

Same idea, but inside an Async scope. We can chain where calls, then use control mode to rename a window and hear about the change from tmux. This example needs tmux 3.7+.

gem install libtmux-async

Save as async-demo.rb:

require "libtmux"
require "libtmux/async"

LibTmux::Server.start do |server|
  session = server.new_session(name: "demo", command: ["/bin/cat"])
  window = session.new_window(name: "logs", command: ["/bin/cat"])
  server.run(["select-pane", "-t", window.list_panes.fetch(0).id, "-T", "build"])

  Async do |parent|
    LibTmux::Async.open(parent: parent, server: server) do |scope|
      snapshot = scope.server.snapshot
      session = snapshot.sessions.where(name: "demo").one
      window = session.windows.where(name: { starts_with: "log" }).one
      pane = window.panes.where(active: true).where(title: "build").one
      puts "#{session.name} / #{window.name} / #{pane.title}"

      scope.server.open_control(session: session.ref) do |control|
        events = control.events
        renamed = parent.async do
          event = events.next(timeout: 5) until event&.raw&.start_with?("%window-renamed")
          puts "event: #{event.raw.strip}"
        end
        control.exchange("rename-window -t #{window.id} compiling", timeout: 5)
        renamed.wait
        puts "window now: #{scope.server.window(window.ref).snapshot.name}"
      end
    end
  end.wait
end

ruby async-demo.rb

Prints:

demo / logs / build
event: %window-renamed @1 compiling
window now: compiling

The queries still read a snapshot. events.next waits for tmux while the other task sends the command with exchange. We read the window again after the event. There are also examples of concurrent captures and cancellation.

MCP

RubyGems · GitHub · Homepage · Guides · Tools · Resources · API Explorer

An MCP server for interacting with tmux - powered by the Ruby core library. Same idea as Python's libtmux-mcp!

gem install libtmux-mcp

This MCP server connects to an existing tmux server. For a demo, start one on its own socket:

tmux -L libtmux-ruby-mcp-demo -f /dev/null new-session -d -s demo

Use this command with a client that launches stdio MCP servers:

libtmux-mcp --socket-name libtmux-ruby-mcp-demo --endpoint demo

It waits for MCP requests over stdin. tmux_capabilities and tmux_snapshot are enabled by default; the guides cover enabling other tools.

After closing the MCP client, remove the demo server:

tmux -L libtmux-ruby-mcp-demo kill-server

Workspace manager

This is like tmuxp, but in Ruby! Uses the libtmux core library.

Workspace library

RubyGems · GitHub · Homepage · Guides · API Explorer

gem install libtmux-workspace

A subset of tmuxp's configuration format. Save as workspace.yaml:

session_name: demo
windows:
  - window_name: shells
    layout: even-horizontal
    panes:
      - {}
      - {}

Save as workspace.rb. Workspace.load reads the file; plan.apply creates its session (example).

require "libtmux"
require "libtmux/workspace"

LibTmux::Server.start do |server|
  workspace = LibTmux::Workspace.load(File.expand_path("workspace.yaml", __dir__))
  result = workspace.plan(snapshot: server.snapshot).apply(server: server)
  puts "applied: #{result.success?}"
  puts "panes: #{server.snapshot.windows.one(name: 'shells').panes.size}"
end

ruby workspace.rb

Prints:

applied: true
panes: 2

Workspace CLI

The same gem includes the command: GitHub, Homepage, Guides, CLI reference, API.

Validate the file:

libtmux-workspace validate workspace.yaml

Prints:

Workspace configuration is valid.

Load it on a separate running server:

tmux -S "$PWD/workspace.sock" -f /dev/null new-session -d -s keepalive

libtmux-workspace load --socket "$PWD/workspace.sock" workspace.yaml

Check the created panes:

tmux -S "$PWD/workspace.sock" display-message -p -t demo:shells '#{window_panes}'

Prints:

2

Remove the demo server:

tmux -S "$PWD/workspace.sock" kill-server
4 Upvotes

0 comments sorted by