Helper utility for Bubble Tea, allowing easy mouse event tracking in terminal applications.
Ruby bindings for lrstanley/bubblezone.
Track clickable regions in terminal UIs. Built for use with Bubble Tea and Lipgloss.
Add to your Gemfile:
gem "bubblezone"Or install directly:
gem install bubblezoneInitialize the global zone manager:
require "bubblezone"
Bubblezone.new_globalMark a region with an ID:
button = Bubblezone.mark("my_button", "Click Me")Build your layout and scan to register zones:
layout = "Header\n#{button}\nFooter"
output = Bubblezone.scan(layout)
puts outputOutput:
Header
Click Me
Footer
Get zone info by ID:
zone = Bubblezone.get("my_button")Check zone bounds:
if zone
puts "Zone bounds: (#{zone.start_x}, #{zone.start_y}) to (#{zone.end_x}, #{zone.end_y})"
endGet coordinates from mouse event:
x, y = message.x, message.yCheck if coordinates are within a zone:
zone = Bubblezone.get("my_button")
if zone&.in_bounds?(x, y)
puts "Button clicked!"
endIterate over all zones containing the coordinates:
Bubblezone.each_in_bounds(x, y) do |id, zone|
puts "Hit zone: #{id}"
endCheck if any zone contains the coordinates:
if Bubblezone.any_in_bounds?(x, y)
puts "Something was clicked"
endGet the first matching zone:
result = Bubblezone.find_in_bounds(x, y)
if result
id, zone = result
puts "First hit: #{id}"
endPrevent ID conflicts between components:
class MyComponent
def initialize
@prefix = Bubblezone.new_prefix
end
def view
items = ["Apple", "Banana", "Cherry"]
items.map.with_index do |item, i|
Bubblezone.mark("#{@prefix}#{i}", item)
end.join("\n")
end
endCreate a dedicated manager:
manager = Bubblezone::Manager.newUse the same API as the global manager:
marked = manager.mark("zone_id", "Content")
output = manager.scan(marked)
zone = manager.get("zone_id")Iterate zones:
manager.each_in_bounds(x, y) { |id, zone| ... }Clean up when done:
manager.closeHandle mouse clicks in a Bubbletea model:
require "bubbletea"
require "bubblezone"
Bubblezone.new_global
class ClickableApp
include Bubbletea::Model
ITEMS = ["Option A", "Option B", "Option C"]
def initialize
@selected = nil
@prefix = Bubblezone.new_prefix
end
def init
[self, nil]
end
def update(message)
case message
when Bubbletea::MouseMessage
if message.release? && (message.left? || message.button == 0)
result = Bubblezone.find_in_bounds(message.x, message.y)
if result
id, _zone = result
@selected = id.sub(@prefix, "").to_i
end
end
[self, nil]
when Bubbletea::KeyMessage
return [self, Bubbletea.quit] if message.to_s == "q"
[self, nil]
else
[self, nil]
end
end
def view
lines = ITEMS.map.with_index do |item, i|
marker = i == @selected ? "[x]" : "[ ]"
content = "#{marker} #{item}"
Bubblezone.mark("#{@prefix}#{i}", content)
end
Bubblezone.scan(lines.join("\n"))
end
end
Bubbletea.run(ClickableApp.new, alt_screen: true, mouse_cell_motion: true)Style your content first:
require "lipgloss"
require "bubblezone"
Bubblezone.new_global
button_style = Lipgloss::Style.new
.background("#7D56F4")
.foreground("#FFFFFF")
.padding(0, 3)Mark the fully styled content:
styled_button = button_style.render("Click Me")
clickable_button = Bubblezone.mark("btn", styled_button)Scan to register zones:
output = Bubblezone.scan(clickable_button)| Method | Description |
|---|---|
Bubblezone.new_global |
Initialize the global zone manager |
Bubblezone.close |
Close the global manager |
Bubblezone.enabled? |
Check if zone tracking is enabled |
Bubblezone.enabled = bool |
Enable/disable zone tracking |
Bubblezone.new_prefix |
Generate a unique zone ID prefix |
Bubblezone.mark(id, text) |
Wrap text with zone markers |
Bubblezone.scan(text) |
Parse zones and strip markers |
Bubblezone.get(id) |
Get ZoneInfo for an ID (or nil) |
Bubblezone.clear(id) |
Remove a stored zone |
Bubblezone.clear_all |
Remove all stored zones |
Bubblezone.zone_ids |
Get array of all tracked zone IDs |
| Method | Description |
|---|---|
Bubblezone.each_in_bounds(x, y) { |id, zone| } |
Yield each zone containing coordinates |
Bubblezone.any_in_bounds?(x, y) |
Check if any zone contains coordinates |
Bubblezone.find_in_bounds(x, y) |
Get first [id, zone] pair, or nil |
| Method | Description |
|---|---|
Manager.new |
Create a new zone manager |
#close |
Close the manager |
#enabled? / #enabled= |
Get/set enabled state |
#new_prefix |
Generate unique prefix |
#mark(id, text) |
Mark text with zone |
#scan(text) |
Parse and strip markers |
#get(id) |
Get zone info |
#clear(id) |
Clear specific zone |
#clear_all |
Clear all zones |
#zone_ids |
Get all zone IDs |
#each_in_bounds(x, y) |
Iterate matching zones |
#any_in_bounds?(x, y) |
Check for any match |
#find_in_bounds(x, y) |
Get first match |
| Method | Description |
|---|---|
#start_x, #start_y |
Zone start coordinates |
#end_x, #end_y |
Zone end coordinates |
#in_bounds?(x, y) |
Check if coordinates are within zone |
#zero? |
Check if zone has no position data |
#pos(x, y) |
Get relative position within zone |
The Go bubblezone library processes zones asynchronously. After calling scan, there may be a brief delay before zones are available via get. In interactive applications with Bubbletea, this is typically not an issue as mouse events occur after rendering.
When using alt_screen: true with Bubbletea, mouse coordinates are relative to (0, 0) at the top-left of the screen, matching zone coordinates exactly. Without alt screen, you may need to account for terminal scroll position.
- Style your content with Lipgloss
- Mark the styled content with
Bubblezone.mark - Build your complete layout
- Call
Bubblezone.scanon the final output - Handle mouse events using
getorfind_in_bounds
Requirements:
- Go 1.23+
- Ruby 3.2+
Install dependencies:
bundle installBuild the Go library and compile the extension:
bundle exec rake compileRun tests:
bundle exec rake testRun demos:
./demo/clickable_alt
./demo/clickable_list
./demo/clickable_simple
./demo/full_layoutBug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/bubblezone-ruby.
The gem is available as open source under the terms of the MIT License.
This gem wraps lrstanley/bubblezone, which provides zone tracking for terminal UIs and builds on the excellent Charm ecosystem, including lipgloss and bubbletea. Charm Ruby is not affiliated with or endorsed by Charmbracelet, Inc.
Part of Charm Ruby.
Lipgloss • Bubble Tea • Bubbles • Glamour • Huh? • Harmonica • Bubblezone • Gum • ntcharts
The terminal doesn't have to be boring.
