An open API service indexing awesome lists of open source software.

https://github.com/marcoroth/huh-ruby

Build Beautiful Terminal forms and prompts in Ruby, based on Charm's Huh.
https://github.com/marcoroth/huh-ruby

Last synced: 15 days ago
JSON representation

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

Awesome Lists containing this project

README

          


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:**

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

## Usage

### Basic Form

**Create a simple form with text inputs:**

```ruby
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:**

```ruby
input = Huh.input
.key("username")
.title("Username")
.placeholder("Enter username...")
.char_limit(20)
.validate(Huh::Validation.not_empty)
```

**Password input:**

```ruby
password = Huh.input
.key("password")
.title("Password")
.echo_mode(:password)
```

#### Text

**Multi-line text area:**

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

#### Select

**Single selection from options:**

```ruby
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:**

```ruby
select = Huh.select
.key("fruit")
.title("Pick a fruit")
.options(*Huh.options("Apple", "Banana", "Cherry"))
```

#### MultiSelect

**Multiple selections:**

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

#### Confirm

**Yes/No confirmation:**

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

#### Note

**Display-only information:**

```ruby
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:**

```ruby
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:**

```ruby
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**

```ruby
input.validate(Huh::Validation.not_empty)
```

**Minimum length**
```ruby
input.validate(Huh::Validation.min_length(3))
```

**Maximum length**
```ruby
input.validate(Huh::Validation.max_length(100))
```

**Length range**
```ruby
input.validate(Huh::Validation.length_range(3, 20))
```
**Email format**
```ruby
input.validate(Huh::Validation.email)
```

**Integer**
```ruby
input.validate(Huh::Validation.integer)
```

**URL**
```ruby
input.validate(Huh::Validation.url)
```

**Custom regex**
```ruby
input.validate(Huh::Validation.match(/^[a-z]+$/, "must be lowercase letters"))
```

**Custom validation:**

```ruby
input.validate do |value|
raise Huh::ValidationError, "must start with @" unless value.start_with?("@")
end
```

**Combine validators:**

```ruby
input.validate(
Huh::Validation.all(
Huh::Validation.not_empty,
Huh::Validation.min_length(3),
Huh::Validation.max_length(20)
)
)
```

**Block navigation on validation errors:**

```ruby
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:**

```ruby
count = 0

input = Huh.input
.key("item")
.title_func { "Item #{count + 1}" }
.description_func { "You've added #{count} items" }
```

**Dynamic options:**

```ruby
items = ["A", "B", "C"]

select = Huh.select
.key("item")
.title("Pick an item")
.options_func { Huh.options(*items) }
```

### Value Binding

**Bind to external variables:**

```ruby
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:**

```ruby
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:**

```ruby
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):**

```ruby
form.with_layout(Huh::Layout.stack)
```

**Column layout:**

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

### Standalone Fields

**Run a single field without a form:**

```ruby
name = Huh.input
.title("What's your name?")
.run

puts "Hello, #{name}!"
```

**Or use the convenience method:**

```ruby
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:**

```ruby
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:**

```bash
bundle install
```

**Run tests:**

```bash
bundle exec rake test
```

**Run demos:**

```bash
./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](https://github.com/charmbracelet/huh), part of the excellent [Charm](https://charm.sh) ecosystem. Charm Ruby is not affiliated with or endorsed by Charmbracelet, Inc.

---

Part of [Charm Ruby](https://charm-ruby.dev).

Charm Ruby

[Lipgloss](https://github.com/marcoroth/lipgloss-ruby) • [Bubble Tea](https://github.com/marcoroth/bubbletea-ruby) • [Bubbles](https://github.com/marcoroth/bubbles-ruby) • [Glamour](https://github.com/marcoroth/glamour-ruby) • [Huh?](https://github.com/marcoroth/huh-ruby) • [Harmonica](https://github.com/marcoroth/harmonica-ruby) • [Bubblezone](https://github.com/marcoroth/bubblezone-ruby) • [Gum](https://github.com/marcoroth/gum-ruby) • [ntcharts](https://github.com/marcoroth/ntcharts-ruby)

The terminal doesn't have to be boring.