Skip to content

marcoroth/bubbles-ruby

Repository files navigation

Bubbles for Ruby

TUI components for Bubble Tea

Gem Version License

Ruby implementation of charmbracelet/bubbles.
Common UI components for building terminal applications with Bubble Tea.

Installation

Add to your Gemfile:

gem "bubbles"

Or install directly:

gem install bubbles

Components

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

Usage

Spinner

Animated loading indicator:

require "bubbles"

spinner = Bubbles::Spinner.new
spinner.spinner = Bubbles::Spinners::DOT

In your update method:

spinner, command = spinner.update(message)

In your view method:

spinner.view

Available 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::ELLIPSIS

Progress

Animated progress bar:

progress = Bubbles::Progress.new(width: 40)
progress.set_percent(0.5)

In your view:

progress.view

Customization:

progress = Bubbles::Progress.new(
  width: 40,
  full: "█",
  empty: "░",
  show_percentage: true
)
progress.full_color = "212"
progress.empty_color = "238"

Timer

Countdown timer (60 seconds):

timer = Bubbles::Timer.new(60)

Start the timer:

command = timer.start

In update:

timer, command = timer.update(message)

Check if done:

timer.timed_out?

In view:

timer.view

Stopwatch

Elapsed time counter:

stopwatch = Bubbles::Stopwatch.new

Start/stop/toggle:

command = stopwatch.start
stopwatch.stop
command = stopwatch.toggle

In view:

stopwatch.view

TextInput

Single-line text input:

input = Bubbles::TextInput.new
input.placeholder = "Enter your name..."
input.prompt = "> "
input.focus

In update:

input, command = input.update(message)

Get value:

input.value

In view:

input.view

Password mode:

input.echo_mode = :password
input.echo_character = "*"

With suggestions:

input.suggestions = ["apple", "apricot", "avocado"]
input.show_suggestions = true

TextArea

Multi-line text input:

textarea = Bubbles::TextArea.new(width: 60, height: 10)
textarea.placeholder = "Type your message..."
textarea.show_line_numbers = true
textarea.focus

In update:

textarea, command = textarea.update(message)

Get value:

textarea.value

Position info:

textarea.row
textarea.col
textarea.line_count

Viewport

Scrollable content pane:

viewport = Bubbles::Viewport.new(width: 80, height: 20)
viewport.content = long_text

In update (handles scroll keys):

viewport, command = viewport.update(message)

Scroll info:

viewport.scroll_percent
viewport.at_top?
viewport.at_bottom?

In view:

viewport.view

Programmatic scrolling:

viewport.scroll_down(5)
viewport.scroll_up(5)
viewport.page_down
viewport.page_up
viewport.goto_top
viewport.goto_bottom

List

Interactive 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_item

Filter state:

list.filter_state

Styling:

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")

Table

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_data

Styling:

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")

FilePicker

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
end

Options:

picker.show_permissions = true
picker.show_size = true
picker.dir_allowed = false
picker.file_allowed = true

Paginator

Pagination controls:

paginator = Bubbles::Paginator.new(type: Bubbles::Paginator::DOTS)
paginator.per_page = 10
paginator.update_total_pages(100)

Navigation:

paginator.next_page
paginator.prev_page

Get slice bounds for your data:

start_index, end_index = paginator.slice_bounds(items.length)
visible_items = items[start_index...end_index]

In view:

paginator.view

Types:

Bubbles::Paginator::ARABIC
Bubbles::Paginator::DOTS

Help

Help 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

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)

Cursor

Blinking cursor for inputs:

cursor = Bubbles::Cursor.new
cursor.char = "_"
cursor.focus

Set cursor mode:

cursor.set_mode(:blink)
cursor.set_mode(:static)
cursor.set_mode(:hide)

In update:

cursor, command = cursor.update(message)

In view:

cursor.view

Complete Example

require "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)

Development

Requirements:

Install dependencies:

bundle install

Run tests:

bundle exec rake test

Run 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/cursor

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/bubbles-ruby.

License

The gem is available as open source under the terms of the MIT License.

Acknowledgments

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.

Charm Ruby

LipglossBubble TeaBubblesGlamourHuh?HarmonicaBubblezoneGumntcharts

The terminal doesn't have to be boring.

About

TUI components for Bubble Tea, based on Charm's Bubbles.

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

 

Languages