clack 0.6.2 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 731bb8423776790866c97bc0a19e5e20c872113a6bdde1d0b631991c7d96257d
4
- data.tar.gz: 892738ddc556225ce2b85db29bc845b0a2665a05b3492d0f98392612247db0ca
3
+ metadata.gz: fa10e11143746ae1c817d66c8a10b8c0a3a261d8379bd29e71c1134b27560bbc
4
+ data.tar.gz: 925aad6ecb7cf502f56ab9584de97c7b23462121881232b76b09214ecabf7fed
5
5
  SHA512:
6
- metadata.gz: 71463cbd46a0aa1a99a15853df6b840e96dbffa9a44e32a3370e89dcb98b4ff9cc0e1b427edbe67f034fe4b6f794b5030c4fa29f5f4e3a174899e37ee6cefae7
7
- data.tar.gz: bae64d320b7ad71ee2db3ef3c49a7c97c041d76edf14996f0fb4fd4bf106edb266f8280d5f211f54692b0356d2cab12c8624c811e8c0ee726e25d6ecf3c14071
6
+ metadata.gz: ef8452b1824f9df76ffbd3b5d4f8ef35d9be69e83265369171caa5b60c95938f3b0f09b40bdd687d0bf07e555a52abac17a4ef0f1674271f2b9ed27297fb9bca
7
+ data.tar.gz: f5818dfc7b664ef7d25852671c8c3ec4778fca5131f9731fe6bb9be67daa3788d4572c1ab264d9401c1fc60537ebc21c6a4f46a5f0a338e99de73a5eff0cb7ef
data/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0] - 2026-08-29
4
+
5
+ ### Added
6
+ - `with_guide: false` is now honored everywhere (it was a documented no-op): all prompts, `intro`/`outro`/`cancel`/`handle_cancel`, `log`, `stream`, `note`, `box`, `spinner`/`spin`, `tasks`, and `task_log`. Every prompt and message helper (except `progress`, which never drew a rail) also accepts a per-call `with_guide:` override, matching upstream `withGuide`
7
+ - Keyboard hint footer for `select` (`↑/↓ to navigate • Enter: confirm`), `autocomplete`, and `path`, using upstream wording; `show_instructions: false` per prompt and `Clack.update_settings(show_instructions: false)` hide it; `instructions:` (a String or an Array of Strings) replaces the text on any prompt, e.g. for localized CLIs
8
+ - `Clack.settings` gains `:show_instructions` (default `true`); `Clack::Core::Settings.with_guide?` and the new `show_instructions?` take an optional per-call override
9
+ - Option-based prompts (`select`, `multiselect`, `autocomplete`, `autocomplete_multiselect`, `select_key`) accept a Hash for `options:`: keys are the values, Hash values are labels or `{label:, hint:, disabled:}` specs (`{pg: "PostgreSQL"}` returns `:pg`)
10
+ - `group_multiselect` accepts a Hash of `group label => options`, matching upstream's shape; each group's options accept the same `value => label` Hash
11
+ - `validate:` now accepts a Regexp (`validate: /\A[a-z]+\z/`, fails with "Invalid format"), a built-in validator Symbol (`validate: :email`), an Array of validators (combined, first failure wins), a Hash of `Regexp`/`Symbol => message` (a `Clack::Warning` message makes the entry a soft check), or any object responding to `#call`. Resolved by the new `Clack::Validators.resolve`, mirroring `Transformers.resolve`
12
+ - `Clack::Validators.combine` and `Clack::Validators.as_warning` accept the same shapes (`combine(:required, /\A\d+\z/)`, `as_warning(:email)`)
13
+ - `Clack::Validators::SHORTCUTS` lists the built-ins addressable by Symbol
14
+ - `confirm` accepts `vertical: true` to stack the Yes/No options on separate lines, matching upstream `@clack/prompts` 1.0.1
15
+ - `select_key` accepts `case_sensitive: true` to match option keys exactly, matching upstream 1.0.1; by default `Y` and `y` are still interchangeable
16
+ - `select_key` accepts `initial_value:` to highlight a default option; Enter submits it and CI mode returns it instead of `nil` (an unknown value warns on stderr and is ignored, like `initial_values` elsewhere)
17
+ - Spinner `cancel_message:`, `error_message:`, and `on_cancel:` options, matching upstream `cancelMessage`/`errorMessage`/`onCancel`
18
+ - `Clack.update_settings(messages: {cancel:, error:})` to localize the cancel/error text used by spinners and `Clack.handle_cancel`
19
+ - `Spinner#running?`
20
+ - `Clack::NotATerminalError` (an `IOError`) with an actionable message is raised when a prompt reads from a piped or redirected stdin with CI mode off, replacing a bare `Errno::ENOTTY` from deep inside the key reader
21
+ - `Clack::Core::Settings::KEY_HOME` / `KEY_END` canonical key codes and `Settings.normalize_key`
22
+
23
+ ### Changed
24
+ - The guide rail next to the active step is now cyan (the upstream look); the connector above it stays gray and error/warning rails stay yellow. The `help:` line follows the same rule (it was gray in every state), and option rows in `select`, `select_key`, `autocomplete`, and `path` now turn yellow on a validation error too (they stayed gray before). Color only: nothing changes when colors are disabled
25
+ - `select`, `autocomplete`, and `path` frames gain one footer line of keyboard hints by default; snapshot tests of their output need updating (`show_instructions: false` restores the old line count)
26
+ - `multiselect`, `group_multiselect`, and `autocomplete_multiselect` footers use the upstream wording and separator (`↑/↓ to navigate • Space: select • Enter: confirm`, ASCII `up/down` and ` | `) instead of three different dialects; `autocomplete_multiselect` no longer shows its hint line while an error or warning is displayed (the other two already hid it). `multiselect`'s `a`/`i` shortcuts still work but are documented in the README instead of the footer
27
+ - `box` renders inside the guide gutter (a gray `│ ` on each line) when guides are on, matching upstream
28
+ - `Spinner#cancel` and `Spinner#error` called without a message now show the configured cancel/error message ("Cancelled" / "Something went wrong") instead of echoing the in-progress text
29
+ - A spinner still running when the process exits (`exit`, Ctrl+C, an uncaught exception) now prints its cancel line (or error line) before the `at_exit` blocks your program registered earlier, instead of leaving a half-drawn frame
30
+ - `ci_mode: :auto` checks the prompt's own `input:` stream instead of `$stdin`, so a `StringIO` or other non-TTY input auto-submits defaults
31
+ - Escape sequences are parsed up to their CSI final byte, so a key typed right after an arrow key is no longer swallowed into the sequence
32
+ - Key codes are normalized before alias lookup, so custom `aliases:` must be keyed on the canonical codes (`"\e[A"`, `Settings::KEY_HOME`); the SS3 and tilde variants (`\eOA`, `\e[1~`) are folded before the lookup and cannot be aliased directly
33
+
34
+ ### Fixed
35
+ - Passing a Hash as `options:` no longer returns `[key, label]` pairs as values (`select`, `multiselect`, `autocomplete`) or raises `TypeError` (`select_key`, `group_multiselect`)
36
+ - `group_multiselect` raises `ArgumentError` naming the group when a group's `options:` is not an option list (`nil`, a String, an Integer), instead of `NoMethodError`
37
+ - `group_multiselect` now renders `Clack.warning` validation results; previously the first Enter looked dead and the second submitted without ever showing the message
38
+ - `validate:` given an unsupported value (an Integer, an unknown Symbol) now raises `ArgumentError` when the prompt is constructed, instead of `NoMethodError` at submit time after the user has typed
39
+ - `Validators.combine` no longer crashes at call time on `nil` entries (they are skipped)
40
+ - `confirm` coerces `initial_value:` to a Boolean, so `initial_value: nil` returns `false` on Enter instead of `nil` (interactive and CI mode), and truthy non-Boolean values return `true`
41
+ - `select_key` now honors `validate:` and `transform:` (they were accepted and silently ignored) and renders validation errors and warnings in its footer like every other prompt
42
+ - `select_key` no longer crashes on the first keypress when an option's `key:` is not a String
43
+ - CI mode confirms a `Clack.warning` validation result the way a second Enter does, so `transform:` is applied; previously the untransformed value was returned with the prompt left in `:warning` state
44
+ - `range` with a fractional `step` (0.1, 0.05, ...) now snaps exactly instead of accumulating Float error, so three 0.1 steps return and display `0.3` rather than `0.30000000000000004`; an `initial_value` exactly halfway between two steps now rounds up to the next step (`0.15` on a `0.1` grid is `0.2`)
45
+ - `Clack.spin` no longer leaves the animation thread running (repainting over later output) when the block calls `exit`, raises `Interrupt` or a non-`StandardError`, or exits via `break`/`throw`; it prints the final line and re-raises
46
+ - `Clack.tasks` finishes its spinner when a task calls `exit`, raises `Interrupt`, or throws
47
+ - Arrow keys in application cursor mode (tmux, after vim/less) no longer type a literal `A`/`B`/`C`/`D`: SS3 sequences (`\eOA`) are assembled and normalized to `\e[A`
48
+ - Home/End variants (`\e[H`, `\eOH`, `\e[1~`, `\e[7~` and the End equivalents) are normalized to one code per key
49
+ - End of input on a piped or `StringIO` input now cancels the prompt instead of spinning the run loop at 100% CPU
50
+ - `FORCE_COLOR=0` (and `FORCE_COLOR=false`) now disables colors, cursor sequences and Unicode symbols instead of forcing them on
51
+ - An empty `NO_COLOR=` is treated as unset, per no-color.org; an empty `FORCE_COLOR=` is likewise ignored
52
+ - Redrawing a frame whose lines soft-wrap in a narrow terminal (for example the keyboard hint footer below about 51 columns) no longer leaves orphaned rail lines above the prompt
53
+ - A child created with a block-less `fork` inside `Clack.spin` no longer prints the parent's cancel line or runs `on_cancel` when it exits
54
+
55
+ ### Docs
56
+ - README gains Troubleshooting, Validation Shorthands, Environment variables (`NO_COLOR`, `FORCE_COLOR`, `CLACK_UNICODE`, `CLACK_ESCAPE_TIMEOUT`), Guide rail, and Keyboard hints sections; ARCHITECTURE.md input, color, symbol, spinner, and chrome descriptions now match the code. The README screenshots predate the cyan rail and the `select` footer
57
+
3
58
  ## [0.6.2] - 2026-08-13
4
59
 
5
60
  ### Added
data/README.md CHANGED
@@ -115,6 +115,8 @@ Clack.outro "You're all set!"
115
115
 
116
116
  All prompts return the user's input, or `Clack::CANCEL` if the user pressed Escape/Ctrl+C.
117
117
 
118
+ > The demo GIF and the screenshots below were recorded before 0.7.0, so they still show a gray rail next to the active step and no keyboard hint footer under `select`. See [Guide rail](#guide-rail) and [Keyboard hints](#keyboard-hints) for the current look.
119
+
118
120
  ### Text
119
121
 
120
122
  Single-line text input with placeholders, defaults, validation, and tab completion.
@@ -188,9 +190,27 @@ proceed = Clack.confirm(
188
190
  )
189
191
  ```
190
192
 
193
+ Long labels read better stacked. Pass `vertical: true` to put each option on its own line (up/down, left/right, and `y`/`n` all still work):
194
+
195
+ ```ruby
196
+ Clack.confirm(
197
+ message: "Overwrite ~/.zshrc?",
198
+ active: "Yes, back it up and replace it",
199
+ inactive: "No, keep my existing file",
200
+ initial_value: false,
201
+ vertical: true
202
+ )
203
+ # ◆ Overwrite ~/.zshrc?
204
+ # │ ○ Yes, back it up and replace it
205
+ # │ ● No, keep my existing file
206
+ # └
207
+ ```
208
+
209
+ `initial_value:` is coerced to a Boolean, so `nil` (for example an unset CLI flag) starts on "No" and the prompt returns `true`, `false`, or `Clack::CANCEL` (unless you pass a `transform:`).
210
+
191
211
  ### Select
192
212
 
193
- Pick one from a list. Navigate with arrow keys or `hjkl`.
213
+ Pick one from a list. Navigate with arrow keys or `hjkl`. A footer of keyboard hints (key names dimmed) lists the keys (`↑/↓ to navigate • Enter: confirm`); pass `show_instructions: false` to hide it.
194
214
 
195
215
  <img src="examples/images/select.svg" alt="Select prompt">
196
216
 
@@ -207,6 +227,31 @@ db = Clack.select(
207
227
  )
208
228
  ```
209
229
 
230
+ #### Option shorthands
231
+
232
+ Every option-based prompt also accepts a Hash instead of an Array. For `select`, `multiselect`, `autocomplete`, `autocomplete_multiselect`, and `select_key`, keys are the values you get back, Hash values are the labels, and insertion order is the display order:
233
+
234
+ ```ruby
235
+ db = Clack.select(message: "Choose a database", options: {pg: "PostgreSQL", mysql: "MySQL", sqlite: "SQLite"})
236
+ # => :pg
237
+
238
+ # Use a nested Hash when you need a hint or a disabled entry
239
+ editor = Clack.select(
240
+ message: "Editor?",
241
+ options: {
242
+ nvim: { label: "Neovim", hint: "recommended" },
243
+ vim: "Vim",
244
+ nano: { label: "nano", disabled: true }
245
+ }
246
+ )
247
+
248
+ features = Clack.multiselect(message: "Features", options: {api: "API", auth: "Auth"}, initial_values: [:api])
249
+ ```
250
+
251
+ `group_multiselect` takes `group label => options` at the top level; each group's options follow the same `value => label` rule (see [Group Multiselect](#group-multiselect)).
252
+
253
+ The orientation is always `value => label`. A `label => value` Hash (the Rails `options_for_select` shape) is not detected; flip it with `.invert` first. Arrays of `[label, value]` pairs are not detected either, since Array-valued options are legal; use `.to_h` to build a Hash instead. A single `{ value:, label: }` option Hash must still be wrapped in an Array; passed bare, it is read as two `value => label` pairs.
254
+
210
255
  ### Multiselect
211
256
 
212
257
  Pick many. Toggle with Space. Select all with `a`. Invert with `i`.
@@ -227,7 +272,7 @@ features = Clack.multiselect(
227
272
  )
228
273
  ```
229
274
 
230
- **Shortcuts:** `Space` toggle | `a` all | `i` invert
275
+ **Shortcuts:** `Space` select | `a` all | `i` invert | `Enter` confirm (the footer lists `Space` and `Enter`; `a` and `i` are extras not shown there; `show_instructions: false` hides the footer)
231
276
 
232
277
  ### Autocomplete
233
278
 
@@ -267,7 +312,7 @@ colors = Clack.autocomplete_multiselect(
267
312
  )
268
313
  ```
269
314
 
270
- **Shortcuts:** `Space` toggle | `Enter` confirm
315
+ **Shortcuts:** `Space` select | `Enter` confirm | type to search
271
316
 
272
317
  > 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.
273
318
 
@@ -283,7 +328,7 @@ project_dir = Clack.path(
283
328
  )
284
329
  ```
285
330
 
286
- **Navigation:** Type to filter | `Tab` to autocomplete | up/down arrows to select
331
+ **Navigation:** Type to filter | `Tab` to complete | `↑/↓` to select (all listed in the footer)
287
332
 
288
333
  ### Date
289
334
 
@@ -315,8 +360,13 @@ volume = Clack.range(
315
360
  initial_value: 50
316
361
  )
317
362
  # Navigate with arrow keys or hjkl
363
+
364
+ # Fractional steps snap exactly: three steps from 0 is 0.3, not 0.30000000000000004
365
+ opacity = Clack.range(message: "Opacity", min: 0, max: 1, step: 0.1)
318
366
  ```
319
367
 
368
+ Returns an `Integer` when `min` and `step` are both integers, otherwise a `Float`.
369
+
320
370
  ### Select Key
321
371
 
322
372
  Instant selection via keyboard shortcuts. No arrow key navigation needed.
@@ -332,6 +382,36 @@ action = Clack.select_key(
332
382
  )
333
383
  ```
334
384
 
385
+ Keys match regardless of case by default. Pass `case_sensitive: true` to tell `Y` and `y` apart, and `initial_value:` to highlight a default that Enter accepts (it is also what CI mode returns). The highlight is a color effect, so when colors are off (`NO_COLOR`, piped output) mark the default in that option's `hint:` as well. `validate:` and `transform:` work like everywhere else.
386
+
387
+ ```ruby
388
+ answer = Clack.select_key(
389
+ message: "Apply migration?",
390
+ options: [
391
+ { value: :yes_all, label: "Yes to all", key: "Y" },
392
+ { value: :yes, label: "Yes", key: "y" },
393
+ { value: :no, label: "No", key: "n", hint: "default" }
394
+ ],
395
+ case_sensitive: true,
396
+ initial_value: :no,
397
+ validate: ->(v) { "Not on Fridays" if v == :yes_all && Date.today.friday? }
398
+ )
399
+ ```
400
+
401
+ With the Hash shorthand the key defaults to the first character of the value. Pass a nested Hash to set it explicitly (`initial_value:` and `case_sensitive:` work the same way):
402
+
403
+ ```ruby
404
+ action = Clack.select_key(
405
+ message: "What would you like to do?",
406
+ options: {
407
+ create: { label: "Create new project", key: "c" },
408
+ open: "Open existing", # key "o"
409
+ quit: "Quit" # key "q"
410
+ },
411
+ initial_value: :open
412
+ )
413
+ ```
414
+
335
415
  ### Group Multiselect
336
416
 
337
417
  Multiselect with options organized into named categories.
@@ -360,6 +440,19 @@ features = Clack.group_multiselect(
360
440
  )
361
441
  ```
362
442
 
443
+ Groups can also be a Hash of `group label => options`, and each group's options accept the same `value => label` shorthand (or a plain Array):
444
+
445
+ ```ruby
446
+ addons = Clack.group_multiselect(
447
+ message: "Add-ons",
448
+ options: {
449
+ "Observability" => { prom: "Prometheus", dd: "Datadog" },
450
+ "Storage" => { s3: "S3", gcs: "GCS" }
451
+ }
452
+ )
453
+ # => [:prom, :s3]
454
+ ```
455
+
363
456
  ### Spinner
364
457
 
365
458
  Non-blocking animated indicator for async work.
@@ -395,6 +488,27 @@ Clack.spin("Working...") do |s|
395
488
  end
396
489
  ```
397
490
 
491
+ **Cancel and error messages** -- `cancel`/`error` with no argument, Ctrl+C, and `exit` use these instead of the in-progress text:
492
+
493
+ ```ruby
494
+ s = Clack.spinner(
495
+ cancel_message: "Deploy aborted", # default: "Cancelled"
496
+ error_message: "Deploy failed", # default: "Something went wrong"
497
+ on_cancel: -> { release_lock } # runs once after a cancel, no arguments
498
+ )
499
+ s.start("Deploying")
500
+ s.cancel # => "■ Deploy aborted", on_cancel runs
501
+ ```
502
+
503
+ Inside `Clack.spin`, an exception raised by the block prints `error:` or the exception message; `error_message:` only applies to a bare `s.error` or an uncaught crash at process exit.
504
+
505
+ **Ctrl+C and `exit` are safe.** A spinner that is still running when the process exits (Ctrl+C, `exit`, an uncaught error) prints its cancel or error line and stops animating before the `at_exit` blocks your program registered earlier, instead of leaving a half-drawn frame. `Clack.spin` does the same when its block exits early (`exit`, `break`, `throw`, `Interrupt`):
506
+
507
+ ```ruby
508
+ Clack.spin("Building", cancel_message: "Build cancelled") { exit 2 }
509
+ # ■ Build cancelled (then the process exits with status 2)
510
+ ```
511
+
398
512
  ### Progress
399
513
 
400
514
  A visual progress bar for measurable operations.
@@ -446,22 +560,22 @@ Clack.tasks(tasks: [
446
560
  |--------|--------|-------------|----------|
447
561
  | Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` | -- |
448
562
  | Password | `Clack.password` | `mask:`, `validate:` | `mask: "▪"` |
449
- | Confirm | `Clack.confirm` | `active:`, `inactive:`, `initial_value:` | `active: "Yes"`, `inactive: "No"`, `initial_value: true` |
450
- | Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` | -- |
451
- | Multiselect | `Clack.multiselect` | `options:`, `initial_values:`, `required:`, `cursor_at:` | `required: true` |
563
+ | Confirm | `Clack.confirm` | `active:`, `inactive:`, `initial_value:`, `vertical:` | `active: "Yes"`, `inactive: "No"`, `initial_value: true`, `vertical: false` |
564
+ | Select | `Clack.select` | `options:` (Array or Hash), `initial_value:`, `max_items:`, `show_instructions:` | -- |
565
+ | Multiselect | `Clack.multiselect` | `options:`, `initial_values:`, `required:`, `cursor_at:`, `show_instructions:` | `required: true` |
452
566
  | Group Multiselect | `Clack.group_multiselect` | `options:` (nested), `selectable_groups:`, `group_spacing:` | `selectable_groups: false` |
453
567
  | Autocomplete | `Clack.autocomplete` | `options:`, `placeholder:`, `filter:`, `max_items:` | `max_items: 5` |
454
568
  | Autocomplete Multiselect | `Clack.autocomplete_multiselect` | `options:`, `required:`, `initial_values:`, `filter:` | `required: true`, `max_items: 5` |
455
- | Select Key | `Clack.select_key` | `options:` (with `:key`) | -- |
569
+ | Select Key | `Clack.select_key` | `options:` (with `:key`), `case_sensitive:`, `initial_value:` | `case_sensitive: false` |
456
570
  | Path | `Clack.path` | `root:`, `only_directories:` | `root: "."` |
457
571
  | Date | `Clack.date` | `format:`, `initial_value:`, `min:`, `max:` | `format: :iso` |
458
572
  | Range | `Clack.range` | `min:`, `max:`, `step:`, `initial_value:` | `min: 0`, `max: 100`, `step: 1` |
459
573
  | Multiline Text | `Clack.multiline_text` | `initial_value:`, `validate:` | Submit with **Ctrl+D** |
460
- | Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error | -- |
574
+ | Spinner | `Clack.spinner` / `Clack.spin` | `indicator:`, `cancel_message:`, `error_message:`, `on_cancel:` | `indicator: :dots` |
461
575
  | Tasks | `Clack.tasks` | `tasks:` (`{title:, task:, enabled:}`) | `enabled: true` |
462
576
  | Progress | `Clack.progress` | `total:`, `message:` | -- |
463
577
 
464
- All prompts accept `message:`, `validate:`, `help:`, and return `Clack::CANCEL` on Escape/Ctrl+C.
578
+ All prompts accept `message:`, `validate:` (a proc, Regexp, Symbol, Array, or Hash; see [Validation Shorthands](#validation-shorthands)), `help:`, `with_guide:`, `instructions:`, and return `Clack::CANCEL` on Escape/Ctrl+C.
465
579
 
466
580
  </details>
467
581
 
@@ -479,6 +593,8 @@ exit 1 if Clack.handle_cancel(result)
479
593
 
480
594
  # With a custom message
481
595
  exit 1 if Clack.handle_cancel(result, "Aborted by user")
596
+
597
+ # The default "Cancelled" text follows `Clack.update_settings(messages: {cancel: ...})`
482
598
  ```
483
599
 
484
600
  ---
@@ -523,6 +639,51 @@ file = Clack.text(
523
639
  )
524
640
  ```
525
641
 
642
+ ### Validation Shorthands
643
+
644
+ `validate:` takes more than a lambda. Everything below is normalized by `Clack::Validators.resolve` when the prompt is built, so a typo raises `ArgumentError` immediately instead of crashing after the user has typed.
645
+
646
+ ```ruby
647
+ # Regexp: fails with "Invalid format" unless the whole value matches
648
+ slug = Clack.text(message: "Slug?", validate: /\A[a-z0-9-]+\z/)
649
+
650
+ # Symbol: a zero-argument built-in validator
651
+ email = Clack.text(message: "Email?", validate: :email)
652
+
653
+ # Hash: pattern (or symbol) => custom message, checked in order
654
+ user = Clack.text(
655
+ message: "Username?",
656
+ validate: {
657
+ required: "Username is required",
658
+ /\A[a-z0-9_]+\z/ => "Lowercase letters, numbers, and underscores only"
659
+ }
660
+ )
661
+
662
+ # Array: combine validators, first failure wins (nil or false entries are skipped)
663
+ handle = Clack.text(
664
+ message: "Handle?",
665
+ validate: [:required, /\A\w+\z/, ->(v) { "Taken" if TAKEN.include?(v) }]
666
+ )
667
+
668
+ # Anything that responds to #call: procs, Method objects, service objects
669
+ port = Clack.text(message: "Port?", validate: method(:check_port))
670
+ ```
671
+
672
+ Symbols available as shorthands: `:required`, `:email`, `:url`, `:integer` (any text-style prompt); `:path_exists`, `:directory_exists`, `:file_exists_warning` (a path string, so `text` or `path`); `:future_date`, `:past_date` (a `Date`, so the `date` prompt). They run against the prompt's raw value, so `:future_date` on a text prompt raises at submit. Built-ins that take arguments (`min_length`, `in_range`, `one_of`, ...) are called explicitly and mixed in: `validate: [:required, Clack::Validators.min_length(3)]`.
673
+
674
+ Regexps are checked with `match?` against `value.to_s`, so anchor with `\A` and `\z` (not `^`/`$`) unless you want per-line matching. A Regexp always means "the value must match"; to soften the check, use a `Clack::Warning` as the Hash message: `{/\A[a-z0-9-]+\z/ => Clack.warning("Unusual characters, continue?")}` warns instead of blocking when the value doesn't match. The same works for symbol keys: `{file_exists_warning: Clack.warning("Overwrite?")}`.
675
+
676
+ `Clack::Validators.combine` and `Clack::Validators.as_warning` accept the same shapes: `combine(:required, /\A\d+\z/)`, `as_warning(:email)`.
677
+
678
+ Using a schema library? Wrap it in a lambda that returns the first error message:
679
+
680
+ ```ruby
681
+ contract = SignupContract.new # dry-validation
682
+ Clack.text(message: "Email?", validate: ->(v) { contract.call(email: v).errors[:email]&.first })
683
+ ```
684
+
685
+ Don't pass the contract itself: its `call` returns a Result object, which is always truthy, so the prompt would treat every submit as an error.
686
+
526
687
  ### Built-in Validators
527
688
 
528
689
  ```ruby
@@ -545,6 +706,10 @@ Clack::Validators.combine(v1, v2) # First error/warning wins
545
706
  # Warning validators -- allow user to confirm or edit
546
707
  Clack::Validators.file_exists_warning # For file overwrite confirmations
547
708
  Clack::Validators.as_warning(validator) # Convert any validator to warning
709
+
710
+ # Zero-argument validators above also work as bare symbols or Hash keys:
711
+ Clack.text(message: "Email?", validate: :email)
712
+ Clack.text(message: "Email?", validate: {email: "That doesn't look right"})
548
713
  ```
549
714
 
550
715
  ### Built-in Transformers
@@ -602,6 +767,8 @@ Clack.log.step("Running migrations")
602
767
  Clack.log.message("Custom message")
603
768
  ```
604
769
 
770
+ `Clack.log.*` and `Clack.stream.*` accept `with_guide: false` (or follow the global setting). The level symbol stays on the first line; only the continuation rail goes away.
771
+
605
772
  ### Stream
606
773
 
607
774
  Stream output from iterables, enumerables, or shell commands:
@@ -648,6 +815,8 @@ Clack.box(
648
815
  )
649
816
  ```
650
817
 
818
+ Boxes sit inside the guide rail (a gray `│ ` on every line) so they line up with the prompts around them; pass `with_guide: false` to render one flush left.
819
+
651
820
  ### Task Log
652
821
 
653
822
  Streaming log that clears on success and shows full output on failure. Great for build output:
@@ -676,6 +845,8 @@ Clack.outro("Done!") # └ Done!
676
845
 
677
846
  # Or on error:
678
847
  Clack.cancel("Aborted") # └ Aborted (red)
848
+
849
+ # with_guide: false drops the ┌ │ └ symbols on all three
679
850
  ```
680
851
 
681
852
  ---
@@ -686,17 +857,80 @@ Clack.cancel("Aborted") # └ Aborted (red)
686
857
  # Add custom key bindings (merged with defaults)
687
858
  Clack.update_settings(aliases: { "y" => :enter, "n" => :cancel })
688
859
 
689
- # Disable guide bars
860
+ # Hide the guide rail everywhere (per-call with_guide: overrides this)
690
861
  Clack.update_settings(with_guide: false)
691
862
 
863
+ # Hide the keyboard hint footer under list prompts
864
+ Clack.update_settings(show_instructions: false)
865
+
866
+ # Localize the cancel/error text used by spinners and Clack.handle_cancel
867
+ Clack.update_settings(messages: { cancel: "Abgebrochen", error: "Etwas ging schief" })
868
+
692
869
  # CI / non-interactive mode (prompts auto-submit with defaults)
693
870
  Clack.update_settings(ci_mode: true) # Always on
694
- Clack.update_settings(ci_mode: :auto) # Auto-detect (non-TTY or CI env vars)
871
+ Clack.update_settings(ci_mode: :auto) # Auto-detect (piped/non-TTY input or CI env vars)
695
872
  ```
696
873
 
697
- 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.
874
+ 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. With `:auto`, detection looks at the prompt's own `input:` stream (stdin by default), so a `StringIO` input auto-submits as well.
698
875
 
699
- Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals.
876
+ Without CI mode, prompting on a piped or redirected stdin raises `Clack::NotATerminalError` (see [Troubleshooting](#troubleshooting)).
877
+
878
+ Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals. Keyboard hint footers soft-wrap below roughly 51 columns rather than corrupting the frame; pass `show_instructions: false` or a shorter `instructions:` to avoid the wrap.
879
+
880
+ ### Environment variables
881
+
882
+ Color, cursor and symbol output are auto-detected from the terminal. Override with the usual variables:
883
+
884
+ | Variable | Effect |
885
+ |---|---|
886
+ | `NO_COLOR=1` | Disable ANSI colors and cursor sequences, use ASCII symbols. Any non-empty value works; an empty value is ignored (per [no-color.org](https://no-color.org)). |
887
+ | `FORCE_COLOR=1` | Enable colors even when output is piped or `TERM=dumb`, e.g. to keep colors in a CI log. `NO_COLOR` still wins. |
888
+ | `FORCE_COLOR=0` | Disable colors everywhere, same as `NO_COLOR=1`. `false` works too. |
889
+ | `CLACK_UNICODE=1` / `0` | Force Unicode or ASCII symbols regardless of color detection. |
890
+ | `CLACK_ESCAPE_TIMEOUT=250` | Escape-key detection window in milliseconds, for slow SSH links where arrow keys get misread as Escape. |
891
+
892
+ ```bash
893
+ FORCE_COLOR=0 ruby my_cli.rb # plain text in a real terminal
894
+ FORCE_COLOR=1 ruby my_cli.rb 2>&1 | tee run.log # keep colors in a captured log
895
+ ```
896
+
897
+ ### Guide rail
898
+
899
+ Every prompt and message helper except `progress` draws the gray rail (`│`, `┌`, `└`) that ties a session together. The rail next to the step you are answering is cyan; it turns yellow on a validation error. Turn the rail off globally or per call:
900
+
901
+ ```ruby
902
+ Clack.update_settings(with_guide: false) # everywhere
903
+ Clack.select(message: "DB?", options: dbs, with_guide: true) # per-call override wins
904
+ Clack.log.info("plain", with_guide: false)
905
+ ```
906
+
907
+ With guides off the connector lines disappear and content starts flush left:
908
+
909
+ ```
910
+ ◆ Choose a database instead of │
911
+ ● PostgreSQL ◆ Choose a database
912
+ ○ MySQL │ ● PostgreSQL
913
+ ↑/↓ to navigate • Enter: confirm │ ○ MySQL
914
+ │ ↑/↓ to navigate • Enter: confirm
915
+
916
+ ```
917
+
918
+ `Clack.log.*` and `Clack.stream.*` keep their level symbol (`●`, `▲`, `■`) with guides off; only the rail goes away.
919
+
920
+ ### Keyboard hints
921
+
922
+ `select`, `multiselect`, `group_multiselect`, `autocomplete`, `autocomplete_multiselect`, and `path` end with a line of keyboard hints (key names dimmed). Hide it per prompt with `show_instructions: false`, everywhere with `Clack.update_settings(show_instructions: false)`, or replace the text (for localized CLIs) with `instructions:`, a String or an Array of Strings joined with ` • `:
923
+
924
+ ```ruby
925
+ Clack.multiselect(
926
+ message: "Funktionen",
927
+ options: features,
928
+ instructions: ["Leertaste: wählen", "a: alle", "i: umkehren", "Enter: bestätigen"]
929
+ )
930
+
931
+ # instructions: works on any prompt, even ones without built-in hints
932
+ Clack.text(message: "Slug?", instructions: "lowercase, digits and dashes")
933
+ ```
700
934
 
701
935
  ---
702
936
 
@@ -735,6 +969,39 @@ The `PromptDriver` yielded to the block provides these methods:
735
969
  | `ctrl_d` | Press Ctrl+D (submit multiline text) |
736
970
  | `key(sym_or_char)` | Press an arbitrary key by symbol (e.g. `:escape`) or raw character |
737
971
 
972
+ Any object that responds to `getc` works as `input:`, so a plain `StringIO` is enough for simple cases. When the input runs out (EOF) the prompt cancels and returns `Clack::CANCEL`, so end scripted input with `"\r"` to submit (or `"\u0004"`, Ctrl+D, for `multiline_text`):
973
+
974
+ ```ruby
975
+ Clack.select(message: "Pick", options: %w[a b c], input: StringIO.new("j\r"), output: StringIO.new)
976
+ # => "b"
977
+ ```
978
+
979
+ Arrow keys do not work in a `StringIO`: it cannot be probed for follow-up bytes, so an escape sequence such as `"\e[B"` is read as a bare Escape (which cancels the prompt) followed by `[` and `B`. Use the vim keys (`j`/`k`/`h`/`l`) in scripted input, or `Clack::Testing.simulate` / `Clack::Testing::KeyQueue`, which deliver whole key codes, for arrow navigation.
980
+
981
+ If your app sets `ci_mode: :auto`, reset it in test setup (`Clack::Core::Settings.reset!`): a simulated input is not a TTY, so `:auto` would auto-submit instead of following your script.
982
+
983
+ ---
984
+
985
+ ## Troubleshooting
986
+
987
+ ### `Clack::NotATerminalError: stdin is not an interactive terminal`
988
+
989
+ Prompts read keystrokes in raw mode, which needs a real terminal. You will see this when stdin is a pipe or a file (`echo y | ruby app.rb`, `ruby app.rb < answers.txt`, a cron job, a CI step). Either run the script from a terminal, or opt in to non-interactive runs so prompts submit their defaults:
990
+
991
+ ```ruby
992
+ Clack.update_settings(ci_mode: :auto) # auto-submit defaults when input is not a TTY or a CI env var is set
993
+ ```
994
+
995
+ To answer prompts from a script rather than skip them, pass the answers as `input:` (see [Testing](#testing)). The error is an `IOError`, so a top-level `rescue IOError` catches it.
996
+
997
+ ### Prompt exits with `CANCEL` as soon as piped input runs out
998
+
999
+ End of input is treated like Ctrl+C. Terminate each answer with `"\r"` (Enter), and for `multiline_text` finish with `"\u0004"` (Ctrl+D).
1000
+
1001
+ ### Arrow keys type letters, or do nothing, in tmux or after vim
1002
+
1003
+ Terminals in application cursor mode send `ESC O A` instead of `ESC [ A` for Up. Clack 0.7.0 understands both, and also folds the various Home/End encodings into one. If arrows still misbehave over a slow SSH or mosh link, raise the Escape detection window: `CLACK_ESCAPE_TIMEOUT=250` (milliseconds, default 50).
1004
+
738
1005
  ---
739
1006
 
740
1007
  ## Try It
@@ -149,18 +149,27 @@ Clack.log.info "Language: #{lang}"
149
149
  # email = prompt.ask("Email?", validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\z/i)
150
150
  # age = prompt.ask("Age?") { |q| q.validate(->(v) { v.to_i > 0 }, "Must be positive") }
151
151
  #
152
- # Clack uses a validate lambda that returns nil (pass) or an error string:
152
+ # Clack takes the same Regexp directly (or :email for the common case).
153
+ # A Hash gives the pattern a custom message. Lambdas return nil (pass)
154
+ # or an error string:
153
155
 
154
156
  email = Clack.text(
155
157
  message: "Email?",
156
- validate: ->(v) {
157
- "Invalid email" unless v.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z]+)*\z/i)
158
- }
158
+ validate: {/\A[\w+\-.]+@[a-z\d-]+(\.[a-z]+)*\z/i => "Invalid email"}
159
159
  )
160
160
  exit 0 if Clack.cancel?(email)
161
161
 
162
162
  Clack.log.info "Email: #{email}"
163
163
 
164
+ age = Clack.text(
165
+ message: "Age?",
166
+ validate: ->(v) { "Must be positive" unless v.to_i.positive? },
167
+ transform: :to_integer
168
+ )
169
+ exit 0 if Clack.cancel?(age)
170
+
171
+ Clack.log.info "Age: #{age}"
172
+
164
173
  # ─────────────────────────────────────────────
165
174
  # 8. Collecting multiple prompts (group)
166
175
  # ─────────────────────────────────────────────
data/lib/clack/box.rb CHANGED
@@ -13,7 +13,10 @@ module Clack
13
13
  # @param title_padding [Integer] Padding around title
14
14
  # @param content_padding [Integer] Padding around content
15
15
  # @param rounded [Boolean] Use rounded corners (default: true)
16
- # @param format_border [Proc] Optional proc to format border characters
16
+ # @param format_border [Proc] Optional proc to format border characters (the guide
17
+ # gutter in front of each line is not passed through it)
18
+ # @param with_guide [Boolean, nil] Prefix every line with the gray guide rail so the box
19
+ # lines up with the prompts around it (default: Clack.settings[:with_guide])
17
20
  # @param output [IO] Output stream
18
21
  def render(
19
22
  message = "",
@@ -25,12 +28,15 @@ module Clack
25
28
  content_padding: 2,
26
29
  rounded: true,
27
30
  format_border: nil,
31
+ with_guide: nil,
28
32
  output: $stdout
29
33
  )
30
34
  ctx = build_context(message, title, title_padding, content_padding, width, rounded, format_border)
31
- output.puts build_top_border(ctx[:display_title], ctx[:inner_width], title_padding, title_align, ctx[:symbols], ctx[:h_symbol])
35
+ ctx[:line_prefix] = Core::Settings.with_guide?(with_guide) ? "#{Colors.gray(Symbols::S_BAR)} " : ""
36
+ top = build_top_border(ctx[:display_title], ctx[:inner_width], title_padding, title_align, ctx[:symbols], ctx[:h_symbol])
37
+ output.puts "#{ctx[:line_prefix]}#{top}"
32
38
  render_content_lines(output, ctx, content_align, content_padding)
33
- output.puts "#{ctx[:symbols][2]}#{ctx[:h_symbol] * ctx[:inner_width]}#{ctx[:symbols][3]}"
39
+ output.puts "#{ctx[:line_prefix]}#{ctx[:symbols][2]}#{ctx[:h_symbol] * ctx[:inner_width]}#{ctx[:symbols][3]}"
34
40
  end
35
41
 
36
42
  private
@@ -57,7 +63,7 @@ module Clack
57
63
  def render_content_lines(output, ctx, content_align, content_padding)
58
64
  ctx[:lines].each do |line|
59
65
  left_pad, right_pad = padding_for_line(Clack::Utils.visible_length(line), ctx[:inner_width], content_padding, content_align)
60
- output.puts "#{ctx[:v_symbol]}#{" " * left_pad}#{line}#{" " * right_pad}#{ctx[:v_symbol]}"
66
+ output.puts "#{ctx[:line_prefix]}#{ctx[:v_symbol]}#{" " * left_pad}#{line}#{" " * right_pad}#{ctx[:v_symbol]}"
61
67
  end
62
68
  end
63
69
 
@@ -83,7 +89,7 @@ module Clack
83
89
  return width + 2 if width.is_a?(Integer) # Add 2 for borders
84
90
 
85
91
  # Auto width: fit to content using display width
86
- max_line = lines.map { |l| Clack::Utils.visible_length(l) }.max || 0
92
+ max_line = lines.map { |line| Clack::Utils.visible_length(line) }.max || 0
87
93
  title_with_padding = title_len + (title_padding * 2)
88
94
  content_with_padding = max_line + (content_padding * 2)
89
95
 
data/lib/clack/colors.rb CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  module Clack
4
4
  # ANSI color codes for terminal output styling.
5
- # Colors are automatically disabled when:
6
- # - Output is not a TTY (piped/redirected)
7
- # - NO_COLOR environment variable is set
8
- # - FORCE_COLOR environment variable forces colors on
5
+ #
6
+ # Colors are enabled when {Environment.colors_supported?} says so: on a TTY
7
+ # that is not +TERM=dumb+, or anywhere when +FORCE_COLOR+ is set to a
8
+ # non-empty value other than +0+ / +false+. A non-empty +NO_COLOR+ or
9
+ # +FORCE_COLOR=0+ disables them everywhere. When disabled every method
10
+ # returns the plain text unchanged.
9
11
  module Colors
10
12
  class << self
13
+ # Whether ANSI styling is currently active for +$stdout+.
14
+ # Delegates to {Environment.colors_supported?} on every call.
15
+ # @return [Boolean]
11
16
  def enabled? = Environment.colors_supported?
12
17
 
13
18
  # @!group Foreground Colors (standard)