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,6 +29,7 @@ module Clack
|
|
|
29
29
|
#
|
|
30
30
|
class Multiselect < Core::Prompt
|
|
31
31
|
include Core::OptionsHelper
|
|
32
|
+
include Core::SelectionManager
|
|
32
33
|
|
|
33
34
|
# @param message [String] the prompt message
|
|
34
35
|
# @param options [Array<Hash, String>] list of options
|
|
@@ -38,15 +39,18 @@ module Clack
|
|
|
38
39
|
# @param cursor_at [Object, nil] value to position cursor at initially
|
|
39
40
|
# @param opts [Hash] additional options passed to {Core::Prompt}
|
|
40
41
|
def initialize(message:, options:, initial_values: [], required: true, max_items: nil, cursor_at: nil, **opts)
|
|
42
|
+
if opts.key?(:initial_value)
|
|
43
|
+
raise ArgumentError, "Multiselect uses initial_values: (plural), not initial_value:"
|
|
44
|
+
end
|
|
41
45
|
super(message:, **opts)
|
|
42
46
|
@options = normalize_options(options)
|
|
43
|
-
valid_values = Set.new(@options.map { |o| o
|
|
47
|
+
valid_values = Set.new(@options.map { |o| o.value })
|
|
44
48
|
@selected = Set.new(initial_values) & valid_values
|
|
45
49
|
@required = required
|
|
46
50
|
@max_items = max_items
|
|
47
51
|
@scroll_offset = 0
|
|
48
|
-
@
|
|
49
|
-
|
|
52
|
+
@option_index = find_initial_cursor(cursor_at)
|
|
53
|
+
update_selection_value
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
protected
|
|
@@ -74,11 +78,8 @@ module Clack
|
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def submit
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@state = :error
|
|
80
|
-
return
|
|
81
|
-
end
|
|
81
|
+
return unless validate_selection
|
|
82
|
+
|
|
82
83
|
super
|
|
83
84
|
end
|
|
84
85
|
|
|
@@ -103,49 +104,37 @@ module Clack
|
|
|
103
104
|
lines.join
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def final_display
|
|
107
|
-
@options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
|
|
108
|
-
end
|
|
107
|
+
def final_display = selected_labels(@options)
|
|
109
108
|
|
|
110
109
|
private
|
|
111
110
|
|
|
112
111
|
def toggle_current
|
|
113
|
-
opt = @options[@
|
|
114
|
-
return if opt
|
|
112
|
+
opt = @options[@option_index]
|
|
113
|
+
return if opt.disabled
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
else
|
|
119
|
-
@selected.add(opt[:value])
|
|
120
|
-
end
|
|
121
|
-
update_value
|
|
115
|
+
toggle_value(opt.value)
|
|
116
|
+
update_selection_value
|
|
122
117
|
end
|
|
123
118
|
|
|
124
119
|
def toggle_all
|
|
125
|
-
enabled = @options.reject { |o| o
|
|
120
|
+
enabled = @options.reject { |o| o.disabled }.map { |o| o.value }
|
|
126
121
|
if enabled.all? { |v| @selected.include?(v) }
|
|
127
122
|
@selected.clear
|
|
128
123
|
else
|
|
129
124
|
@selected.merge(enabled)
|
|
130
125
|
end
|
|
131
|
-
|
|
126
|
+
update_selection_value
|
|
132
127
|
end
|
|
133
128
|
|
|
134
129
|
def invert_selection
|
|
135
130
|
@options.each do |opt|
|
|
136
|
-
next if opt
|
|
131
|
+
next if opt.disabled
|
|
137
132
|
|
|
138
|
-
|
|
139
|
-
@selected.delete(opt[:value])
|
|
140
|
-
else
|
|
141
|
-
@selected.add(opt[:value])
|
|
142
|
-
end
|
|
133
|
+
toggle_value(opt.value)
|
|
143
134
|
end
|
|
144
|
-
|
|
135
|
+
update_selection_value
|
|
145
136
|
end
|
|
146
137
|
|
|
147
|
-
def update_value = @value = @selected.to_a
|
|
148
|
-
|
|
149
138
|
def keyboard_hints
|
|
150
139
|
hints = [
|
|
151
140
|
"#{Colors.dim("space")} select",
|
|
@@ -156,23 +145,23 @@ module Clack
|
|
|
156
145
|
end
|
|
157
146
|
|
|
158
147
|
def option_display(opt, idx)
|
|
159
|
-
active = idx == @
|
|
160
|
-
selected = @selected.include?(opt
|
|
148
|
+
active = idx == @option_index
|
|
149
|
+
selected = @selected.include?(opt.value)
|
|
161
150
|
|
|
162
151
|
symbol, label = option_parts(opt, active, selected)
|
|
163
152
|
"#{symbol} #{label}"
|
|
164
153
|
end
|
|
165
154
|
|
|
166
155
|
def option_parts(opt, active, selected)
|
|
167
|
-
if opt
|
|
156
|
+
if opt.disabled
|
|
168
157
|
return [Colors.dim(Symbols::S_CHECKBOX_INACTIVE),
|
|
169
|
-
Colors.strikethrough(Colors.dim(opt
|
|
158
|
+
Colors.strikethrough(Colors.dim(opt.label))]
|
|
170
159
|
end
|
|
171
|
-
return [Colors.green(Symbols::S_CHECKBOX_SELECTED), opt
|
|
172
|
-
return [Colors.cyan(Symbols::S_CHECKBOX_ACTIVE), opt
|
|
173
|
-
return [Colors.green(Symbols::S_CHECKBOX_SELECTED), Colors.dim(opt
|
|
160
|
+
return [Colors.green(Symbols::S_CHECKBOX_SELECTED), opt.label] if active && selected
|
|
161
|
+
return [Colors.cyan(Symbols::S_CHECKBOX_ACTIVE), opt.label] if active
|
|
162
|
+
return [Colors.green(Symbols::S_CHECKBOX_SELECTED), Colors.dim(opt.label)] if selected
|
|
174
163
|
|
|
175
|
-
[Colors.dim(Symbols::S_CHECKBOX_INACTIVE), Colors.dim(opt
|
|
164
|
+
[Colors.dim(Symbols::S_CHECKBOX_INACTIVE), Colors.dim(opt.label)]
|
|
176
165
|
end
|
|
177
166
|
end
|
|
178
167
|
end
|
|
@@ -35,20 +35,7 @@ module Clack
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def build_frame
|
|
38
|
-
|
|
39
|
-
lines << "#{bar}\n"
|
|
40
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
41
|
-
lines << help_line
|
|
42
|
-
lines << "#{active_bar} #{masked_display}\n"
|
|
43
|
-
lines << "#{bar_end}\n"
|
|
44
|
-
|
|
45
|
-
validation_lines = validation_message_lines
|
|
46
|
-
if validation_lines.any?
|
|
47
|
-
lines[-1] = validation_lines.first
|
|
48
|
-
lines.concat(validation_lines[1..])
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
lines.join
|
|
38
|
+
"#{frame_header}#{active_bar} #{masked_display}\n#{frame_footer}"
|
|
52
39
|
end
|
|
53
40
|
|
|
54
41
|
def final_display = @mask * @value.grapheme_clusters.length
|
data/lib/clack/prompts/path.rb
CHANGED
|
@@ -25,8 +25,8 @@ module Clack
|
|
|
25
25
|
# )
|
|
26
26
|
#
|
|
27
27
|
class Path < Core::Prompt
|
|
28
|
+
include Core::OptionsHelper
|
|
28
29
|
include Core::TextInputHelper
|
|
29
|
-
include Core::ScrollHelper
|
|
30
30
|
|
|
31
31
|
# @param message [String] the prompt message
|
|
32
32
|
# @param root [String] starting/base directory (default: ".")
|
|
@@ -41,7 +41,7 @@ module Clack
|
|
|
41
41
|
@max_items = max_items
|
|
42
42
|
@value = ""
|
|
43
43
|
@cursor = 0
|
|
44
|
-
@
|
|
44
|
+
@option_index = 0
|
|
45
45
|
@scroll_offset = 0
|
|
46
46
|
@suggestions = []
|
|
47
47
|
@dir_cache = {} # directory path => sorted entries array
|
|
@@ -69,7 +69,7 @@ module Clack
|
|
|
69
69
|
def handle_text_input(key)
|
|
70
70
|
return unless super
|
|
71
71
|
|
|
72
|
-
@
|
|
72
|
+
@option_index = 0
|
|
73
73
|
@scroll_offset = 0
|
|
74
74
|
update_suggestions
|
|
75
75
|
end
|
|
@@ -77,7 +77,7 @@ module Clack
|
|
|
77
77
|
def autocomplete_selection
|
|
78
78
|
return if @suggestions.empty?
|
|
79
79
|
|
|
80
|
-
@value = @suggestions[@
|
|
80
|
+
@value = @suggestions[@option_index]
|
|
81
81
|
@cursor = @value.length
|
|
82
82
|
update_suggestions
|
|
83
83
|
end
|
|
@@ -102,26 +102,12 @@ module Clack
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def build_frame
|
|
105
|
-
|
|
106
|
-
lines << "#{bar}\n"
|
|
107
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
108
|
-
lines << help_line
|
|
109
|
-
lines << "#{active_bar} #{input_display}\n"
|
|
110
|
-
|
|
111
|
-
visible_items.each_with_index do |path, idx|
|
|
105
|
+
suggestion_lines = visible_options.each_with_index.map do |path, idx|
|
|
112
106
|
actual_idx = @scroll_offset + idx
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
lines << "#{bar_end}\n"
|
|
117
|
-
|
|
118
|
-
validation_lines = validation_message_lines
|
|
119
|
-
if validation_lines.any?
|
|
120
|
-
lines[-1] = validation_lines.first
|
|
121
|
-
lines.concat(validation_lines[1..])
|
|
122
|
-
end
|
|
107
|
+
"#{bar} #{suggestion_display(path, actual_idx == @option_index)}\n"
|
|
108
|
+
end.join
|
|
123
109
|
|
|
124
|
-
|
|
110
|
+
"#{frame_header}#{active_bar} #{input_display}\n#{suggestion_lines}#{frame_footer}"
|
|
125
111
|
end
|
|
126
112
|
|
|
127
113
|
private
|
|
@@ -202,7 +188,7 @@ module Clack
|
|
|
202
188
|
expanded == @root || expanded.start_with?("#{@root}/")
|
|
203
189
|
end
|
|
204
190
|
|
|
205
|
-
def
|
|
191
|
+
def navigable_items = @suggestions
|
|
206
192
|
|
|
207
193
|
# Override to use @root as placeholder
|
|
208
194
|
def placeholder_display
|
|
@@ -36,6 +36,7 @@ module Clack
|
|
|
36
36
|
@output = output
|
|
37
37
|
@started = false
|
|
38
38
|
@width = 40
|
|
39
|
+
@last_frame = nil
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Start displaying the progress bar.
|
|
@@ -105,8 +106,11 @@ module Clack
|
|
|
105
106
|
def render
|
|
106
107
|
return unless @started
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
frame = "#{symbol} #{progress_bar} #{percentage}#{message_text}"
|
|
110
|
+
return if frame == @last_frame
|
|
111
|
+
|
|
112
|
+
@last_frame = frame
|
|
113
|
+
@output.print "\r\e[2K#{frame}"
|
|
110
114
|
@output.flush
|
|
111
115
|
end
|
|
112
116
|
|
data/lib/clack/prompts/range.rb
CHANGED
|
@@ -51,20 +51,7 @@ module Clack
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def build_frame
|
|
54
|
-
|
|
55
|
-
lines << "#{bar}\n"
|
|
56
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
57
|
-
lines << help_line
|
|
58
|
-
lines << "#{active_bar} #{slider_display}\n"
|
|
59
|
-
lines << "#{bar_end}\n" if %i[active initial].include?(@state)
|
|
60
|
-
|
|
61
|
-
validation_lines = validation_message_lines
|
|
62
|
-
if validation_lines.any?
|
|
63
|
-
lines[-1] = validation_lines.first
|
|
64
|
-
lines.concat(validation_lines[1..])
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
lines.join
|
|
54
|
+
"#{frame_header}#{active_bar} #{slider_display}\n#{frame_footer}"
|
|
68
55
|
end
|
|
69
56
|
|
|
70
57
|
def final_display = format_value(@value)
|
data/lib/clack/prompts/select.rb
CHANGED
|
@@ -40,47 +40,33 @@ module Clack
|
|
|
40
40
|
def initialize(message:, options:, initial_value: nil, max_items: nil, **opts)
|
|
41
41
|
super(message:, **opts)
|
|
42
42
|
@options = normalize_options(options)
|
|
43
|
-
@cursor = find_initial_cursor(initial_value)
|
|
44
43
|
@max_items = max_items
|
|
45
44
|
@scroll_offset = 0
|
|
45
|
+
@option_index = find_initial_cursor(initial_value)
|
|
46
46
|
update_value
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
protected
|
|
50
50
|
|
|
51
|
-
def
|
|
52
|
-
return if terminal_state?
|
|
53
|
-
|
|
54
|
-
action = Core::Settings.action?(key)
|
|
55
|
-
|
|
51
|
+
def handle_input(_key, action)
|
|
56
52
|
case action
|
|
57
|
-
when :
|
|
58
|
-
|
|
59
|
-
when :enter
|
|
60
|
-
submit unless current_option[:disabled]
|
|
61
|
-
when :up, :left
|
|
62
|
-
move_cursor(-1)
|
|
63
|
-
when :down, :right
|
|
64
|
-
move_cursor(1)
|
|
53
|
+
when :up, :left then move_cursor(-1)
|
|
54
|
+
when :down, :right then move_cursor(1)
|
|
65
55
|
end
|
|
66
56
|
end
|
|
67
57
|
|
|
68
|
-
def
|
|
69
|
-
lines = []
|
|
70
|
-
lines << "#{bar}\n"
|
|
71
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
72
|
-
lines << help_line
|
|
58
|
+
def can_submit? = !current_option.disabled
|
|
73
59
|
|
|
74
|
-
|
|
60
|
+
def build_frame
|
|
61
|
+
option_lines = visible_options.each_with_index.map do |opt, idx|
|
|
75
62
|
actual_idx = @scroll_offset + idx
|
|
76
|
-
|
|
77
|
-
end
|
|
63
|
+
"#{bar} #{option_display(opt, actual_idx == @option_index)}\n"
|
|
64
|
+
end.join
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
lines.join
|
|
66
|
+
"#{frame_header}#{option_lines}#{frame_footer}"
|
|
81
67
|
end
|
|
82
68
|
|
|
83
|
-
def final_display = current_option
|
|
69
|
+
def final_display = current_option.label
|
|
84
70
|
|
|
85
71
|
private
|
|
86
72
|
|
|
@@ -89,12 +75,12 @@ module Clack
|
|
|
89
75
|
update_value
|
|
90
76
|
end
|
|
91
77
|
|
|
92
|
-
def update_value = @value = current_option
|
|
78
|
+
def update_value = @value = current_option.value
|
|
93
79
|
|
|
94
|
-
def current_option = @options[@
|
|
80
|
+
def current_option = @options[@option_index]
|
|
95
81
|
|
|
96
82
|
def option_display(opt, active)
|
|
97
|
-
return disabled_option_display(opt) if opt
|
|
83
|
+
return disabled_option_display(opt) if opt.disabled
|
|
98
84
|
return active_option_display(opt) if active
|
|
99
85
|
|
|
100
86
|
inactive_option_display(opt)
|
|
@@ -102,19 +88,19 @@ module Clack
|
|
|
102
88
|
|
|
103
89
|
def disabled_option_display(opt)
|
|
104
90
|
symbol = Colors.dim(Symbols::S_RADIO_INACTIVE)
|
|
105
|
-
label = Colors.strikethrough(Colors.dim(opt
|
|
91
|
+
label = Colors.strikethrough(Colors.dim(opt.label))
|
|
106
92
|
"#{symbol} #{label}"
|
|
107
93
|
end
|
|
108
94
|
|
|
109
95
|
def active_option_display(opt)
|
|
110
96
|
symbol = Colors.green(Symbols::S_RADIO_ACTIVE)
|
|
111
|
-
hint = opt
|
|
112
|
-
"#{symbol} #{opt
|
|
97
|
+
hint = opt.hint ? " #{Colors.dim("(#{opt.hint})")}" : ""
|
|
98
|
+
"#{symbol} #{opt.label}#{hint}"
|
|
113
99
|
end
|
|
114
100
|
|
|
115
101
|
def inactive_option_display(opt)
|
|
116
102
|
symbol = Colors.dim(Symbols::S_RADIO_INACTIVE)
|
|
117
|
-
"#{symbol} #{Colors.dim(opt
|
|
103
|
+
"#{symbol} #{Colors.dim(opt.label)}"
|
|
118
104
|
end
|
|
119
105
|
end
|
|
120
106
|
end
|
|
@@ -41,10 +41,10 @@ module Clack
|
|
|
41
41
|
when :cancel
|
|
42
42
|
@state = :cancel
|
|
43
43
|
else
|
|
44
|
-
opt = @options.find { |o| o
|
|
44
|
+
opt = @options.find { |o| o.key&.downcase == key&.downcase }
|
|
45
45
|
return unless opt
|
|
46
46
|
|
|
47
|
-
@value = opt
|
|
47
|
+
@value = opt.value
|
|
48
48
|
@state = :submit
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -63,25 +63,25 @@ module Clack
|
|
|
63
63
|
lines.join
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def final_display = @options.find { |o| o
|
|
66
|
+
def final_display = @options.find { |o| o.value == @value }&.label.to_s
|
|
67
67
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
70
|
def normalize_options(options)
|
|
71
71
|
options.map do |opt|
|
|
72
|
-
|
|
72
|
+
Clack::Core::SelectKeyOption.new(
|
|
73
73
|
value: opt[:value],
|
|
74
74
|
label: opt[:label] || opt[:value].to_s,
|
|
75
75
|
key: opt[:key] || opt[:value].to_s[0],
|
|
76
76
|
hint: opt[:hint]
|
|
77
|
-
|
|
77
|
+
)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def option_display(opt)
|
|
82
|
-
key_display = Colors.cyan("[#{opt
|
|
83
|
-
hint = opt
|
|
84
|
-
"#{key_display} #{opt
|
|
82
|
+
key_display = Colors.cyan("[#{opt.key}]")
|
|
83
|
+
hint = opt.hint ? " #{Colors.dim("(#{opt.hint})")}" : ""
|
|
84
|
+
"#{key_display} #{opt.label}#{hint}"
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -49,9 +49,7 @@ module Clack
|
|
|
49
49
|
@frames = frames || Symbols::SPINNER_FRAMES
|
|
50
50
|
@delay = delay || Symbols::SPINNER_DELAY
|
|
51
51
|
@style_frame = style_frame || ->(frame) { Colors.magenta(frame) }
|
|
52
|
-
@
|
|
53
|
-
@cancelled = false
|
|
54
|
-
@finished = false
|
|
52
|
+
@state = :idle
|
|
55
53
|
@message = ""
|
|
56
54
|
@thread = nil
|
|
57
55
|
@frame_idx = 0
|
|
@@ -66,12 +64,10 @@ module Clack
|
|
|
66
64
|
# @return [self] for method chaining
|
|
67
65
|
def start(message = nil)
|
|
68
66
|
@mutex.synchronize do
|
|
69
|
-
return
|
|
67
|
+
return unless @state == :idle
|
|
70
68
|
|
|
71
69
|
@message = remove_trailing_dots(message || "")
|
|
72
|
-
@
|
|
73
|
-
@cancelled = false
|
|
74
|
-
@finished = false
|
|
70
|
+
@state = :running
|
|
75
71
|
@prev_frame = nil
|
|
76
72
|
@frame_idx = 0
|
|
77
73
|
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -102,7 +98,7 @@ module Clack
|
|
|
102
98
|
#
|
|
103
99
|
# @param message [String, nil] cancellation message
|
|
104
100
|
def cancel(message = nil)
|
|
105
|
-
finish(:
|
|
101
|
+
finish(:cancelled, message)
|
|
106
102
|
end
|
|
107
103
|
|
|
108
104
|
# Update the spinner message while running.
|
|
@@ -116,7 +112,7 @@ module Clack
|
|
|
116
112
|
# Clear the spinner without showing a final message.
|
|
117
113
|
def clear
|
|
118
114
|
@mutex.synchronize do
|
|
119
|
-
@
|
|
115
|
+
@state = :idle
|
|
120
116
|
end
|
|
121
117
|
@thread&.join
|
|
122
118
|
restore_cursor
|
|
@@ -124,7 +120,7 @@ module Clack
|
|
|
124
120
|
@output.print Core::Cursor.show
|
|
125
121
|
end
|
|
126
122
|
|
|
127
|
-
def cancelled? = @mutex.synchronize { @cancelled }
|
|
123
|
+
def cancelled? = @mutex.synchronize { @state == :cancelled }
|
|
128
124
|
|
|
129
125
|
private
|
|
130
126
|
|
|
@@ -139,7 +135,8 @@ module Clack
|
|
|
139
135
|
|
|
140
136
|
def spin_loop
|
|
141
137
|
frame_count = 0
|
|
142
|
-
while @mutex.synchronize { @running }
|
|
138
|
+
while @mutex.synchronize { @state == :running }
|
|
139
|
+
break if @output.closed?
|
|
143
140
|
frame = @style_frame.call(@frames[@frame_idx])
|
|
144
141
|
msg = @mutex.synchronize { @message }
|
|
145
142
|
render_frame(frame, msg, frame_count)
|
|
@@ -171,28 +168,26 @@ module Clack
|
|
|
171
168
|
end
|
|
172
169
|
end
|
|
173
170
|
|
|
174
|
-
def finish(
|
|
171
|
+
def finish(end_state, message)
|
|
175
172
|
thread_to_join = nil
|
|
176
173
|
msg, timer_suffix = @mutex.synchronize do
|
|
177
|
-
return
|
|
174
|
+
return unless @state == :running
|
|
178
175
|
|
|
179
|
-
@
|
|
180
|
-
@running = false
|
|
176
|
+
@state = end_state
|
|
181
177
|
thread_to_join = @thread
|
|
182
178
|
suffix = (@indicator == :timer && @start_time) ? " #{format_timer}" : ""
|
|
183
179
|
[message || @message, suffix]
|
|
184
180
|
end
|
|
185
181
|
|
|
186
|
-
thread_to_join&.join
|
|
182
|
+
thread_to_join&.join(5)
|
|
183
|
+
thread_to_join&.kill if thread_to_join&.alive?
|
|
187
184
|
|
|
188
185
|
@output.print "\r#{Core::Cursor.clear_to_end}"
|
|
189
186
|
|
|
190
|
-
symbol = case
|
|
187
|
+
symbol = case end_state
|
|
191
188
|
when :success then Colors.green(Symbols::S_STEP_SUBMIT)
|
|
192
189
|
when :error then Colors.red(Symbols::S_STEP_ERROR)
|
|
193
|
-
when :
|
|
194
|
-
@mutex.synchronize { @cancelled = true }
|
|
195
|
-
Colors.red(Symbols::S_STEP_CANCEL)
|
|
190
|
+
when :cancelled then Colors.red(Symbols::S_STEP_CANCEL)
|
|
196
191
|
end
|
|
197
192
|
|
|
198
193
|
@output.print "#{symbol} #{msg}#{timer_suffix}\n"
|
data/lib/clack/prompts/tasks.rb
CHANGED
data/lib/clack/prompts/text.rb
CHANGED
|
@@ -88,20 +88,7 @@ module Clack
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def build_frame
|
|
91
|
-
|
|
92
|
-
lines << "#{bar}\n"
|
|
93
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
94
|
-
lines << help_line
|
|
95
|
-
lines << "#{active_bar} #{input_display}\n"
|
|
96
|
-
lines << "#{bar_end}\n" if @state in :active | :initial
|
|
97
|
-
|
|
98
|
-
validation_lines = validation_message_lines
|
|
99
|
-
if validation_lines.any?
|
|
100
|
-
lines[-1] = validation_lines.first
|
|
101
|
-
lines.concat(validation_lines[1..])
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
lines.join
|
|
91
|
+
"#{frame_header}#{active_bar} #{input_display}\n#{frame_footer}"
|
|
105
92
|
end
|
|
106
93
|
|
|
107
94
|
private
|
data/lib/clack/testing.rb
CHANGED
|
@@ -105,6 +105,25 @@ module Clack
|
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# A StringIO-like object that feeds keys from a queue.
|
|
109
|
+
# Passed as the +input:+ parameter to prompts for testing.
|
|
110
|
+
class KeyQueue
|
|
111
|
+
def initialize(keys)
|
|
112
|
+
@keys = keys
|
|
113
|
+
@read_count = 0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def getc
|
|
117
|
+
@read_count += 1
|
|
118
|
+
raise "Too many reads (#{@read_count}) - possible infinite loop in test" if @read_count > 100
|
|
119
|
+
|
|
120
|
+
@keys.shift || KEYS[:enter]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Not a real TTY
|
|
124
|
+
def tty? = false
|
|
125
|
+
end
|
|
126
|
+
|
|
108
127
|
class << self
|
|
109
128
|
# Simulate a prompt interaction by feeding a predefined key sequence.
|
|
110
129
|
#
|
|
@@ -113,9 +132,13 @@ module Clack
|
|
|
113
132
|
# @yield [PromptDriver] block to define the interaction
|
|
114
133
|
# @return [Object] the prompt result
|
|
115
134
|
def simulate(prompt_method, **kwargs, &block)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
driver = PromptDriver.new
|
|
136
|
+
block.call(driver)
|
|
137
|
+
|
|
138
|
+
input = KeyQueue.new(driver.keys.dup)
|
|
139
|
+
output = StringIO.new
|
|
140
|
+
|
|
141
|
+
prompt_method.call(**kwargs, input: input, output: output)
|
|
119
142
|
end
|
|
120
143
|
|
|
121
144
|
# Capture the rendered output of a prompt simulation.
|
|
@@ -126,43 +149,14 @@ module Clack
|
|
|
126
149
|
# @yield [PromptDriver] block to define the interaction
|
|
127
150
|
# @return [Array(Object, String)] [result, output_string]
|
|
128
151
|
def simulate_with_output(prompt_method, **kwargs, &block)
|
|
129
|
-
output_io = nil
|
|
130
|
-
result = with_stubbed_input(block) do |output|
|
|
131
|
-
output_io = output
|
|
132
|
-
prompt_method.call(**kwargs, output: output)
|
|
133
|
-
end
|
|
134
|
-
[result, output_io.string]
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
private
|
|
138
|
-
|
|
139
|
-
def with_stubbed_input(driver_block)
|
|
140
152
|
driver = PromptDriver.new
|
|
141
|
-
|
|
153
|
+
block.call(driver)
|
|
142
154
|
|
|
143
|
-
|
|
155
|
+
input = KeyQueue.new(driver.keys.dup)
|
|
144
156
|
output = StringIO.new
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Core::KeyReader.singleton_class.alias_method(:_original_read, :read)
|
|
149
|
-
|
|
150
|
-
Core::KeyReader.define_singleton_method(:read) do
|
|
151
|
-
read_count += 1
|
|
152
|
-
raise "Too many reads (#{read_count}) - possible infinite loop in test" if read_count > 100
|
|
153
|
-
|
|
154
|
-
queue.shift || KEYS[:enter]
|
|
155
|
-
end
|
|
156
|
-
$VERBOSE = verbose
|
|
157
|
-
|
|
158
|
-
yield output
|
|
159
|
-
ensure
|
|
160
|
-
if Core::KeyReader.singleton_class.method_defined?(:_original_read)
|
|
161
|
-
verbose, $VERBOSE = $VERBOSE, nil
|
|
162
|
-
Core::KeyReader.singleton_class.alias_method(:read, :_original_read)
|
|
163
|
-
Core::KeyReader.singleton_class.remove_method(:_original_read)
|
|
164
|
-
$VERBOSE = verbose
|
|
165
|
-
end
|
|
157
|
+
|
|
158
|
+
result = prompt_method.call(**kwargs, input: input, output: output)
|
|
159
|
+
[result, output.string]
|
|
166
160
|
end
|
|
167
161
|
end
|
|
168
162
|
end
|