clack 0.4.4 → 0.4.5
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 +15 -0
- data/README.md +45 -8
- data/examples/migration_from_tty_prompt.rb +218 -0
- data/examples/showcase.rb +224 -0
- data/lib/clack/prompts/autocomplete.rb +12 -10
- data/lib/clack/prompts/autocomplete_multiselect.rb +37 -72
- data/lib/clack/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efda51d5b5f8c7aac33181bc36f72761117fc3b3e6c5707d8197d55d2fde99c0
|
|
4
|
+
data.tar.gz: 1e324cda3ac98ccefd939e213827e89f36e4cdd22aa237186412ce82bb43d915
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96263cb6a717237125ba7e48cc6bdce5166a49b7ed65d7755c86a0ca3ff0db5e7a4616cc4e84b88577dd67cbda189a02dc7f8dc6f2b2b998c72b7c2475c5ca65
|
|
7
|
+
data.tar.gz: 5a3179bd6b5d393bbc4dc871584101a929b21fc1ee081b109845905a716e19a54ed2dbd39f6a13b5d3acdfa20c13a8f73d1cb9fd05991d16b4dbcbc74d48a3f5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.5] - 2026-02-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Autocomplete` and `AutocompleteMultiselect` now route all printable characters to the search field first, fixing vim aliases (`j`/`k`/`h`/`l`) hijacking text input
|
|
7
|
+
- `AutocompleteMultiselect` no longer intercepts `a`/`i` keys as shortcuts — they conflicted with typing in the search field
|
|
8
|
+
- `AutocompleteMultiselect#toggle_current` now respects `:disabled` options, matching `Multiselect` behavior
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `AutocompleteMultiselect` internals aligned with `Multiselect`: `@selected` naming, `submit` override pattern, `keyboard_hints` method, `build_frame` validation rendering, `initial_values: []` default
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `examples/showcase.rb` — full-featured demo with branching, autocomplete, date, range, group multiselect, tasks
|
|
15
|
+
- `examples/migration_from_tty_prompt.rb` — side-by-side migration guide from tty-prompt
|
|
16
|
+
- README: "Why Clack?" comparison table vs tty-prompt/highline, quick reference table, TL;DR tagline, vim key notes on autocomplete sections
|
|
17
|
+
|
|
3
18
|
## [0.4.4] - 2026-02-21
|
|
4
19
|
|
|
5
20
|
### 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!"
|
|
@@ -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
|
|
|
@@ -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,13 +119,12 @@ 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
|
|
@@ -168,7 +135,7 @@ module Clack
|
|
|
168
135
|
lines << "#{bar}\n"
|
|
169
136
|
lines << "#{symbol_for_state} #{@message}\n"
|
|
170
137
|
|
|
171
|
-
labels = @all_options.select { |o| @
|
|
138
|
+
labels = @all_options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }
|
|
172
139
|
display_text = labels.join(", ")
|
|
173
140
|
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
174
141
|
lines << "#{bar} #{display}\n"
|
|
@@ -199,12 +166,10 @@ module Clack
|
|
|
199
166
|
Colors.dim(" (#{@filtered.size} match#{"es" unless @filtered.size == 1})")
|
|
200
167
|
end
|
|
201
168
|
|
|
202
|
-
def
|
|
169
|
+
def keyboard_hints
|
|
203
170
|
Colors.dim([
|
|
204
171
|
"up/down: navigate",
|
|
205
172
|
"space: select",
|
|
206
|
-
"a: all",
|
|
207
|
-
"i: invert",
|
|
208
173
|
"enter: confirm"
|
|
209
174
|
].join(" | "))
|
|
210
175
|
end
|
|
@@ -220,8 +185,8 @@ module Clack
|
|
|
220
185
|
def scroll_items = @filtered
|
|
221
186
|
|
|
222
187
|
def option_display(opt, active)
|
|
223
|
-
|
|
224
|
-
checkbox = if
|
|
188
|
+
selected = @selected.include?(opt[:value])
|
|
189
|
+
checkbox = if selected
|
|
225
190
|
Colors.green(Symbols::S_CHECKBOX_SELECTED)
|
|
226
191
|
else
|
|
227
192
|
Colors.dim(Symbols::S_CHECKBOX_INACTIVE)
|
data/lib/clack/version.rb
CHANGED
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.5
|
|
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
|