clack 0.4.4 → 0.4.6
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 +28 -0
- data/README.md +45 -8
- data/examples/migration_from_tty_prompt.rb +218 -0
- data/examples/showcase.rb +224 -0
- data/lib/clack/colors.rb +1 -6
- data/lib/clack/core/cursor.rb +1 -3
- data/lib/clack/core/prompt.rb +18 -3
- data/lib/clack/core/settings.rb +2 -2
- data/lib/clack/prompts/autocomplete.rb +13 -21
- data/lib/clack/prompts/autocomplete_multiselect.rb +38 -82
- data/lib/clack/prompts/confirm.rb +1 -11
- data/lib/clack/prompts/date.rb +26 -42
- data/lib/clack/prompts/group_multiselect.rb +1 -12
- data/lib/clack/prompts/multiselect.rb +2 -11
- data/lib/clack/prompts/password.rb +1 -11
- data/lib/clack/prompts/path.rb +0 -11
- data/lib/clack/prompts/range.rb +1 -11
- data/lib/clack/prompts/select.rb +1 -11
- data/lib/clack/prompts/select_key.rb +1 -11
- data/lib/clack/prompts/text.rb +0 -11
- data/lib/clack/symbols.rb +2 -3
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +12 -7
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e27f6e6ab9835d2855570ebdcd9b74a581becaffad5ba6a814e54ec5d44097b2
|
|
4
|
+
data.tar.gz: 8788e05802f917a1a6abe06697a4ca47ff451da0235d7336b560adf8558451a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 173ead18da184ea230092d263bb4e607eb623c788fcdfae7d4315afbebe140bbe5335e42fa7c7ee460481916e46a6fda464f9b13f8d011228648dcf13a799704
|
|
7
|
+
data.tar.gz: 56fd6c8cbca6c610cb73941f565c84e62c6bcc38d5b48741c35c52b86c14e45ff858219e5973669d1db7beb4bb045259d11daa60d172d3e2603ad16c04e2e812
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.6] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Settings.printable?` now accepts combining characters and multi-codepoint grapheme clusters (accented letters, emoji)
|
|
7
|
+
- Signal handlers use `write_nonblock` instead of `print` for async safety
|
|
8
|
+
- `S_STEP_ERROR` ASCII fallback changed from `x` to `!` (was identical to `S_STEP_CANCEL`)
|
|
9
|
+
- CI mode validation warnings now print to stderr instead of stdout
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Colors, Cursor, and Symbols ANSI detection unified through `Environment.colors_supported?`
|
|
13
|
+
- Extracted `build_final_frame` template into `Core::Prompt` with `final_display` hook (11 prompt classes simplified)
|
|
14
|
+
- Date prompt: consolidated duplicated segment logic into `update_segment`
|
|
15
|
+
|
|
16
|
+
## [0.4.5] - 2026-02-22
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `Autocomplete` and `AutocompleteMultiselect` now route all printable characters to the search field first, fixing vim aliases (`j`/`k`/`h`/`l`) hijacking text input
|
|
20
|
+
- `AutocompleteMultiselect` no longer intercepts `a`/`i` keys as shortcuts — they conflicted with typing in the search field
|
|
21
|
+
- `AutocompleteMultiselect#toggle_current` now respects `:disabled` options, matching `Multiselect` behavior
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `AutocompleteMultiselect` internals aligned with `Multiselect`: `@selected` naming, `submit` override pattern, `keyboard_hints` method, `build_frame` validation rendering, `initial_values: []` default
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `examples/showcase.rb` — full-featured demo with branching, autocomplete, date, range, group multiselect, tasks
|
|
28
|
+
- `examples/migration_from_tty_prompt.rb` — side-by-side migration guide from tty-prompt
|
|
29
|
+
- README: "Why Clack?" comparison table vs tty-prompt/highline, quick reference table, TL;DR tagline, vim key notes on autocomplete sections
|
|
30
|
+
|
|
3
31
|
## [0.4.4] - 2026-02-21
|
|
4
32
|
|
|
5
33
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Effortlessly beautiful CLI prompts for Ruby.**
|
|
4
4
|
|
|
5
|
-
A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack).
|
|
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.
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="examples/demo.gif?v=2" width="640" alt="Clack demo">
|
|
@@ -10,10 +10,11 @@ A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack)
|
|
|
10
10
|
|
|
11
11
|
## Why Clack?
|
|
12
12
|
|
|
13
|
-
- **Beautiful by default**
|
|
14
|
-
- **Vim-friendly**
|
|
15
|
-
- **Accessible**
|
|
16
|
-
- **Composable**
|
|
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
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -299,7 +300,8 @@ Type to filter from a list of options. Filtering uses **fuzzy matching** by defa
|
|
|
299
300
|
color = Clack.autocomplete(
|
|
300
301
|
message: "Pick a color",
|
|
301
302
|
options: %w[red orange yellow green blue indigo violet],
|
|
302
|
-
placeholder: "Type to search..."
|
|
303
|
+
placeholder: "Type to search...",
|
|
304
|
+
max_items: 5 # Default; scrollable via ↑↓
|
|
303
305
|
)
|
|
304
306
|
|
|
305
307
|
# Custom filter logic (receives option hash and query string)
|
|
@@ -310,6 +312,8 @@ cmd = Clack.autocomplete(
|
|
|
310
312
|
)
|
|
311
313
|
```
|
|
312
314
|
|
|
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.
|
|
316
|
+
|
|
313
317
|
### Autocomplete Multiselect
|
|
314
318
|
|
|
315
319
|
Type to filter with multi-selection support.
|
|
@@ -320,11 +324,14 @@ colors = Clack.autocomplete_multiselect(
|
|
|
320
324
|
options: %w[red orange yellow green blue indigo violet],
|
|
321
325
|
placeholder: "Type to filter...",
|
|
322
326
|
required: true, # At least one selection required
|
|
323
|
-
initial_values: ["red"]
|
|
327
|
+
initial_values: ["red"], # Pre-selected values
|
|
328
|
+
max_items: 5 # Default; scrollable via ↑↓
|
|
324
329
|
)
|
|
325
330
|
```
|
|
326
331
|
|
|
327
|
-
**Shortcuts:** `Space` toggle | `
|
|
332
|
+
**Shortcuts:** `Space` toggle | `Enter` confirm
|
|
333
|
+
|
|
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.
|
|
328
335
|
|
|
329
336
|
### Path
|
|
330
337
|
|
|
@@ -493,6 +500,32 @@ features = Clack.group_multiselect(
|
|
|
493
500
|
)
|
|
494
501
|
```
|
|
495
502
|
|
|
503
|
+
<details>
|
|
504
|
+
<summary><h3 style="display:inline">Quick Reference</h3></summary>
|
|
505
|
+
|
|
506
|
+
| Prompt | Method | Key Options | Defaults |
|
|
507
|
+
|--------|--------|-------------|----------|
|
|
508
|
+
| Text | `Clack.text` | `placeholder:`, `default_value:`, `initial_value:`, `completions:` | — |
|
|
509
|
+
| Password | `Clack.password` | `mask:`, `validate:` | `mask: "▪"` |
|
|
510
|
+
| Confirm | `Clack.confirm` | `active:`, `inactive:`, `initial_value:` | `active: "Yes"`, `inactive: "No"`, `initial_value: true` |
|
|
511
|
+
| Select | `Clack.select` | `options:`, `initial_value:`, `max_items:` | — |
|
|
512
|
+
| Multiselect | `Clack.multiselect` | `options:`, `initial_values:`, `required:`, `cursor_at:` | `required: true` |
|
|
513
|
+
| Group Multiselect | `Clack.group_multiselect` | `options:` (nested), `selectable_groups:`, `group_spacing:` | `selectable_groups: false` |
|
|
514
|
+
| Autocomplete | `Clack.autocomplete` | `options:`, `placeholder:`, `filter:`, `max_items:` | `max_items: 5` |
|
|
515
|
+
| Autocomplete Multiselect | `Clack.autocomplete_multiselect` | `options:`, `required:`, `initial_values:`, `filter:` | `required: true`, `max_items: 5` |
|
|
516
|
+
| Select Key | `Clack.select_key` | `options:` (with `:key`) | — |
|
|
517
|
+
| Path | `Clack.path` | `root:`, `only_directories:` | `root: "."` |
|
|
518
|
+
| Date | `Clack.date` | `format:`, `initial_value:`, `min:`, `max:` | `format: :iso` |
|
|
519
|
+
| Range | `Clack.range` | `min:`, `max:`, `step:`, `initial_value:` | `min: 0`, `max: 100`, `step: 1` |
|
|
520
|
+
| Multiline Text | `Clack.multiline_text` | `initial_value:`, `validate:` | Submit with **Ctrl+D** |
|
|
521
|
+
| Spinner | `Clack.spinner` / `Clack.spin` | block form auto-handles success/error | — |
|
|
522
|
+
| Tasks | `Clack.tasks` | `tasks:` (`{title:, task:, enabled:}`) | `enabled: true` |
|
|
523
|
+
| Progress | `Clack.progress` | `total:`, `message:` | — |
|
|
524
|
+
|
|
525
|
+
All prompts accept `message:`, `validate:`, `help:`, and return `Clack::CANCEL` on Escape/Ctrl+C.
|
|
526
|
+
|
|
527
|
+
</details>
|
|
528
|
+
|
|
496
529
|
## Prompt Groups
|
|
497
530
|
|
|
498
531
|
Chain multiple prompts and collect results in a hash. Cancellation is handled automatically.
|
|
@@ -673,6 +706,10 @@ bundle exec rake spec # Tests only
|
|
|
673
706
|
COVERAGE=true bundle exec rake spec # With coverage
|
|
674
707
|
```
|
|
675
708
|
|
|
709
|
+
## Roadmap
|
|
710
|
+
|
|
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.
|
|
712
|
+
|
|
676
713
|
## Credits
|
|
677
714
|
|
|
678
715
|
This is a Ruby port of [Clack](https://github.com/bombshell-dev/clack), created by [Nate Moore](https://github.com/natemoo-re) and the [Astro](https://astro.build) team.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Migration Guide: tty-prompt -> Clack
|
|
5
|
+
#
|
|
6
|
+
# This file shows common tty-prompt patterns and their Clack equivalents.
|
|
7
|
+
# Each section shows the tty-prompt code (commented out) followed by
|
|
8
|
+
# runnable Clack code that does the same thing.
|
|
9
|
+
#
|
|
10
|
+
# Run with: ruby examples/migration_from_tty_prompt.rb
|
|
11
|
+
|
|
12
|
+
require_relative "../lib/clack" # Or: require "clack" if installed as a gem
|
|
13
|
+
|
|
14
|
+
Clack.intro "Migrating from tty-prompt to Clack"
|
|
15
|
+
|
|
16
|
+
# ─────────────────────────────────────────────
|
|
17
|
+
# 1. Basic text input
|
|
18
|
+
# ─────────────────────────────────────────────
|
|
19
|
+
#
|
|
20
|
+
# tty-prompt:
|
|
21
|
+
# prompt = TTY::Prompt.new
|
|
22
|
+
# name = prompt.ask("What is your name?", default: "World")
|
|
23
|
+
#
|
|
24
|
+
# Clack equivalent:
|
|
25
|
+
|
|
26
|
+
name = Clack.text(
|
|
27
|
+
message: "What is your name?",
|
|
28
|
+
placeholder: "World",
|
|
29
|
+
default_value: "World"
|
|
30
|
+
)
|
|
31
|
+
exit 0 if Clack.cancel?(name)
|
|
32
|
+
|
|
33
|
+
Clack.log.info "Name: #{name}"
|
|
34
|
+
|
|
35
|
+
# ─────────────────────────────────────────────
|
|
36
|
+
# 2. Password input
|
|
37
|
+
# ─────────────────────────────────────────────
|
|
38
|
+
#
|
|
39
|
+
# tty-prompt:
|
|
40
|
+
# secret = prompt.mask("Enter your API key:")
|
|
41
|
+
#
|
|
42
|
+
# Clack equivalent:
|
|
43
|
+
|
|
44
|
+
secret = Clack.password(message: "Enter your API key:")
|
|
45
|
+
exit 0 if Clack.cancel?(secret)
|
|
46
|
+
|
|
47
|
+
Clack.log.info "Key entered (#{secret.length} chars)"
|
|
48
|
+
|
|
49
|
+
# ─────────────────────────────────────────────
|
|
50
|
+
# 3. Yes/No confirmation
|
|
51
|
+
# ─────────────────────────────────────────────
|
|
52
|
+
#
|
|
53
|
+
# tty-prompt:
|
|
54
|
+
# prompt.yes?("Continue setup?") # => true/false
|
|
55
|
+
# prompt.no?("Skip optional steps?") # => true/false (inverted default)
|
|
56
|
+
#
|
|
57
|
+
# Clack equivalent:
|
|
58
|
+
# Use initial_value: false to mimic prompt.no? (default to "No")
|
|
59
|
+
|
|
60
|
+
continue = Clack.confirm(message: "Continue setup?")
|
|
61
|
+
exit 0 if Clack.cancel?(continue)
|
|
62
|
+
|
|
63
|
+
Clack.log.info "Continue: #{continue}"
|
|
64
|
+
|
|
65
|
+
# ─────────────────────────────────────────────
|
|
66
|
+
# 4. Single select
|
|
67
|
+
# ─────────────────────────────────────────────
|
|
68
|
+
#
|
|
69
|
+
# tty-prompt:
|
|
70
|
+
# color = prompt.select("Pick a color", %w[Red Green Blue])
|
|
71
|
+
#
|
|
72
|
+
# # or with values:
|
|
73
|
+
# color = prompt.select("Pick a color") do |menu|
|
|
74
|
+
# menu.choice "Red", :red
|
|
75
|
+
# menu.choice "Green", :green
|
|
76
|
+
# menu.choice "Blue", :blue
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# Clack equivalent:
|
|
80
|
+
|
|
81
|
+
color = Clack.select(
|
|
82
|
+
message: "Pick a color",
|
|
83
|
+
options: [
|
|
84
|
+
{value: :red, label: "Red"},
|
|
85
|
+
{value: :green, label: "Green"},
|
|
86
|
+
{value: :blue, label: "Blue"}
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
exit 0 if Clack.cancel?(color)
|
|
90
|
+
|
|
91
|
+
Clack.log.info "Color: #{color}"
|
|
92
|
+
|
|
93
|
+
# ─────────────────────────────────────────────
|
|
94
|
+
# 5. Multi-select
|
|
95
|
+
# ─────────────────────────────────────────────
|
|
96
|
+
#
|
|
97
|
+
# tty-prompt:
|
|
98
|
+
# toppings = prompt.multi_select("Choose toppings") do |menu|
|
|
99
|
+
# menu.choice "Cheese", :cheese
|
|
100
|
+
# menu.choice "Pepperoni", :pepperoni
|
|
101
|
+
# menu.choice "Mushrooms", :mushrooms
|
|
102
|
+
# menu.choice "Olives", :olives
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
# Clack equivalent:
|
|
106
|
+
|
|
107
|
+
toppings = Clack.multiselect(
|
|
108
|
+
message: "Choose toppings",
|
|
109
|
+
options: [
|
|
110
|
+
{value: :cheese, label: "Cheese"},
|
|
111
|
+
{value: :pepperoni, label: "Pepperoni"},
|
|
112
|
+
{value: :mushrooms, label: "Mushrooms"},
|
|
113
|
+
{value: :olives, label: "Olives"}
|
|
114
|
+
],
|
|
115
|
+
required: false
|
|
116
|
+
)
|
|
117
|
+
exit 0 if Clack.cancel?(toppings)
|
|
118
|
+
|
|
119
|
+
Clack.log.info "Toppings: #{toppings.join(", ")}"
|
|
120
|
+
|
|
121
|
+
# ─────────────────────────────────────────────
|
|
122
|
+
# 6. Autocomplete / filtering
|
|
123
|
+
# ─────────────────────────────────────────────
|
|
124
|
+
#
|
|
125
|
+
# tty-prompt:
|
|
126
|
+
# lang = prompt.select("Pick a language", %w[Ruby Python JavaScript Go Rust], filter: true)
|
|
127
|
+
#
|
|
128
|
+
# Clack has a dedicated autocomplete prompt with built-in fuzzy matching:
|
|
129
|
+
|
|
130
|
+
lang = Clack.autocomplete(
|
|
131
|
+
message: "Pick a language (type to filter)",
|
|
132
|
+
options: [
|
|
133
|
+
{value: "ruby", label: "Ruby"},
|
|
134
|
+
{value: "python", label: "Python"},
|
|
135
|
+
{value: "javascript", label: "JavaScript"},
|
|
136
|
+
{value: "go", label: "Go"},
|
|
137
|
+
{value: "rust", label: "Rust"}
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
exit 0 if Clack.cancel?(lang)
|
|
141
|
+
|
|
142
|
+
Clack.log.info "Language: #{lang}"
|
|
143
|
+
|
|
144
|
+
# ─────────────────────────────────────────────
|
|
145
|
+
# 7. Validation
|
|
146
|
+
# ─────────────────────────────────────────────
|
|
147
|
+
#
|
|
148
|
+
# tty-prompt:
|
|
149
|
+
# email = prompt.ask("Email?", validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\z/i)
|
|
150
|
+
# age = prompt.ask("Age?") { |q| q.validate(->(v) { v.to_i > 0 }, "Must be positive") }
|
|
151
|
+
#
|
|
152
|
+
# Clack uses a validate lambda that returns nil (pass) or an error string:
|
|
153
|
+
|
|
154
|
+
email = Clack.text(
|
|
155
|
+
message: "Email?",
|
|
156
|
+
validate: ->(v) {
|
|
157
|
+
"Invalid email" unless v.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z]+)*\z/i)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
exit 0 if Clack.cancel?(email)
|
|
161
|
+
|
|
162
|
+
Clack.log.info "Email: #{email}"
|
|
163
|
+
|
|
164
|
+
# ─────────────────────────────────────────────
|
|
165
|
+
# 8. Collecting multiple prompts (group)
|
|
166
|
+
# ─────────────────────────────────────────────
|
|
167
|
+
#
|
|
168
|
+
# tty-prompt:
|
|
169
|
+
# result = prompt.collect do
|
|
170
|
+
# key(:name).ask("Name?")
|
|
171
|
+
# key(:role).select("Role?", %w[Admin User Guest])
|
|
172
|
+
# key(:notify).yes?("Send welcome email?")
|
|
173
|
+
# end
|
|
174
|
+
# # => { name: "Alice", role: "User", notify: true }
|
|
175
|
+
#
|
|
176
|
+
# Clack.group collects answers into a hash and handles cancellation:
|
|
177
|
+
|
|
178
|
+
result = Clack.group(
|
|
179
|
+
on_cancel: ->(_partial) {
|
|
180
|
+
Clack.cancel "Setup cancelled"
|
|
181
|
+
exit 0
|
|
182
|
+
}
|
|
183
|
+
) do |g|
|
|
184
|
+
g.prompt(:name) { Clack.text(message: "Name?", placeholder: "Alice") }
|
|
185
|
+
|
|
186
|
+
g.prompt(:role) do
|
|
187
|
+
Clack.select(
|
|
188
|
+
message: "Role?",
|
|
189
|
+
options: %w[Admin User Guest].map { |r| {value: r.downcase, label: r} }
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
g.prompt(:notify) { Clack.confirm(message: "Send welcome email?") }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Clack.log.info "Name: #{result[:name]}"
|
|
197
|
+
Clack.log.info "Role: #{result[:role]}"
|
|
198
|
+
Clack.log.info "Notify: #{result[:notify]}"
|
|
199
|
+
|
|
200
|
+
# ─────────────────────────────────────────────
|
|
201
|
+
# Quick reference
|
|
202
|
+
# ─────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
Clack.note <<~TABLE, title: "Quick Reference"
|
|
205
|
+
tty-prompt Clack
|
|
206
|
+
───────────────────── ──────────────────────────────
|
|
207
|
+
prompt.ask Clack.text
|
|
208
|
+
prompt.mask Clack.password
|
|
209
|
+
prompt.yes? / prompt.no? Clack.confirm
|
|
210
|
+
prompt.select Clack.select
|
|
211
|
+
prompt.multi_select Clack.multiselect
|
|
212
|
+
prompt.select(filter:) Clack.autocomplete
|
|
213
|
+
prompt.collect Clack.group
|
|
214
|
+
prompt.say / prompt.warn Clack.log.info / .warning
|
|
215
|
+
TTY::Spinner Clack.spinner / Clack.spin
|
|
216
|
+
TABLE
|
|
217
|
+
|
|
218
|
+
Clack.outro "Migration complete! See the README for the full API."
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Ultimate Clack showcase — every impressive feature in one flow.
|
|
5
|
+
# Run with: ruby -Ilib examples/showcase.rb
|
|
6
|
+
|
|
7
|
+
require "clack"
|
|
8
|
+
|
|
9
|
+
Clack.intro "deploy-wizard"
|
|
10
|
+
|
|
11
|
+
# --- Text with tab completions ---
|
|
12
|
+
project = Clack.text(
|
|
13
|
+
message: "Project name:",
|
|
14
|
+
placeholder: "my-app",
|
|
15
|
+
completions: %w[acme-api acme-web acme-admin acme-workers billing-service payment-gateway
|
|
16
|
+
notification-hub auth-proxy data-pipeline analytics-engine search-indexer
|
|
17
|
+
cdn-edge config-server feature-flags rate-limiter],
|
|
18
|
+
validate: ->(v) { "Name is required" if v.to_s.strip.empty? }
|
|
19
|
+
)
|
|
20
|
+
exit 0 if Clack.cancel?(project)
|
|
21
|
+
|
|
22
|
+
# --- Autocomplete with fuzzy search (big list) ---
|
|
23
|
+
stack = Clack.autocomplete(
|
|
24
|
+
message: "Tech stack:",
|
|
25
|
+
placeholder: "Type to search...",
|
|
26
|
+
options: [
|
|
27
|
+
{value: "rails", label: "Ruby on Rails", hint: "full-stack, batteries included"},
|
|
28
|
+
{value: "sinatra", label: "Sinatra", hint: "micro, DSL-style"},
|
|
29
|
+
{value: "hanami", label: "Hanami", hint: "clean architecture"},
|
|
30
|
+
{value: "roda", label: "Roda", hint: "routing tree"},
|
|
31
|
+
{value: "grape", label: "Grape", hint: "API framework"},
|
|
32
|
+
{value: "nextjs", label: "Next.js", hint: "React SSR"},
|
|
33
|
+
{value: "nuxt", label: "Nuxt", hint: "Vue SSR"},
|
|
34
|
+
{value: "sveltekit", label: "SvelteKit", hint: "Svelte SSR"},
|
|
35
|
+
{value: "phoenix", label: "Phoenix", hint: "Elixir"},
|
|
36
|
+
{value: "django", label: "Django", hint: "Python full-stack"},
|
|
37
|
+
{value: "fastapi", label: "FastAPI", hint: "Python async"},
|
|
38
|
+
{value: "express", label: "Express.js", hint: "Node.js minimal"},
|
|
39
|
+
{value: "fastify", label: "Fastify", hint: "Node.js fast"},
|
|
40
|
+
{value: "gin", label: "Gin", hint: "Go HTTP"},
|
|
41
|
+
{value: "actix", label: "Actix Web", hint: "Rust async"},
|
|
42
|
+
{value: "spring", label: "Spring Boot", hint: "Java enterprise"},
|
|
43
|
+
{value: "laravel", label: "Laravel", hint: "PHP full-stack"}
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
exit 0 if Clack.cancel?(stack)
|
|
47
|
+
|
|
48
|
+
# --- Branching: choice determines follow-up ---
|
|
49
|
+
deploy_target = Clack.select(
|
|
50
|
+
message: "Deploy target:",
|
|
51
|
+
options: [
|
|
52
|
+
{value: "kubernetes", label: "Kubernetes", hint: "container orchestration"},
|
|
53
|
+
{value: "serverless", label: "Serverless", hint: "Lambda / Cloud Functions"},
|
|
54
|
+
{value: "bare_metal", label: "Bare Metal", hint: "traditional VPS"}
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
exit 0 if Clack.cancel?(deploy_target)
|
|
58
|
+
|
|
59
|
+
cloud = nil
|
|
60
|
+
replicas = nil
|
|
61
|
+
|
|
62
|
+
case deploy_target
|
|
63
|
+
when "kubernetes"
|
|
64
|
+
cloud = Clack.select(
|
|
65
|
+
message: "Cloud provider:",
|
|
66
|
+
options: [
|
|
67
|
+
{value: "aws", label: "AWS EKS", hint: "Amazon"},
|
|
68
|
+
{value: "gcp", label: "GCP GKE", hint: "Google"},
|
|
69
|
+
{value: "azure", label: "Azure AKS", hint: "Microsoft"},
|
|
70
|
+
{value: "do", label: "DigitalOcean DOKS"}
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
exit 0 if Clack.cancel?(cloud)
|
|
74
|
+
|
|
75
|
+
replicas = Clack.range(
|
|
76
|
+
message: "Pod replicas:",
|
|
77
|
+
min: 1,
|
|
78
|
+
max: 20,
|
|
79
|
+
step: 1,
|
|
80
|
+
initial_value: 3
|
|
81
|
+
)
|
|
82
|
+
exit 0 if Clack.cancel?(replicas)
|
|
83
|
+
|
|
84
|
+
when "serverless"
|
|
85
|
+
cloud = Clack.select_key(
|
|
86
|
+
message: "Serverless platform:",
|
|
87
|
+
options: [
|
|
88
|
+
{value: "lambda", label: "AWS Lambda", key: "a"},
|
|
89
|
+
{value: "cloud_functions", label: "GCP Cloud Functions", key: "g"},
|
|
90
|
+
{value: "azure_functions", label: "Azure Functions", key: "z"},
|
|
91
|
+
{value: "cloudflare", label: "Cloudflare Workers", key: "c"}
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
exit 0 if Clack.cancel?(cloud)
|
|
95
|
+
replicas = "auto"
|
|
96
|
+
|
|
97
|
+
when "bare_metal"
|
|
98
|
+
cloud = "self-hosted"
|
|
99
|
+
replicas = Clack.range(
|
|
100
|
+
message: "Server count:",
|
|
101
|
+
min: 1,
|
|
102
|
+
max: 50,
|
|
103
|
+
step: 1,
|
|
104
|
+
initial_value: 2
|
|
105
|
+
)
|
|
106
|
+
exit 0 if Clack.cancel?(replicas)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- Group multiselect ---
|
|
110
|
+
addons = Clack.group_multiselect(
|
|
111
|
+
message: "Add-ons:",
|
|
112
|
+
selectable_groups: true,
|
|
113
|
+
group_spacing: 1,
|
|
114
|
+
options: [
|
|
115
|
+
{
|
|
116
|
+
label: "Observability",
|
|
117
|
+
options: [
|
|
118
|
+
{value: "prometheus", label: "Prometheus + Grafana"},
|
|
119
|
+
{value: "datadog", label: "Datadog"},
|
|
120
|
+
{value: "sentry", label: "Sentry", hint: "error tracking"}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: "Data",
|
|
125
|
+
options: [
|
|
126
|
+
{value: "postgres", label: "PostgreSQL"},
|
|
127
|
+
{value: "redis", label: "Redis"},
|
|
128
|
+
{value: "elasticsearch", label: "Elasticsearch"},
|
|
129
|
+
{value: "kafka", label: "Kafka", hint: "event streaming"}
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: "Security",
|
|
134
|
+
options: [
|
|
135
|
+
{value: "vault", label: "HashiCorp Vault", hint: "secrets"},
|
|
136
|
+
{value: "oauth", label: "OAuth2 / OIDC"},
|
|
137
|
+
{value: "waf", label: "WAF", hint: "web application firewall"}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
required: false
|
|
142
|
+
)
|
|
143
|
+
exit 0 if Clack.cancel?(addons)
|
|
144
|
+
|
|
145
|
+
# --- Autocomplete multiselect ---
|
|
146
|
+
team = Clack.autocomplete_multiselect(
|
|
147
|
+
message: "Notify team members:",
|
|
148
|
+
options: [
|
|
149
|
+
{value: "alice", label: "Alice Chen", hint: "backend"},
|
|
150
|
+
{value: "bob", label: "Bob Martinez", hint: "frontend"},
|
|
151
|
+
{value: "carol", label: "Carol Kim", hint: "SRE"},
|
|
152
|
+
{value: "dave", label: "Dave Patel", hint: "security"},
|
|
153
|
+
{value: "eve", label: "Eve Johansson", hint: "PM"},
|
|
154
|
+
{value: "frank", label: "Frank Okafor", hint: "QA"},
|
|
155
|
+
{value: "grace", label: "Grace Liu", hint: "data"},
|
|
156
|
+
{value: "hank", label: "Hank Müller", hint: "DevOps"}
|
|
157
|
+
],
|
|
158
|
+
required: false
|
|
159
|
+
)
|
|
160
|
+
exit 0 if Clack.cancel?(team)
|
|
161
|
+
|
|
162
|
+
# --- Date picker ---
|
|
163
|
+
deploy_date = Clack.date(
|
|
164
|
+
message: "Target deploy date:",
|
|
165
|
+
initial_value: Date.today + 7
|
|
166
|
+
)
|
|
167
|
+
exit 0 if Clack.cancel?(deploy_date)
|
|
168
|
+
|
|
169
|
+
# --- Confirm ---
|
|
170
|
+
go_live = Clack.confirm(
|
|
171
|
+
message: "Schedule deployment?",
|
|
172
|
+
initial_value: true
|
|
173
|
+
)
|
|
174
|
+
exit 0 if Clack.cancel?(go_live)
|
|
175
|
+
|
|
176
|
+
unless go_live
|
|
177
|
+
Clack.outro "Deployment cancelled."
|
|
178
|
+
exit 0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# --- Tasks with mid-task message updates ---
|
|
182
|
+
Clack.tasks(tasks: [
|
|
183
|
+
{title: "Validating configuration", task: -> {
|
|
184
|
+
sleep 0.6
|
|
185
|
+
}},
|
|
186
|
+
{title: "Provisioning infrastructure", task: ->(msg) {
|
|
187
|
+
msg.call("Creating #{deploy_target} cluster...")
|
|
188
|
+
sleep 0.8
|
|
189
|
+
msg.call("Configuring networking...")
|
|
190
|
+
sleep 0.5
|
|
191
|
+
msg.call("Setting up DNS...")
|
|
192
|
+
sleep 0.4
|
|
193
|
+
}},
|
|
194
|
+
{title: "Deploying #{project}", task: ->(msg) {
|
|
195
|
+
msg.call("Building container image...")
|
|
196
|
+
sleep 0.7
|
|
197
|
+
msg.call("Pushing to registry...")
|
|
198
|
+
sleep 0.5
|
|
199
|
+
msg.call("Rolling out #{replicas} replicas...")
|
|
200
|
+
sleep 0.6
|
|
201
|
+
}},
|
|
202
|
+
{title: "Running health checks", task: -> {
|
|
203
|
+
sleep 0.8
|
|
204
|
+
}}
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
# --- Summary ---
|
|
208
|
+
Clack.log.success "Deployment scheduled!"
|
|
209
|
+
Clack.log.step "Project: #{project}"
|
|
210
|
+
Clack.log.step "Stack: #{stack}"
|
|
211
|
+
Clack.log.step "Target: #{deploy_target} (#{cloud})"
|
|
212
|
+
Clack.log.step "Replicas: #{replicas}"
|
|
213
|
+
Clack.log.step "Add-ons: #{addons.join(", ")}" unless addons.empty?
|
|
214
|
+
Clack.log.step "Team: #{team.join(", ")}" unless team.empty?
|
|
215
|
+
Clack.log.step "Deploy date: #{deploy_date}"
|
|
216
|
+
|
|
217
|
+
Clack.note <<~MSG, title: "What happens next"
|
|
218
|
+
1. CI pipeline will run at #{deploy_date}
|
|
219
|
+
2. Canary deploy to 10% traffic
|
|
220
|
+
3. Full rollout after 30min soak
|
|
221
|
+
4. Team notified via Slack
|
|
222
|
+
MSG
|
|
223
|
+
|
|
224
|
+
Clack.outro "Happy shipping!"
|
data/lib/clack/colors.rb
CHANGED
|
@@ -8,12 +8,7 @@ module Clack
|
|
|
8
8
|
# - FORCE_COLOR environment variable forces colors on
|
|
9
9
|
module Colors
|
|
10
10
|
class << self
|
|
11
|
-
def enabled?
|
|
12
|
-
return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
|
|
13
|
-
return false if ENV["NO_COLOR"]
|
|
14
|
-
|
|
15
|
-
$stdout.tty?
|
|
16
|
-
end
|
|
11
|
+
def enabled? = Environment.colors_supported?
|
|
17
12
|
|
|
18
13
|
# @!group Foreground Colors (standard)
|
|
19
14
|
|
data/lib/clack/core/cursor.rb
CHANGED
|
@@ -13,9 +13,7 @@ module Clack
|
|
|
13
13
|
def enabled?
|
|
14
14
|
return @enabled unless @enabled.nil?
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
|
|
18
|
-
$stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
|
|
16
|
+
Environment.colors_supported?
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
# Visibility
|
data/lib/clack/core/prompt.rb
CHANGED
|
@@ -316,13 +316,23 @@ module Clack
|
|
|
316
316
|
end
|
|
317
317
|
|
|
318
318
|
# Build the final frame shown after interaction ends.
|
|
319
|
-
#
|
|
319
|
+
# Default renders a one-line summary: bar, symbol+message, styled final value.
|
|
320
|
+
# Override {#final_display} to customize what value is shown.
|
|
321
|
+
# Override this entirely for multi-line final output (e.g. MultilineText).
|
|
320
322
|
#
|
|
321
323
|
# @return [String] the final frame content
|
|
322
324
|
def build_final_frame
|
|
323
|
-
|
|
325
|
+
"#{bar}\n" \
|
|
326
|
+
"#{symbol_for_state} #{@message}\n" \
|
|
327
|
+
"#{bar} #{styled_final_display}\n"
|
|
324
328
|
end
|
|
325
329
|
|
|
330
|
+
# The text to display in the final frame after submit/cancel.
|
|
331
|
+
# Override in subclasses to customize (e.g. masked password, formatted date).
|
|
332
|
+
#
|
|
333
|
+
# @return [String]
|
|
334
|
+
def final_display = @value.to_s
|
|
335
|
+
|
|
326
336
|
# Check if prompt has reached a terminal state.
|
|
327
337
|
#
|
|
328
338
|
# @return [Boolean] true if state is :submit or :cancel
|
|
@@ -337,13 +347,18 @@ module Clack
|
|
|
337
347
|
def run_ci_mode
|
|
338
348
|
submit
|
|
339
349
|
if @state == :error
|
|
340
|
-
|
|
350
|
+
$stderr.print "#{Colors.yellow("!")} #{Colors.yellow("CI mode: validation failed for")} \"#{@message}\": #{@error_message}\n"
|
|
341
351
|
end
|
|
342
352
|
@value
|
|
343
353
|
end
|
|
344
354
|
|
|
345
355
|
private
|
|
346
356
|
|
|
357
|
+
def styled_final_display
|
|
358
|
+
text = final_display
|
|
359
|
+
(@state == :cancel) ? Colors.strikethrough(Colors.dim(text)) : Colors.dim(text)
|
|
360
|
+
end
|
|
361
|
+
|
|
347
362
|
def warn_narrow_terminal
|
|
348
363
|
return unless Environment.tty?(@output)
|
|
349
364
|
|
data/lib/clack/core/settings.rb
CHANGED
|
@@ -91,9 +91,9 @@ module Clack
|
|
|
91
91
|
aliases[key] if ACTIONS.include?(aliases[key])
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# Check if a key is a printable character
|
|
94
|
+
# Check if a key is a printable character (handles combining marks and multi-codepoint grapheme clusters)
|
|
95
95
|
def printable?(key)
|
|
96
|
-
key && key.length == 1 && key.ord >= PRINTABLE_CHAR_MIN
|
|
96
|
+
key && key.grapheme_clusters.length == 1 && key.ord >= PRINTABLE_CHAR_MIN
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Check if a key is a backspace/delete
|
|
@@ -63,19 +63,21 @@ module Clack
|
|
|
63
63
|
def handle_key(key)
|
|
64
64
|
return if terminal_state?
|
|
65
65
|
|
|
66
|
+
# Printable characters always feed the search field.
|
|
67
|
+
# This prevents vim aliases (j/k/h/l) from hijacking text input.
|
|
68
|
+
if Core::Settings.printable?(key)
|
|
69
|
+
handle_text_input(key)
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
action = Core::Settings.action?(key)
|
|
67
74
|
|
|
68
75
|
case action
|
|
69
|
-
when :cancel
|
|
70
|
-
|
|
71
|
-
when :
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
move_selection(-1)
|
|
75
|
-
when :down
|
|
76
|
-
move_selection(1)
|
|
77
|
-
else
|
|
78
|
-
handle_text_input(key)
|
|
76
|
+
when :cancel then @state = :cancel
|
|
77
|
+
when :enter then submit_selection
|
|
78
|
+
when :up then move_selection(-1)
|
|
79
|
+
when :down then move_selection(1)
|
|
80
|
+
else handle_text_input(key)
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
83
|
|
|
@@ -119,17 +121,7 @@ module Clack
|
|
|
119
121
|
lines.join
|
|
120
122
|
end
|
|
121
123
|
|
|
122
|
-
def
|
|
123
|
-
lines = []
|
|
124
|
-
lines << "#{bar}\n"
|
|
125
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
126
|
-
|
|
127
|
-
display_value = @filtered[@selected_index]&.[](:label) || @value
|
|
128
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_value)) : Colors.dim(display_value)
|
|
129
|
-
lines << "#{bar} #{display}\n"
|
|
130
|
-
|
|
131
|
-
lines.join
|
|
132
|
-
end
|
|
124
|
+
def final_display = @filtered[@selected_index]&.[](:label) || @value
|
|
133
125
|
|
|
134
126
|
private
|
|
135
127
|
|
|
@@ -7,10 +7,9 @@ module Clack
|
|
|
7
7
|
# Combines text input filtering with checkbox-style selection.
|
|
8
8
|
# Type to filter, Space to toggle, Enter to confirm.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# -
|
|
13
|
-
# - 'i': invert selection
|
|
10
|
+
# Unlike {Multiselect}, the 'a' (select all) and 'i' (invert) shortcuts
|
|
11
|
+
# are not available because those characters are needed for the search field.
|
|
12
|
+
# Similarly, vim-style j/k navigation is disabled in favor of typing.
|
|
14
13
|
#
|
|
15
14
|
# @example Basic usage
|
|
16
15
|
# colors = Clack.autocomplete_multiselect(
|
|
@@ -37,12 +36,12 @@ module Clack
|
|
|
37
36
|
# @param max_items [Integer] max visible options (default: 5)
|
|
38
37
|
# @param placeholder [String, nil] placeholder text when empty
|
|
39
38
|
# @param required [Boolean] require at least one selection (default: true)
|
|
40
|
-
# @param initial_values [Array
|
|
39
|
+
# @param initial_values [Array] values to pre-select
|
|
41
40
|
# @param filter [Proc, nil] custom filter proc receiving (option_hash, query_string)
|
|
42
41
|
# and returning true/false. When nil, the default fuzzy matching
|
|
43
42
|
# across label, value, and hint is used.
|
|
44
43
|
# @param opts [Hash] additional options passed to {Core::Prompt}
|
|
45
|
-
def initialize(message:, options:, max_items: 5, placeholder: nil, required: true, initial_values:
|
|
44
|
+
def initialize(message:, options:, max_items: 5, placeholder: nil, required: true, initial_values: [], filter: nil, **opts)
|
|
46
45
|
super(message:, **opts)
|
|
47
46
|
@all_options = normalize_options(options)
|
|
48
47
|
@max_items = max_items
|
|
@@ -54,7 +53,7 @@ module Clack
|
|
|
54
53
|
@selected_index = 0
|
|
55
54
|
@scroll_offset = 0
|
|
56
55
|
valid_values = Set.new(@all_options.map { |o| o[:value] })
|
|
57
|
-
@
|
|
56
|
+
@selected = Set.new(initial_values) & valid_values
|
|
58
57
|
update_filtered
|
|
59
58
|
end
|
|
60
59
|
|
|
@@ -63,78 +62,47 @@ module Clack
|
|
|
63
62
|
def handle_key(key)
|
|
64
63
|
return if terminal_state?
|
|
65
64
|
|
|
65
|
+
# Printable characters always feed the search field (except space, which toggles).
|
|
66
|
+
# This prevents vim aliases (j/k/h/l) from hijacking text input.
|
|
67
|
+
if Core::Settings.printable?(key) && key != " "
|
|
68
|
+
handle_text_input(key)
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
66
72
|
action = Core::Settings.action?(key)
|
|
67
73
|
|
|
68
74
|
case action
|
|
69
|
-
when :cancel
|
|
70
|
-
|
|
71
|
-
when :
|
|
72
|
-
|
|
73
|
-
when :
|
|
74
|
-
|
|
75
|
-
when :down
|
|
76
|
-
move_selection(1)
|
|
77
|
-
when :space
|
|
78
|
-
toggle_current
|
|
79
|
-
else
|
|
80
|
-
handle_char(key)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def handle_char(key)
|
|
85
|
-
# Shortcut keys only work when search field is empty
|
|
86
|
-
# to avoid interfering with typing filter text
|
|
87
|
-
if @search_text.empty?
|
|
88
|
-
case key&.downcase
|
|
89
|
-
when "a"
|
|
90
|
-
toggle_all
|
|
91
|
-
return
|
|
92
|
-
when "i"
|
|
93
|
-
invert_selection
|
|
94
|
-
return
|
|
95
|
-
end
|
|
75
|
+
when :cancel then @state = :cancel
|
|
76
|
+
when :enter then submit
|
|
77
|
+
when :up then move_selection(-1)
|
|
78
|
+
when :down then move_selection(1)
|
|
79
|
+
when :space then toggle_current
|
|
80
|
+
else handle_text_input(key)
|
|
96
81
|
end
|
|
97
|
-
handle_text_input(key)
|
|
98
82
|
end
|
|
99
83
|
|
|
100
84
|
def toggle_current
|
|
101
85
|
return if @filtered.empty?
|
|
102
86
|
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
@selected_values.delete(current_value)
|
|
106
|
-
else
|
|
107
|
-
@selected_values.add(current_value)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
87
|
+
opt = @filtered[@selected_index]
|
|
88
|
+
return if opt[:disabled]
|
|
110
89
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@selected_values.clear
|
|
90
|
+
if @selected.include?(opt[:value])
|
|
91
|
+
@selected.delete(opt[:value])
|
|
114
92
|
else
|
|
115
|
-
@
|
|
93
|
+
@selected.add(opt[:value])
|
|
116
94
|
end
|
|
117
95
|
end
|
|
118
96
|
|
|
119
|
-
def
|
|
120
|
-
@
|
|
121
|
-
if @selected_values.include?(opt[:value])
|
|
122
|
-
@selected_values.delete(opt[:value])
|
|
123
|
-
else
|
|
124
|
-
@selected_values.add(opt[:value])
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def submit_selection
|
|
130
|
-
if @required && @selected_values.empty?
|
|
97
|
+
def submit
|
|
98
|
+
if @required && @selected.empty?
|
|
131
99
|
@error_message = "Please select at least one option. Press #{Colors.cyan("space")} to select, #{Colors.cyan("enter")} to submit"
|
|
132
100
|
@state = :error
|
|
133
101
|
return
|
|
134
102
|
end
|
|
135
103
|
|
|
136
|
-
@value = @
|
|
137
|
-
|
|
104
|
+
@value = @selected.to_a
|
|
105
|
+
super
|
|
138
106
|
end
|
|
139
107
|
|
|
140
108
|
def build_frame
|
|
@@ -151,29 +119,19 @@ module Clack
|
|
|
151
119
|
|
|
152
120
|
lines << "#{active_bar} #{Colors.yellow("No matches found")}\n" if @filtered.empty? && !@search_text.empty?
|
|
153
121
|
|
|
154
|
-
lines << "#{active_bar} #{
|
|
155
|
-
lines << "#{bar_end}\n"
|
|
122
|
+
lines << "#{active_bar} #{keyboard_hints}\n"
|
|
156
123
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
lines
|
|
124
|
+
if @state in :error | :warning
|
|
125
|
+
lines.concat(validation_message_lines)
|
|
126
|
+
else
|
|
127
|
+
lines << "#{bar_end}\n"
|
|
161
128
|
end
|
|
162
129
|
|
|
163
130
|
lines.join
|
|
164
131
|
end
|
|
165
132
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
lines << "#{bar}\n"
|
|
169
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
170
|
-
|
|
171
|
-
labels = @all_options.select { |o| @selected_values.include?(o[:value]) }.map { |o| o[:label] }
|
|
172
|
-
display_text = labels.join(", ")
|
|
173
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
174
|
-
lines << "#{bar} #{display}\n"
|
|
175
|
-
|
|
176
|
-
lines.join
|
|
133
|
+
def final_display
|
|
134
|
+
@all_options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
|
|
177
135
|
end
|
|
178
136
|
|
|
179
137
|
private
|
|
@@ -199,12 +157,10 @@ module Clack
|
|
|
199
157
|
Colors.dim(" (#{@filtered.size} match#{"es" unless @filtered.size == 1})")
|
|
200
158
|
end
|
|
201
159
|
|
|
202
|
-
def
|
|
160
|
+
def keyboard_hints
|
|
203
161
|
Colors.dim([
|
|
204
162
|
"up/down: navigate",
|
|
205
163
|
"space: select",
|
|
206
|
-
"a: all",
|
|
207
|
-
"i: invert",
|
|
208
164
|
"enter: confirm"
|
|
209
165
|
].join(" | "))
|
|
210
166
|
end
|
|
@@ -220,8 +176,8 @@ module Clack
|
|
|
220
176
|
def scroll_items = @filtered
|
|
221
177
|
|
|
222
178
|
def option_display(opt, active)
|
|
223
|
-
|
|
224
|
-
checkbox = if
|
|
179
|
+
selected = @selected.include?(opt[:value])
|
|
180
|
+
checkbox = if selected
|
|
225
181
|
Colors.green(Symbols::S_CHECKBOX_SELECTED)
|
|
226
182
|
else
|
|
227
183
|
Colors.dim(Symbols::S_CHECKBOX_INACTIVE)
|
|
@@ -71,17 +71,7 @@ module Clack
|
|
|
71
71
|
lines.join
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def
|
|
75
|
-
lines = []
|
|
76
|
-
lines << "#{bar}\n"
|
|
77
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
78
|
-
|
|
79
|
-
selected = @value ? @active_label : @inactive_label
|
|
80
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(selected)) : Colors.dim(selected)
|
|
81
|
-
lines << "#{bar} #{display}\n"
|
|
82
|
-
|
|
83
|
-
lines.join
|
|
84
|
-
end
|
|
74
|
+
def final_display = @value ? @active_label : @inactive_label
|
|
85
75
|
|
|
86
76
|
private
|
|
87
77
|
|
data/lib/clack/prompts/date.rb
CHANGED
|
@@ -99,17 +99,7 @@ module Clack
|
|
|
99
99
|
lines.join
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
def
|
|
103
|
-
lines = []
|
|
104
|
-
lines << "#{bar}\n"
|
|
105
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
106
|
-
|
|
107
|
-
display_text = formatted_date
|
|
108
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
109
|
-
lines << "#{bar} #{display}\n"
|
|
110
|
-
|
|
111
|
-
lines.join
|
|
112
|
-
end
|
|
102
|
+
def final_display = formatted_date
|
|
113
103
|
|
|
114
104
|
private
|
|
115
105
|
|
|
@@ -164,28 +154,7 @@ module Clack
|
|
|
164
154
|
def adjust_segment(delta)
|
|
165
155
|
commit_input_buffer
|
|
166
156
|
@input_buffer = ""
|
|
167
|
-
|
|
168
|
-
case current_segment_type
|
|
169
|
-
when :year
|
|
170
|
-
@year = (@year + delta).clamp(1, 9999)
|
|
171
|
-
clamp_day_to_month
|
|
172
|
-
when :month
|
|
173
|
-
@month += delta
|
|
174
|
-
@month = wrap_value(@month, 1, 12)
|
|
175
|
-
clamp_day_to_month
|
|
176
|
-
when :day
|
|
177
|
-
max_day = days_in_month(@year, @month)
|
|
178
|
-
@day = wrap_value(@day + delta, 1, max_day)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
enforce_bounds
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def wrap_value(val, min, max)
|
|
185
|
-
return min if val > max
|
|
186
|
-
return max if val < min
|
|
187
|
-
|
|
188
|
-
val
|
|
157
|
+
update_segment(segment_value + delta, wrap: true)
|
|
189
158
|
end
|
|
190
159
|
|
|
191
160
|
def handle_digit(digit)
|
|
@@ -201,23 +170,38 @@ module Clack
|
|
|
201
170
|
def commit_input_buffer
|
|
202
171
|
return if @input_buffer.empty?
|
|
203
172
|
|
|
204
|
-
|
|
173
|
+
update_segment(@input_buffer.to_i)
|
|
205
174
|
@input_buffer = ""
|
|
175
|
+
end
|
|
206
176
|
|
|
177
|
+
def segment_value
|
|
207
178
|
case current_segment_type
|
|
208
|
-
when :year
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
when :month
|
|
212
|
-
@month = value.clamp(1, 12)
|
|
213
|
-
clamp_day_to_month
|
|
214
|
-
when :day
|
|
215
|
-
@day = value.clamp(1, days_in_month(@year, @month))
|
|
179
|
+
when :year then @year
|
|
180
|
+
when :month then @month
|
|
181
|
+
when :day then @day
|
|
216
182
|
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def update_segment(value, wrap: false)
|
|
186
|
+
constrain = wrap ? method(:wrap_value) : :clamp.to_proc
|
|
217
187
|
|
|
188
|
+
case current_segment_type
|
|
189
|
+
when :year then @year = value.clamp(1, 9999)
|
|
190
|
+
when :month then @month = constrain.call(value, 1, 12)
|
|
191
|
+
when :day then @day = constrain.call(value, 1, days_in_month(@year, @month))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
clamp_day_to_month
|
|
218
195
|
enforce_bounds
|
|
219
196
|
end
|
|
220
197
|
|
|
198
|
+
def wrap_value(val, min, max)
|
|
199
|
+
return min if val > max
|
|
200
|
+
return max if val < min
|
|
201
|
+
|
|
202
|
+
val
|
|
203
|
+
end
|
|
204
|
+
|
|
221
205
|
def current_segment_type = FORMATS[@format][:order][@segment]
|
|
222
206
|
|
|
223
207
|
def days_in_month(year, month)
|
|
@@ -126,18 +126,7 @@ module Clack
|
|
|
126
126
|
lines.join
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
def
|
|
130
|
-
lines = []
|
|
131
|
-
lines << "#{bar}\n"
|
|
132
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
133
|
-
|
|
134
|
-
labels = selected_options.map { |o| o[:label] }
|
|
135
|
-
display_text = labels.join(", ")
|
|
136
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
137
|
-
lines << "#{bar} #{display}\n"
|
|
138
|
-
|
|
139
|
-
lines.join
|
|
140
|
-
end
|
|
129
|
+
def final_display = selected_options.map { |o| o[:label] }.join(", ")
|
|
141
130
|
|
|
142
131
|
private
|
|
143
132
|
|
|
@@ -103,17 +103,8 @@ module Clack
|
|
|
103
103
|
lines.join
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
lines << "#{bar}\n"
|
|
109
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
110
|
-
|
|
111
|
-
labels = @options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }
|
|
112
|
-
display_text = labels.join(", ")
|
|
113
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
114
|
-
lines << "#{bar} #{display}\n"
|
|
115
|
-
|
|
116
|
-
lines.join
|
|
106
|
+
def final_display
|
|
107
|
+
@options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
|
|
117
108
|
end
|
|
118
109
|
|
|
119
110
|
private
|
|
@@ -51,17 +51,7 @@ module Clack
|
|
|
51
51
|
lines.join
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def
|
|
55
|
-
lines = []
|
|
56
|
-
lines << "#{bar}\n"
|
|
57
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
58
|
-
|
|
59
|
-
masked = @mask * @value.grapheme_clusters.length
|
|
60
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(masked)) : Colors.dim(masked)
|
|
61
|
-
lines << "#{bar} #{display}\n"
|
|
62
|
-
|
|
63
|
-
lines.join
|
|
64
|
-
end
|
|
54
|
+
def final_display = @mask * @value.grapheme_clusters.length
|
|
65
55
|
|
|
66
56
|
private
|
|
67
57
|
|
data/lib/clack/prompts/path.rb
CHANGED
|
@@ -124,17 +124,6 @@ module Clack
|
|
|
124
124
|
lines.join
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
def build_final_frame
|
|
128
|
-
lines = []
|
|
129
|
-
lines << "#{bar}\n"
|
|
130
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
131
|
-
|
|
132
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
|
|
133
|
-
lines << "#{bar} #{display}\n"
|
|
134
|
-
|
|
135
|
-
lines.join
|
|
136
|
-
end
|
|
137
|
-
|
|
138
127
|
private
|
|
139
128
|
|
|
140
129
|
def update_suggestions
|
data/lib/clack/prompts/range.rb
CHANGED
|
@@ -67,17 +67,7 @@ module Clack
|
|
|
67
67
|
lines.join
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def
|
|
71
|
-
lines = []
|
|
72
|
-
lines << "#{bar}\n"
|
|
73
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
74
|
-
|
|
75
|
-
display = format_value(@value)
|
|
76
|
-
styled = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display)) : Colors.dim(display)
|
|
77
|
-
lines << "#{bar} #{styled}\n"
|
|
78
|
-
|
|
79
|
-
lines.join
|
|
80
|
-
end
|
|
70
|
+
def final_display = format_value(@value)
|
|
81
71
|
|
|
82
72
|
private
|
|
83
73
|
|
data/lib/clack/prompts/select.rb
CHANGED
|
@@ -80,17 +80,7 @@ module Clack
|
|
|
80
80
|
lines.join
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def
|
|
84
|
-
lines = []
|
|
85
|
-
lines << "#{bar}\n"
|
|
86
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
87
|
-
|
|
88
|
-
label = current_option[:label]
|
|
89
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
|
|
90
|
-
lines << "#{bar} #{display}\n"
|
|
91
|
-
|
|
92
|
-
lines.join
|
|
93
|
-
end
|
|
83
|
+
def final_display = current_option[:label]
|
|
94
84
|
|
|
95
85
|
private
|
|
96
86
|
|
|
@@ -63,17 +63,7 @@ module Clack
|
|
|
63
63
|
lines.join
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
lines = []
|
|
68
|
-
lines << "#{bar}\n"
|
|
69
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
70
|
-
|
|
71
|
-
label = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
|
|
72
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
|
|
73
|
-
lines << "#{bar} #{display}\n"
|
|
74
|
-
|
|
75
|
-
lines.join
|
|
76
|
-
end
|
|
66
|
+
def final_display = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
|
|
77
67
|
|
|
78
68
|
private
|
|
79
69
|
|
data/lib/clack/prompts/text.rb
CHANGED
|
@@ -104,17 +104,6 @@ module Clack
|
|
|
104
104
|
lines.join
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def build_final_frame
|
|
108
|
-
lines = []
|
|
109
|
-
lines << "#{bar}\n"
|
|
110
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
111
|
-
|
|
112
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
|
|
113
|
-
lines << "#{bar} #{display}\n"
|
|
114
|
-
|
|
115
|
-
lines.join
|
|
116
|
-
end
|
|
117
|
-
|
|
118
107
|
private
|
|
119
108
|
|
|
120
109
|
# Complete the current input using the longest common prefix of matching candidates.
|
data/lib/clack/symbols.rb
CHANGED
|
@@ -26,8 +26,7 @@ module Clack
|
|
|
26
26
|
# Explicit override
|
|
27
27
|
return ENV["CLACK_UNICODE"] == "1" if ENV["CLACK_UNICODE"]
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
$stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
|
|
29
|
+
Environment.colors_supported?
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -36,7 +35,7 @@ module Clack
|
|
|
36
35
|
# Unicode cancel step indicator, or ASCII fallback.
|
|
37
36
|
S_STEP_CANCEL = unicode? ? "■" : "x"
|
|
38
37
|
# Unicode error step indicator, or ASCII fallback.
|
|
39
|
-
S_STEP_ERROR = unicode? ? "▲" : "
|
|
38
|
+
S_STEP_ERROR = unicode? ? "▲" : "!"
|
|
40
39
|
# Unicode submit step indicator, or ASCII fallback.
|
|
41
40
|
S_STEP_SUBMIT = unicode? ? "◇" : "o"
|
|
42
41
|
|
data/lib/clack/version.rb
CHANGED
data/lib/clack.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "clack/version"
|
|
4
|
+
require_relative "clack/environment"
|
|
4
5
|
require_relative "clack/symbols"
|
|
5
6
|
require_relative "clack/colors"
|
|
6
|
-
require_relative "clack/environment"
|
|
7
7
|
require_relative "clack/utils"
|
|
8
8
|
require_relative "clack/core/cursor"
|
|
9
9
|
require_relative "clack/core/settings"
|
|
@@ -508,15 +508,20 @@ module Clack
|
|
|
508
508
|
end
|
|
509
509
|
end
|
|
510
510
|
|
|
511
|
-
# Terminal cleanup on exit
|
|
511
|
+
# Terminal cleanup on exit — show cursor if it was hidden.
|
|
512
|
+
# Uses raw write(2) for async-signal safety in trap handlers.
|
|
513
|
+
CURSOR_SHOW = "\e[?25h"
|
|
514
|
+
|
|
512
515
|
at_exit do
|
|
513
|
-
print
|
|
516
|
+
$stdout.print Clack::Core::Cursor.show
|
|
517
|
+
rescue IOError, SystemCallError
|
|
518
|
+
# Output unavailable
|
|
514
519
|
end
|
|
515
520
|
|
|
516
|
-
# Chain INT handler to restore cursor before passing to previous handler
|
|
521
|
+
# Chain INT handler to restore cursor before passing to previous handler.
|
|
517
522
|
previous_int_handler = trap("INT") do
|
|
518
523
|
begin
|
|
519
|
-
|
|
524
|
+
$stdout.write_nonblock(CURSOR_SHOW)
|
|
520
525
|
rescue IOError, SystemCallError
|
|
521
526
|
# Output unavailable — nothing we can do
|
|
522
527
|
end
|
|
@@ -532,10 +537,10 @@ previous_int_handler = trap("INT") do
|
|
|
532
537
|
end
|
|
533
538
|
end
|
|
534
539
|
|
|
535
|
-
# Handle SIGTERM similarly to INT — restore cursor on graceful kill
|
|
540
|
+
# Handle SIGTERM similarly to INT — restore cursor on graceful kill.
|
|
536
541
|
trap("TERM") do
|
|
537
542
|
begin
|
|
538
|
-
|
|
543
|
+
$stdout.write_nonblock(CURSOR_SHOW)
|
|
539
544
|
rescue IOError, SystemCallError
|
|
540
545
|
# Output unavailable
|
|
541
546
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Whittaker
|
|
@@ -34,6 +34,8 @@ files:
|
|
|
34
34
|
- examples/images/select_example.rb
|
|
35
35
|
- examples/images/spinner_example.rb
|
|
36
36
|
- examples/images/text_example.rb
|
|
37
|
+
- examples/migration_from_tty_prompt.rb
|
|
38
|
+
- examples/showcase.rb
|
|
37
39
|
- examples/spinner_demo.rb
|
|
38
40
|
- examples/tasks_demo.rb
|
|
39
41
|
- examples/validation.rb
|
|
@@ -101,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
101
103
|
- !ruby/object:Gem::Version
|
|
102
104
|
version: '0'
|
|
103
105
|
requirements: []
|
|
104
|
-
rubygems_version:
|
|
106
|
+
rubygems_version: 4.0.8
|
|
105
107
|
specification_version: 4
|
|
106
108
|
summary: Beautiful, minimal CLI prompts
|
|
107
109
|
test_files: []
|