clack 0.4.6 → 0.6.0
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 +38 -0
- data/README.md +267 -197
- data/lib/clack/box.rb +6 -6
- data/lib/clack/core/fuzzy_matcher.rb +3 -3
- data/lib/clack/core/key_reader.rb +30 -20
- data/lib/clack/core/options_helper.rb +96 -29
- data/lib/clack/core/prompt.rb +45 -12
- data/lib/clack/core/scroll_helper.rb +10 -41
- data/lib/clack/core/selection_manager.rb +49 -0
- data/lib/clack/note.rb +4 -3
- data/lib/clack/prompts/autocomplete.rb +21 -15
- data/lib/clack/prompts/autocomplete_multiselect.rb +19 -26
- data/lib/clack/prompts/confirm.rb +8 -30
- data/lib/clack/prompts/date.rb +1 -14
- data/lib/clack/prompts/group_multiselect.rb +48 -67
- data/lib/clack/prompts/multiline_text.rb +33 -53
- data/lib/clack/prompts/multiselect.rb +27 -38
- data/lib/clack/prompts/password.rb +1 -14
- data/lib/clack/prompts/path.rb +9 -23
- data/lib/clack/prompts/progress.rb +6 -2
- data/lib/clack/prompts/range.rb +1 -14
- data/lib/clack/prompts/select.rb +18 -32
- data/lib/clack/prompts/select_key.rb +8 -8
- data/lib/clack/prompts/spinner.rb +15 -20
- data/lib/clack/prompts/tasks.rb +6 -1
- data/lib/clack/prompts/text.rb +1 -14
- data/lib/clack/testing.rb +31 -37
- data/lib/clack/utils.rb +106 -22
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +71 -37
- metadata +3 -3
|
@@ -29,7 +29,7 @@ module Clack
|
|
|
29
29
|
class AutocompleteMultiselect < Core::Prompt
|
|
30
30
|
include Core::OptionsHelper
|
|
31
31
|
include Core::TextInputHelper
|
|
32
|
-
include Core::
|
|
32
|
+
include Core::SelectionManager
|
|
33
33
|
|
|
34
34
|
# @param message [String] the prompt message
|
|
35
35
|
# @param options [Array<Hash, String>] list of options to filter
|
|
@@ -42,6 +42,9 @@ module Clack
|
|
|
42
42
|
# across label, value, and hint is used.
|
|
43
43
|
# @param opts [Hash] additional options passed to {Core::Prompt}
|
|
44
44
|
def initialize(message:, options:, max_items: 5, placeholder: nil, required: true, initial_values: [], filter: nil, **opts)
|
|
45
|
+
if opts.key?(:initial_value)
|
|
46
|
+
raise ArgumentError, "AutocompleteMultiselect uses initial_values: (plural), not initial_value:"
|
|
47
|
+
end
|
|
45
48
|
super(message:, **opts)
|
|
46
49
|
@all_options = normalize_options(options)
|
|
47
50
|
@max_items = max_items
|
|
@@ -50,9 +53,9 @@ module Clack
|
|
|
50
53
|
@filter = filter
|
|
51
54
|
@search_text = ""
|
|
52
55
|
@cursor = 0
|
|
53
|
-
@
|
|
56
|
+
@option_index = 0
|
|
54
57
|
@scroll_offset = 0
|
|
55
|
-
valid_values = Set.new(@all_options.map { |o| o
|
|
58
|
+
valid_values = Set.new(@all_options.map { |o| o.value })
|
|
56
59
|
@selected = Set.new(initial_values) & valid_values
|
|
57
60
|
update_filtered
|
|
58
61
|
end
|
|
@@ -84,24 +87,16 @@ module Clack
|
|
|
84
87
|
def toggle_current
|
|
85
88
|
return if @filtered.empty?
|
|
86
89
|
|
|
87
|
-
opt = @filtered[@
|
|
88
|
-
return if opt
|
|
90
|
+
opt = @filtered[@option_index]
|
|
91
|
+
return if opt.disabled
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
@selected.delete(opt[:value])
|
|
92
|
-
else
|
|
93
|
-
@selected.add(opt[:value])
|
|
94
|
-
end
|
|
93
|
+
toggle_value(opt.value)
|
|
95
94
|
end
|
|
96
95
|
|
|
97
96
|
def submit
|
|
98
|
-
|
|
99
|
-
@error_message = "Please select at least one option. Press #{Colors.cyan("space")} to select, #{Colors.cyan("enter")} to submit"
|
|
100
|
-
@state = :error
|
|
101
|
-
return
|
|
102
|
-
end
|
|
97
|
+
return unless validate_selection
|
|
103
98
|
|
|
104
|
-
|
|
99
|
+
update_selection_value
|
|
105
100
|
super
|
|
106
101
|
end
|
|
107
102
|
|
|
@@ -112,9 +107,9 @@ module Clack
|
|
|
112
107
|
lines << help_line
|
|
113
108
|
lines << "#{active_bar} #{Colors.dim("Search:")} #{input_display}#{match_count}\n"
|
|
114
109
|
|
|
115
|
-
|
|
110
|
+
visible_options.each_with_index do |opt, idx|
|
|
116
111
|
actual_idx = @scroll_offset + idx
|
|
117
|
-
lines << "#{active_bar} #{option_display(opt, actual_idx == @
|
|
112
|
+
lines << "#{active_bar} #{option_display(opt, actual_idx == @option_index)}\n"
|
|
118
113
|
end
|
|
119
114
|
|
|
120
115
|
lines << "#{active_bar} #{Colors.yellow("No matches found")}\n" if @filtered.empty? && !@search_text.empty?
|
|
@@ -130,9 +125,7 @@ module Clack
|
|
|
130
125
|
lines.join
|
|
131
126
|
end
|
|
132
127
|
|
|
133
|
-
def final_display
|
|
134
|
-
@all_options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
|
|
135
|
-
end
|
|
128
|
+
def final_display = selected_labels(@all_options)
|
|
136
129
|
|
|
137
130
|
private
|
|
138
131
|
|
|
@@ -146,7 +139,7 @@ module Clack
|
|
|
146
139
|
def handle_text_input(key)
|
|
147
140
|
return unless super
|
|
148
141
|
|
|
149
|
-
@
|
|
142
|
+
@option_index = 0
|
|
150
143
|
@scroll_offset = 0
|
|
151
144
|
update_filtered
|
|
152
145
|
end
|
|
@@ -173,18 +166,18 @@ module Clack
|
|
|
173
166
|
end
|
|
174
167
|
end
|
|
175
168
|
|
|
176
|
-
def
|
|
169
|
+
def navigable_items = @filtered
|
|
177
170
|
|
|
178
171
|
def option_display(opt, active)
|
|
179
|
-
selected = @selected.include?(opt
|
|
172
|
+
selected = @selected.include?(opt.value)
|
|
180
173
|
checkbox = if selected
|
|
181
174
|
Colors.green(Symbols::S_CHECKBOX_SELECTED)
|
|
182
175
|
else
|
|
183
176
|
Colors.dim(Symbols::S_CHECKBOX_INACTIVE)
|
|
184
177
|
end
|
|
185
178
|
|
|
186
|
-
label = active ? opt
|
|
187
|
-
hint = (opt
|
|
179
|
+
label = active ? opt.label : Colors.dim(opt.label)
|
|
180
|
+
hint = (opt.hint && active) ? Colors.dim(" (#{opt.hint})") : ""
|
|
188
181
|
|
|
189
182
|
"#{checkbox} #{label}#{hint}"
|
|
190
183
|
end
|
|
@@ -33,42 +33,20 @@ module Clack
|
|
|
33
33
|
|
|
34
34
|
protected
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
return if terminal_state?
|
|
38
|
-
|
|
39
|
-
action = Core::Settings.action?(key)
|
|
40
|
-
|
|
36
|
+
def handle_input(key, action)
|
|
41
37
|
case action
|
|
42
|
-
when :
|
|
43
|
-
|
|
44
|
-
when :enter
|
|
45
|
-
submit
|
|
46
|
-
when :left, :up
|
|
47
|
-
@value = true
|
|
48
|
-
when :right, :down
|
|
49
|
-
@value = false
|
|
38
|
+
when :left, :up then @value = true
|
|
39
|
+
when :right, :down then @value = false
|
|
50
40
|
else
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def handle_char(key)
|
|
56
|
-
case key&.downcase
|
|
57
|
-
when "y"
|
|
58
|
-
@value = true
|
|
59
|
-
when "n"
|
|
60
|
-
@value = false
|
|
41
|
+
case key&.downcase
|
|
42
|
+
when "y" then @value = true
|
|
43
|
+
when "n" then @value = false
|
|
44
|
+
end
|
|
61
45
|
end
|
|
62
46
|
end
|
|
63
47
|
|
|
64
48
|
def build_frame
|
|
65
|
-
|
|
66
|
-
lines << "#{bar}\n"
|
|
67
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
68
|
-
lines << help_line
|
|
69
|
-
lines << "#{bar} #{options_display}\n"
|
|
70
|
-
lines << "#{Colors.gray(Symbols::S_BAR_END)}\n"
|
|
71
|
-
lines.join
|
|
49
|
+
"#{frame_header}#{bar} #{options_display}\n#{frame_footer}"
|
|
72
50
|
end
|
|
73
51
|
|
|
74
52
|
def final_display = @value ? @active_label : @inactive_label
|
data/lib/clack/prompts/date.rb
CHANGED
|
@@ -83,20 +83,7 @@ module Clack
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def build_frame
|
|
86
|
-
|
|
87
|
-
lines << "#{bar}\n"
|
|
88
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
89
|
-
lines << help_line
|
|
90
|
-
lines << "#{active_bar} #{date_display}\n"
|
|
91
|
-
lines << "#{bar_end}\n" if %i[active initial].include?(@state)
|
|
92
|
-
|
|
93
|
-
validation_lines = validation_message_lines
|
|
94
|
-
if validation_lines.any?
|
|
95
|
-
lines[-1] = validation_lines.first
|
|
96
|
-
lines.concat(validation_lines[1..])
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
lines.join
|
|
86
|
+
"#{frame_header}#{active_bar} #{date_display}\n#{frame_footer}"
|
|
100
87
|
end
|
|
101
88
|
|
|
102
89
|
def final_display = formatted_date
|
|
@@ -34,6 +34,8 @@ module Clack
|
|
|
34
34
|
# )
|
|
35
35
|
#
|
|
36
36
|
class GroupMultiselect < Core::Prompt
|
|
37
|
+
include Core::SelectionManager
|
|
38
|
+
|
|
37
39
|
# @param message [String] the prompt message
|
|
38
40
|
# @param options [Array<Hash>] groups, each with :label and :options (Array<Hash, String>)
|
|
39
41
|
# @param initial_values [Array] values to pre-select
|
|
@@ -52,45 +54,34 @@ module Clack
|
|
|
52
54
|
cursor_at: nil,
|
|
53
55
|
**opts
|
|
54
56
|
)
|
|
57
|
+
if opts.key?(:initial_value)
|
|
58
|
+
raise ArgumentError, "GroupMultiselect uses initial_values: (plural), not initial_value:"
|
|
59
|
+
end
|
|
55
60
|
super(message:, **opts)
|
|
56
61
|
@groups = normalize_groups(options)
|
|
57
62
|
@flat_items = build_flat_items
|
|
58
|
-
valid_values = Set.new(@flat_items.select { |item| item
|
|
63
|
+
valid_values = Set.new(@flat_items.select { |item| item.is_a?(Core::GroupOption) }.map(&:value))
|
|
59
64
|
@selected = Set.new(initial_values) & valid_values
|
|
60
65
|
@required = required
|
|
61
66
|
@selectable_groups = selectable_groups
|
|
62
67
|
@group_spacing = group_spacing
|
|
63
|
-
@
|
|
68
|
+
@option_index = find_initial_cursor(cursor_at)
|
|
64
69
|
update_value
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
protected
|
|
68
73
|
|
|
69
|
-
def
|
|
70
|
-
return if terminal_state?
|
|
71
|
-
|
|
72
|
-
action = Core::Settings.action?(key)
|
|
73
|
-
|
|
74
|
+
def handle_input(_key, action)
|
|
74
75
|
case action
|
|
75
|
-
when :
|
|
76
|
-
|
|
77
|
-
when :
|
|
78
|
-
submit
|
|
79
|
-
when :up
|
|
80
|
-
move_cursor(-1)
|
|
81
|
-
when :down
|
|
82
|
-
move_cursor(1)
|
|
83
|
-
when :space
|
|
84
|
-
toggle_current
|
|
76
|
+
when :up then move_cursor(-1)
|
|
77
|
+
when :down then move_cursor(1)
|
|
78
|
+
when :space then toggle_current
|
|
85
79
|
end
|
|
86
80
|
end
|
|
87
81
|
|
|
88
82
|
def submit
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@state = :error
|
|
92
|
-
return
|
|
93
|
-
end
|
|
83
|
+
return unless validate_selection
|
|
84
|
+
|
|
94
85
|
super
|
|
95
86
|
end
|
|
96
87
|
|
|
@@ -102,8 +93,8 @@ module Clack
|
|
|
102
93
|
|
|
103
94
|
prev_was_group = false
|
|
104
95
|
@flat_items.each_with_index do |item, idx|
|
|
105
|
-
is_group = item
|
|
106
|
-
is_last_in_group = item
|
|
96
|
+
is_group = item.is_a?(Core::GroupHeader)
|
|
97
|
+
is_last_in_group = item.is_a?(Core::GroupOption) ? item.last_in_group : false
|
|
107
98
|
|
|
108
99
|
# Add group spacing before groups (except first)
|
|
109
100
|
if is_group && !prev_was_group && idx.positive? && @group_spacing.positive?
|
|
@@ -111,9 +102,9 @@ module Clack
|
|
|
111
102
|
end
|
|
112
103
|
|
|
113
104
|
lines << if is_group
|
|
114
|
-
group_display(item, idx == @
|
|
105
|
+
group_display(item, idx == @option_index)
|
|
115
106
|
else
|
|
116
|
-
option_display(item, idx == @
|
|
107
|
+
option_display(item, idx == @option_index, is_last_in_group)
|
|
117
108
|
end
|
|
118
109
|
|
|
119
110
|
prev_was_group = is_group
|
|
@@ -126,7 +117,7 @@ module Clack
|
|
|
126
117
|
lines.join
|
|
127
118
|
end
|
|
128
119
|
|
|
129
|
-
def final_display = selected_options.map
|
|
120
|
+
def final_display = selected_options.map(&:label).join(", ")
|
|
130
121
|
|
|
131
122
|
private
|
|
132
123
|
|
|
@@ -140,41 +131,35 @@ module Clack
|
|
|
140
131
|
end
|
|
141
132
|
|
|
142
133
|
def normalize_option(opt)
|
|
143
|
-
|
|
144
|
-
when Hash
|
|
145
|
-
{value: opt[:value], label: opt[:label] || opt[:value].to_s, hint: opt[:hint], disabled: opt[:disabled] || false}
|
|
146
|
-
else
|
|
147
|
-
{value: opt, label: opt.to_s, hint: nil, disabled: false}
|
|
148
|
-
end
|
|
134
|
+
Core::OptionsHelper.normalize_option(opt)
|
|
149
135
|
end
|
|
150
136
|
|
|
151
137
|
def build_flat_items = @groups.flat_map { |group| flatten_group(group) }
|
|
152
138
|
|
|
153
139
|
def flatten_group(group)
|
|
154
|
-
group_item =
|
|
140
|
+
group_item = Core::GroupHeader.new(label: group[:label], options: group[:options])
|
|
155
141
|
option_items = group[:options].each_with_index.map do |opt, idx|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
disabled: opt[:disabled],
|
|
142
|
+
Core::GroupOption.new(
|
|
143
|
+
value: opt.value,
|
|
144
|
+
label: opt.label,
|
|
145
|
+
hint: opt.hint,
|
|
146
|
+
disabled: opt.disabled,
|
|
162
147
|
group: group,
|
|
163
148
|
last_in_group: idx == group[:options].length - 1
|
|
164
|
-
|
|
149
|
+
)
|
|
165
150
|
end
|
|
166
151
|
[group_item, *option_items]
|
|
167
152
|
end
|
|
168
153
|
|
|
169
154
|
def selected_options
|
|
170
|
-
@flat_items.select { |item| item
|
|
155
|
+
@flat_items.select { |item| item.is_a?(Core::GroupOption) && @selected.include?(item.value) }
|
|
171
156
|
end
|
|
172
157
|
|
|
173
158
|
def find_initial_cursor(cursor_at)
|
|
174
159
|
return 0 if @flat_items.empty?
|
|
175
160
|
|
|
176
161
|
if cursor_at
|
|
177
|
-
idx = @flat_items.find_index { |item| item
|
|
162
|
+
idx = @flat_items.find_index { |item| item.respond_to?(:value) && item.value == cursor_at }
|
|
178
163
|
return idx if idx
|
|
179
164
|
end
|
|
180
165
|
|
|
@@ -182,14 +167,14 @@ module Clack
|
|
|
182
167
|
end
|
|
183
168
|
|
|
184
169
|
def can_select?(item)
|
|
185
|
-
return false if item
|
|
186
|
-
return @selectable_groups if item
|
|
170
|
+
return false if item.is_a?(Core::GroupOption) && item.disabled
|
|
171
|
+
return @selectable_groups if item.is_a?(Core::GroupHeader)
|
|
187
172
|
|
|
188
173
|
true
|
|
189
174
|
end
|
|
190
175
|
|
|
191
176
|
def move_cursor(delta)
|
|
192
|
-
new_cursor = @
|
|
177
|
+
new_cursor = @option_index
|
|
193
178
|
attempts = @flat_items.length
|
|
194
179
|
|
|
195
180
|
loop do
|
|
@@ -198,14 +183,14 @@ module Clack
|
|
|
198
183
|
break if can_select?(@flat_items[new_cursor]) || attempts <= 0
|
|
199
184
|
end
|
|
200
185
|
|
|
201
|
-
@
|
|
186
|
+
@option_index = new_cursor
|
|
202
187
|
end
|
|
203
188
|
|
|
204
189
|
def toggle_current
|
|
205
|
-
item = @flat_items[@
|
|
190
|
+
item = @flat_items[@option_index]
|
|
206
191
|
return unless can_select?(item)
|
|
207
192
|
|
|
208
|
-
if item
|
|
193
|
+
if item.is_a?(Core::GroupHeader)
|
|
209
194
|
toggle_group(item)
|
|
210
195
|
else
|
|
211
196
|
toggle_option(item)
|
|
@@ -214,7 +199,7 @@ module Clack
|
|
|
214
199
|
end
|
|
215
200
|
|
|
216
201
|
def toggle_group(group_item)
|
|
217
|
-
group_values = group_item
|
|
202
|
+
group_values = group_item.options.reject { |o| o.disabled }.map(&:value)
|
|
218
203
|
all_selected = group_values.all? { |v| @selected.include?(v) }
|
|
219
204
|
|
|
220
205
|
if all_selected
|
|
@@ -225,45 +210,41 @@ module Clack
|
|
|
225
210
|
end
|
|
226
211
|
|
|
227
212
|
def toggle_option(item)
|
|
228
|
-
|
|
229
|
-
@selected.delete(item[:value])
|
|
230
|
-
else
|
|
231
|
-
@selected.add(item[:value])
|
|
232
|
-
end
|
|
213
|
+
toggle_value(item.value)
|
|
233
214
|
end
|
|
234
215
|
|
|
235
|
-
def update_value =
|
|
216
|
+
def update_value = update_selection_value
|
|
236
217
|
|
|
237
218
|
def group_display(item, active)
|
|
238
219
|
if @selectable_groups
|
|
239
|
-
all_selected = item
|
|
220
|
+
all_selected = item.options.reject { |o| o.disabled }.all? { |o| @selected.include?(o.value) }
|
|
240
221
|
checkbox = all_selected ? Colors.green(Symbols::S_CHECKBOX_SELECTED) : Colors.dim(Symbols::S_CHECKBOX_INACTIVE)
|
|
241
|
-
label = active ? item
|
|
222
|
+
label = active ? item.label : Colors.dim(item.label)
|
|
242
223
|
"#{active_bar} #{checkbox} #{label}\n"
|
|
243
224
|
else
|
|
244
|
-
"#{active_bar} #{Colors.dim(item
|
|
225
|
+
"#{active_bar} #{Colors.dim(item.label)}\n"
|
|
245
226
|
end
|
|
246
227
|
end
|
|
247
228
|
|
|
248
229
|
def option_display(item, active, is_last)
|
|
249
|
-
selected = @selected.include?(item
|
|
230
|
+
selected = @selected.include?(item.value)
|
|
250
231
|
prefix = if @selectable_groups
|
|
251
232
|
"#{is_last ? Symbols::S_BAR_END : Symbols::S_BAR} "
|
|
252
233
|
else
|
|
253
234
|
" "
|
|
254
235
|
end
|
|
255
|
-
hint = (item
|
|
236
|
+
hint = (item.hint && active) ? " #{Colors.dim("(#{item.hint})")}" : ""
|
|
256
237
|
|
|
257
|
-
if item
|
|
258
|
-
"#{active_bar} #{Colors.dim(prefix)}#{Colors.dim(Symbols::S_CHECKBOX_INACTIVE)} #{Colors.strikethrough(Colors.dim(item
|
|
238
|
+
if item.disabled
|
|
239
|
+
"#{active_bar} #{Colors.dim(prefix)}#{Colors.dim(Symbols::S_CHECKBOX_INACTIVE)} #{Colors.strikethrough(Colors.dim(item.label))}\n"
|
|
259
240
|
elsif active && selected
|
|
260
|
-
"#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{item
|
|
241
|
+
"#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{item.label}#{hint}\n"
|
|
261
242
|
elsif active
|
|
262
|
-
"#{active_bar} #{Colors.dim(prefix)}#{Colors.cyan(Symbols::S_CHECKBOX_ACTIVE)} #{item
|
|
243
|
+
"#{active_bar} #{Colors.dim(prefix)}#{Colors.cyan(Symbols::S_CHECKBOX_ACTIVE)} #{item.label}#{hint}\n"
|
|
263
244
|
elsif selected
|
|
264
|
-
"#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{Colors.dim(item
|
|
245
|
+
"#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{Colors.dim(item.label)}\n"
|
|
265
246
|
else
|
|
266
|
-
"#{active_bar} #{Colors.dim(prefix)}#{Colors.dim(Symbols::S_CHECKBOX_INACTIVE)} #{Colors.dim(item
|
|
247
|
+
"#{active_bar} #{Colors.dim(prefix)}#{Colors.dim(Symbols::S_CHECKBOX_INACTIVE)} #{Colors.dim(item.label)}\n"
|
|
267
248
|
end
|
|
268
249
|
end
|
|
269
250
|
end
|
|
@@ -21,6 +21,8 @@ module Clack
|
|
|
21
21
|
# )
|
|
22
22
|
#
|
|
23
23
|
class MultilineText < Core::Prompt
|
|
24
|
+
include Core::TextInputHelper
|
|
25
|
+
|
|
24
26
|
# @param message [String] the prompt message
|
|
25
27
|
# @param initial_value [String, nil] pre-filled editable text (can contain newlines)
|
|
26
28
|
# @option opts [Proc, nil] :validate validation proc returning error string or nil
|
|
@@ -29,7 +31,7 @@ module Clack
|
|
|
29
31
|
super(message:, **opts)
|
|
30
32
|
@lines = parse_initial_value(initial_value)
|
|
31
33
|
@line_index = @lines.length - 1
|
|
32
|
-
@
|
|
34
|
+
@cursor = current_line.grapheme_clusters.length
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
protected
|
|
@@ -64,24 +66,17 @@ module Clack
|
|
|
64
66
|
super
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
lines << "#{symbol_for_state} #{@message} #{Colors.dim("(Ctrl+D to submit)")}\n"
|
|
71
|
-
lines << help_line
|
|
72
|
-
|
|
73
|
-
@lines.each_with_index do |line, idx|
|
|
74
|
-
display = (idx == @line_index) ? line_with_cursor(line) : line
|
|
75
|
-
lines << "#{active_bar} #{display}\n"
|
|
76
|
-
end
|
|
69
|
+
def frame_header
|
|
70
|
+
"#{bar}\n#{symbol_for_state} #{@message} #{Colors.dim("(Ctrl+D to submit)")}\n#{help_line}"
|
|
71
|
+
end
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
73
|
+
def build_frame
|
|
74
|
+
body = @lines.each_with_index.map do |line, idx|
|
|
75
|
+
display = (idx == @line_index) ? value_with_cursor : line
|
|
76
|
+
"#{active_bar} #{display}\n"
|
|
77
|
+
end.join
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
"#{frame_header}#{body}#{frame_footer}"
|
|
85
80
|
end
|
|
86
81
|
|
|
87
82
|
def build_final_frame
|
|
@@ -98,6 +93,13 @@ module Clack
|
|
|
98
93
|
lines.join
|
|
99
94
|
end
|
|
100
95
|
|
|
96
|
+
# TextInputHelper backing store: delegate to current line.
|
|
97
|
+
def text_value = current_line
|
|
98
|
+
|
|
99
|
+
def text_value=(val)
|
|
100
|
+
@lines[@line_index] = val
|
|
101
|
+
end
|
|
102
|
+
|
|
101
103
|
private
|
|
102
104
|
|
|
103
105
|
def parse_initial_value(value)
|
|
@@ -108,26 +110,15 @@ module Clack
|
|
|
108
110
|
|
|
109
111
|
def current_line = @lines[@line_index] || ""
|
|
110
112
|
|
|
111
|
-
def line_with_cursor(line)
|
|
112
|
-
chars = line.grapheme_clusters
|
|
113
|
-
return cursor_block if chars.empty?
|
|
114
|
-
return "#{line}#{cursor_block}" if @column >= chars.length
|
|
115
|
-
|
|
116
|
-
before = chars[0...@column].join
|
|
117
|
-
current = Colors.inverse(chars[@column])
|
|
118
|
-
after = chars[(@column + 1)..].join
|
|
119
|
-
"#{before}#{current}#{after}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
113
|
def insert_newline
|
|
123
114
|
chars = current_line.grapheme_clusters
|
|
124
|
-
before = chars[0...@
|
|
125
|
-
after = chars[@
|
|
115
|
+
before = chars[0...@cursor].join
|
|
116
|
+
after = chars[@cursor..].join
|
|
126
117
|
|
|
127
118
|
@lines[@line_index] = before
|
|
128
119
|
@lines.insert(@line_index + 1, after)
|
|
129
120
|
@line_index += 1
|
|
130
|
-
@
|
|
121
|
+
@cursor = 0
|
|
131
122
|
end
|
|
132
123
|
|
|
133
124
|
def move_up
|
|
@@ -145,43 +136,32 @@ module Clack
|
|
|
145
136
|
end
|
|
146
137
|
|
|
147
138
|
def move_left
|
|
148
|
-
return if @
|
|
139
|
+
return if @cursor.zero?
|
|
149
140
|
|
|
150
|
-
@
|
|
141
|
+
@cursor -= 1
|
|
151
142
|
end
|
|
152
143
|
|
|
153
144
|
def move_right
|
|
154
145
|
max = current_line.grapheme_clusters.length
|
|
155
|
-
return if @
|
|
146
|
+
return if @cursor >= max
|
|
156
147
|
|
|
157
|
-
@
|
|
148
|
+
@cursor += 1
|
|
158
149
|
end
|
|
159
150
|
|
|
160
151
|
def clamp_column
|
|
161
152
|
max = current_line.grapheme_clusters.length
|
|
162
|
-
@
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def handle_text_input(key)
|
|
166
|
-
return handle_backspace if Core::Settings.backspace?(key)
|
|
167
|
-
return unless Core::Settings.printable?(key)
|
|
168
|
-
|
|
169
|
-
chars = current_line.grapheme_clusters
|
|
170
|
-
chars.insert(@column, key)
|
|
171
|
-
@lines[@line_index] = chars.join
|
|
172
|
-
@column += 1
|
|
153
|
+
@cursor = [@cursor, max].min
|
|
173
154
|
end
|
|
174
155
|
|
|
156
|
+
# Override backspace to handle line merging at column 0
|
|
175
157
|
def handle_backspace
|
|
176
|
-
if @
|
|
177
|
-
return if @line_index.zero?
|
|
158
|
+
if @cursor.zero?
|
|
159
|
+
return false if @line_index.zero?
|
|
178
160
|
|
|
179
161
|
merge_line_up
|
|
162
|
+
true
|
|
180
163
|
else
|
|
181
|
-
|
|
182
|
-
chars.delete_at(@column - 1)
|
|
183
|
-
@lines[@line_index] = chars.join
|
|
184
|
-
@column -= 1
|
|
164
|
+
super
|
|
185
165
|
end
|
|
186
166
|
end
|
|
187
167
|
|
|
@@ -192,7 +172,7 @@ module Clack
|
|
|
192
172
|
@line_index -= 1
|
|
193
173
|
prev_length = current_line.grapheme_clusters.length
|
|
194
174
|
@lines[@line_index] = current_line + current_content
|
|
195
|
-
@
|
|
175
|
+
@cursor = prev_length
|
|
196
176
|
end
|
|
197
177
|
end
|
|
198
178
|
end
|