clack 0.1.4 → 0.4.1

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/README.md +184 -10
  4. data/examples/advanced_prompts.rb +63 -0
  5. data/examples/basic.rb +15 -0
  6. data/examples/create_app.rb +86 -0
  7. data/examples/date_demo.rb +40 -0
  8. data/examples/demo.rb +179 -0
  9. data/examples/full_demo.rb +84 -0
  10. data/examples/group_demo.rb +79 -0
  11. data/examples/images/confirm_example.rb +12 -0
  12. data/examples/images/multiselect_example.rb +15 -0
  13. data/examples/images/password_example.rb +10 -0
  14. data/examples/images/select_example.rb +15 -0
  15. data/examples/images/spinner_example.rb +11 -0
  16. data/examples/images/text_example.rb +11 -0
  17. data/examples/spinner_demo.rb +38 -0
  18. data/examples/tasks_demo.rb +59 -0
  19. data/examples/validation.rb +73 -0
  20. data/lib/clack/colors.rb +97 -3
  21. data/lib/clack/core/ci_mode.rb +35 -0
  22. data/lib/clack/core/cursor.rb +1 -0
  23. data/lib/clack/core/fuzzy_matcher.rb +121 -0
  24. data/lib/clack/core/key_reader.rb +5 -0
  25. data/lib/clack/core/prompt.rb +98 -20
  26. data/lib/clack/core/scroll_helper.rb +54 -0
  27. data/lib/clack/core/settings.rb +11 -3
  28. data/lib/clack/core/text_input_helper.rb +28 -8
  29. data/lib/clack/environment.rb +1 -1
  30. data/lib/clack/log.rb +51 -0
  31. data/lib/clack/note.rb +7 -0
  32. data/lib/clack/prompts/autocomplete.rb +27 -34
  33. data/lib/clack/prompts/autocomplete_multiselect.rb +23 -66
  34. data/lib/clack/prompts/date.rb +280 -0
  35. data/lib/clack/prompts/group_multiselect.rb +46 -18
  36. data/lib/clack/prompts/multiline_text.rb +8 -9
  37. data/lib/clack/prompts/multiselect.rb +3 -5
  38. data/lib/clack/prompts/password.rb +5 -10
  39. data/lib/clack/prompts/path.rb +24 -27
  40. data/lib/clack/prompts/progress.rb +2 -6
  41. data/lib/clack/prompts/range.rb +112 -0
  42. data/lib/clack/prompts/select.rb +2 -6
  43. data/lib/clack/prompts/select_key.rb +5 -8
  44. data/lib/clack/prompts/spinner.rb +12 -10
  45. data/lib/clack/prompts/tasks.rb +47 -62
  46. data/lib/clack/prompts/text.rb +61 -5
  47. data/lib/clack/stream.rb +32 -3
  48. data/lib/clack/symbols.rb +25 -0
  49. data/lib/clack/task_log.rb +3 -5
  50. data/lib/clack/testing.rb +171 -0
  51. data/lib/clack/transformers.rb +8 -7
  52. data/lib/clack/validators.rb +33 -2
  53. data/lib/clack/version.rb +2 -1
  54. data/lib/clack.rb +123 -215
  55. metadata +23 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 671583e7a4d14fd6951369f179ed7ba6baf97c20fb54e202e08e710763448ee4
4
- data.tar.gz: 28469575a56abce1883038147ac2ce5735b79cd976938d448b37227fe044f941
3
+ metadata.gz: c0622f234b9f83906c6440e54c29a12715ecfc7a2b3777ec68efaf3882defcb7
4
+ data.tar.gz: 450c6376c40ad88a27683294e97292f227e2081ccc90a0c076949bba3e5afd1d
5
5
  SHA512:
6
- metadata.gz: 0a2d08f4e1eaf1434d7692666fb145ba6bd98c2f368193565a4ed0f4a23bf1b937b9909be4fd16541b7f26f7dc018b2f06b95215104c8121f0af964ea4487193
7
- data.tar.gz: 879876e598e6213239b3aaf945e909587c02fa34fcbf2b971d816a41c21149fefbca1065aca5890e4cbb7148382b4a54262a21a2309dabcc5466cd4e43d13aea
6
+ metadata.gz: '08a7d471122e503168817430b65ad3618488eef6a2c79229a50e20a0a3ff3f2df741faef330a81b57875ddca3a37a68d0aebd19e770b7bbe73addd9a737bc9ec'
7
+ data.tar.gz: 76e01f5eef142fd64069e1d9ba705edebc5694834ab68d5f7a8370eb345f1bf1473f2f1771d0178ed80e0b6c977c05d032d0059112bf36e72dd2fbe18f3850a3
data/CHANGELOG.md CHANGED
@@ -1,6 +1,81 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.4.1] - 2026-02-20
4
+
5
+ ### Fixed
6
+ - `AutocompleteMultiselect` now renders warning validation messages (was error-only)
7
+ - CI mode no longer writes ANSI escape codes to non-TTY output
8
+ - CI mode prints a warning when validation fails instead of silently returning
9
+ - Spinner animation restarts from frame 0 when reused
10
+ - `Environment.raw_mode_supported?` catches specific exceptions instead of bare rescue
11
+ - `FuzzyMatcher.filter` pre-computes downcased query (performance optimization for large lists)
12
+ - Removed dead instance variables in Path, Testing, and Spinner
13
+ - Simplified `Range#clamp` by removing unreachable branch
14
+
15
+ ### Changed
16
+ - Updated gem dependencies (rubocop 1.84, standard 1.54, prism 1.9, bigdecimal 4.0)
17
+ - Autocomplete YARD docs corrected: default filter is fuzzy matching, not substring
18
+ - Expanded YARD `@option` documentation for spinner, multiselect, group_multiselect, path, tasks
19
+ - README and ARCHITECTURE.md updated to cover all v0.3.0-v0.4.0 features
20
+
21
+ ## [0.4.0] - 2026-02-19
22
+
23
+ ### Added
24
+ - `range` slider prompt for numeric selection (`Clack.range(message:, min:, max:, step:, default:)`)
25
+ - Tab completion on `text` prompt via `completions:` parameter (array or proc)
26
+ - Minimum terminal width warning (non-blocking, 40 columns)
27
+
28
+ ### Changed
29
+ - Path prompt caches directory listings to avoid repeated filesystem scans on every keystroke
30
+
31
+ ## [0.3.0] - 2026-02-19
32
+
33
+ ### Added
34
+ - `Clack::Testing` module with first-class test helpers (`simulate`, `simulate_with_output`, `PromptDriver`)
35
+ - `Clack::Core::FuzzyMatcher` with scored fuzzy matching (consecutive/boundary/start bonuses)
36
+ - CI / non-interactive mode: `Clack.update_settings(ci_mode: true)` or `:auto` to auto-detect
37
+ - `autocomplete_multiselect` now accepts `filter:` proc for custom matching logic
38
+
39
+ ### Changed
40
+ - Autocomplete prompts default to fuzzy matching instead of substring matching
41
+ - Spinner is now thread-safe: guards against double-finish, protects `@cancelled` reads with mutex
42
+
43
+ ## [0.2.1] - 2026-02-19
44
+
45
+ ### Added
46
+ - `Core::ScrollHelper` mixin extracted from scroll/filter logic across 3 prompts
47
+
48
+ ### Changed
49
+ - `TextInputHelper` parameterized via `text_value`/`text_value=` for custom backing stores
50
+ - Tasks prompt now reuses `Core::Spinner` instead of inline spinner implementation
51
+ - Removed redundant `@value = nil` from SelectKey
52
+
53
+ ## [0.2.0] - 2026-02-19
54
+
55
+ ### Added
56
+ - `date` prompt for inline segmented date selection with Tab/arrow navigation and digit typing
57
+ - Date min/max enforcement: clamps values to bounds during navigation
58
+ - Date-specific validators: `Validators.future_date`, `Validators.past_date`, `Validators.date_range`
59
+ - `autocomplete` now accepts `filter:` proc for custom matching logic
60
+ - `tasks` now passes a message-update proc to task callbacks for mid-task status updates
61
+ - `tasks` now supports `enabled:` flag to conditionally skip tasks
62
+ - 100% YARD documentation coverage (was 79%)
63
+ - 30 new edge-case tests covering warning validation, transforms, date boundaries, and more
64
+
65
+ ### Changed
66
+ - `Transformers.resolve` now accepts any object responding to `#call` (not just Proc)
67
+ - Standardized required-validation error messages across multiselect variants
68
+ - Extracted `dispatch_key` from `handle_key` in base `Prompt` so warning/error state transitions are handled centrally for all prompts
69
+ - Ruby idiom improvements: pattern matching, endless methods, guard clauses, `find_index`
70
+ - Gemspec now excludes dev-only files (cast, exp, gif, svg) from the gem package
71
+ - Examples include `require "clack"` alternative comment for gem users
72
+
73
+ ### Fixed
74
+ - `Password#handle_input` now correctly handles backspace (was unreachable due to guard order)
75
+ - `Validators.as_warning` no longer double-wraps values that are already `Warning` instances
76
+ - `AutocompleteMultiselect` backspace was dead code (printable guard blocked it)
77
+ - `MultilineText` and `Autocomplete` now render warning validation messages (was error-only)
78
+ - Removed dead `@buffer` and `@name` instance variables from `TaskLogGroup`
4
79
 
5
80
  ## [0.1.4] - 2026-01-23
6
81
 
data/README.md CHANGED
@@ -95,9 +95,16 @@ agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --sp
95
95
  All prompts return the user's input, or `Clack::CANCEL` if they pressed Escape/Ctrl+C.
96
96
 
97
97
  ```ruby
98
- # Always check for cancellation
98
+ # Check for cancellation
99
99
  result = Clack.text(message: "Name?")
100
100
  exit 1 if Clack.cancel?(result)
101
+
102
+ # Or use handle_cancel for a one-liner that prints "Cancelled" and returns true
103
+ result = Clack.text(message: "Name?")
104
+ exit 1 if Clack.handle_cancel(result)
105
+
106
+ # With a custom message
107
+ exit 1 if Clack.handle_cancel(result, "Aborted by user")
101
108
  ```
102
109
 
103
110
  ### Validation & Transforms
@@ -144,15 +151,25 @@ Built-in validators and transformers:
144
151
 
145
152
  ```ruby
146
153
  # Validators - return error message, Warning, or nil
147
- Clack::Validators.required
148
- Clack::Validators.min_length(8)
154
+ Clack::Validators.required # Non-empty input
155
+ Clack::Validators.min_length(3) # Minimum character count
156
+ Clack::Validators.max_length(100) # Maximum character count
149
157
  Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
150
- Clack::Validators.email
151
- Clack::Validators.combine(v1, v2) # First error/warning wins
158
+ Clack::Validators.email # Email format (user@host.tld)
159
+ Clack::Validators.url # URL format (http/https)
160
+ Clack::Validators.integer # Integer string ("-5", "42")
161
+ Clack::Validators.in_range(1..100) # Numeric range (parses as int)
162
+ Clack::Validators.one_of(%w[a b c]) # Allowlist check
163
+ Clack::Validators.path_exists # File/dir exists on disk
164
+ Clack::Validators.directory_exists # Directory exists on disk
165
+ Clack::Validators.future_date # Date strictly after today
166
+ Clack::Validators.past_date # Date strictly before today
167
+ Clack::Validators.date_range(min: d1, max: d2) # Date within range
168
+ Clack::Validators.combine(v1, v2) # First error/warning wins
152
169
 
153
170
  # Warning validators - allow user to confirm or edit
154
- Clack::Validators.file_exists_warning # For file overwrite confirmations
155
- Clack::Validators.as_warning(validator) # Convert any validator to warning
171
+ Clack::Validators.file_exists_warning # For file overwrite confirmations
172
+ Clack::Validators.as_warning(validator) # Convert any validator to warning
156
173
 
157
174
  # Transformers - normalize the value (use :symbol or Clack::Transformers.name)
158
175
  :strip / :trim # Remove leading/trailing whitespace
@@ -176,7 +193,24 @@ name = Clack.text(
176
193
  placeholder: "my-project", # Shown when empty (dim)
177
194
  default_value: "untitled", # Used if submitted empty
178
195
  initial_value: "hello-world", # Pre-filled, editable
179
- validate: ->(v) { "Required!" if v.empty? }
196
+ validate: ->(v) { "Required!" if v.empty? },
197
+ help: "Letters, numbers, and dashes only" # Contextual help text
198
+ )
199
+ ```
200
+
201
+ **Tab completion** - press `Tab` to cycle through matching candidates:
202
+
203
+ ```ruby
204
+ # Tab completion from a static list
205
+ cmd = Clack.text(
206
+ message: "Command?",
207
+ completions: %w[build test deploy lint format]
208
+ )
209
+
210
+ # Dynamic tab completion
211
+ file = Clack.text(
212
+ message: "File?",
213
+ completions: ->(input) { Dir.glob("#{input}*") }
180
214
  )
181
215
  ```
182
216
 
@@ -191,6 +225,18 @@ secret = Clack.password(
191
225
  )
192
226
  ```
193
227
 
228
+ ### Multiline Text
229
+
230
+ Multi-line text input. Enter inserts a newline, Ctrl+D submits.
231
+
232
+ ```ruby
233
+ bio = Clack.multiline_text(
234
+ message: "Tell us about yourself",
235
+ initial_value: "Hello!\n",
236
+ validate: ->(v) { "Too short" if v.strip.length < 10 }
237
+ )
238
+ ```
239
+
194
240
  ### Confirm
195
241
 
196
242
  <img src="examples/images/confirm.svg" alt="Confirm prompt">
@@ -247,7 +293,7 @@ features = Clack.multiselect(
247
293
 
248
294
  ### Autocomplete
249
295
 
250
- Type to filter from a list of options.
296
+ Type to filter from a list of options. Filtering uses **fuzzy matching** by default -- characters must appear in order but don't need to be consecutive (e.g. "fb" matches "foobar"). Pass `filter:` to override with custom logic.
251
297
 
252
298
  ```ruby
253
299
  color = Clack.autocomplete(
@@ -255,6 +301,13 @@ color = Clack.autocomplete(
255
301
  options: %w[red orange yellow green blue indigo violet],
256
302
  placeholder: "Type to search..."
257
303
  )
304
+
305
+ # Custom filter logic (receives option hash and query string)
306
+ cmd = Clack.autocomplete(
307
+ message: "Select command",
308
+ options: commands,
309
+ filter: ->(opt, query) { opt[:label].start_with?(query) }
310
+ )
258
311
  ```
259
312
 
260
313
  ### Autocomplete Multiselect
@@ -287,6 +340,37 @@ project_dir = Clack.path(
287
340
 
288
341
  **Navigation:** Type to filter | `Tab` to autocomplete | `↑↓` to select
289
342
 
343
+ ### Date
344
+
345
+ Segmented date picker with three format modes.
346
+
347
+ ```ruby
348
+ date = Clack.date(
349
+ message: "Release date?",
350
+ format: :us, # :iso (YYYY-MM-DD), :us (MM/DD/YYYY), :eu (DD/MM/YYYY)
351
+ initial_value: Date.today + 7,
352
+ min: Date.today,
353
+ max: Date.today + 365,
354
+ validate: ->(d) { "Not a Friday" unless d.friday? }
355
+ )
356
+ ```
357
+
358
+ **Navigation:** `Tab`/`←→` between segments | `↑↓` adjust value | type digits directly
359
+
360
+ ### Range
361
+
362
+ Numeric selection with a visual slider track. Navigate with `←→` or `↑↓` arrow keys (or `hjkl`).
363
+
364
+ ```ruby
365
+ volume = Clack.range(
366
+ message: "Set volume",
367
+ min: 0,
368
+ max: 100,
369
+ step: 5,
370
+ default: 50
371
+ )
372
+ ```
373
+
290
374
  ### Select Key
291
375
 
292
376
  Quick selection using keyboard shortcuts.
@@ -320,6 +404,23 @@ spinner.stop("Dependencies installed!")
320
404
  # Or: spinner.cancel("Cancelled")
321
405
  ```
322
406
 
407
+ **Block form** - wraps a block with automatic success/error handling:
408
+
409
+ ```ruby
410
+ result = Clack.spin("Installing dependencies...") { system("npm install") }
411
+
412
+ # With custom messages
413
+ Clack.spin("Compiling...", success: "Build complete!") { build_project }
414
+
415
+ # Access the spinner inside the block
416
+ Clack.spin("Working...") do |s|
417
+ s.message "Step 1..."
418
+ do_step_1
419
+ s.message "Step 2..."
420
+ do_step_2
421
+ end
422
+ ```
423
+
323
424
  ### Progress
324
425
 
325
426
  Visual progress bar for measurable operations.
@@ -346,6 +447,22 @@ results = Clack.tasks(tasks: [
346
447
  { title: "Building project", task: -> { build } },
347
448
  { title: "Running tests", task: -> { run_tests } }
348
449
  ])
450
+
451
+ # Update the spinner message mid-task
452
+ Clack.tasks(tasks: [
453
+ { title: "Installing", task: ->(message) {
454
+ message.call("Fetching packages...")
455
+ fetch_packages
456
+ message.call("Compiling...")
457
+ compile
458
+ }}
459
+ ])
460
+
461
+ # Conditionally skip tasks with enabled:
462
+ Clack.tasks(tasks: [
463
+ { title: "Lint", task: -> { lint } },
464
+ { title: "Deploy", task: -> { deploy }, enabled: ENV["DEPLOY"] == "true" }
465
+ ])
349
466
  ```
350
467
 
351
468
  ### Group Multiselect
@@ -370,7 +487,9 @@ features = Clack.group_multiselect(
370
487
  { value: "solid_queue", label: "Solid Queue" }
371
488
  ]
372
489
  }
373
- ]
490
+ ],
491
+ selectable_groups: true, # Toggle all options in a group at once
492
+ group_spacing: 1 # Blank lines between groups
374
493
  )
375
494
  ```
376
495
 
@@ -485,6 +604,61 @@ Clack.outro("Done!") # └ Done!
485
604
  Clack.cancel("Aborted") # └ Aborted (red)
486
605
  ```
487
606
 
607
+ ## Configuration
608
+
609
+ Customize key bindings and display options globally:
610
+
611
+ ```ruby
612
+ # Add custom key bindings (merged with defaults)
613
+ Clack.update_settings(aliases: { "y" => :enter, "n" => :cancel })
614
+
615
+ # Disable guide bars
616
+ Clack.update_settings(with_guide: false)
617
+
618
+ # CI / non-interactive mode (prompts auto-submit with defaults)
619
+ Clack.update_settings(ci_mode: true) # Always on
620
+ Clack.update_settings(ci_mode: :auto) # Auto-detect (non-TTY or CI env vars)
621
+ ```
622
+
623
+ When CI mode is active, prompts immediately submit with their default values instead of waiting for input. Useful for CI pipelines and scripted environments where stdin is not a TTY.
624
+
625
+ Clack also warns when terminal width is below 40 columns, since prompts may not render cleanly in very narrow terminals.
626
+
627
+ ## Testing
628
+
629
+ Clack ships with first-class test helpers. Require `clack/testing` explicitly (it is not auto-loaded):
630
+
631
+ ```ruby
632
+ require "clack/testing"
633
+
634
+ # Simulate a text prompt
635
+ result = Clack::Testing.simulate(Clack.method(:text), message: "Name?") do |prompt|
636
+ prompt.type("Alice")
637
+ prompt.submit
638
+ end
639
+ # => "Alice"
640
+
641
+ # Capture rendered output alongside the result
642
+ result, output = Clack::Testing.simulate_with_output(Clack.method(:confirm), message: "Sure?") do |prompt|
643
+ prompt.left # switch to "No"
644
+ prompt.submit
645
+ end
646
+ ```
647
+
648
+ The `PromptDriver` yielded to the block provides these methods:
649
+
650
+ | Method | Description |
651
+ |---|---|
652
+ | `type(text)` | Type a string character by character |
653
+ | `submit` | Press Enter |
654
+ | `cancel` | Press Escape |
655
+ | `up` / `down` / `left` / `right` | Arrow keys |
656
+ | `toggle` | Press Space (for multiselect) |
657
+ | `tab` | Press Tab |
658
+ | `backspace` | Press Backspace |
659
+ | `ctrl_d` | Press Ctrl+D (submit multiline text) |
660
+ | `key(sym_or_char)` | Press an arbitrary key by symbol (e.g. `:escape`) or raw character |
661
+
488
662
  ## Requirements
489
663
 
490
664
  - Ruby 3.2+
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demonstrates advanced prompt types: multiline_text, path, select_key, autocomplete
5
+ # Run with: ruby examples/advanced_prompts.rb
6
+
7
+ require_relative "../lib/clack" # Or: require "clack" if installed as a gem
8
+
9
+ Clack.intro "advanced-prompts"
10
+
11
+ # Multi-line text with initial value (Ctrl+D to submit)
12
+ description = Clack.multiline_text(
13
+ message: "Project description:",
14
+ initial_value: "A Ruby CLI tool that "
15
+ )
16
+ exit 0 if Clack.cancel?(description)
17
+
18
+ Clack.log.info "Description:\n#{description}"
19
+
20
+ # Path selector (directories only, rooted at home)
21
+ project_dir = Clack.path(
22
+ message: "Choose project directory:",
23
+ root: Dir.home,
24
+ only_directories: true
25
+ )
26
+ exit 0 if Clack.cancel?(project_dir)
27
+
28
+ Clack.log.info "Directory: #{project_dir}"
29
+
30
+ # Select by key press (instant selection)
31
+ action = Clack.select_key(
32
+ message: "What next?",
33
+ options: [
34
+ {value: "init", label: "Initialize repo", key: "i", hint: "git init"},
35
+ {value: "clone", label: "Clone existing", key: "c", hint: "git clone"},
36
+ {value: "open", label: "Open in editor", key: "o"},
37
+ {value: "skip", label: "Skip", key: "s"}
38
+ ]
39
+ )
40
+ exit 0 if Clack.cancel?(action)
41
+
42
+ Clack.log.step "Action: #{action}"
43
+
44
+ # Autocomplete with placeholder
45
+ license = Clack.autocomplete(
46
+ message: "Pick a license:",
47
+ placeholder: "Type to search...",
48
+ options: [
49
+ {value: "mit", label: "MIT", hint: "permissive"},
50
+ {value: "apache2", label: "Apache 2.0", hint: "permissive"},
51
+ {value: "gpl3", label: "GPL 3.0", hint: "copyleft"},
52
+ {value: "agpl3", label: "AGPL 3.0", hint: "copyleft"},
53
+ {value: "bsd2", label: "BSD 2-Clause", hint: "permissive"},
54
+ {value: "bsd3", label: "BSD 3-Clause", hint: "permissive"},
55
+ {value: "mpl2", label: "MPL 2.0", hint: "weak copyleft"},
56
+ {value: "unlicense", label: "Unlicense", hint: "public domain"}
57
+ ]
58
+ )
59
+ exit 0 if Clack.cancel?(license)
60
+
61
+ Clack.log.success "License: #{license}"
62
+
63
+ Clack.outro "Done!"
data/examples/basic.rb ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run with: ruby examples/basic.rb
5
+
6
+ require_relative "../lib/clack" # Or: require "clack" if installed as a gem
7
+
8
+ Clack.intro "basic-example"
9
+
10
+ name = Clack.text(message: "What is your name?", placeholder: "Anonymous")
11
+ exit 0 if Clack.cancel?(name)
12
+
13
+ Clack.log.success "Hello, #{name}!"
14
+
15
+ Clack.outro "Goodbye!"
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run with: ruby examples/create_app.rb
5
+
6
+ require_relative "../lib/clack" # Or: require "clack" if installed as a gem
7
+
8
+ Clack.intro "create-my-app"
9
+
10
+ # Project name
11
+ name = Clack.text(
12
+ message: "Project name?",
13
+ placeholder: "my-app",
14
+ validate: ->(v) { "Project name is required" if v.to_s.strip.empty? }
15
+ )
16
+ exit 0 if Clack.cancel?(name)
17
+
18
+ # Framework selection
19
+ framework = Clack.select(
20
+ message: "Pick a framework",
21
+ options: [
22
+ {value: "rails", label: "Ruby on Rails", hint: "full-stack"},
23
+ {value: "sinatra", label: "Sinatra", hint: "micro"},
24
+ {value: "hanami", label: "Hanami"},
25
+ {value: "roda", label: "Roda"}
26
+ ]
27
+ )
28
+ exit 0 if Clack.cancel?(framework)
29
+
30
+ # Feature selection
31
+ features = Clack.multiselect(
32
+ message: "Select features",
33
+ options: [
34
+ {value: "api", label: "API Mode"},
35
+ {value: "auth", label: "Authentication"},
36
+ {value: "admin", label: "Admin Panel"},
37
+ {value: "docker", label: "Docker Setup"},
38
+ {value: "ci", label: "GitHub Actions CI"}
39
+ ],
40
+ required: false
41
+ )
42
+ exit 0 if Clack.cancel?(features)
43
+
44
+ # Database
45
+ database = Clack.select(
46
+ message: "Select database",
47
+ options: [
48
+ {value: "postgresql", label: "PostgreSQL", hint: "recommended"},
49
+ {value: "mysql", label: "MySQL"},
50
+ {value: "sqlite", label: "SQLite"}
51
+ ]
52
+ )
53
+ exit 0 if Clack.cancel?(database)
54
+
55
+ # Confirm
56
+ proceed = Clack.confirm(message: "Create project?")
57
+ exit 0 if Clack.cancel?(proceed)
58
+
59
+ unless proceed
60
+ Clack.outro "Project creation cancelled."
61
+ exit 0
62
+ end
63
+
64
+ # Simulate installation
65
+ s = Clack.spinner
66
+ s.start "Creating project..."
67
+ sleep 1
68
+ s.message "Installing dependencies..."
69
+ sleep 1
70
+ s.message "Configuring #{framework}..."
71
+ sleep 0.5
72
+ s.stop "Project created!"
73
+
74
+ # Summary
75
+ Clack.log.info "Project: #{name}"
76
+ Clack.log.success "Framework: #{framework}"
77
+ Clack.log.step "Database: #{database}"
78
+ Clack.log.step "Features: #{features.join(", ")}" unless features.empty?
79
+
80
+ Clack.note <<~NOTE, title: "Next Steps"
81
+ cd #{name}
82
+ bundle install
83
+ bin/rails server
84
+ NOTE
85
+
86
+ Clack.outro "Happy coding!"
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run with: ruby examples/date_demo.rb
5
+
6
+ require_relative "../lib/clack" # Or: require "clack" if installed as a gem
7
+
8
+ Clack.intro "date-picker-demo"
9
+
10
+ # Basic date selection
11
+ date = Clack.date(
12
+ message: "When should this deploy?",
13
+ help: "Use Tab/arrows to navigate, Up/Down to adjust, or type digits"
14
+ )
15
+ exit 0 if Clack.cancel?(date)
16
+
17
+ Clack.log.info "Selected: #{date}"
18
+
19
+ # Date with bounds
20
+ bounded_date = Clack.date(
21
+ message: "Pick a date this year",
22
+ format: :us,
23
+ initial_value: Date.today,
24
+ min: Date.new(Date.today.year, 1, 1),
25
+ max: Date.new(Date.today.year, 12, 31)
26
+ )
27
+ exit 0 if Clack.cancel?(bounded_date)
28
+
29
+ Clack.log.info "Bounded date: #{bounded_date}"
30
+
31
+ # Date with custom validation
32
+ future_date = Clack.date(
33
+ message: "Schedule for future date",
34
+ validate: Clack::Validators.future_date
35
+ )
36
+ exit 0 if Clack.cancel?(future_date)
37
+
38
+ Clack.log.success "Scheduled for #{future_date}"
39
+
40
+ Clack.outro "Done!"