clack 0.4.6 → 0.6.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 +38 -0
- data/README.md +267 -197
- data/lib/clack/box.rb +6 -6
- data/lib/clack/core/fuzzy_matcher.rb +3 -3
- data/lib/clack/core/key_reader.rb +30 -20
- data/lib/clack/core/options_helper.rb +96 -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/note.rb +4 -3
- data/lib/clack/prompts/autocomplete.rb +21 -15
- data/lib/clack/prompts/autocomplete_multiselect.rb +19 -26
- data/lib/clack/prompts/confirm.rb +8 -30
- data/lib/clack/prompts/date.rb +1 -14
- data/lib/clack/prompts/group_multiselect.rb +48 -67
- data/lib/clack/prompts/multiline_text.rb +33 -53
- data/lib/clack/prompts/multiselect.rb +27 -38
- data/lib/clack/prompts/password.rb +1 -14
- data/lib/clack/prompts/path.rb +9 -23
- data/lib/clack/prompts/progress.rb +6 -2
- data/lib/clack/prompts/range.rb +1 -14
- data/lib/clack/prompts/select.rb +18 -32
- data/lib/clack/prompts/select_key.rb +8 -8
- data/lib/clack/prompts/spinner.rb +15 -20
- data/lib/clack/prompts/tasks.rb +6 -1
- data/lib/clack/prompts/text.rb +1 -14
- data/lib/clack/testing.rb +31 -37
- data/lib/clack/utils.rb +106 -22
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +71 -37
- metadata +3 -3
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,17 +228,18 @@ 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
|
-
# Custom filter logic (receives option
|
|
241
|
+
# Custom filter logic (receives the option and query string).
|
|
242
|
+
# The option is a value object; opt.label and opt[:label] both work.
|
|
308
243
|
cmd = Clack.autocomplete(
|
|
309
244
|
message: "Select command",
|
|
310
245
|
options: commands,
|
|
@@ -312,11 +247,11 @@ cmd = Clack.autocomplete(
|
|
|
312
247
|
)
|
|
313
248
|
```
|
|
314
249
|
|
|
315
|
-
> Vim-style `j`/`k`/`h`/`l` navigation is not available
|
|
250
|
+
> 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
251
|
|
|
317
252
|
### Autocomplete Multiselect
|
|
318
253
|
|
|
319
|
-
Type
|
|
254
|
+
Type-to-filter with multi-selection support.
|
|
320
255
|
|
|
321
256
|
```ruby
|
|
322
257
|
colors = Clack.autocomplete_multiselect(
|
|
@@ -325,17 +260,17 @@ colors = Clack.autocomplete_multiselect(
|
|
|
325
260
|
placeholder: "Type to filter...",
|
|
326
261
|
required: true, # At least one selection required
|
|
327
262
|
initial_values: ["red"], # Pre-selected values
|
|
328
|
-
max_items: 5 # Default; scrollable via
|
|
263
|
+
max_items: 5 # Default; scrollable via up/down arrows
|
|
329
264
|
)
|
|
330
265
|
```
|
|
331
266
|
|
|
332
267
|
**Shortcuts:** `Space` toggle | `Enter` confirm
|
|
333
268
|
|
|
334
|
-
> The `a` (select all), `i` (invert), and `j`/`k`/`h`/`l` shortcuts from
|
|
269
|
+
> 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
270
|
|
|
336
271
|
### Path
|
|
337
272
|
|
|
338
|
-
|
|
273
|
+
Filesystem navigation with Tab completion and arrow key selection.
|
|
339
274
|
|
|
340
275
|
```ruby
|
|
341
276
|
project_dir = Clack.path(
|
|
@@ -345,7 +280,7 @@ project_dir = Clack.path(
|
|
|
345
280
|
)
|
|
346
281
|
```
|
|
347
282
|
|
|
348
|
-
**Navigation:** Type to filter | `Tab` to autocomplete |
|
|
283
|
+
**Navigation:** Type to filter | `Tab` to autocomplete | up/down arrows to select
|
|
349
284
|
|
|
350
285
|
### Date
|
|
351
286
|
|
|
@@ -362,11 +297,11 @@ date = Clack.date(
|
|
|
362
297
|
)
|
|
363
298
|
```
|
|
364
299
|
|
|
365
|
-
**Navigation:** `Tab
|
|
300
|
+
**Navigation:** `Tab`/left-right arrows between segments | up/down arrows to adjust value | type digits directly
|
|
366
301
|
|
|
367
302
|
### Range
|
|
368
303
|
|
|
369
|
-
|
|
304
|
+
Visual slider for numeric selection.
|
|
370
305
|
|
|
371
306
|
```ruby
|
|
372
307
|
volume = Clack.range(
|
|
@@ -376,11 +311,12 @@ volume = Clack.range(
|
|
|
376
311
|
step: 5,
|
|
377
312
|
initial_value: 50
|
|
378
313
|
)
|
|
314
|
+
# Navigate with arrow keys or hjkl
|
|
379
315
|
```
|
|
380
316
|
|
|
381
317
|
### Select Key
|
|
382
318
|
|
|
383
|
-
|
|
319
|
+
Instant selection via keyboard shortcuts. No arrow key navigation needed.
|
|
384
320
|
|
|
385
321
|
```ruby
|
|
386
322
|
action = Clack.select_key(
|
|
@@ -393,6 +329,34 @@ action = Clack.select_key(
|
|
|
393
329
|
)
|
|
394
330
|
```
|
|
395
331
|
|
|
332
|
+
### Group Multiselect
|
|
333
|
+
|
|
334
|
+
Multiselect with options organized into named categories.
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
features = Clack.group_multiselect(
|
|
338
|
+
message: "Select features",
|
|
339
|
+
options: [
|
|
340
|
+
{
|
|
341
|
+
label: "Frontend",
|
|
342
|
+
options: [
|
|
343
|
+
{ value: "hotwire", label: "Hotwire" },
|
|
344
|
+
{ value: "stimulus", label: "Stimulus" }
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
label: "Background",
|
|
349
|
+
options: [
|
|
350
|
+
{ value: "sidekiq", label: "Sidekiq" },
|
|
351
|
+
{ value: "solid_queue", label: "Solid Queue" }
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
selectable_groups: true, # Toggle all options in a group at once
|
|
356
|
+
group_spacing: 1 # Blank lines between groups
|
|
357
|
+
)
|
|
358
|
+
```
|
|
359
|
+
|
|
396
360
|
### Spinner
|
|
397
361
|
|
|
398
362
|
Non-blocking animated indicator for async work.
|
|
@@ -411,7 +375,7 @@ spinner.stop("Dependencies installed!")
|
|
|
411
375
|
# Or: spinner.cancel("Cancelled")
|
|
412
376
|
```
|
|
413
377
|
|
|
414
|
-
**Block form**
|
|
378
|
+
**Block form** -- wraps a block with automatic success/error handling:
|
|
415
379
|
|
|
416
380
|
```ruby
|
|
417
381
|
result = Clack.spin("Installing dependencies...") { system("npm install") }
|
|
@@ -430,7 +394,7 @@ end
|
|
|
430
394
|
|
|
431
395
|
### Progress
|
|
432
396
|
|
|
433
|
-
|
|
397
|
+
A visual progress bar for measurable operations.
|
|
434
398
|
|
|
435
399
|
```ruby
|
|
436
400
|
progress = Clack.progress(total: 100, message: "Downloading...")
|
|
@@ -446,7 +410,7 @@ progress.stop("Download complete!")
|
|
|
446
410
|
|
|
447
411
|
### Tasks
|
|
448
412
|
|
|
449
|
-
Run multiple tasks with status indicators.
|
|
413
|
+
Run multiple tasks sequentially with status indicators.
|
|
450
414
|
|
|
451
415
|
```ruby
|
|
452
416
|
results = Clack.tasks(tasks: [
|
|
@@ -472,63 +436,133 @@ Clack.tasks(tasks: [
|
|
|
472
436
|
])
|
|
473
437
|
```
|
|
474
438
|
|
|
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
439
|
<details>
|
|
504
|
-
<summary><
|
|
440
|
+
<summary><strong>Quick Reference Table</strong> (click to expand)</summary>
|
|
505
441
|
|
|
506
442
|
| Prompt | Method | Key Options | Defaults |
|
|
507
443
|
|--------|--------|-------------|----------|
|
|
508
|
-
| Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` |
|
|
444
|
+
| Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` | -- |
|
|
509
445
|
| Password | `Clack.password` | `mask:`, `validate:` | `mask: "▪"` |
|
|
510
446
|
| Confirm | `Clack.confirm` | `active:`, `inactive:`, `initial_value:` | `active: "Yes"`, `inactive: "No"`, `initial_value: true` |
|
|
511
|
-
| Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` |
|
|
447
|
+
| Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` | -- |
|
|
512
448
|
| Multiselect | `Clack.multiselect` | `options:`, `initial_values:`, `required:`, `cursor_at:` | `required: true` |
|
|
513
449
|
| Group Multiselect | `Clack.group_multiselect` | `options:` (nested), `selectable_groups:`, `group_spacing:` | `selectable_groups: false` |
|
|
514
450
|
| Autocomplete | `Clack.autocomplete` | `options:`, `placeholder:`, `filter:`, `max_items:` | `max_items: 5` |
|
|
515
451
|
| Autocomplete Multiselect | `Clack.autocomplete_multiselect` | `options:`, `required:`, `initial_values:`, `filter:` | `required: true`, `max_items: 5` |
|
|
516
|
-
| Select Key | `Clack.select_key` | `options:` (with `:key`) |
|
|
452
|
+
| Select Key | `Clack.select_key` | `options:` (with `:key`) | -- |
|
|
517
453
|
| Path | `Clack.path` | `root:`, `only_directories:` | `root: "."` |
|
|
518
454
|
| Date | `Clack.date` | `format:`, `initial_value:`, `min:`, `max:` | `format: :iso` |
|
|
519
455
|
| Range | `Clack.range` | `min:`, `max:`, `step:`, `initial_value:` | `min: 0`, `max: 100`, `step: 1` |
|
|
520
456
|
| Multiline Text | `Clack.multiline_text` | `initial_value:`, `validate:` | Submit with **Ctrl+D** |
|
|
521
|
-
| Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error |
|
|
457
|
+
| Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error | -- |
|
|
522
458
|
| Tasks | `Clack.tasks` | `tasks:` (`{title:, task:, enabled:}`) | `enabled: true` |
|
|
523
|
-
| Progress | `Clack.progress` | `total:`, `message:` |
|
|
459
|
+
| Progress | `Clack.progress` | `total:`, `message:` | -- |
|
|
524
460
|
|
|
525
461
|
All prompts accept `message:`, `validate:`, `help:`, and return `Clack::CANCEL` on Escape/Ctrl+C.
|
|
526
462
|
|
|
527
463
|
</details>
|
|
528
464
|
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Cancellation
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
result = Clack.text(message: "Name?")
|
|
471
|
+
exit 1 if Clack.cancel?(result)
|
|
472
|
+
|
|
473
|
+
# Or the one-liner version (prints "Cancelled" and returns true)
|
|
474
|
+
result = Clack.text(message: "Name?")
|
|
475
|
+
exit 1 if Clack.handle_cancel(result)
|
|
476
|
+
|
|
477
|
+
# With a custom message
|
|
478
|
+
exit 1 if Clack.handle_cancel(result, "Aborted by user")
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Validation and Transforms
|
|
484
|
+
|
|
485
|
+
Every prompt supports `validate:` and `transform:`. The pipeline looks like this:
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
User Input --> Validation (raw) --> Transform (if valid) --> Final Value
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Validation returns an error message, a `Clack::Warning`, or `nil` to pass.
|
|
492
|
+
|
|
493
|
+
**Validation results:**
|
|
494
|
+
- `nil` or `false` -- passes validation
|
|
495
|
+
- String -- shows error (red), user must fix input
|
|
496
|
+
- `Clack::Warning.new(message)` -- shows warning (yellow), user can confirm with Enter or edit
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
# Symbol shortcuts (clean and idiomatic)
|
|
500
|
+
name = Clack.text(message: "Name?", transform: :strip)
|
|
501
|
+
code = Clack.text(message: "Code?", transform: :upcase)
|
|
502
|
+
|
|
503
|
+
# Chain multiple transforms
|
|
504
|
+
username = Clack.text(
|
|
505
|
+
message: "Username?",
|
|
506
|
+
transform: Clack::Transformers.chain(:strip, :downcase)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Combine validation and transform
|
|
510
|
+
amount = Clack.text(
|
|
511
|
+
message: "Amount?",
|
|
512
|
+
validate: ->(v) { "Must be a number" unless v.match?(/\A\d+\z/) },
|
|
513
|
+
transform: :to_integer
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Warning validation (soft failure -- user can confirm or edit)
|
|
517
|
+
file = Clack.text(
|
|
518
|
+
message: "Output file?",
|
|
519
|
+
validate: ->(v) { Clack::Warning.new("File exists. Overwrite?") if File.exist?(v) }
|
|
520
|
+
)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Built-in Validators
|
|
524
|
+
|
|
525
|
+
```ruby
|
|
526
|
+
Clack::Validators.required # Non-empty input
|
|
527
|
+
Clack::Validators.min_length(3) # Minimum character count
|
|
528
|
+
Clack::Validators.max_length(100) # Maximum character count
|
|
529
|
+
Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
|
|
530
|
+
Clack::Validators.email # Email format (user@host.tld)
|
|
531
|
+
Clack::Validators.url # URL format (http/https)
|
|
532
|
+
Clack::Validators.integer # Integer string ("-5", "42")
|
|
533
|
+
Clack::Validators.in_range(1..100) # Numeric range (parses as int)
|
|
534
|
+
Clack::Validators.one_of(%w[a b c]) # Allowlist check
|
|
535
|
+
Clack::Validators.path_exists # File/dir exists on disk
|
|
536
|
+
Clack::Validators.directory_exists # Directory exists on disk
|
|
537
|
+
Clack::Validators.future_date # Date strictly after today
|
|
538
|
+
Clack::Validators.past_date # Date strictly before today
|
|
539
|
+
Clack::Validators.date_range(min: d1, max: d2) # Date within range
|
|
540
|
+
Clack::Validators.combine(v1, v2) # First error/warning wins
|
|
541
|
+
|
|
542
|
+
# Warning validators -- allow user to confirm or edit
|
|
543
|
+
Clack::Validators.file_exists_warning # For file overwrite confirmations
|
|
544
|
+
Clack::Validators.as_warning(validator) # Convert any validator to warning
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Built-in Transformers
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
:strip / :trim # Remove leading/trailing whitespace
|
|
551
|
+
:downcase / :upcase # Change case
|
|
552
|
+
:capitalize # "hello world" -> "Hello world"
|
|
553
|
+
:titlecase # "hello world" -> "Hello World"
|
|
554
|
+
:squish # Collapse whitespace to single spaces
|
|
555
|
+
:compact # Remove all whitespace
|
|
556
|
+
:to_integer # Parse as integer
|
|
557
|
+
:to_float # Parse as float
|
|
558
|
+
:digits_only # Extract only digits
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
529
563
|
## Prompt Groups
|
|
530
564
|
|
|
531
|
-
Chain multiple prompts and collect results in a hash.
|
|
565
|
+
Chain multiple prompts and collect results in a hash. If the user cancels any prompt, the whole group returns `Clack::CANCEL`.
|
|
532
566
|
|
|
533
567
|
```ruby
|
|
534
568
|
result = Clack.group do |g|
|
|
@@ -550,9 +584,11 @@ Clack.group(on_cancel: ->(r) { cleanup(r) }) do |g|
|
|
|
550
584
|
end
|
|
551
585
|
```
|
|
552
586
|
|
|
553
|
-
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Pretty Printing
|
|
554
590
|
|
|
555
|
-
|
|
591
|
+
### Logging
|
|
556
592
|
|
|
557
593
|
```ruby
|
|
558
594
|
Clack.log.info("Starting build...")
|
|
@@ -579,9 +615,9 @@ success = Clack.stream.command("npm install", type: :info)
|
|
|
579
615
|
Clack.stream.success(io_stream)
|
|
580
616
|
```
|
|
581
617
|
|
|
582
|
-
|
|
618
|
+
### Note
|
|
583
619
|
|
|
584
|
-
Display important information in a box
|
|
620
|
+
Display important information in a box:
|
|
585
621
|
|
|
586
622
|
```ruby
|
|
587
623
|
Clack.note(<<~MSG, title: "Next Steps")
|
|
@@ -593,7 +629,7 @@ MSG
|
|
|
593
629
|
|
|
594
630
|
### Box
|
|
595
631
|
|
|
596
|
-
Render a customizable bordered box
|
|
632
|
+
Render a customizable bordered box:
|
|
597
633
|
|
|
598
634
|
```ruby
|
|
599
635
|
Clack.box("Hello, World!", title: "Greeting")
|
|
@@ -611,7 +647,7 @@ Clack.box(
|
|
|
611
647
|
|
|
612
648
|
### Task Log
|
|
613
649
|
|
|
614
|
-
Streaming log that clears on success and shows full output on failure.
|
|
650
|
+
Streaming log that clears on success and shows full output on failure. Great for build output:
|
|
615
651
|
|
|
616
652
|
```ruby
|
|
617
653
|
tl = Clack.task_log(title: "Building...", limit: 10)
|
|
@@ -626,6 +662,8 @@ tl.success("Build complete!")
|
|
|
626
662
|
# tl.error("Build failed!")
|
|
627
663
|
```
|
|
628
664
|
|
|
665
|
+
---
|
|
666
|
+
|
|
629
667
|
## Session Markers
|
|
630
668
|
|
|
631
669
|
```ruby
|
|
@@ -637,9 +675,9 @@ Clack.outro("Done!") # └ Done!
|
|
|
637
675
|
Clack.cancel("Aborted") # └ Aborted (red)
|
|
638
676
|
```
|
|
639
677
|
|
|
640
|
-
|
|
678
|
+
---
|
|
641
679
|
|
|
642
|
-
|
|
680
|
+
## Configuration
|
|
643
681
|
|
|
644
682
|
```ruby
|
|
645
683
|
# Add custom key bindings (merged with defaults)
|
|
@@ -657,6 +695,8 @@ When CI mode is active, prompts immediately submit with their default values ins
|
|
|
657
695
|
|
|
658
696
|
Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals.
|
|
659
697
|
|
|
698
|
+
---
|
|
699
|
+
|
|
660
700
|
## Testing
|
|
661
701
|
|
|
662
702
|
Clack ships with first-class test helpers. Require `clack/testing` explicitly (it is not auto-loaded):
|
|
@@ -692,12 +732,40 @@ The `PromptDriver` yielded to the block provides these methods:
|
|
|
692
732
|
| `ctrl_d` | Press Ctrl+D (submit multiline text) |
|
|
693
733
|
| `key(sym_or_char)` | Press an arbitrary key by symbol (e.g. `:escape`) or raw character |
|
|
694
734
|
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Try It
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
ruby examples/full_demo.rb
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
<details>
|
|
744
|
+
<summary>Recording the demo GIF</summary>
|
|
745
|
+
|
|
746
|
+
Requires [asciinema](https://asciinema.org/), [agg](https://github.com/asciinema/agg), and [expect](https://core.tcl-lang.org/expect/index):
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
# Record the demo (automated via expect script)
|
|
750
|
+
asciinema rec examples/demo.cast --command "expect examples/demo.exp" --overwrite -q
|
|
751
|
+
|
|
752
|
+
# Split batched frames to show typing (Ruby buffers terminal output)
|
|
753
|
+
ruby examples/split_cast.rb
|
|
754
|
+
|
|
755
|
+
# Convert to GIF
|
|
756
|
+
agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --speed 0.6
|
|
757
|
+
```
|
|
758
|
+
</details>
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
695
762
|
## Requirements
|
|
696
763
|
|
|
697
764
|
- Ruby 3.2+
|
|
698
765
|
- No runtime dependencies
|
|
766
|
+
- Unicode terminal recommended (ASCII fallbacks included)
|
|
699
767
|
|
|
700
|
-
|
|
768
|
+
### Development
|
|
701
769
|
|
|
702
770
|
```bash
|
|
703
771
|
bundle install
|
|
@@ -706,9 +774,11 @@ bundle exec rake spec # Tests only
|
|
|
706
774
|
COVERAGE=true bundle exec rake spec # With coverage
|
|
707
775
|
```
|
|
708
776
|
|
|
709
|
-
|
|
777
|
+
### Roadmap
|
|
778
|
+
|
|
779
|
+
- **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
780
|
|
|
711
|
-
|
|
781
|
+
---
|
|
712
782
|
|
|
713
783
|
## Credits
|
|
714
784
|
|
|
@@ -716,4 +786,4 @@ This is a Ruby port of [Clack](https://github.com/bombshell-dev/clack), created
|
|
|
716
786
|
|
|
717
787
|
## License
|
|
718
788
|
|
|
719
|
-
MIT
|
|
789
|
+
MIT -- See [LICENSE](LICENSE)
|