clack 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +265 -196
- data/lib/clack/core/key_reader.rb +30 -20
- data/lib/clack/core/options_helper.rb +71 -29
- data/lib/clack/core/prompt.rb +45 -12
- data/lib/clack/core/scroll_helper.rb +10 -41
- data/lib/clack/core/selection_manager.rb +49 -0
- data/lib/clack/prompts/autocomplete.rb +17 -11
- data/lib/clack/prompts/autocomplete_multiselect.rb +14 -21
- data/lib/clack/prompts/confirm.rb +8 -30
- data/lib/clack/prompts/date.rb +1 -14
- data/lib/clack/prompts/group_multiselect.rb +20 -38
- data/lib/clack/prompts/multiline_text.rb +33 -53
- data/lib/clack/prompts/multiselect.rb +16 -27
- data/lib/clack/prompts/password.rb +1 -14
- data/lib/clack/prompts/path.rb +9 -23
- data/lib/clack/prompts/range.rb +1 -14
- data/lib/clack/prompts/select.rb +11 -25
- data/lib/clack/prompts/spinner.rb +15 -20
- data/lib/clack/prompts/text.rb +1 -14
- data/lib/clack/testing.rb +31 -37
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +71 -37
- metadata +2 -2
data/README.md
CHANGED
|
@@ -1,29 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<br>
|
|
3
|
+
<br>
|
|
4
|
+
<code> C L A C K </code>
|
|
5
|
+
<br>
|
|
6
|
+
<br>
|
|
7
|
+
<i>CLI prompts for Ruby. Zero dependencies.</i>
|
|
8
|
+
<br>
|
|
9
|
+
<br>
|
|
10
|
+
<a href="https://rubygems.org/gems/clack"><img src="https://img.shields.io/gem/v/clack?style=flat-square&color=cc342d" alt="Gem Version"></a>
|
|
11
|
+
<a href="https://github.com/swhitt/clackrb/actions"><img src="https://img.shields.io/github/actions/workflow/status/swhitt/clackrb/ci.yml?style=flat-square&label=tests" alt="Tests"></a>
|
|
12
|
+
<a href="https://www.ruby-lang.org"><img src="https://img.shields.io/badge/ruby-3.2%2B-cc342d?style=flat-square" alt="Ruby 3.2+"></a>
|
|
13
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License"></a>
|
|
14
|
+
</p>
|
|
6
15
|
|
|
7
16
|
<p align="center">
|
|
8
|
-
<img src="examples/demo.gif?v=2" width="640" alt="Clack demo">
|
|
17
|
+
<img src="examples/demo.gif?v=2" width="640" alt="Clack demo showing beautiful terminal prompts">
|
|
9
18
|
</p>
|
|
10
19
|
|
|
20
|
+
---
|
|
21
|
+
|
|
11
22
|
## Why Clack?
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
The standard approach:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
print "What is your project named? "
|
|
28
|
+
name = gets.chomp
|
|
29
|
+
print "Pick a framework (1=Rails, 2=Sinatra, 3=Roda): "
|
|
30
|
+
framework = gets.chomp.to_i
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
No navigation, no validation, no visual feedback. With Clack:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require "clack"
|
|
37
|
+
|
|
38
|
+
Clack.intro "create-app"
|
|
39
|
+
|
|
40
|
+
name = Clack.text(message: "What is your project named?", placeholder: "my-app")
|
|
41
|
+
# => Renders a gorgeous, navigable text input with placeholder text
|
|
42
|
+
|
|
43
|
+
framework = Clack.select(
|
|
44
|
+
message: "Pick a framework",
|
|
45
|
+
options: [
|
|
46
|
+
{ value: "rails", label: "Ruby on Rails", hint: "recommended" },
|
|
47
|
+
{ value: "sinatra", label: "Sinatra" },
|
|
48
|
+
{ value: "roda", label: "Roda" }
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
# => Arrow-key navigation, vim bindings, instant submit
|
|
52
|
+
|
|
53
|
+
Clack.outro "You're all set!"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
18
57
|
|
|
19
58
|
## Installation
|
|
20
59
|
|
|
21
60
|
```ruby
|
|
22
61
|
# Gemfile
|
|
23
62
|
gem "clack"
|
|
24
|
-
|
|
25
|
-
# Or from GitHub
|
|
26
|
-
gem "clack", github: "swhitt/clackrb"
|
|
27
63
|
```
|
|
28
64
|
|
|
29
65
|
```bash
|
|
@@ -31,15 +67,18 @@ gem "clack", github: "swhitt/clackrb"
|
|
|
31
67
|
gem install clack
|
|
32
68
|
```
|
|
33
69
|
|
|
70
|
+
---
|
|
71
|
+
|
|
34
72
|
## Quick Start
|
|
35
73
|
|
|
36
74
|
```ruby
|
|
37
75
|
require "clack"
|
|
38
76
|
|
|
39
|
-
Clack.intro "
|
|
77
|
+
Clack.intro "create-app"
|
|
40
78
|
|
|
41
79
|
result = Clack.group do |g|
|
|
42
80
|
g.prompt(:name) { Clack.text(message: "Project name?", placeholder: "my-app") }
|
|
81
|
+
|
|
43
82
|
g.prompt(:framework) do
|
|
44
83
|
Clack.select(
|
|
45
84
|
message: "Pick a framework",
|
|
@@ -50,6 +89,7 @@ result = Clack.group do |g|
|
|
|
50
89
|
]
|
|
51
90
|
)
|
|
52
91
|
end
|
|
92
|
+
|
|
53
93
|
g.prompt(:features) do
|
|
54
94
|
Clack.multiselect(
|
|
55
95
|
message: "Select features",
|
|
@@ -66,126 +106,16 @@ end
|
|
|
66
106
|
Clack.outro "You're all set!"
|
|
67
107
|
```
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Try it yourself:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
ruby examples/full_demo.rb
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
<details>
|
|
78
|
-
<summary>Recording the demo GIF</summary>
|
|
79
|
-
|
|
80
|
-
Requires [asciinema](https://asciinema.org/), [agg](https://github.com/asciinema/agg), and [expect](https://core.tcl-lang.org/expect/index):
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
# Record the demo (automated via expect script)
|
|
84
|
-
asciinema rec examples/demo.cast --command "expect examples/demo.exp" --overwrite -q
|
|
85
|
-
|
|
86
|
-
# Split batched frames to show typing (Ruby buffers terminal output)
|
|
87
|
-
ruby examples/split_cast.rb
|
|
88
|
-
|
|
89
|
-
# Convert to GIF
|
|
90
|
-
agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --speed 0.6
|
|
91
|
-
```
|
|
92
|
-
</details>
|
|
109
|
+
---
|
|
93
110
|
|
|
94
111
|
## Prompts
|
|
95
112
|
|
|
96
|
-
All prompts return the user's input, or `Clack::CANCEL` if
|
|
97
|
-
|
|
98
|
-
```ruby
|
|
99
|
-
# Check for cancellation
|
|
100
|
-
result = Clack.text(message: "Name?")
|
|
101
|
-
exit 1 if Clack.cancel?(result)
|
|
102
|
-
|
|
103
|
-
# Or use handle_cancel for a one-liner that prints "Cancelled" and returns true
|
|
104
|
-
result = Clack.text(message: "Name?")
|
|
105
|
-
exit 1 if Clack.handle_cancel(result)
|
|
106
|
-
|
|
107
|
-
# With a custom message
|
|
108
|
-
exit 1 if Clack.handle_cancel(result, "Aborted by user")
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Validation & Transforms
|
|
112
|
-
|
|
113
|
-
Prompts support `validate:` and `transform:` options.
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
User Input → Validation (raw) → Transform (if valid) → Final Value
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Validation returns an error message, a `Clack::Warning`, or `nil` to pass. Transforms normalize the value after validation passes.
|
|
120
|
-
|
|
121
|
-
**Validation results:**
|
|
122
|
-
- `nil` or `false` - passes validation
|
|
123
|
-
- String - shows error (red), user must fix input
|
|
124
|
-
- `Clack::Warning.new(message)` - shows warning (yellow), user can confirm with Enter or edit
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
# Use symbol shortcuts (preferred)
|
|
128
|
-
name = Clack.text(message: "Name?", transform: :strip)
|
|
129
|
-
code = Clack.text(message: "Code?", transform: :upcase)
|
|
130
|
-
|
|
131
|
-
# Chain multiple transforms
|
|
132
|
-
username = Clack.text(
|
|
133
|
-
message: "Username?",
|
|
134
|
-
transform: Clack::Transformers.chain(:strip, :downcase)
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Combine validation and transform
|
|
138
|
-
amount = Clack.text(
|
|
139
|
-
message: "Amount?",
|
|
140
|
-
validate: ->(v) { "Must be a number" unless v.match?(/\A\d+\z/) },
|
|
141
|
-
transform: :to_integer
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
# Warning validation (soft failure, user can confirm or edit)
|
|
145
|
-
file = Clack.text(
|
|
146
|
-
message: "Output file?",
|
|
147
|
-
validate: ->(v) { Clack::Warning.new("File exists. Overwrite?") if File.exist?(v) }
|
|
148
|
-
)
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Built-in validators and transformers:
|
|
152
|
-
|
|
153
|
-
```ruby
|
|
154
|
-
# Validators - return error message, Warning, or nil
|
|
155
|
-
Clack::Validators.required # Non-empty input
|
|
156
|
-
Clack::Validators.min_length(3) # Minimum character count
|
|
157
|
-
Clack::Validators.max_length(100) # Maximum character count
|
|
158
|
-
Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
|
|
159
|
-
Clack::Validators.email # Email format (user@host.tld)
|
|
160
|
-
Clack::Validators.url # URL format (http/https)
|
|
161
|
-
Clack::Validators.integer # Integer string ("-5", "42")
|
|
162
|
-
Clack::Validators.in_range(1..100) # Numeric range (parses as int)
|
|
163
|
-
Clack::Validators.one_of(%w[a b c]) # Allowlist check
|
|
164
|
-
Clack::Validators.path_exists # File/dir exists on disk
|
|
165
|
-
Clack::Validators.directory_exists # Directory exists on disk
|
|
166
|
-
Clack::Validators.future_date # Date strictly after today
|
|
167
|
-
Clack::Validators.past_date # Date strictly before today
|
|
168
|
-
Clack::Validators.date_range(min: d1, max: d2) # Date within range
|
|
169
|
-
Clack::Validators.combine(v1, v2) # First error/warning wins
|
|
170
|
-
|
|
171
|
-
# Warning validators - allow user to confirm or edit
|
|
172
|
-
Clack::Validators.file_exists_warning # For file overwrite confirmations
|
|
173
|
-
Clack::Validators.as_warning(validator) # Convert any validator to warning
|
|
174
|
-
|
|
175
|
-
# Transformers - normalize the value (use :symbol or Clack::Transformers.name)
|
|
176
|
-
:strip / :trim # Remove leading/trailing whitespace
|
|
177
|
-
:downcase / :upcase # Change case
|
|
178
|
-
:capitalize # "hello world" -> "Hello world"
|
|
179
|
-
:titlecase # "hello world" -> "Hello World"
|
|
180
|
-
:squish # Collapse whitespace to single spaces
|
|
181
|
-
:compact # Remove all whitespace
|
|
182
|
-
:to_integer # Parse as integer
|
|
183
|
-
:to_float # Parse as float
|
|
184
|
-
:digits_only # Extract only digits
|
|
185
|
-
```
|
|
113
|
+
All prompts return the user's input, or `Clack::CANCEL` if the user pressed Escape/Ctrl+C.
|
|
186
114
|
|
|
187
115
|
### Text
|
|
188
116
|
|
|
117
|
+
Single-line text input with placeholders, defaults, validation, and tab completion.
|
|
118
|
+
|
|
189
119
|
<img src="examples/images/text.svg" alt="Text prompt">
|
|
190
120
|
|
|
191
121
|
```ruby
|
|
@@ -199,16 +129,16 @@ name = Clack.text(
|
|
|
199
129
|
)
|
|
200
130
|
```
|
|
201
131
|
|
|
202
|
-
**Tab completion**
|
|
132
|
+
**Tab completion** -- press `Tab` to fill the longest common prefix of matching candidates:
|
|
203
133
|
|
|
204
134
|
```ruby
|
|
205
|
-
#
|
|
135
|
+
# Static list
|
|
206
136
|
cmd = Clack.text(
|
|
207
137
|
message: "Command?",
|
|
208
138
|
completions: %w[build test deploy lint format]
|
|
209
139
|
)
|
|
210
140
|
|
|
211
|
-
# Dynamic
|
|
141
|
+
# Dynamic completions
|
|
212
142
|
file = Clack.text(
|
|
213
143
|
message: "File?",
|
|
214
144
|
completions: ->(input) { Dir.glob("#{input}*") }
|
|
@@ -217,6 +147,8 @@ file = Clack.text(
|
|
|
217
147
|
|
|
218
148
|
### Password
|
|
219
149
|
|
|
150
|
+
Masked text input for secrets and API keys.
|
|
151
|
+
|
|
220
152
|
<img src="examples/images/password.svg" alt="Password prompt">
|
|
221
153
|
|
|
222
154
|
```ruby
|
|
@@ -228,7 +160,7 @@ secret = Clack.password(
|
|
|
228
160
|
|
|
229
161
|
### Multiline Text
|
|
230
162
|
|
|
231
|
-
|
|
163
|
+
For when one line isn't enough. Enter inserts a newline, Ctrl+D submits.
|
|
232
164
|
|
|
233
165
|
```ruby
|
|
234
166
|
bio = Clack.multiline_text(
|
|
@@ -240,6 +172,8 @@ bio = Clack.multiline_text(
|
|
|
240
172
|
|
|
241
173
|
### Confirm
|
|
242
174
|
|
|
175
|
+
Yes/no toggle with customizable labels.
|
|
176
|
+
|
|
243
177
|
<img src="examples/images/confirm.svg" alt="Confirm prompt">
|
|
244
178
|
|
|
245
179
|
```ruby
|
|
@@ -253,7 +187,7 @@ proceed = Clack.confirm(
|
|
|
253
187
|
|
|
254
188
|
### Select
|
|
255
189
|
|
|
256
|
-
|
|
190
|
+
Pick one from a list. Navigate with arrow keys or `hjkl`.
|
|
257
191
|
|
|
258
192
|
<img src="examples/images/select.svg" alt="Select prompt">
|
|
259
193
|
|
|
@@ -272,7 +206,7 @@ db = Clack.select(
|
|
|
272
206
|
|
|
273
207
|
### Multiselect
|
|
274
208
|
|
|
275
|
-
|
|
209
|
+
Pick many. Toggle with Space. Select all with `a`. Invert with `i`.
|
|
276
210
|
|
|
277
211
|
<img src="examples/images/multiselect.svg" alt="Multiselect prompt">
|
|
278
212
|
|
|
@@ -294,14 +228,14 @@ features = Clack.multiselect(
|
|
|
294
228
|
|
|
295
229
|
### Autocomplete
|
|
296
230
|
|
|
297
|
-
Type to filter
|
|
231
|
+
Type to filter with fuzzy matching -- "fb" matches "foobar". Pass `filter:` to override with custom logic.
|
|
298
232
|
|
|
299
233
|
```ruby
|
|
300
234
|
color = Clack.autocomplete(
|
|
301
235
|
message: "Pick a color",
|
|
302
236
|
options: %w[red orange yellow green blue indigo violet],
|
|
303
237
|
placeholder: "Type to search...",
|
|
304
|
-
max_items: 5 # Default; scrollable via
|
|
238
|
+
max_items: 5 # Default; scrollable via up/down arrows
|
|
305
239
|
)
|
|
306
240
|
|
|
307
241
|
# Custom filter logic (receives option hash and query string)
|
|
@@ -312,11 +246,11 @@ cmd = Clack.autocomplete(
|
|
|
312
246
|
)
|
|
313
247
|
```
|
|
314
248
|
|
|
315
|
-
> Vim-style `j`/`k`/`h`/`l` navigation is not available
|
|
249
|
+
> Vim-style `j`/`k`/`h`/`l` navigation is not available in autocomplete -- all keyboard input feeds into the search field. Use arrow keys to navigate results.
|
|
316
250
|
|
|
317
251
|
### Autocomplete Multiselect
|
|
318
252
|
|
|
319
|
-
Type
|
|
253
|
+
Type-to-filter with multi-selection support.
|
|
320
254
|
|
|
321
255
|
```ruby
|
|
322
256
|
colors = Clack.autocomplete_multiselect(
|
|
@@ -325,17 +259,17 @@ colors = Clack.autocomplete_multiselect(
|
|
|
325
259
|
placeholder: "Type to filter...",
|
|
326
260
|
required: true, # At least one selection required
|
|
327
261
|
initial_values: ["red"], # Pre-selected values
|
|
328
|
-
max_items: 5 # Default; scrollable via
|
|
262
|
+
max_items: 5 # Default; scrollable via up/down arrows
|
|
329
263
|
)
|
|
330
264
|
```
|
|
331
265
|
|
|
332
266
|
**Shortcuts:** `Space` toggle | `Enter` confirm
|
|
333
267
|
|
|
334
|
-
> The `a` (select all), `i` (invert), and `j`/`k`/`h`/`l` shortcuts from
|
|
268
|
+
> The `a` (select all), `i` (invert), and `j`/`k`/`h`/`l` shortcuts from Multiselect are not available here -- all keyboard input feeds into the search field instead. Use arrow keys to navigate.
|
|
335
269
|
|
|
336
270
|
### Path
|
|
337
271
|
|
|
338
|
-
|
|
272
|
+
Filesystem navigation with Tab completion and arrow key selection.
|
|
339
273
|
|
|
340
274
|
```ruby
|
|
341
275
|
project_dir = Clack.path(
|
|
@@ -345,7 +279,7 @@ project_dir = Clack.path(
|
|
|
345
279
|
)
|
|
346
280
|
```
|
|
347
281
|
|
|
348
|
-
**Navigation:** Type to filter | `Tab` to autocomplete |
|
|
282
|
+
**Navigation:** Type to filter | `Tab` to autocomplete | up/down arrows to select
|
|
349
283
|
|
|
350
284
|
### Date
|
|
351
285
|
|
|
@@ -362,11 +296,11 @@ date = Clack.date(
|
|
|
362
296
|
)
|
|
363
297
|
```
|
|
364
298
|
|
|
365
|
-
**Navigation:** `Tab
|
|
299
|
+
**Navigation:** `Tab`/left-right arrows between segments | up/down arrows to adjust value | type digits directly
|
|
366
300
|
|
|
367
301
|
### Range
|
|
368
302
|
|
|
369
|
-
|
|
303
|
+
Visual slider for numeric selection.
|
|
370
304
|
|
|
371
305
|
```ruby
|
|
372
306
|
volume = Clack.range(
|
|
@@ -376,11 +310,12 @@ volume = Clack.range(
|
|
|
376
310
|
step: 5,
|
|
377
311
|
initial_value: 50
|
|
378
312
|
)
|
|
313
|
+
# Navigate with arrow keys or hjkl
|
|
379
314
|
```
|
|
380
315
|
|
|
381
316
|
### Select Key
|
|
382
317
|
|
|
383
|
-
|
|
318
|
+
Instant selection via keyboard shortcuts. No arrow key navigation needed.
|
|
384
319
|
|
|
385
320
|
```ruby
|
|
386
321
|
action = Clack.select_key(
|
|
@@ -393,6 +328,34 @@ action = Clack.select_key(
|
|
|
393
328
|
)
|
|
394
329
|
```
|
|
395
330
|
|
|
331
|
+
### Group Multiselect
|
|
332
|
+
|
|
333
|
+
Multiselect with options organized into named categories.
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
features = Clack.group_multiselect(
|
|
337
|
+
message: "Select features",
|
|
338
|
+
options: [
|
|
339
|
+
{
|
|
340
|
+
label: "Frontend",
|
|
341
|
+
options: [
|
|
342
|
+
{ value: "hotwire", label: "Hotwire" },
|
|
343
|
+
{ value: "stimulus", label: "Stimulus" }
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
label: "Background",
|
|
348
|
+
options: [
|
|
349
|
+
{ value: "sidekiq", label: "Sidekiq" },
|
|
350
|
+
{ value: "solid_queue", label: "Solid Queue" }
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
selectable_groups: true, # Toggle all options in a group at once
|
|
355
|
+
group_spacing: 1 # Blank lines between groups
|
|
356
|
+
)
|
|
357
|
+
```
|
|
358
|
+
|
|
396
359
|
### Spinner
|
|
397
360
|
|
|
398
361
|
Non-blocking animated indicator for async work.
|
|
@@ -411,7 +374,7 @@ spinner.stop("Dependencies installed!")
|
|
|
411
374
|
# Or: spinner.cancel("Cancelled")
|
|
412
375
|
```
|
|
413
376
|
|
|
414
|
-
**Block form**
|
|
377
|
+
**Block form** -- wraps a block with automatic success/error handling:
|
|
415
378
|
|
|
416
379
|
```ruby
|
|
417
380
|
result = Clack.spin("Installing dependencies...") { system("npm install") }
|
|
@@ -430,7 +393,7 @@ end
|
|
|
430
393
|
|
|
431
394
|
### Progress
|
|
432
395
|
|
|
433
|
-
|
|
396
|
+
A visual progress bar for measurable operations.
|
|
434
397
|
|
|
435
398
|
```ruby
|
|
436
399
|
progress = Clack.progress(total: 100, message: "Downloading...")
|
|
@@ -446,7 +409,7 @@ progress.stop("Download complete!")
|
|
|
446
409
|
|
|
447
410
|
### Tasks
|
|
448
411
|
|
|
449
|
-
Run multiple tasks with status indicators.
|
|
412
|
+
Run multiple tasks sequentially with status indicators.
|
|
450
413
|
|
|
451
414
|
```ruby
|
|
452
415
|
results = Clack.tasks(tasks: [
|
|
@@ -472,63 +435,133 @@ Clack.tasks(tasks: [
|
|
|
472
435
|
])
|
|
473
436
|
```
|
|
474
437
|
|
|
475
|
-
### Group Multiselect
|
|
476
|
-
|
|
477
|
-
Multiselect with options organized into groups.
|
|
478
|
-
|
|
479
|
-
```ruby
|
|
480
|
-
features = Clack.group_multiselect(
|
|
481
|
-
message: "Select features",
|
|
482
|
-
options: [
|
|
483
|
-
{
|
|
484
|
-
label: "Frontend",
|
|
485
|
-
options: [
|
|
486
|
-
{ value: "hotwire", label: "Hotwire" },
|
|
487
|
-
{ value: "stimulus", label: "Stimulus" }
|
|
488
|
-
]
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
label: "Background",
|
|
492
|
-
options: [
|
|
493
|
-
{ value: "sidekiq", label: "Sidekiq" },
|
|
494
|
-
{ value: "solid_queue", label: "Solid Queue" }
|
|
495
|
-
]
|
|
496
|
-
}
|
|
497
|
-
],
|
|
498
|
-
selectable_groups: true, # Toggle all options in a group at once
|
|
499
|
-
group_spacing: 1 # Blank lines between groups
|
|
500
|
-
)
|
|
501
|
-
```
|
|
502
|
-
|
|
503
438
|
<details>
|
|
504
|
-
<summary><
|
|
439
|
+
<summary><strong>Quick Reference Table</strong> (click to expand)</summary>
|
|
505
440
|
|
|
506
441
|
| Prompt | Method | Key Options | Defaults |
|
|
507
442
|
|--------|--------|-------------|----------|
|
|
508
|
-
| Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` |
|
|
443
|
+
| Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` | -- |
|
|
509
444
|
| Password | `Clack.password` | `mask:`, `validate:` | `mask: "▪"` |
|
|
510
445
|
| Confirm | `Clack.confirm` | `active:`, `inactive:`, `initial_value:` | `active: "Yes"`, `inactive: "No"`, `initial_value: true` |
|
|
511
|
-
| Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` |
|
|
446
|
+
| Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` | -- |
|
|
512
447
|
| Multiselect | `Clack.multiselect` | `options:`, `initial_values:`, `required:`, `cursor_at:` | `required: true` |
|
|
513
448
|
| Group Multiselect | `Clack.group_multiselect` | `options:` (nested), `selectable_groups:`, `group_spacing:` | `selectable_groups: false` |
|
|
514
449
|
| Autocomplete | `Clack.autocomplete` | `options:`, `placeholder:`, `filter:`, `max_items:` | `max_items: 5` |
|
|
515
450
|
| Autocomplete Multiselect | `Clack.autocomplete_multiselect` | `options:`, `required:`, `initial_values:`, `filter:` | `required: true`, `max_items: 5` |
|
|
516
|
-
| Select Key | `Clack.select_key` | `options:` (with `:key`) |
|
|
451
|
+
| Select Key | `Clack.select_key` | `options:` (with `:key`) | -- |
|
|
517
452
|
| Path | `Clack.path` | `root:`, `only_directories:` | `root: "."` |
|
|
518
453
|
| Date | `Clack.date` | `format:`, `initial_value:`, `min:`, `max:` | `format: :iso` |
|
|
519
454
|
| Range | `Clack.range` | `min:`, `max:`, `step:`, `initial_value:` | `min: 0`, `max: 100`, `step: 1` |
|
|
520
455
|
| Multiline Text | `Clack.multiline_text` | `initial_value:`, `validate:` | Submit with **Ctrl+D** |
|
|
521
|
-
| Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error |
|
|
456
|
+
| Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error | -- |
|
|
522
457
|
| Tasks | `Clack.tasks` | `tasks:` (`{title:, task:, enabled:}`) | `enabled: true` |
|
|
523
|
-
| Progress | `Clack.progress` | `total:`, `message:` |
|
|
458
|
+
| Progress | `Clack.progress` | `total:`, `message:` | -- |
|
|
524
459
|
|
|
525
460
|
All prompts accept `message:`, `validate:`, `help:`, and return `Clack::CANCEL` on Escape/Ctrl+C.
|
|
526
461
|
|
|
527
462
|
</details>
|
|
528
463
|
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Cancellation
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
result = Clack.text(message: "Name?")
|
|
470
|
+
exit 1 if Clack.cancel?(result)
|
|
471
|
+
|
|
472
|
+
# Or the one-liner version (prints "Cancelled" and returns true)
|
|
473
|
+
result = Clack.text(message: "Name?")
|
|
474
|
+
exit 1 if Clack.handle_cancel(result)
|
|
475
|
+
|
|
476
|
+
# With a custom message
|
|
477
|
+
exit 1 if Clack.handle_cancel(result, "Aborted by user")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Validation and Transforms
|
|
483
|
+
|
|
484
|
+
Every prompt supports `validate:` and `transform:`. The pipeline looks like this:
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
User Input --> Validation (raw) --> Transform (if valid) --> Final Value
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Validation returns an error message, a `Clack::Warning`, or `nil` to pass.
|
|
491
|
+
|
|
492
|
+
**Validation results:**
|
|
493
|
+
- `nil` or `false` -- passes validation
|
|
494
|
+
- String -- shows error (red), user must fix input
|
|
495
|
+
- `Clack::Warning.new(message)` -- shows warning (yellow), user can confirm with Enter or edit
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
# Symbol shortcuts (clean and idiomatic)
|
|
499
|
+
name = Clack.text(message: "Name?", transform: :strip)
|
|
500
|
+
code = Clack.text(message: "Code?", transform: :upcase)
|
|
501
|
+
|
|
502
|
+
# Chain multiple transforms
|
|
503
|
+
username = Clack.text(
|
|
504
|
+
message: "Username?",
|
|
505
|
+
transform: Clack::Transformers.chain(:strip, :downcase)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Combine validation and transform
|
|
509
|
+
amount = Clack.text(
|
|
510
|
+
message: "Amount?",
|
|
511
|
+
validate: ->(v) { "Must be a number" unless v.match?(/\A\d+\z/) },
|
|
512
|
+
transform: :to_integer
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Warning validation (soft failure -- user can confirm or edit)
|
|
516
|
+
file = Clack.text(
|
|
517
|
+
message: "Output file?",
|
|
518
|
+
validate: ->(v) { Clack::Warning.new("File exists. Overwrite?") if File.exist?(v) }
|
|
519
|
+
)
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Built-in Validators
|
|
523
|
+
|
|
524
|
+
```ruby
|
|
525
|
+
Clack::Validators.required # Non-empty input
|
|
526
|
+
Clack::Validators.min_length(3) # Minimum character count
|
|
527
|
+
Clack::Validators.max_length(100) # Maximum character count
|
|
528
|
+
Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
|
|
529
|
+
Clack::Validators.email # Email format (user@host.tld)
|
|
530
|
+
Clack::Validators.url # URL format (http/https)
|
|
531
|
+
Clack::Validators.integer # Integer string ("-5", "42")
|
|
532
|
+
Clack::Validators.in_range(1..100) # Numeric range (parses as int)
|
|
533
|
+
Clack::Validators.one_of(%w[a b c]) # Allowlist check
|
|
534
|
+
Clack::Validators.path_exists # File/dir exists on disk
|
|
535
|
+
Clack::Validators.directory_exists # Directory exists on disk
|
|
536
|
+
Clack::Validators.future_date # Date strictly after today
|
|
537
|
+
Clack::Validators.past_date # Date strictly before today
|
|
538
|
+
Clack::Validators.date_range(min: d1, max: d2) # Date within range
|
|
539
|
+
Clack::Validators.combine(v1, v2) # First error/warning wins
|
|
540
|
+
|
|
541
|
+
# Warning validators -- allow user to confirm or edit
|
|
542
|
+
Clack::Validators.file_exists_warning # For file overwrite confirmations
|
|
543
|
+
Clack::Validators.as_warning(validator) # Convert any validator to warning
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Built-in Transformers
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
:strip / :trim # Remove leading/trailing whitespace
|
|
550
|
+
:downcase / :upcase # Change case
|
|
551
|
+
:capitalize # "hello world" -> "Hello world"
|
|
552
|
+
:titlecase # "hello world" -> "Hello World"
|
|
553
|
+
:squish # Collapse whitespace to single spaces
|
|
554
|
+
:compact # Remove all whitespace
|
|
555
|
+
:to_integer # Parse as integer
|
|
556
|
+
:to_float # Parse as float
|
|
557
|
+
:digits_only # Extract only digits
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
529
562
|
## Prompt Groups
|
|
530
563
|
|
|
531
|
-
Chain multiple prompts and collect results in a hash.
|
|
564
|
+
Chain multiple prompts and collect results in a hash. If the user cancels any prompt, the whole group returns `Clack::CANCEL`.
|
|
532
565
|
|
|
533
566
|
```ruby
|
|
534
567
|
result = Clack.group do |g|
|
|
@@ -550,9 +583,11 @@ Clack.group(on_cancel: ->(r) { cleanup(r) }) do |g|
|
|
|
550
583
|
end
|
|
551
584
|
```
|
|
552
585
|
|
|
553
|
-
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## Pretty Printing
|
|
554
589
|
|
|
555
|
-
|
|
590
|
+
### Logging
|
|
556
591
|
|
|
557
592
|
```ruby
|
|
558
593
|
Clack.log.info("Starting build...")
|
|
@@ -579,9 +614,9 @@ success = Clack.stream.command("npm install", type: :info)
|
|
|
579
614
|
Clack.stream.success(io_stream)
|
|
580
615
|
```
|
|
581
616
|
|
|
582
|
-
|
|
617
|
+
### Note
|
|
583
618
|
|
|
584
|
-
Display important information in a box
|
|
619
|
+
Display important information in a box:
|
|
585
620
|
|
|
586
621
|
```ruby
|
|
587
622
|
Clack.note(<<~MSG, title: "Next Steps")
|
|
@@ -593,7 +628,7 @@ MSG
|
|
|
593
628
|
|
|
594
629
|
### Box
|
|
595
630
|
|
|
596
|
-
Render a customizable bordered box
|
|
631
|
+
Render a customizable bordered box:
|
|
597
632
|
|
|
598
633
|
```ruby
|
|
599
634
|
Clack.box("Hello, World!", title: "Greeting")
|
|
@@ -611,7 +646,7 @@ Clack.box(
|
|
|
611
646
|
|
|
612
647
|
### Task Log
|
|
613
648
|
|
|
614
|
-
Streaming log that clears on success and shows full output on failure.
|
|
649
|
+
Streaming log that clears on success and shows full output on failure. Great for build output:
|
|
615
650
|
|
|
616
651
|
```ruby
|
|
617
652
|
tl = Clack.task_log(title: "Building...", limit: 10)
|
|
@@ -626,6 +661,8 @@ tl.success("Build complete!")
|
|
|
626
661
|
# tl.error("Build failed!")
|
|
627
662
|
```
|
|
628
663
|
|
|
664
|
+
---
|
|
665
|
+
|
|
629
666
|
## Session Markers
|
|
630
667
|
|
|
631
668
|
```ruby
|
|
@@ -637,9 +674,9 @@ Clack.outro("Done!") # └ Done!
|
|
|
637
674
|
Clack.cancel("Aborted") # └ Aborted (red)
|
|
638
675
|
```
|
|
639
676
|
|
|
640
|
-
|
|
677
|
+
---
|
|
641
678
|
|
|
642
|
-
|
|
679
|
+
## Configuration
|
|
643
680
|
|
|
644
681
|
```ruby
|
|
645
682
|
# Add custom key bindings (merged with defaults)
|
|
@@ -657,6 +694,8 @@ When CI mode is active, prompts immediately submit with their default values ins
|
|
|
657
694
|
|
|
658
695
|
Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals.
|
|
659
696
|
|
|
697
|
+
---
|
|
698
|
+
|
|
660
699
|
## Testing
|
|
661
700
|
|
|
662
701
|
Clack ships with first-class test helpers. Require `clack/testing` explicitly (it is not auto-loaded):
|
|
@@ -692,12 +731,40 @@ The `PromptDriver` yielded to the block provides these methods:
|
|
|
692
731
|
| `ctrl_d` | Press Ctrl+D (submit multiline text) |
|
|
693
732
|
| `key(sym_or_char)` | Press an arbitrary key by symbol (e.g. `:escape`) or raw character |
|
|
694
733
|
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Try It
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
ruby examples/full_demo.rb
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
<details>
|
|
743
|
+
<summary>Recording the demo GIF</summary>
|
|
744
|
+
|
|
745
|
+
Requires [asciinema](https://asciinema.org/), [agg](https://github.com/asciinema/agg), and [expect](https://core.tcl-lang.org/expect/index):
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
# Record the demo (automated via expect script)
|
|
749
|
+
asciinema rec examples/demo.cast --command "expect examples/demo.exp" --overwrite -q
|
|
750
|
+
|
|
751
|
+
# Split batched frames to show typing (Ruby buffers terminal output)
|
|
752
|
+
ruby examples/split_cast.rb
|
|
753
|
+
|
|
754
|
+
# Convert to GIF
|
|
755
|
+
agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --speed 0.6
|
|
756
|
+
```
|
|
757
|
+
</details>
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
695
761
|
## Requirements
|
|
696
762
|
|
|
697
763
|
- Ruby 3.2+
|
|
698
764
|
- No runtime dependencies
|
|
765
|
+
- Unicode terminal recommended (ASCII fallbacks included)
|
|
699
766
|
|
|
700
|
-
|
|
767
|
+
### Development
|
|
701
768
|
|
|
702
769
|
```bash
|
|
703
770
|
bundle install
|
|
@@ -706,9 +773,11 @@ bundle exec rake spec # Tests only
|
|
|
706
773
|
COVERAGE=true bundle exec rake spec # With coverage
|
|
707
774
|
```
|
|
708
775
|
|
|
709
|
-
|
|
776
|
+
### Roadmap
|
|
777
|
+
|
|
778
|
+
- **Wizard mode** -- Multi-step flows with back navigation (`Clack.wizard`). The current `Clack.group` runs 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.
|
|
710
779
|
|
|
711
|
-
|
|
780
|
+
---
|
|
712
781
|
|
|
713
782
|
## Credits
|
|
714
783
|
|
|
@@ -716,4 +785,4 @@ This is a Ruby port of [Clack](https://github.com/bombshell-dev/clack), created
|
|
|
716
785
|
|
|
717
786
|
## License
|
|
718
787
|
|
|
719
|
-
MIT
|
|
788
|
+
MIT -- See [LICENSE](LICENSE)
|