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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +88 -3
- data/lib/clack/core/prompt.rb +93 -16
- data/lib/clack/prompts/multiselect.rb +16 -13
- data/lib/clack/prompts/password.rb +5 -1
- data/lib/clack/prompts/path.rb +14 -22
- data/lib/clack/prompts/text.rb +5 -1
- data/lib/clack/transformers.rb +144 -0
- data/lib/clack/validators.rb +32 -0
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +29 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 150bf149feda282a5c07f5e4db2c307bb770b55492a93550937dd0374da6a6cb
|
|
4
|
+
data.tar.gz: a7468424209339c0ef1dd608a8c332f722a5293c8a5a01097760514602b2d601
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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...")
|
data/lib/clack/core/prompt.rb
CHANGED
|
@@ -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, :
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
305
|
+
%i[error warning].include?(@state) ? Colors.yellow(Symbols::S_BAR) : bar
|
|
245
306
|
end
|
|
246
307
|
|
|
247
308
|
def bar_end
|
|
248
|
-
(@state
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
data/lib/clack/prompts/path.rb
CHANGED
|
@@ -48,17 +48,8 @@ module Clack
|
|
|
48
48
|
|
|
49
49
|
protected
|
|
50
50
|
|
|
51
|
-
def
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/clack/prompts/text.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/clack/validators.rb
CHANGED
|
@@ -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
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
|
|
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
|
|
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.
|
|
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
|