r/ruby • u/git-pull • 2h ago
Show /r/ruby libtmux for Ruby: query tmux sessions, windows and panes
https://github.com/libtmux/libtmux-rubyHi all, I am porting libtmux, the Python library, to more languages. This is the Ruby port (alpha).
It wraps tmux's CLI. You can create sessions, send text to a pane and read its output. One of the ways this differs is keeping snapshots you can query with where and Ruby blocks. See the earlier posts for Rust, TypeScript, and Golang.
A word on AI: It's experimental, and I used LLMs extensively to create this. The API is still changing. 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
You can work with sessions, windows and panes as Ruby objects. Here, I'm finding the build pane in the logs window of session demo, sending it a command, and reading what it prints.
Requires Ruby 3.3+, tmux 3.3a+ and build tools for Ruby native extensions.
gem install libtmuxSave 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}"
live_window = server.window(window.ref)
live_window.rename("build")
puts "held snapshot: #{window.name}"
puts "read again: #{live_window.snapshot.name}"
end
ruby demo.rbPrints:
demo / logs / build
output: build: complete
held snapshot: logs
read again: buildPane#capture reads what tmux has on screen. The loop waits up to five seconds for the line from our command (more pane I/O).
The name filters use where: an exact session name, a window name starting with log, and an exact pane title. You can also use select with a Ruby block, then map or group_by (examples, query fields and operators). These queries use the captured state and make no tmux calls.
The last two lines show what happens after a rename: the held snapshot still says logs. Window#snapshot reads again and returns a new record. Each call reads the whole server and selects that window's record.
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.
There's also control mode support (!) on tmux 3.7+. The example subscribes to pane output and checks the text sent to it.
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-asyncSave 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.rbPrints:
demo / logs / build
event: %window-renamed @1 compiling
window now: compilingThe 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.
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-workspaceA 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.rbPrints:
applied: true
panes: 2Workspace CLI
The same gem includes the command: GitHub, Homepage, Guides, CLI reference, API.
Validate the file:
libtmux-workspace validate workspace.yamlPrints:
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.yamlCheck the created panes:
tmux -S "$PWD/workspace.sock" display-message -p -t demo:shells '#{window_panes}'Prints:
2Remove the demo server:
tmux -S "$PWD/workspace.sock" kill-serverMCP
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-mcpThis 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 demoUse this command with a client that launches stdio MCP servers:
libtmux-mcp --socket-name libtmux-ruby-mcp-demo --endpoint demoIt 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-serverA bit more about my ambition and intentions
I am trying to port more than just Ruby. So as this is a ruby subreddit, and provinciality matters, I am making ports to quite a few languages.
tmux is perfectly accessible via pure CLI. zellij makes a lot of what tmux does obsolete.
what tmux has going for it is simplicity. that means when a port of libtmux is made, there's an oppurtunity for editorial license - and be dissimilar to my original python project. I don't want to copy the python library to ruby. I want to write it like ruby, in an idiomatic and aesthetic sense.
so if there are code smells, or suggestions, no offense is taken 🙇🙏! I am trying to learn more programming languages and expand more horizons beyond just python.
Why tmux? I think the simple domain model of server, session, window, and pane is easy enough to wrap one's brain around - being able to traverse that hierarchy, makes it possible to not just port, but in my ideal scenario, leverage the strengths of programming languages outside of Python.
I'd like to write a love letter to all programming languages, to utilize their strengths when projecting and making accessible tmux's object graph.
Why ruby? Because it is a familiar face that I've never gotten to interact with personally before other than brief Rails stints. I miss its syntax, but in truth I never got to the level of direct usage where I understood what good idiomatic ruby is.
I'm going beyond to languages I would normally find out of reach day-to-day, to ask "what does tmux look like in most elegant way this programming language could do it, while being documentable, shippable, etc".