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.
data/README.md CHANGED
@@ -1,29 +1,65 @@
1
- # Clack
2
-
3
- **Effortlessly beautiful CLI prompts for Ruby.**
4
-
5
- A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack). 16+ prompt types, zero dependencies, Ruby 3.2+. `gem "clack"` and go.
1
+ <p align="center">
2
+ <br>
3
+ <br>
4
+ <code>&nbsp;C&nbsp;L&nbsp;A&nbsp;C&nbsp;K&nbsp;</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
- - **Beautiful by default** — Thoughtfully designed prompts that just look right
14
- - **Vim-friendly** — Navigate with `hjkl` or arrow keys
15
- - **Accessible** — Graceful ASCII fallbacks for limited terminals
16
- - **Composable** Group prompts together with `Clack.group`
17
- - **Zero dependencies** — Everything in one gem
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 "project-setup"
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
- ## Demo
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 they pressed Escape/Ctrl+C.
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** - press `Tab` to fill the longest common prefix of matching candidates:
132
+ **Tab completion** -- press `Tab` to fill the longest common prefix of matching candidates:
203
133
 
204
134
  ```ruby
205
- # Tab completion from a static list
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 tab completion
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
- Multi-line text input. Enter inserts a newline, Ctrl+D submits.
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
- Single selection with keyboard navigation.
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
- Multiple selections with toggle controls.
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 from a list of options. Filtering uses **fuzzy matching** by default -- characters must appear in order but don't need to be consecutive (e.g. "fb" matches "foobar"). Pass `filter:` to override with custom logic.
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 hash and query string)
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 all keyboard input feeds into the search field. Use `↑↓` arrow keys to navigate results.
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 to filter with multi-selection support.
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 `Multiselect` are not available here all keyboard input feeds into the search field instead. Use `↑↓` arrow keys to navigate.
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
- File/directory path selector with filesystem navigation.
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 | `↑↓` to select
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`/`←→` between segments | `↑↓` adjust value | type digits directly
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
- Numeric selection with a visual slider track. Navigate with `←→` or `↑↓` arrow keys (or `hjkl`).
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
- Quick selection using keyboard shortcuts.
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** - wraps a block with automatic success/error handling:
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
- Visual progress bar for measurable operations.
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><h3 style="display:inline">Quick Reference</h3></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. Cancellation is handled automatically.
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
- ## Logging
587
+ ---
588
+
589
+ ## Pretty Printing
554
590
 
555
- Beautiful, consistent log messages.
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
- ## Note
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. Useful for build output.
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
- ## Configuration
678
+ ---
641
679
 
642
- Customize key bindings and display options globally:
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
- ## Development
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
- ## Roadmap
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
- - **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.
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 - See [LICENSE](LICENSE)
789
+ MIT -- See [LICENSE](LICENSE)