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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5a8d3270e58cb1ad88664e73215c2ea5217211829680707d7616714ba93c926
4
- data.tar.gz: c8f58966b18a015c26b7669ebd495c0e7ba75d830088ad3135a0971dd03bc874
3
+ metadata.gz: efda51d5b5f8c7aac33181bc36f72761117fc3b3e6c5707d8197d55d2fde99c0
4
+ data.tar.gz: 1e324cda3ac98ccefd939e213827e89f36e4cdd22aa237186412ce82bb43d915
5
5
  SHA512:
6
- metadata.gz: f3dbfd192f557ee0cd476be3ed80f54a57273e786d9120e3372c85527582ad8a30994b4202c83c309135383e6bedaaf275e756961912420961c09a24e53576e2
7
- data.tar.gz: a692229800f14a3160aee3eca80f6c12b296c288e27dbf57b307b509090fdf0c72ca4e3712213f9515427ed0a48c70ee73534f85db887bb02ea1bfb9f0cf0177
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** - 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`
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"] # Pre-selected values
327
+ initial_values: ["red"], # Pre-selected values
328
+ max_items: 5 # Default; scrollable via ↑↓
324
329
  )
325
330
  ```
326
331
 
327
- **Shortcuts:** `Space` toggle | `a` toggle all | `i` invert | `Enter` confirm
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
- @state = :cancel
71
- when :enter
72
- submit_selection
73
- when :up
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
- # Shortcuts:
11
- # - Space: toggle current option
12
- # - 'a': toggle all options
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, nil] values to pre-select
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: nil, filter: nil, **opts)
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
- @selected_values = Set.new(initial_values || []) & valid_values
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
- @state = :cancel
71
- when :enter
72
- submit_selection
73
- when :up
74
- move_selection(-1)
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
- current_value = @filtered[@selected_index][:value]
104
- if @selected_values.include?(current_value)
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
- def toggle_all
112
- if @selected_values.size == @all_options.size
113
- @selected_values.clear
90
+ if @selected.include?(opt[:value])
91
+ @selected.delete(opt[:value])
114
92
  else
115
- @all_options.each { |opt| @selected_values.add(opt[:value]) }
93
+ @selected.add(opt[:value])
116
94
  end
117
95
  end
118
96
 
119
- def invert_selection
120
- @all_options.each do |opt|
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 = @selected_values.to_a
137
- submit
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} #{instructions}\n"
155
- lines << "#{bar_end}\n"
122
+ lines << "#{active_bar} #{keyboard_hints}\n"
156
123
 
157
- validation_lines = validation_message_lines
158
- if validation_lines.any?
159
- lines[-1] = validation_lines.first
160
- lines.concat(validation_lines[1..])
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| @selected_values.include?(o[:value]) }.map { |o| o[:label] }
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 instructions
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
- is_selected = @selected_values.include?(opt[:value])
224
- checkbox = if is_selected
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Clack
4
4
  # Current gem version.
5
- VERSION = "0.4.4"
5
+ VERSION = "0.4.5"
6
6
  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
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