clack 0.4.5 → 0.5.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 +33 -0
- data/README.md +265 -196
- data/lib/clack/colors.rb +1 -6
- data/lib/clack/core/cursor.rb +1 -3
- data/lib/clack/core/key_reader.rb +30 -20
- data/lib/clack/core/options_helper.rb +71 -29
- data/lib/clack/core/prompt.rb +63 -15
- data/lib/clack/core/scroll_helper.rb +10 -41
- data/lib/clack/core/selection_manager.rb +49 -0
- data/lib/clack/core/settings.rb +2 -2
- data/lib/clack/prompts/autocomplete.rb +17 -21
- data/lib/clack/prompts/autocomplete_multiselect.rb +14 -30
- data/lib/clack/prompts/confirm.rb +9 -41
- data/lib/clack/prompts/date.rb +27 -56
- data/lib/clack/prompts/group_multiselect.rb +21 -50
- data/lib/clack/prompts/multiline_text.rb +33 -53
- data/lib/clack/prompts/multiselect.rb +16 -36
- data/lib/clack/prompts/password.rb +2 -25
- data/lib/clack/prompts/path.rb +9 -34
- data/lib/clack/prompts/range.rb +2 -25
- data/lib/clack/prompts/select.rb +12 -36
- data/lib/clack/prompts/select_key.rb +1 -11
- data/lib/clack/prompts/spinner.rb +15 -20
- data/lib/clack/prompts/text.rb +1 -25
- data/lib/clack/symbols.rb +2 -3
- data/lib/clack/testing.rb +31 -37
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +73 -34
- 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,6 +39,9 @@ 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
47
|
valid_values = Set.new(@options.map { |o| o[:value] })
|
|
@@ -45,8 +49,8 @@ module Clack
|
|
|
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,31 +104,16 @@ module Clack
|
|
|
103
104
|
lines.join
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
-
lines = []
|
|
108
|
-
lines << "#{bar}\n"
|
|
109
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
110
|
-
|
|
111
|
-
labels = @options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }
|
|
112
|
-
display_text = labels.join(", ")
|
|
113
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
|
|
114
|
-
lines << "#{bar} #{display}\n"
|
|
115
|
-
|
|
116
|
-
lines.join
|
|
117
|
-
end
|
|
107
|
+
def final_display = selected_labels(@options)
|
|
118
108
|
|
|
119
109
|
private
|
|
120
110
|
|
|
121
111
|
def toggle_current
|
|
122
|
-
opt = @options[@
|
|
112
|
+
opt = @options[@option_index]
|
|
123
113
|
return if opt[:disabled]
|
|
124
114
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
else
|
|
128
|
-
@selected.add(opt[:value])
|
|
129
|
-
end
|
|
130
|
-
update_value
|
|
115
|
+
toggle_value(opt[:value])
|
|
116
|
+
update_selection_value
|
|
131
117
|
end
|
|
132
118
|
|
|
133
119
|
def toggle_all
|
|
@@ -137,24 +123,18 @@ module Clack
|
|
|
137
123
|
else
|
|
138
124
|
@selected.merge(enabled)
|
|
139
125
|
end
|
|
140
|
-
|
|
126
|
+
update_selection_value
|
|
141
127
|
end
|
|
142
128
|
|
|
143
129
|
def invert_selection
|
|
144
130
|
@options.each do |opt|
|
|
145
131
|
next if opt[:disabled]
|
|
146
132
|
|
|
147
|
-
|
|
148
|
-
@selected.delete(opt[:value])
|
|
149
|
-
else
|
|
150
|
-
@selected.add(opt[:value])
|
|
151
|
-
end
|
|
133
|
+
toggle_value(opt[:value])
|
|
152
134
|
end
|
|
153
|
-
|
|
135
|
+
update_selection_value
|
|
154
136
|
end
|
|
155
137
|
|
|
156
|
-
def update_value = @value = @selected.to_a
|
|
157
|
-
|
|
158
138
|
def keyboard_hints
|
|
159
139
|
hints = [
|
|
160
140
|
"#{Colors.dim("space")} select",
|
|
@@ -165,7 +145,7 @@ module Clack
|
|
|
165
145
|
end
|
|
166
146
|
|
|
167
147
|
def option_display(opt, idx)
|
|
168
|
-
active = idx == @
|
|
148
|
+
active = idx == @option_index
|
|
169
149
|
selected = @selected.include?(opt[:value])
|
|
170
150
|
|
|
171
151
|
symbol, label = option_parts(opt, active, selected)
|
|
@@ -35,33 +35,10 @@ 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
|
-
def
|
|
55
|
-
lines = []
|
|
56
|
-
lines << "#{bar}\n"
|
|
57
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
58
|
-
|
|
59
|
-
masked = @mask * @value.grapheme_clusters.length
|
|
60
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(masked)) : Colors.dim(masked)
|
|
61
|
-
lines << "#{bar} #{display}\n"
|
|
62
|
-
|
|
63
|
-
lines.join
|
|
64
|
-
end
|
|
41
|
+
def final_display = @mask * @value.grapheme_clusters.length
|
|
65
42
|
|
|
66
43
|
private
|
|
67
44
|
|
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,37 +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
|
|
123
|
-
|
|
124
|
-
lines.join
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def build_final_frame
|
|
128
|
-
lines = []
|
|
129
|
-
lines << "#{bar}\n"
|
|
130
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
131
|
-
|
|
132
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
|
|
133
|
-
lines << "#{bar} #{display}\n"
|
|
107
|
+
"#{bar} #{suggestion_display(path, actual_idx == @option_index)}\n"
|
|
108
|
+
end.join
|
|
134
109
|
|
|
135
|
-
|
|
110
|
+
"#{frame_header}#{active_bar} #{input_display}\n#{suggestion_lines}#{frame_footer}"
|
|
136
111
|
end
|
|
137
112
|
|
|
138
113
|
private
|
|
@@ -213,7 +188,7 @@ module Clack
|
|
|
213
188
|
expanded == @root || expanded.start_with?("#{@root}/")
|
|
214
189
|
end
|
|
215
190
|
|
|
216
|
-
def
|
|
191
|
+
def navigable_items = @suggestions
|
|
217
192
|
|
|
218
193
|
# Override to use @root as placeholder
|
|
219
194
|
def placeholder_display
|
data/lib/clack/prompts/range.rb
CHANGED
|
@@ -51,33 +51,10 @@ 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
|
-
def
|
|
71
|
-
lines = []
|
|
72
|
-
lines << "#{bar}\n"
|
|
73
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
74
|
-
|
|
75
|
-
display = format_value(@value)
|
|
76
|
-
styled = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display)) : Colors.dim(display)
|
|
77
|
-
lines << "#{bar} #{styled}\n"
|
|
78
|
-
|
|
79
|
-
lines.join
|
|
80
|
-
end
|
|
57
|
+
def final_display = format_value(@value)
|
|
81
58
|
|
|
82
59
|
private
|
|
83
60
|
|
data/lib/clack/prompts/select.rb
CHANGED
|
@@ -40,57 +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
|
|
84
|
-
lines = []
|
|
85
|
-
lines << "#{bar}\n"
|
|
86
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
87
|
-
|
|
88
|
-
label = current_option[:label]
|
|
89
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
|
|
90
|
-
lines << "#{bar} #{display}\n"
|
|
91
|
-
|
|
92
|
-
lines.join
|
|
93
|
-
end
|
|
69
|
+
def final_display = current_option[:label]
|
|
94
70
|
|
|
95
71
|
private
|
|
96
72
|
|
|
@@ -101,7 +77,7 @@ module Clack
|
|
|
101
77
|
|
|
102
78
|
def update_value = @value = current_option[:value]
|
|
103
79
|
|
|
104
|
-
def current_option = @options[@
|
|
80
|
+
def current_option = @options[@option_index]
|
|
105
81
|
|
|
106
82
|
def option_display(opt, active)
|
|
107
83
|
return disabled_option_display(opt) if opt[:disabled]
|
|
@@ -63,17 +63,7 @@ module Clack
|
|
|
63
63
|
lines.join
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
lines = []
|
|
68
|
-
lines << "#{bar}\n"
|
|
69
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
70
|
-
|
|
71
|
-
label = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
|
|
72
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
|
|
73
|
-
lines << "#{bar} #{display}\n"
|
|
74
|
-
|
|
75
|
-
lines.join
|
|
76
|
-
end
|
|
66
|
+
def final_display = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
|
|
77
67
|
|
|
78
68
|
private
|
|
79
69
|
|
|
@@ -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/text.rb
CHANGED
|
@@ -88,31 +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
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def build_final_frame
|
|
108
|
-
lines = []
|
|
109
|
-
lines << "#{bar}\n"
|
|
110
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
111
|
-
|
|
112
|
-
display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
|
|
113
|
-
lines << "#{bar} #{display}\n"
|
|
114
|
-
|
|
115
|
-
lines.join
|
|
91
|
+
"#{frame_header}#{active_bar} #{input_display}\n#{frame_footer}"
|
|
116
92
|
end
|
|
117
93
|
|
|
118
94
|
private
|
data/lib/clack/symbols.rb
CHANGED
|
@@ -26,8 +26,7 @@ module Clack
|
|
|
26
26
|
# Explicit override
|
|
27
27
|
return ENV["CLACK_UNICODE"] == "1" if ENV["CLACK_UNICODE"]
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
$stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
|
|
29
|
+
Environment.colors_supported?
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -36,7 +35,7 @@ module Clack
|
|
|
36
35
|
# Unicode cancel step indicator, or ASCII fallback.
|
|
37
36
|
S_STEP_CANCEL = unicode? ? "■" : "x"
|
|
38
37
|
# Unicode error step indicator, or ASCII fallback.
|
|
39
|
-
S_STEP_ERROR = unicode? ? "▲" : "
|
|
38
|
+
S_STEP_ERROR = unicode? ? "▲" : "!"
|
|
40
39
|
# Unicode submit step indicator, or ASCII fallback.
|
|
41
40
|
S_STEP_SUBMIT = unicode? ? "◇" : "o"
|
|
42
41
|
|
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
|
data/lib/clack/version.rb
CHANGED