Ruby implementation of charmbracelet/bubbles.
Common UI components for building terminal applications with Bubble Tea.
Add to your Gemfile:
gem "bubbles"Or install directly:
gem install bubbles| Component | Description |
|---|---|
| Spinner | Loading spinners with multiple styles |
| Progress | Animated progress bars |
| Timer | Countdown timer |
| Stopwatch | Elapsed time counter |
| TextInput | Single-line text input with cursor |
| TextArea | Multi-line text input |
| Viewport | Scrollable content pane |
| List | Interactive list with filtering |
| Table | Data table with columns |
| FilePicker | File and directory browser |
| Paginator | Pagination controls |
| Help | Help text generator |
| Key | Key binding definitions |
| Cursor | Blinking cursor for inputs |
Animated loading indicator:
require "bubbles"
spinner = Bubbles::Spinner.new
spinner.spinner = Bubbles::Spinners::DOTIn your update method:
spinner, command = spinner.update(message)In your view method:
spinner.viewAvailable spinner styles:
Bubbles::Spinners::LINE
Bubbles::Spinners::DOT
Bubbles::Spinners::MINI_DOT
Bubbles::Spinners::JUMP
Bubbles::Spinners::PULSE
Bubbles::Spinners::POINTS
Bubbles::Spinners::GLOBE
Bubbles::Spinners::MOON
Bubbles::Spinners::MONKEY
Bubbles::Spinners::METER
Bubbles::Spinners::HAMBURGER
Bubbles::Spinners::ELLIPSISAnimated progress bar:
progress = Bubbles::Progress.new(width: 40)
progress.set_percent(0.5)In your view:
progress.viewCustomization:
progress = Bubbles::Progress.new(
width: 40,
full: "█",
empty: "░",
show_percentage: true
)
progress.full_color = "212"
progress.empty_color = "238"Countdown timer (60 seconds):
timer = Bubbles::Timer.new(60)Start the timer:
command = timer.startIn update:
timer, command = timer.update(message)Check if done:
timer.timed_out?In view:
timer.viewElapsed time counter:
stopwatch = Bubbles::Stopwatch.newStart/stop/toggle:
command = stopwatch.start
stopwatch.stop
command = stopwatch.toggleIn view:
stopwatch.viewSingle-line text input:
input = Bubbles::TextInput.new
input.placeholder = "Enter your name..."
input.prompt = "> "
input.focusIn update:
input, command = input.update(message)Get value:
input.valueIn view:
input.viewPassword mode:
input.echo_mode = :password
input.echo_character = "*"With suggestions:
input.suggestions = ["apple", "apricot", "avocado"]
input.show_suggestions = trueMulti-line text input:
textarea = Bubbles::TextArea.new(width: 60, height: 10)
textarea.placeholder = "Type your message..."
textarea.show_line_numbers = true
textarea.focusIn update:
textarea, command = textarea.update(message)Get value:
textarea.valuePosition info:
textarea.row
textarea.col
textarea.line_countScrollable content pane:
viewport = Bubbles::Viewport.new(width: 80, height: 20)
viewport.content = long_textIn update (handles scroll keys):
viewport, command = viewport.update(message)Scroll info:
viewport.scroll_percent
viewport.at_top?
viewport.at_bottom?In view:
viewport.viewProgrammatic scrolling:
viewport.scroll_down(5)
viewport.scroll_up(5)
viewport.page_down
viewport.page_up
viewport.goto_top
viewport.goto_bottomInteractive list with filtering:
items = [
{ title: "Item 1", description: "First item" },
{ title: "Item 2", description: "Second item" }
]
list = Bubbles::List.new(items, width: 40, height: 10)
list.title = "My List"In update:
list, command = list.update(message)Get selection:
list.selected_itemFilter state:
list.filter_stateStyling:
list.title_style = Lipgloss::Style.new.bold(true).foreground("212")
list.selected_item_style = Lipgloss::Style.new.foreground("212")
list.item_style = Lipgloss::Style.new.foreground("252")Data table with columns:
columns = [
{ title: "Name", width: 20 },
{ title: "Age", width: 5 },
{ title: "City", width: 15 }
]
rows = [
["Alice", "30", "New York"],
["Bob", "25", "London"]
]
table = Bubbles::Table.new(columns: columns, rows: rows, height: 10)In update:
table, command = table.update(message)Get selection:
table.selected_row
table.selected_row_dataStyling:
table.header_style = Lipgloss::Style.new.bold(true).foreground("212")
table.cell_style = Lipgloss::Style.new.padding_left(1)
table.selected_style = Lipgloss::Style.new.bold(true).background("57")File and directory browser:
picker = Bubbles::FilePicker.new(directory: ".")
picker.height = 15
picker.show_hidden = false
picker.allowed_types = ["rb", "txt"]In update:
picker, command = picker.update(message)Check for selection:
if picker.did_select_file?
selected_path = picker.path
endOptions:
picker.show_permissions = true
picker.show_size = true
picker.dir_allowed = false
picker.file_allowed = truePagination controls:
paginator = Bubbles::Paginator.new(type: Bubbles::Paginator::DOTS)
paginator.per_page = 10
paginator.update_total_pages(100)Navigation:
paginator.next_page
paginator.prev_pageGet slice bounds for your data:
start_index, end_index = paginator.slice_bounds(items.length)
visible_items = items[start_index...end_index]In view:
paginator.viewTypes:
Bubbles::Paginator::ARABIC
Bubbles::Paginator::DOTSHelp text generator:
help = Bubbles::Help.new
bindings = [
Bubbles::Key.binding(keys: ["up", "k"], help: ["↑/k", "up"]),
Bubbles::Key.binding(keys: ["down", "j"], help: ["↓/j", "down"]),
Bubbles::Key.binding(keys: ["q"], help: ["q", "quit"])
]
help.short_help_view(bindings)Key binding definitions:
quit_binding = Bubbles::Key.binding(
keys: ["q", "ctrl+c"],
help: ["q", "quit"]
)Check if a key matches:
Bubbles::Key.matches?(message, quit_binding)Blinking cursor for inputs:
cursor = Bubbles::Cursor.new
cursor.char = "_"
cursor.focusSet cursor mode:
cursor.set_mode(:blink)
cursor.set_mode(:static)
cursor.set_mode(:hide)In update:
cursor, command = cursor.update(message)In view:
cursor.viewrequire "bubbletea"
require "lipgloss"
require "bubbles"
class MyApp
include Bubbletea::Model
def initialize
@spinner = Bubbles::Spinner.new
@spinner.spinner = Bubbles::Spinners::DOT
end
def init
[self, @spinner.tick]
end
def update(message)
case message
when Bubbletea::KeyMessage
return [self, Bubbletea.quit] if message.to_s == "q"
end
@spinner, command = @spinner.update(message)
[self, command]
end
def view
"#{@spinner.view} Loading...\n\nPress q to quit"
end
end
Bubbletea.run(MyApp.new)Requirements:
- Ruby 3.2+
- bubbletea-ruby
- lipgloss-ruby (optional, for styling)
Install dependencies:
bundle installRun tests:
bundle exec rake testRun demos:
./demo/spinner
./demo/progress
./demo/textinput
./demo/textarea
./demo/viewport
./demo/list
./demo/table
./demo/filepicker
./demo/timer
./demo/stopwatch
./demo/paginator
./demo/help
./demo/cursorBug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/bubbles-ruby.
The gem is available as open source under the terms of the MIT License.
This gem is a Ruby implementation of charmbracelet/bubbles, part of the excellent Charm ecosystem. 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.
