Ruby port of charmbracelet/huh.
A simple, powerful library for building interactive forms and prompts in the terminal.
Add to your Gemfile:
gem "huh", github: "marcoroth/huh-ruby"Create a simple form with text inputs:
require "huh"
form = Huh.form(
Huh.group(
Huh.input
.key("name")
.title("What's your name?")
.placeholder("Enter your name..."),
Huh.input
.key("email")
.title("Email address")
.placeholder("you@example.com")
)
)
form.run
puts "Hello, #{form["name"]}!"
puts "Email: #{form["email"]}"Single-line text input:
input = Huh.input
.key("username")
.title("Username")
.placeholder("Enter username...")
.char_limit(20)
.validate(Huh::Validation.not_empty)Password input:
password = Huh.input
.key("password")
.title("Password")
.echo_mode(:password)Multi-line text area:
text = Huh.text
.key("bio")
.title("Tell us about yourself")
.placeholder("Write something...")
.lines(5)
.char_limit(500)Single selection from options:
select = Huh.select
.key("color")
.title("Pick a color")
.options(
Huh.option("Red", "#ff0000"),
Huh.option("Green", "#00ff00"),
Huh.option("Blue", "#0000ff")
)From simple values:
select = Huh.select
.key("fruit")
.title("Pick a fruit")
.options(*Huh.options("Apple", "Banana", "Cherry"))Multiple selections:
multi = Huh.multi_select
.key("toppings")
.title("Choose toppings")
.options(*Huh.options("Cheese", "Pepperoni", "Mushrooms", "Olives"))
.limit(3)Yes/No confirmation:
confirm = Huh.confirm
.key("agree")
.title("Do you agree to the terms?")
.affirmative("I agree")
.negative("I disagree")Display-only information:
note = Huh.note
.title("Welcome!")
.description("This wizard will help you set up your project.")
.next(true)
.next_label("Get Started")Loading indicator with background action:
spinner = Huh.spinner
.title("Loading data...")
.type(:dots)
.action { fetch_from_api }
error = spinner.run
puts "Failed: #{error.message}" if errorAvailable spinner types:
| Type | Animation |
|---|---|
:line |
| / - \ |
:dots |
⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷ |
:mini_dot |
⠋ ⠙ ⠹ ⠸ ... |
:moon |
🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘 |
:globe |
🌍 🌎 🌏 |
:monkey |
🙈 🙉 🙊 |
:meter |
▱▱▱ ▰▱▱ ▰▰▱ ▰▰▰ |
:pulse |
█ ▓ ▒ ░ |
:points |
∙∙∙ ●∙∙ ∙●∙ ∙∙● |
:hamburger |
☱ ☲ ☴ |
:ellipsis |
. .. ... |
Apply built-in themes:
form = Huh.form(
Huh.group(
Huh.input.key("name").title("Name")
)
).with_theme(Huh::Themes.charm)Available themes:
| Theme | Description |
|---|---|
Themes.base |
Minimal styling |
Themes.charm |
Indigo and fuchsia |
Themes.dracula |
Dark purple |
Themes.catppuccin |
Pastel colors |
Themes.base16 |
Terminal colors |
Required field
input.validate(Huh::Validation.not_empty)Minimum length
input.validate(Huh::Validation.min_length(3))Maximum length
input.validate(Huh::Validation.max_length(100))Length range
input.validate(Huh::Validation.length_range(3, 20))Email format
input.validate(Huh::Validation.email)Integer
input.validate(Huh::Validation.integer)URL
input.validate(Huh::Validation.url)Custom regex
input.validate(Huh::Validation.match(/^[a-z]+$/, "must be lowercase letters"))Custom validation:
input.validate do |value|
raise Huh::ValidationError, "must start with @" unless value.start_with?("@")
endCombine validators:
input.validate(
Huh::Validation.all(
Huh::Validation.not_empty,
Huh::Validation.min_length(3),
Huh::Validation.max_length(20)
)
)Block navigation on validation errors:
form = Huh.form(
Huh.group(
Huh.input.key("email").validate(Huh::Validation.email)
)
).with_validate_on_submit(true) # Users can't proceed until field is validDynamic titles and descriptions:
count = 0
input = Huh.input
.key("item")
.title_func { "Item #{count + 1}" }
.description_func { "You've added #{count} items" }Dynamic options:
items = ["A", "B", "C"]
select = Huh.select
.key("item")
.title("Pick an item")
.options_func { Huh.options(*items) }Bind to external variables:
user = Struct.new(:name, :email).new("", "")
form = Huh.form(
Huh.group(
Huh.input.key("name").title("Name").value(user, :name),
Huh.input.key("email").title("Email").value(user, :email)
)
)
form.run
puts user.name # Value is automatically synced
puts user.emailHash-style access:
errors = form.run
if errors.any?
errors.each { |e| puts "Error: #{e.message}" }
else
form["name"] # Get by key
form.get("name") # Same as above
form.to_h # Get all results as hash
endOrganize fields into groups:
form = Huh.form(
Huh.group(
Huh.input.key("name").title("Name"),
Huh.input.key("email").title("Email")
).title("Personal Info").description("Enter your details"),
Huh.group(
Huh.input.key("company").title("Company"),
Huh.input.key("role").title("Role")
).title("Work Info")
)Stack layout (default):
form.with_layout(Huh::Layout.stack)Column layout:
form.with_layout(Huh::Layout.columns(2))Run a single field without a form:
name = Huh.input
.title("What's your name?")
.run
puts "Hello, #{name}!"Or use the convenience method:
result = Huh.run(
Huh.confirm.title("Continue?")
)| Method | Description |
|---|---|
.key(string) |
Set result key |
.title(string) |
Set title text |
.title_func { } |
Set dynamic title |
.description(string) |
Set description text |
.description_func { } |
Set dynamic description |
.value(accessor) |
Bind to value accessor |
.value(object, :attr) |
Bind to object attribute |
.validate(validator) |
Set validation |
.validate { |v| } |
Set validation block |
.with_theme(theme) |
Apply theme |
.run |
Run field standalone |
| Method | Description |
|---|---|
.placeholder(text) |
Set placeholder text |
.char_limit(int) |
Set character limit |
.echo_mode(mode) |
Set echo mode (:normal, :password, :none) |
.prompt(text) |
Set prompt character |
.suggestions(array) |
Set autocomplete suggestions |
.inline(bool) |
Enable inline mode |
| Method | Description |
|---|---|
.placeholder(text) |
Set placeholder text |
.char_limit(int) |
Set character limit |
.lines(int) |
Set visible lines |
.show_line_numbers(bool) |
Show line numbers |
| Method | Description |
|---|---|
.options(*opts) |
Set available options |
.options_func { } |
Set dynamic options |
.height(int) |
Set visible height |
.filtering(bool) |
Enable filtering |
.limit(int) |
Max selections (MultiSelect) |
| Method | Description |
|---|---|
.affirmative(text) |
Set "yes" button text |
.negative(text) |
Set "no" button text |
.inline(bool) |
Enable inline mode |
| Method | Description |
|---|---|
.next(bool) |
Show next button |
.next_label(text) |
Set next button text |
| Method | Description |
|---|---|
.title(text) |
Set spinner title |
.title_func { } |
Set dynamic title |
.type(symbol) |
Set spinner type |
.action { } |
Set background action |
.with_accessible(bool) |
Enable static mode |
| Method | Description |
|---|---|
.with_theme(theme) |
Apply theme to all fields |
.with_layout(layout) |
Set form layout |
.with_width(int) |
Set form width |
.with_height(int) |
Set form height |
.with_accessible(bool) |
Enable accessible mode |
.with_show_help(bool) |
Show/hide help |
.with_validate_on_submit(bool) |
Block navigation on validation errors |
.run |
Run the form, returns Array[ValidationError] |
.errors |
Get all validation errors |
.to_h |
Get all results as hash |
[key] |
Get result by key |
.get(key) |
Get result by key |
Alternative Ruby-idiomatic syntax using blocks:
form = Huh::Form.new do |form|
form.theme(Huh::Themes.charm)
form.group do |group|
group.title("User Registration")
group.input do |input|
input.key("name")
input.title("What's your name?")
input.placeholder("Enter your name")
end
group.select do |select|
select.key("role")
select.title("Your role")
select.options("Developer", "Designer", "Manager")
end
group.confirm do |confirm|
confirm.key("newsletter")
confirm.title("Subscribe?")
end
end
end
form.runRequirements:
- Ruby 3.2+
Install dependencies:
bundle installRun tests:
bundle exec rake testRun demos:
./demo/basic
./demo/dsl
./demo/dynamic
./demo/spinner
./demo/validationBug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/huh-ruby.
The gem is available as open source under the terms of the MIT License.
This gem is a Ruby port of charmbracelet/huh, 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.
