clack 0.1.2 → 0.1.3

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: 3035b53cb615be8f42827389fc118990457a06e20e9a118c9eb77a97ff5352f8
4
- data.tar.gz: 98b12f8e74d475a50987a10c719f231050d83484989acd101c4e03bf11ad4b5e
3
+ metadata.gz: 150bf149feda282a5c07f5e4db2c307bb770b55492a93550937dd0374da6a6cb
4
+ data.tar.gz: a7468424209339c0ef1dd608a8c332f722a5293c8a5a01097760514602b2d601
5
5
  SHA512:
6
- metadata.gz: 770e87fe78a09c35ed8150aaee98dbc75ccadb8d23ba39a2b2ba684e00e5cf50b536e1cc0bce00aee9e9aaf22baa2dad566c75c0f64d7b80d799a70009bd2057
7
- data.tar.gz: f77bf005240ce8533d1b1110bcd430409847fa9263682df3b7362c72b599570a77ad9eb2fad31bbc8690599348bdbd026a05733d5dad357aaa9824120382c8f5
6
+ metadata.gz: 37b1e190c92651377058ee130eef9126ce08da57ed24354f1a517e745585a3ec497f1ddb62799762dfe49333201541aad867d8f14f0b286534ecea7f40f39860
7
+ data.tar.gz: 0da475188a4c750f5a2d892e47fe5a1eae58f2606032e1820cc4f614cce81e2f518e15a9c822ebd02978d99efca418f4135449dd15019c6c14fa698c95811adb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.3] - 2026-01-23
6
+
7
+ ### Added
8
+ - Warning validation: Validators can return `Clack::Warning.new(message)` for soft failures that allow user confirmation
9
+ - Built-in warning validators: `Validators.file_exists_warning` and `Validators.as_warning(validator)`
10
+ - Test coverage for warning state machine and multiselect keyboard shortcuts
11
+
12
+ ### Changed
13
+ - Simplified Multiselect implementation by using base class warning/error handling (37 lines → 12 lines)
14
+ - Unified validation message rendering across all prompt types
15
+
5
16
  ## [0.1.2]
6
17
 
7
18
  ### Added
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack).
6
6
 
7
7
  <p align="center">
8
- <img src="examples/demo.gif" width="640" alt="Clack demo">
8
+ <img src="examples/demo.gif?v=2" width="640" alt="Clack demo">
9
9
  </p>
10
10
 
11
11
  ## Why Clack?
@@ -76,10 +76,17 @@ ruby examples/full_demo.rb
76
76
  <details>
77
77
  <summary>Recording the demo GIF</summary>
78
78
 
79
- Install [VHS](https://github.com/charmbracelet/vhs) and run:
79
+ Requires [asciinema](https://asciinema.org/), [agg](https://github.com/asciinema/agg), and [expect](https://core.tcl-lang.org/expect/index):
80
80
 
81
81
  ```bash
82
- vhs examples/demo.tape
82
+ # Record the demo (automated via expect script)
83
+ asciinema rec examples/demo.cast --command "expect examples/demo.exp" --overwrite -q
84
+
85
+ # Split batched frames to show typing (Ruby buffers terminal output)
86
+ ruby examples/split_cast.rb
87
+
88
+ # Convert to GIF
89
+ agg examples/demo.cast examples/demo.gif --font-size 18 --cols 80 --rows 28 --speed 0.6
83
90
  ```
84
91
  </details>
85
92
 
@@ -93,8 +100,76 @@ result = Clack.text(message: "Name?")
93
100
  exit 1 if Clack.cancel?(result)
94
101
  ```
95
102
 
103
+ ### Validation & Transforms
104
+
105
+ Prompts support `validate:` and `transform:` options.
106
+
107
+ ```
108
+ User Input → Validation (raw) → Transform (if valid) → Final Value
109
+ ```
110
+
111
+ Validation returns an error message, a `Clack::Warning`, or `nil` to pass. Transforms normalize the value after validation passes.
112
+
113
+ **Validation results:**
114
+ - `nil` or `false` - passes validation
115
+ - String - shows error (red), user must fix input
116
+ - `Clack::Warning.new(message)` - shows warning (yellow), user can confirm with Enter or edit
117
+
118
+ ```ruby
119
+ # Use symbol shortcuts (preferred)
120
+ name = Clack.text(message: "Name?", transform: :strip)
121
+ code = Clack.text(message: "Code?", transform: :upcase)
122
+
123
+ # Chain multiple transforms
124
+ username = Clack.text(
125
+ message: "Username?",
126
+ transform: Clack::Transformers.chain(:strip, :downcase)
127
+ )
128
+
129
+ # Combine validation and transform
130
+ amount = Clack.text(
131
+ message: "Amount?",
132
+ validate: ->(v) { "Must be a number" unless v.match?(/\A\d+\z/) },
133
+ transform: :to_integer
134
+ )
135
+
136
+ # Warning validation (soft failure, user can confirm or edit)
137
+ file = Clack.text(
138
+ message: "Output file?",
139
+ validate: ->(v) { Clack::Warning.new("File exists. Overwrite?") if File.exist?(v) }
140
+ )
141
+ ```
142
+
143
+ Built-in validators and transformers:
144
+
145
+ ```ruby
146
+ # Validators - return error message, Warning, or nil
147
+ Clack::Validators.required
148
+ Clack::Validators.min_length(8)
149
+ Clack::Validators.format(/\A[a-z]+\z/, "Only lowercase")
150
+ Clack::Validators.email
151
+ Clack::Validators.combine(v1, v2) # First error/warning wins
152
+
153
+ # 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
156
+
157
+ # Transformers - normalize the value (use :symbol or Clack::Transformers.name)
158
+ :strip / :trim # Remove leading/trailing whitespace
159
+ :downcase / :upcase # Change case
160
+ :capitalize # "hello world" -> "Hello world"
161
+ :titlecase # "hello world" -> "Hello World"
162
+ :squish # Collapse whitespace to single spaces
163
+ :compact # Remove all whitespace
164
+ :to_integer # Parse as integer
165
+ :to_float # Parse as float
166
+ :digits_only # Extract only digits
167
+ ```
168
+
96
169
  ### Text
97
170
 
171
+ <img src="examples/images/text.svg" alt="Text prompt">
172
+
98
173
  ```ruby
99
174
  name = Clack.text(
100
175
  message: "What is your project named?",
@@ -107,6 +182,8 @@ name = Clack.text(
107
182
 
108
183
  ### Password
109
184
 
185
+ <img src="examples/images/password.svg" alt="Password prompt">
186
+
110
187
  ```ruby
111
188
  secret = Clack.password(
112
189
  message: "Enter your API key",
@@ -116,6 +193,8 @@ secret = Clack.password(
116
193
 
117
194
  ### Confirm
118
195
 
196
+ <img src="examples/images/confirm.svg" alt="Confirm prompt">
197
+
119
198
  ```ruby
120
199
  proceed = Clack.confirm(
121
200
  message: "Deploy to production?",
@@ -129,6 +208,8 @@ proceed = Clack.confirm(
129
208
 
130
209
  Single selection with keyboard navigation.
131
210
 
211
+ <img src="examples/images/select.svg" alt="Select prompt">
212
+
132
213
  ```ruby
133
214
  db = Clack.select(
134
215
  message: "Choose a database",
@@ -146,6 +227,8 @@ db = Clack.select(
146
227
 
147
228
  Multiple selections with toggle controls.
148
229
 
230
+ <img src="examples/images/multiselect.svg" alt="Multiselect prompt">
231
+
149
232
  ```ruby
150
233
  features = Clack.multiselect(
151
234
  message: "Select features to install",
@@ -223,6 +306,8 @@ action = Clack.select_key(
223
306
 
224
307
  Non-blocking animated indicator for async work.
225
308
 
309
+ <img src="examples/images/spinner.svg" alt="Spinner">
310
+
226
311
  ```ruby
227
312
  spinner = Clack.spinner
228
313
  spinner.start("Installing dependencies...")
@@ -6,7 +6,7 @@ module Clack
6
6
  module Core
7
7
  # Base class for all interactive prompts.
8
8
  #
9
- # Implements a state machine with states: :initial, :active, :error, :submit, :cancel.
9
+ # Implements a state machine with states: :initial, :active, :error, :warning, :submit, :cancel.
10
10
  # Subclasses override {#handle_input}, {#build_frame}, and {#build_final_frame}
11
11
  # to customize behavior and rendering.
12
12
  #
@@ -57,27 +57,33 @@ module Clack
57
57
  end
58
58
  end
59
59
 
60
- # @return [Symbol] current state (:initial, :active, :error, :submit, :cancel)
60
+ # @return [Symbol] current state (:initial, :active, :error, :warning, :submit, :cancel)
61
61
  attr_reader :state
62
62
  # @return [Object] the current/final value
63
63
  attr_reader :value
64
64
  # @return [String, nil] validation error message, if any
65
65
  attr_reader :error_message
66
+ # @return [String, nil] validation warning message, if any
67
+ attr_reader :warning_message
66
68
 
67
69
  # @param message [String] the prompt message to display
68
70
  # @param help [String, nil] optional help text shown below the message
69
- # @param validate [Proc, nil] optional validation proc; returns error string or nil
71
+ # @param validate [Proc, nil] optional validation proc; returns error string, Warning, or nil
72
+ # @param transform [Symbol, Proc, nil] transformer (symbol shortcut or proc); applied after validation
70
73
  # @param input [IO] input stream (default: $stdin)
71
74
  # @param output [IO] output stream (default: $stdout)
72
- def initialize(message:, help: nil, validate: nil, input: $stdin, output: $stdout)
75
+ def initialize(message:, help: nil, validate: nil, transform: nil, input: $stdin, output: $stdout)
73
76
  @message = message
74
77
  @help = help
75
78
  @validate = validate
79
+ @transform = Transformers.resolve(transform)
76
80
  @input = input
77
81
  @output = output
78
82
  @state = :initial
79
83
  @value = nil
80
84
  @error_message = nil
85
+ @warning_message = nil
86
+ @warning_confirmed = false
81
87
  @prev_frame = nil
82
88
  @cursor = 0
83
89
  @needs_redraw = false
@@ -125,10 +131,25 @@ module Clack
125
131
  def handle_key(key)
126
132
  return if terminal_state?
127
133
 
128
- @state = :active if @state == :error
129
-
130
134
  action = Settings.action?(key)
131
135
 
136
+ # Handle warning state: Enter confirms, Cancel aborts, other input clears warning
137
+ if @state == :warning
138
+ case action
139
+ when :enter
140
+ confirm_warning
141
+ submit
142
+ when :cancel
143
+ @state = :cancel
144
+ else
145
+ clear_warning
146
+ handle_input(key, action)
147
+ end
148
+ return
149
+ end
150
+
151
+ @state = :active if @state == :error
152
+
132
153
  case action
133
154
  when :cancel
134
155
  @state = :cancel
@@ -148,17 +169,57 @@ module Clack
148
169
  end
149
170
 
150
171
  # Validate and submit the current value.
151
- # Sets state to :error if validation fails, :submit otherwise.
172
+ # Sets state to :error if validation fails, :warning if warning returned,
173
+ # or :submit otherwise. Applies transform after successful validation.
152
174
  def submit
153
- if @validate
154
- result = @validate.call(@value)
155
- if result
156
- @error_message = result.is_a?(Exception) ? result.message : result.to_s
175
+ @state = validate_value(@value)
176
+ return unless @state == :submit
177
+
178
+ if @transform
179
+ begin
180
+ @value = @transform.call(@value)
181
+ rescue => error
182
+ @error_message = "Transform failed: #{error.message}"
157
183
  @state = :error
158
- return
159
184
  end
160
185
  end
161
- @state = :submit
186
+ end
187
+
188
+ # Validate a value and return the resulting state.
189
+ # Handles errors, warnings, and the warning confirmation flow.
190
+ #
191
+ # @param value [Object] the value to validate
192
+ # @return [Symbol] :submit, :warning, or :error
193
+ def validate_value(value)
194
+ return :submit unless @validate
195
+
196
+ result = @validate.call(value)
197
+ return :submit unless result
198
+
199
+ if result.is_a?(Warning)
200
+ return :submit if @warning_confirmed
201
+
202
+ @warning_message = result.message
203
+ :warning
204
+ else
205
+ @error_message = result.is_a?(Exception) ? result.message : result.to_s
206
+ :error
207
+ end
208
+ end
209
+
210
+ # Clear warning state and return to active.
211
+ # Call this when user edits input during warning state.
212
+ def clear_warning
213
+ @warning_confirmed = false
214
+ @warning_message = nil
215
+ @state = :active
216
+ end
217
+
218
+ # Confirm warning and prepare for resubmission.
219
+ # Call this when user confirms a warning.
220
+ def confirm_warning
221
+ @warning_confirmed = true
222
+ @warning_message = nil
162
223
  end
163
224
 
164
225
  # Render the current frame using differential rendering.
@@ -241,11 +302,11 @@ module Clack
241
302
  end
242
303
 
243
304
  def active_bar
244
- (@state == :error) ? Colors.yellow(Symbols::S_BAR) : bar
305
+ %i[error warning].include?(@state) ? Colors.yellow(Symbols::S_BAR) : bar
245
306
  end
246
307
 
247
308
  def bar_end
248
- (@state == :error) ? Colors.yellow(Symbols::S_BAR_END) : Colors.gray(Symbols::S_BAR_END)
309
+ %i[error warning].include?(@state) ? Colors.yellow(Symbols::S_BAR_END) : Colors.gray(Symbols::S_BAR_END)
249
310
  end
250
311
 
251
312
  def help_line
@@ -263,7 +324,23 @@ module Clack
263
324
  when :initial, :active then Colors.cyan(Symbols::S_STEP_ACTIVE)
264
325
  when :submit then Colors.green(Symbols::S_STEP_SUBMIT)
265
326
  when :cancel then Colors.red(Symbols::S_STEP_CANCEL)
266
- when :error then Colors.yellow(Symbols::S_STEP_ERROR)
327
+ when :error, :warning then Colors.yellow(Symbols::S_STEP_ERROR)
328
+ end
329
+ end
330
+
331
+ # Build validation message lines for error or warning states.
332
+ # Returns array of lines to append, or empty array if no validation message.
333
+ def validation_message_lines
334
+ case @state
335
+ when :error
336
+ ["#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n"]
337
+ when :warning
338
+ [
339
+ "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@warning_message)}\n",
340
+ "#{bar} #{Colors.dim("Press Enter to confirm, or edit your input")}\n"
341
+ ]
342
+ else
343
+ []
267
344
  end
268
345
  end
269
346
  end
@@ -50,17 +50,8 @@ module Clack
50
50
 
51
51
  protected
52
52
 
53
- def handle_key(key)
54
- return if terminal_state?
55
-
56
- @state = :active if @state == :error
57
- action = Core::Settings.action?(key)
58
-
53
+ def handle_input(key, action)
59
54
  case action
60
- when :cancel
61
- @state = :cancel
62
- when :enter
63
- submit
64
55
  when :up
65
56
  move_cursor(-1)
66
57
  when :down
@@ -101,9 +92,12 @@ module Clack
101
92
  lines << "#{active_bar} #{option_display(opt, actual_idx)}\n"
102
93
  end
103
94
 
104
- lines << "#{bar_end}\n"
105
-
106
- lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
95
+ if @state == :error || @state == :warning
96
+ lines.concat(validation_message_lines)
97
+ else
98
+ lines << "#{bar} #{keyboard_hints}\n"
99
+ lines << "#{bar_end}\n"
100
+ end
107
101
 
108
102
  lines.join
109
103
  end
@@ -162,6 +156,15 @@ module Clack
162
156
  @value = @selected.to_a
163
157
  end
164
158
 
159
+ def keyboard_hints
160
+ hints = [
161
+ "#{Colors.dim("space")} select",
162
+ "#{Colors.dim("a")} all",
163
+ "#{Colors.dim("i")} invert"
164
+ ]
165
+ Colors.dim(hints.join(Colors.dim(" / ")))
166
+ end
167
+
165
168
  def option_display(opt, idx)
166
169
  active = idx == @cursor
167
170
  selected = @selected.include?(opt[:value])
@@ -45,7 +45,11 @@ module Clack
45
45
  lines << "#{active_bar} #{masked_display}\n"
46
46
  lines << "#{bar_end}\n"
47
47
 
48
- lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
48
+ validation_lines = validation_message_lines
49
+ if validation_lines.any?
50
+ lines[-1] = validation_lines.first
51
+ lines.concat(validation_lines[1..])
52
+ end
49
53
 
50
54
  lines.join
51
55
  end
@@ -48,17 +48,8 @@ module Clack
48
48
 
49
49
  protected
50
50
 
51
- def handle_key(key)
52
- return if terminal_state?
53
-
54
- @state = :active if @state == :error
55
- action = Core::Settings.action?(key)
56
-
51
+ def handle_input(key, action)
57
52
  case action
58
- when :cancel
59
- @state = :cancel
60
- when :enter
61
- submit_selection
62
53
  when :up
63
54
  move_selection(-1)
64
55
  when :down
@@ -89,7 +80,7 @@ module Clack
89
80
  update_suggestions
90
81
  end
91
82
 
92
- def submit_selection
83
+ def submit
93
84
  path = @value.empty? ? @root : resolve_path(@value)
94
85
 
95
86
  unless path_within_root?(path)
@@ -98,17 +89,14 @@ module Clack
98
89
  return
99
90
  end
100
91
 
101
- if @validate
102
- result = @validate.call(path)
103
- if result
104
- @error_message = result.is_a?(Exception) ? result.message : result.to_s
105
- @state = :error
106
- return
107
- end
108
- end
109
-
92
+ # Temporarily set value to resolved path for validation
93
+ original_value = @value
110
94
  @value = path
111
- @state = :submit
95
+
96
+ super
97
+
98
+ # Restore input buffer if validation or transform failed (but not for warnings)
99
+ @value = original_value if @state == :error || @state == :warning
112
100
  end
113
101
 
114
102
  def build_frame
@@ -125,7 +113,11 @@ module Clack
125
113
 
126
114
  lines << "#{bar_end}\n"
127
115
 
128
- lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
116
+ validation_lines = validation_message_lines
117
+ if validation_lines.any?
118
+ lines[-1] = validation_lines.first
119
+ lines.concat(validation_lines[1..])
120
+ end
129
121
 
130
122
  lines.join
131
123
  end
@@ -74,7 +74,11 @@ module Clack
74
74
  lines << "#{active_bar} #{input_display}\n"
75
75
  lines << "#{bar_end}\n" if @state == :active || @state == :initial
76
76
 
77
- lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
77
+ validation_lines = validation_message_lines
78
+ if validation_lines.any?
79
+ lines[-1] = validation_lines.first
80
+ lines.concat(validation_lines[1..])
81
+ end
78
82
 
79
83
  lines.join
80
84
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clack
4
+ # Built-in transformers for normalizing user input.
5
+ # Use these with the `transform:` option on prompts.
6
+ #
7
+ # Transforms are applied after validation passes, so you can validate
8
+ # the raw input and transform it into a normalized form.
9
+ #
10
+ # @example Using symbol shortcuts (preferred)
11
+ # Clack.text(message: "Name?", transform: :strip)
12
+ # Clack.text(message: "Code?", transform: :upcase)
13
+ #
14
+ # @example Using module methods
15
+ # Clack.text(message: "Name?", transform: Clack::Transformers.strip)
16
+ #
17
+ # @example Custom transformer
18
+ # Clack.text(
19
+ # message: "Amount?",
20
+ # transform: ->(v) { v.to_f.round(2) }
21
+ # )
22
+ #
23
+ # @example Chaining multiple transforms
24
+ # Clack.text(
25
+ # message: "Username?",
26
+ # transform: Clack::Transformers.chain(:strip, :downcase)
27
+ # )
28
+ #
29
+ module Transformers
30
+ # Lookup table for symbol shortcuts
31
+ REGISTRY = {}
32
+
33
+ class << self
34
+ # Resolve a transformer from a symbol, proc, or return as-is.
35
+ # @param transformer [Symbol, Proc, nil] the transformer to resolve
36
+ # @return [Proc, nil] the resolved transformer proc
37
+ def resolve(transformer)
38
+ case transformer
39
+ when Symbol
40
+ REGISTRY[transformer] || raise(ArgumentError, "Unknown transformer: #{transformer}")
41
+ when Proc
42
+ transformer
43
+ when nil
44
+ nil
45
+ else
46
+ raise ArgumentError, "Transform must be a Symbol or Proc, got #{transformer.class}"
47
+ end
48
+ end
49
+
50
+ # Strip leading/trailing whitespace.
51
+ # @return [Proc] Transformer proc
52
+ def strip
53
+ REGISTRY[:strip]
54
+ end
55
+
56
+ # Alias for strip (for JS developers).
57
+ # @return [Proc] Transformer proc
58
+ def trim
59
+ REGISTRY[:trim]
60
+ end
61
+
62
+ # Convert to lowercase.
63
+ # @return [Proc] Transformer proc
64
+ def downcase
65
+ REGISTRY[:downcase]
66
+ end
67
+
68
+ # Convert to uppercase.
69
+ # @return [Proc] Transformer proc
70
+ def upcase
71
+ REGISTRY[:upcase]
72
+ end
73
+
74
+ # Capitalize first letter, lowercase rest.
75
+ # @return [Proc] Transformer proc
76
+ def capitalize
77
+ REGISTRY[:capitalize]
78
+ end
79
+
80
+ # Capitalize first letter of each word.
81
+ # @return [Proc] Transformer proc
82
+ def titlecase
83
+ REGISTRY[:titlecase]
84
+ end
85
+
86
+ # Strip and collapse whitespace to single spaces.
87
+ # @return [Proc] Transformer proc
88
+ def squish
89
+ REGISTRY[:squish]
90
+ end
91
+
92
+ # Remove all whitespace.
93
+ # @return [Proc] Transformer proc
94
+ def compact
95
+ REGISTRY[:compact]
96
+ end
97
+
98
+ # Parse as integer.
99
+ # @return [Proc] Transformer proc
100
+ def to_integer
101
+ REGISTRY[:to_integer]
102
+ end
103
+
104
+ # Parse as float.
105
+ # @return [Proc] Transformer proc
106
+ def to_float
107
+ REGISTRY[:to_float]
108
+ end
109
+
110
+ # Extract only digits.
111
+ # @return [Proc] Transformer proc
112
+ def digits_only
113
+ REGISTRY[:digits_only]
114
+ end
115
+
116
+ # Combine multiple transformers, applied in order.
117
+ # Accepts symbols or procs.
118
+ #
119
+ # @param transformers [Array<Symbol, Proc>] Transformers to combine
120
+ # @return [Proc] Combined transformer proc
121
+ #
122
+ # @example
123
+ # Clack::Transformers.chain(:strip, :downcase)
124
+ # Clack::Transformers.chain(:strip, ->(v) { v.reverse })
125
+ def chain(*transformers)
126
+ resolved = transformers.map { |xform| resolve(xform) }
127
+ ->(value) { resolved.reduce(value) { |val, xform| xform.call(val) } }
128
+ end
129
+ end
130
+
131
+ # Register built-in transformers
132
+ REGISTRY[:strip] = ->(value) { value.to_s.strip }
133
+ REGISTRY[:trim] = REGISTRY[:strip]
134
+ REGISTRY[:downcase] = ->(value) { value.to_s.downcase }
135
+ REGISTRY[:upcase] = ->(value) { value.to_s.upcase }
136
+ REGISTRY[:capitalize] = ->(value) { value.to_s.capitalize }
137
+ REGISTRY[:titlecase] = ->(value) { value.to_s.split.map(&:capitalize).join(" ") }
138
+ REGISTRY[:squish] = ->(value) { value.to_s.strip.gsub(/\s+/, " ") }
139
+ REGISTRY[:compact] = ->(value) { value.to_s.gsub(/\s+/, "") }
140
+ REGISTRY[:to_integer] = ->(value) { value.to_s.to_i }
141
+ REGISTRY[:to_float] = ->(value) { value.to_s.to_f }
142
+ REGISTRY[:digits_only] = ->(value) { value.to_s.gsub(/\D/, "") }
143
+ end
144
+ end
@@ -142,6 +142,38 @@ module Clack
142
142
  ->(value) { message unless File.directory?(value.to_s) }
143
143
  end
144
144
 
145
+ # Warning if file exists. Allows user to confirm overwrite.
146
+ #
147
+ # @param message [String] Warning message
148
+ # @return [Proc] Validator proc returning Warning
149
+ #
150
+ # @example
151
+ # Clack.text(message: "Output file?", validate: Clack::Validators.file_exists_warning)
152
+ def file_exists_warning(message = "File already exists. Overwrite?")
153
+ ->(value) { Clack::Warning.new(message) if File.exist?(value.to_s) }
154
+ end
155
+
156
+ # Convert any validator to return a warning instead of an error.
157
+ # Warnings allow the user to proceed with confirmation.
158
+ #
159
+ # @param validator [Proc] Original validator
160
+ # @return [Proc] Validator that returns Warning instead of String
161
+ #
162
+ # @example
163
+ # # Make max_length a warning instead of error
164
+ # Clack.text(
165
+ # message: "Bio?",
166
+ # validate: Clack::Validators.as_warning(
167
+ # Clack::Validators.max_length(100, "Bio is quite long")
168
+ # )
169
+ # )
170
+ def as_warning(validator)
171
+ lambda do |value|
172
+ result = validator.call(value)
173
+ Clack::Warning.new(result) if result
174
+ end
175
+ end
176
+
145
177
  private
146
178
 
147
179
  def first_failing_validation(validators, value)
data/lib/clack/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clack
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/clack.rb CHANGED
@@ -32,6 +32,7 @@ require_relative "clack/group"
32
32
  require_relative "clack/stream"
33
33
  require_relative "clack/task_log"
34
34
  require_relative "clack/validators"
35
+ require_relative "clack/transformers"
35
36
 
36
37
  # Clack - Beautiful CLI prompts for Ruby
37
38
  #
@@ -55,7 +56,28 @@ module Clack
55
56
  # Sentinel value returned when user cancels a prompt (Escape or Ctrl+C)
56
57
  CANCEL = Object.new.tap { |o| o.define_singleton_method(:inspect) { "Clack::CANCEL" } }.freeze
57
58
 
59
+ # Warning result from validation - allows user to proceed with confirmation.
60
+ # Validators can return a Warning to show a yellow message that doesn't block
61
+ # submission if the user confirms by pressing Enter again.
62
+ #
63
+ # @example Validator returning a warning
64
+ # validate: ->(v) { Clack::Warning.new("File exists, overwrite?") if File.exist?(v) }
65
+ Warning = Data.define(:message) do
66
+ def to_s = message
67
+ end
68
+
58
69
  class << self
70
+ # Create a validation warning that allows the user to proceed with confirmation.
71
+ #
72
+ # @param message [String] the warning message
73
+ # @return [Warning] a warning object
74
+ #
75
+ # @example
76
+ # validate: ->(v) { Clack.warning("Unusual value") if v.length > 100 }
77
+ def warning(message)
78
+ Warning.new(message)
79
+ end
80
+
59
81
  # Check if a prompt result was cancelled by the user.
60
82
  #
61
83
  # @param value [Object] the result from a prompt
@@ -123,7 +145,9 @@ module Clack
123
145
  # @param placeholder [String, nil] dim text shown when input is empty
124
146
  # @param default_value [String, nil] value used if submitted empty
125
147
  # @param initial_value [String, nil] pre-filled editable text
126
- # @param validate [Proc, nil] validation function returning error message or nil
148
+ # @param validate [Proc, nil] validation function returning error string, Warning, or nil
149
+ # @param transform [Symbol, Proc, nil] transform function to normalize the value
150
+ # @param help [String, nil] help text shown below the message
127
151
  # @return [String, CANCEL] user input or CANCEL if cancelled
128
152
  def text(message:, **opts)
129
153
  Prompts::Text.new(message:, **opts).run
@@ -136,7 +160,8 @@ module Clack
136
160
  #
137
161
  # @param message [String] the prompt message
138
162
  # @param initial_value [String, nil] pre-filled editable text (can contain newlines)
139
- # @param validate [Proc, nil] validation function returning error message or nil
163
+ # @param validate [Proc, nil] validation function returning error string, Warning, or nil
164
+ # @param help [String, nil] help text shown below the message
140
165
  # @return [String, CANCEL] user input (lines joined with \n) or CANCEL if cancelled
141
166
  def multiline_text(message:, **opts)
142
167
  Prompts::MultilineText.new(message:, **opts).run
@@ -146,7 +171,8 @@ module Clack
146
171
  #
147
172
  # @param message [String] the prompt message
148
173
  # @param mask [String] character to display for each input character (default: ▪)
149
- # @param validate [Proc, nil] validation function
174
+ # @param validate [Proc, nil] validation function returning error string, Warning, or nil
175
+ # @param help [String, nil] help text shown below the message
150
176
  # @return [String, CANCEL] password or CANCEL if cancelled
151
177
  def password(message:, **opts)
152
178
  Prompts::Password.new(message:, **opts).run
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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Whittaker
@@ -52,6 +52,7 @@ files:
52
52
  - lib/clack/stream.rb
53
53
  - lib/clack/symbols.rb
54
54
  - lib/clack/task_log.rb
55
+ - lib/clack/transformers.rb
55
56
  - lib/clack/utils.rb
56
57
  - lib/clack/validators.rb
57
58
  - lib/clack/version.rb