Skip to content

marcoroth/huh-ruby

Repository files navigation

Huh? for Ruby

Build Beautiful Terminal forms and prompts in Ruby

License

Ruby port of charmbracelet/huh.
A simple, powerful library for building interactive forms and prompts in the terminal.

Installation

Add to your Gemfile:

gem "huh", github: "marcoroth/huh-ruby"

Usage

Basic Form

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"]}"

Fields

Input

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)

Text

Multi-line text area:

text = Huh.text
  .key("bio")
  .title("Tell us about yourself")
  .placeholder("Write something...")
  .lines(5)
  .char_limit(500)

Select

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

MultiSelect

Multiple selections:

multi = Huh.multi_select
  .key("toppings")
  .title("Choose toppings")
  .options(*Huh.options("Cheese", "Pepperoni", "Mushrooms", "Olives"))
  .limit(3)

Confirm

Yes/No confirmation:

confirm = Huh.confirm
  .key("agree")
  .title("Do you agree to the terms?")
  .affirmative("I agree")
  .negative("I disagree")

Note

Display-only information:

note = Huh.note
  .title("Welcome!")
  .description("This wizard will help you set up your project.")
  .next(true)
  .next_label("Get Started")

Spinner

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 error

Available spinner types:

Type Animation
:line | / - \
:dots
:mini_dot ...
:moon 🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘
:globe 🌍 🌎 🌏
:monkey 🙈 🙉 🙊
:meter ▱▱▱ ▰▱▱ ▰▰▱ ▰▰▰
:pulse
:points ∙∙∙ ●∙∙ ∙●∙ ∙∙●
:hamburger
:ellipsis . .. ...

Themes

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

Validation

Built-in validators

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?("@")
end

Combine 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 valid

Dynamic Content

Dynamic 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) }

Value Binding

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.email

Accessing Results

Hash-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
end

Groups

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

Layouts

Stack layout (default):

form.with_layout(Huh::Layout.stack)

Column layout:

form.with_layout(Huh::Layout.columns(2))

Standalone Fields

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

Field Methods

Common Methods

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

Input Methods

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

Text Methods

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

Select/MultiSelect Methods

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)

Confirm Methods

Method Description
.affirmative(text) Set "yes" button text
.negative(text) Set "no" button text
.inline(bool) Enable inline mode

Note Methods

Method Description
.next(bool) Show next button
.next_label(text) Set next button text

Spinner Methods

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

Form Methods

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

Block-based DSL

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.run

Development

Requirements:

  • Ruby 3.2+

Install dependencies:

bundle install

Run tests:

bundle exec rake test

Run demos:

./demo/basic
./demo/dsl
./demo/dynamic
./demo/spinner
./demo/validation

Contributing

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

License

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

Acknowledgments

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.

Charm Ruby

LipglossBubble TeaBubblesGlamourHuh?HarmonicaBubblezoneGumntcharts

The terminal doesn't have to be boring.

About

Build Beautiful Terminal forms and prompts in Ruby, based on Charm's Huh.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Languages