C L A C K
CLI prompts for Ruby. Zero dependencies.
The standard approach:
print "What is your project named? "
name = gets.chomp
print "Pick a framework (1=Rails, 2=Sinatra, 3=Roda): "
framework = gets.chomp.to_iNo navigation, no validation, no visual feedback. With Clack:
require "clack"
Clack.intro "create-app"
name = Clack.text(message: "What is your project named?", placeholder: "my-app")
# => Renders a gorgeous, navigable text input with placeholder text
framework = Clack.select(
message: "Pick a framework",
options: [
{ value: "rails", label: "Ruby on Rails", hint: "recommended" },
{ value: "sinatra", label: "Sinatra" },
{ value: "roda", label: "Roda" }
]
)
# => Arrow-key navigation, vim bindings, instant submit
Clack.outro "You're all set!"# Gemfile
gem "clack"# Or install directly
gem install clackrequire "clack"
Clack.intro "create-app"
result = Clack.group do |g|
g.prompt(:name) { Clack.text(message: "Project name?", placeholder: "my-app") }
g.prompt(:framework) do
Clack.select(
message: "Pick a framework",
options: [
{ value: "rails", label: "Ruby on Rails", hint: "recommended" },
{ value: "sinatra", label: "Sinatra" },
{ value: "roda", label: "Roda" }
]
)
end
g.prompt(:features) do
Clack.multiselect(
message: "Select features",
options: %w[api auth admin websockets]
)
end
end
if Clack.cancel?(result)
Clack.cancel("Setup cancelled")
exit 1
end
Clack.outro "You're all set!"All prompts return the user's input, or Clack::CANCEL if the user pressed Escape/Ctrl+C.
Single-line text input with placeholders, defaults, validation, and tab completion.
name = Clack.text(
message: "What is your project named?",
placeholder: "my-project", # Shown when empty (dim)
default_value: "untitled", # Used if submitted empty
initial_value: "hello-world", # Pre-filled, editable
validate: ->(v) { "Required!" if v.empty? },
help: "Letters, numbers, and dashes only" # Contextual help text
)Tab completion -- press Tab to fill the longest common prefix of matching candidates:
# Static list
cmd = Clack.text(
message: "Command?",
completions: %w[build test deploy lint format]
)
# Dynamic completions
file = Clack.text(
message: "File?",
completions: ->(input) { Dir.glob("#{input}*") }
)Masked text input for secrets and API keys.
secret = Clack.password(
message: "Enter your API key",
mask: "*" # Default: "▪"
)For when one line isn't enough. Enter inserts a newline, Ctrl+D submits.
bio = Clack.multiline_text(
message: "Tell us about yourself",
initial_value: "Hello!\n",
validate: ->(v) { "Too short" if v.strip.length < 10 }
)Yes/no toggle with customizable labels.
proceed = Clack.confirm(
message: "Deploy to production?",
active: "Yes, ship it!",
inactive: "No, abort",
initial_value: false
)Pick one from a list. Navigate with arrow keys or hjkl.
db = Clack.select(
message: "Choose a database",
options: [
{ value: "pg", label: "PostgreSQL", hint: "recommended" },
{ value: "mysql", label: "MySQL" },
{ value: "sqlite", label: "SQLite", disabled: true }
],
initial_value: "pg",
max_items: 5 # Enable scrolling
)Pick many. Toggle with Space. Select all with a. Invert with i.
features = Clack.multiselect(
message: "Select features to install",
options: [
{ value: "api", label: "API Mode" },
{ value: "auth", label: "Authentication" },
{ value: "jobs", label: "Background Jobs" }
],
initial_values: ["api"],
required: true, # Must select at least one
max_items: 5 # Enable scrolling
)Shortcuts: Space toggle | a all | i invert
Type to filter with fuzzy matching -- "fb" matches "foobar". Pass filter: to override with custom logic.
color = Clack.autocomplete(
message: "Pick a color",
options: %w[red orange yellow green blue indigo violet],
placeholder: "Type to search...",
max_items: 5 # Default; scrollable via up/down arrows
)
# Custom filter logic (receives option hash and query string)
cmd = Clack.autocomplete(
message: "Select command",
options: commands,
filter: ->(opt, query) { opt[:label].start_with?(query) }
)Vim-style
j/k/h/lnavigation is not available in autocomplete -- all keyboard input feeds into the search field. Use arrow keys to navigate results.
Type-to-filter with multi-selection support.
colors = Clack.autocomplete_multiselect(
message: "Pick colors",
options: %w[red orange yellow green blue indigo violet],
placeholder: "Type to filter...",
required: true, # At least one selection required
initial_values: ["red"], # Pre-selected values
max_items: 5 # Default; scrollable via up/down arrows
)Shortcuts: Space toggle | Enter confirm
The
a(select all),i(invert), andj/k/h/lshortcuts from Multiselect are not available here -- all keyboard input feeds into the search field instead. Use arrow keys to navigate.
Filesystem navigation with Tab completion and arrow key selection.
project_dir = Clack.path(
message: "Where should we create your project?",
only_directories: true, # Only show directories
root: "." # Starting directory
)Navigation: Type to filter | Tab to autocomplete | up/down arrows to select
Segmented date picker with three format modes.
date = Clack.date(
message: "Release date?",
format: :us, # :iso (YYYY-MM-DD), :us (MM/DD/YYYY), :eu (DD/MM/YYYY)
initial_value: Date.today + 7,
min: Date.today,
max: Date.today + 365,
validate: ->(d) { "Not a Friday" unless d.friday? }
)Navigation: Tab/left-right arrows between segments | up/down arrows to adjust value | type digits directly
Visual slider for numeric selection.
volume = Clack.range(
message: "Set volume",
min: 0,
max: 100,
step: 5,
initial_value: 50
)
# Navigate with arrow keys or hjklInstant selection via keyboard shortcuts. No arrow key navigation needed.
action = Clack.select_key(
message: "What would you like to do?",
options: [
{ value: "create", label: "Create new project", key: "c" },
{ value: "open", label: "Open existing", key: "o" },
{ value: "quit", label: "Quit", key: "q" }
]
)Multiselect with options organized into named categories.
features = Clack.group_multiselect(
message: "Select features",
options: [
{
label: "Frontend",
options: [
{ value: "hotwire", label: "Hotwire" },
{ value: "stimulus", label: "Stimulus" }
]
},
{
label: "Background",
options: [
{ value: "sidekiq", label: "Sidekiq" },
{ value: "solid_queue", label: "Solid Queue" }
]
}
],
selectable_groups: true, # Toggle all options in a group at once
group_spacing: 1 # Blank lines between groups
)Non-blocking animated indicator for async work.
spinner = Clack.spinner
spinner.start("Installing dependencies...")
# Do your work...
sleep 2
spinner.stop("Dependencies installed!")
# Or: spinner.error("Installation failed")
# Or: spinner.cancel("Cancelled")Block form -- wraps a block with automatic success/error handling:
result = Clack.spin("Installing dependencies...") { system("npm install") }
# With custom messages
Clack.spin("Compiling...", success: "Build complete!") { build_project }
# Access the spinner inside the block
Clack.spin("Working...") do |s|
s.message "Step 1..."
do_step_1
s.message "Step 2..."
do_step_2
endA visual progress bar for measurable operations.
progress = Clack.progress(total: 100, message: "Downloading...")
progress.start
files.each_with_index do |file, i|
download(file)
progress.update(i + 1)
end
progress.stop("Download complete!")Run multiple tasks sequentially with status indicators.
results = Clack.tasks(tasks: [
{ title: "Checking dependencies", task: -> { check_deps } },
{ title: "Building project", task: -> { build } },
{ title: "Running tests", task: -> { run_tests } }
])
# Update the spinner message mid-task
Clack.tasks(tasks: [
{ title: "Installing", task: ->(message) {
message.call("Fetching packages...")
fetch_packages
message.call("Compiling...")
compile
}}
])
# Conditionally skip tasks with enabled:
Clack.tasks(tasks: [
{ title: "Lint", task: -> { lint } },
{ title: "Deploy", task: -> { deploy }, enabled: ENV["DEPLOY"] == "true" }
])Quick Reference Table (click to expand)
| Prompt | Method | Key Options | Defaults |
|---|---|---|---|
| Text | Clack.text |
placeholder:, default_value:, initial_value:, completions: |
-- |
| Password | Clack.password |
mask:, validate: |
mask: "▪" |
| Confirm | Clack.confirm |
active:, inactive:, initial_value: |
active: "Yes", inactive: "No", initial_value: true |
| Select | Clack.select |
options:, initial_value:, max_items: |
-- |
| Multiselect | Clack.multiselect |
options:, initial_values:, required:, cursor_at: |
required: true |
| Group Multiselect | Clack.group_multiselect |
options: (nested), selectable_groups:, group_spacing: |
selectable_groups: false |
| Autocomplete | Clack.autocomplete |
options:, placeholder:, filter:, max_items: |
max_items: 5 |
| Autocomplete Multiselect | Clack.autocomplete_multiselect |
options:, required:, initial_values:, filter: |
required: true, max_items: 5 |
| Select Key | Clack.select_key |
options: (with :key) |
-- |
| Path | Clack.path |
root:, only_directories: |
root: "." |
| Date | Clack.date |
format:, initial_value:, min:, max: |
format: :iso |
| Range | Clack.range |
min:, max:, step:, initial_value: |
min: 0, max: 100, step: 1 |
| Multiline Text | Clack.multiline_text |
initial_value:, validate: |
Submit with Ctrl+D |
| Spinner | Clack.spinner / Clack.spin |
block form auto-handles success/error | -- |
| Tasks | Clack.tasks |
tasks: ({title:, task:, enabled:}) |
enabled: true |
| Progress | Clack.progress |
total:, message: |
-- |
All prompts accept message:, validate:, help:, and return Clack::CANCEL on Escape/Ctrl+C.
result = Clack.text(message: "Name?")
exit 1 if Clack.cancel?(result)
# Or the one-liner version (prints "Cancelled" and returns true)
result = Clack.text(message: "Name?")
exit 1 if Clack.handle_cancel(result)
# With a custom message
exit 1 if Clack.handle_cancel(result, "Aborted by user")Every prompt supports validate: and transform:. The pipeline looks like this:
User Input --> Validation (raw) --> Transform (if valid) --> Final Value
Validation returns an error message, a Clack::Warning, or nil to pass.
Validation results:
nilorfalse-- passes validation- String -- shows error (red), user must fix input
Clack::Warning.new(message)-- shows warning (yellow), user can confirm with Enter or edit
# Symbol shortcuts (clean and idiomatic)
name = Clack.text(message: "Name?", transform: :strip)
code = Clack.text(message: "Code?", transform: :upcase)
# Chain multiple transforms
username = Clack.text(
message: "Username?",
transform: Clack::Transformers.chain(:strip, :downcase)
)
# Combine validation and transform
amount = Clack.text(
message: "Amount?",
validate: ->(v) { "Must be a number" unless v.match?(/\A\d+\z/) },
transform: :to_integer
)
# Warning validation (soft failure -- user can confirm or edit)
file = Clack.text(
message: "Output file?",
validate: ->(v) { Clack::Warning.new("File exists. Overwrite?") if File.exist?(v) }
)Clack::Validators.required # Non-empty input
Clack::Validators.min_length(3) # Minimum character count
Clack::Validators.max_length(100) # Maximum character count
Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
Clack::Validators.email # Email format (user@host.tld)
Clack::Validators.url # URL format (http/https)
Clack::Validators.integer # Integer string ("-5", "42")
Clack::Validators.in_range(1..100) # Numeric range (parses as int)
Clack::Validators.one_of(%w[a b c]) # Allowlist check
Clack::Validators.path_exists # File/dir exists on disk
Clack::Validators.directory_exists # Directory exists on disk
Clack::Validators.future_date # Date strictly after today
Clack::Validators.past_date # Date strictly before today
Clack::Validators.date_range(min: d1, max: d2) # Date within range
Clack::Validators.combine(v1, v2) # First error/warning wins
# Warning validators -- allow user to confirm or edit
Clack::Validators.file_exists_warning # For file overwrite confirmations
Clack::Validators.as_warning(validator) # Convert any validator to warning:strip / :trim # Remove leading/trailing whitespace
:downcase / :upcase # Change case
:capitalize # "hello world" -> "Hello world"
:titlecase # "hello world" -> "Hello World"
:squish # Collapse whitespace to single spaces
:compact # Remove all whitespace
:to_integer # Parse as integer
:to_float # Parse as float
:digits_only # Extract only digitsChain multiple prompts and collect results in a hash. If the user cancels any prompt, the whole group returns Clack::CANCEL.
result = Clack.group do |g|
g.prompt(:name) { Clack.text(message: "Your name?") }
g.prompt(:email) { Clack.text(message: "Your email?") }
g.prompt(:confirm) { |r| Clack.confirm(message: "Create account for #{r[:email]}?") }
end
return if Clack.cancel?(result)
puts "Welcome, #{result[:name]}!"Handle cancellation with a callback:
Clack.group(on_cancel: ->(r) { cleanup(r) }) do |g|
# prompts...
endClack.log.info("Starting build...")
Clack.log.success("Build completed!")
Clack.log.warn("Cache is stale")
Clack.log.error("Build failed")
Clack.log.step("Running migrations")
Clack.log.message("Custom message")Stream output from iterables, enumerables, or shell commands:
# Stream from an array or enumerable
Clack.stream.info(["Line 1", "Line 2", "Line 3"])
Clack.stream.step(["Step 1", "Step 2", "Step 3"])
# Stream from a shell command (returns true/false for success)
success = Clack.stream.command("npm install", type: :info)
# Stream from any IO or StringIO
Clack.stream.success(io_stream)Display important information in a box:
Clack.note(<<~MSG, title: "Next Steps")
cd my-project
bundle install
bin/rails server
MSGRender a customizable bordered box:
Clack.box("Hello, World!", title: "Greeting")
# With options
Clack.box(
"Centered content",
title: "My Box",
content_align: :center, # :left, :center, :right
title_align: :center,
width: 40, # or :auto to fit content
rounded: true # rounded or square corners
)Streaming log that clears on success and shows full output on failure. Great for build output:
tl = Clack.task_log(title: "Building...", limit: 10)
tl.message("Compiling file 1...")
tl.message("Compiling file 2...")
# On success: clears the log
tl.success("Build complete!")
# On error: keeps the log visible
# tl.error("Build failed!")Clack.intro("my-cli v1.0") # ┌ my-cli v1.0
# ... your prompts ...
Clack.outro("Done!") # └ Done!
# Or on error:
Clack.cancel("Aborted") # └ Aborted (red)# Add custom key bindings (merged with defaults)
Clack.update_settings(aliases: { "y" => :enter, "n" => :cancel })
# Disable guide bars
Clack.update_settings(with_guide: false)
# CI / non-interactive mode (prompts auto-submit with defaults)
Clack.update_settings(ci_mode: true) # Always on
Clack.update_settings(ci_mode: :auto) # Auto-detect (non-TTY or CI env vars)When CI mode is active, prompts immediately submit with their default values instead of waiting for input. Useful for CI pipelines and scripted environments where stdin is not a TTY.
Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals.
Clack ships with first-class test helpers. Require clack/testing explicitly (it is not auto-loaded):
require "clack/testing"
# Simulate a text prompt
result = Clack::Testing.simulate(Clack.method(:text), message: "Name?") do |prompt|
prompt.type("Alice")
prompt.submit
end
# => "Alice"
# Capture rendered output alongside the result
result, output = Clack::Testing.simulate_with_output(Clack.method(:confirm), message: "Sure?") do |prompt|
prompt.left # switch to "No"
prompt.submit
endThe PromptDriver yielded to the block provides these methods:
| Method | Description |
|---|---|
type(text) |
Type a string character by character |
submit |
Press Enter |
cancel |
Press Escape |
up / down / left / right |
Arrow keys |
toggle |
Press Space (for multiselect) |
tab |
Press Tab |
backspace |
Press Backspace |
ctrl_d |
Press Ctrl+D (submit multiline text) |
key(sym_or_char) |
Press an arbitrary key by symbol (e.g. :escape) or raw character |
ruby examples/full_demo.rbRecording the demo GIF
Requires asciinema, agg, and expect:
# Record the demo (automated via expect script)
asciinema rec examples/demo.cast --command "expect examples/demo.exp" --overwrite -q
# Split batched frames to show typing (Ruby buffers terminal output)
ruby examples/split_cast.rb
# Convert to GIF
agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --speed 0.6- Ruby 3.2+
- No runtime dependencies
- Unicode terminal recommended (ASCII fallbacks included)
bundle install
bundle exec rake # Lint + tests
bundle exec rake spec # Tests only
COVERAGE=true bundle exec rake spec # With coverage- Wizard mode -- Multi-step flows with back navigation (
Clack.wizard). The currentClack.groupruns prompts as sequential Ruby code, so there's no way to "go back" and re-answer a previous question. A declarative wizard API would define steps as a graph with branching and let the engine handle forward/back navigation.
This is a Ruby port of Clack, created by Nate Moore and the Astro team.
MIT -- See LICENSE
