tty2-prompt 0.23.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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +23 -0
- data/README.md +52 -0
- data/lib/tty2/prompt/answers_collector.rb +78 -0
- data/lib/tty2/prompt/block_paginator.rb +59 -0
- data/lib/tty2/prompt/choice.rb +147 -0
- data/lib/tty2/prompt/choices.rb +129 -0
- data/lib/tty2/prompt/confirm_question.rb +158 -0
- data/lib/tty2/prompt/const.rb +17 -0
- data/lib/tty2/prompt/converter_dsl.rb +21 -0
- data/lib/tty2/prompt/converter_registry.rb +69 -0
- data/lib/tty2/prompt/converters.rb +182 -0
- data/lib/tty2/prompt/distance.rb +49 -0
- data/lib/tty2/prompt/enum_list.rb +433 -0
- data/lib/tty2/prompt/errors.rb +31 -0
- data/lib/tty2/prompt/evaluator.rb +29 -0
- data/lib/tty2/prompt/expander.rb +321 -0
- data/lib/tty2/prompt/keypress.rb +98 -0
- data/lib/tty2/prompt/list.rb +589 -0
- data/lib/tty2/prompt/mask_question.rb +96 -0
- data/lib/tty2/prompt/multi_list.rb +224 -0
- data/lib/tty2/prompt/multiline.rb +72 -0
- data/lib/tty2/prompt/paginator.rb +111 -0
- data/lib/tty2/prompt/question/checks.rb +105 -0
- data/lib/tty2/prompt/question/modifier.rb +96 -0
- data/lib/tty2/prompt/question/validation.rb +72 -0
- data/lib/tty2/prompt/question.rb +391 -0
- data/lib/tty2/prompt/result.rb +42 -0
- data/lib/tty2/prompt/selected_choices.rb +77 -0
- data/lib/tty2/prompt/slider.rb +286 -0
- data/lib/tty2/prompt/statement.rb +55 -0
- data/lib/tty2/prompt/suggestion.rb +113 -0
- data/lib/tty2/prompt/symbols.rb +89 -0
- data/lib/tty2/prompt/test.rb +36 -0
- data/lib/tty2/prompt/timer.rb +75 -0
- data/lib/tty2/prompt/utils.rb +42 -0
- data/lib/tty2/prompt/version.rb +7 -0
- data/lib/tty2/prompt.rb +589 -0
- data/lib/tty2-prompt.rb +1 -0
- metadata +148 -0
@@ -0,0 +1,321 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "choices"
|
4
|
+
|
5
|
+
module TTY2
|
6
|
+
class Prompt
|
7
|
+
# A class responsible for rendering expanding options
|
8
|
+
# Used by {Prompt} to display key options question.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Expander
|
12
|
+
HELP_CHOICE = {
|
13
|
+
key: "h",
|
14
|
+
name: "print help",
|
15
|
+
value: :help
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
# Names for delete keys
|
19
|
+
DELETE_KEYS = %i[backspace delete].freeze
|
20
|
+
|
21
|
+
# Create instance of Expander
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def initialize(prompt, options = {})
|
25
|
+
@prompt = prompt
|
26
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
27
|
+
@default = options.fetch(:default, 1)
|
28
|
+
@auto_hint = options.fetch(:auto_hint, false)
|
29
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
30
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
31
|
+
@quiet = options.fetch(:quiet) { @prompt.quiet }
|
32
|
+
@choices = Choices.new
|
33
|
+
@selected = nil
|
34
|
+
@done = false
|
35
|
+
@status = :collapsed
|
36
|
+
@hint = nil
|
37
|
+
@default_key = false
|
38
|
+
end
|
39
|
+
|
40
|
+
def expanded?
|
41
|
+
@status == :expanded
|
42
|
+
end
|
43
|
+
|
44
|
+
def collapsed?
|
45
|
+
@status == :collapsed
|
46
|
+
end
|
47
|
+
|
48
|
+
def expand
|
49
|
+
@status = :expanded
|
50
|
+
end
|
51
|
+
|
52
|
+
# Respond to submit event
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def keyenter(_)
|
56
|
+
if @input.nil? || @input.empty?
|
57
|
+
@input = @choices[@default - 1].key
|
58
|
+
@default_key = true
|
59
|
+
end
|
60
|
+
|
61
|
+
selected = select_choice(@input)
|
62
|
+
|
63
|
+
if selected && selected.key.to_s == "h"
|
64
|
+
expand
|
65
|
+
@selected = nil
|
66
|
+
@input = ""
|
67
|
+
elsif selected
|
68
|
+
@done = true
|
69
|
+
@selected = selected
|
70
|
+
@hint = nil
|
71
|
+
else
|
72
|
+
@input = ""
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias keyreturn keyenter
|
76
|
+
|
77
|
+
# Respond to key press event
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def keypress(event)
|
81
|
+
if DELETE_KEYS.include?(event.key.name)
|
82
|
+
@input.chop! unless @input.empty?
|
83
|
+
elsif event.value =~ /^[^\e\n\r]/
|
84
|
+
@input += event.value
|
85
|
+
end
|
86
|
+
|
87
|
+
@selected = select_choice(@input)
|
88
|
+
if @selected && !@default_key && collapsed?
|
89
|
+
@hint = @selected.name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Select choice by given key
|
94
|
+
#
|
95
|
+
# @return [Choice]
|
96
|
+
#
|
97
|
+
# @api private
|
98
|
+
def select_choice(key)
|
99
|
+
@choices.find_by(:key, key)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set default value.
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def default(value = (not_set = true))
|
106
|
+
return @default if not_set
|
107
|
+
|
108
|
+
@default = value
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set quiet mode.
|
112
|
+
#
|
113
|
+
# @api public
|
114
|
+
def quiet(value)
|
115
|
+
@quiet = value
|
116
|
+
end
|
117
|
+
|
118
|
+
# Add a single choice
|
119
|
+
#
|
120
|
+
# @api public
|
121
|
+
def choice(value, &block)
|
122
|
+
if block
|
123
|
+
@choices << value.update(value: block)
|
124
|
+
else
|
125
|
+
@choices << value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add multiple choices
|
130
|
+
#
|
131
|
+
# @param [Array[Object]] values
|
132
|
+
# the values to add as choices
|
133
|
+
#
|
134
|
+
# @api public
|
135
|
+
def choices(values)
|
136
|
+
values.each { |val| choice(val) }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Execute this prompt
|
140
|
+
#
|
141
|
+
# @api public
|
142
|
+
def call(message, possibilities, &block)
|
143
|
+
choices(possibilities)
|
144
|
+
@message = message
|
145
|
+
block.call(self) if block
|
146
|
+
setup_defaults
|
147
|
+
choice(HELP_CHOICE)
|
148
|
+
@prompt.subscribe(self) do
|
149
|
+
render
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Create possible keys with current choice highlighted
|
156
|
+
#
|
157
|
+
# @return [String]
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
def possible_keys
|
161
|
+
keys = @choices.pluck(:key)
|
162
|
+
default_key = keys[@default - 1]
|
163
|
+
if @selected
|
164
|
+
index = keys.index(@selected.key)
|
165
|
+
keys[index] = @prompt.decorate(keys[index], @active_color)
|
166
|
+
elsif @input.to_s.empty? && default_key
|
167
|
+
keys[@default - 1] = @prompt.decorate(default_key, @active_color)
|
168
|
+
end
|
169
|
+
keys.join(",")
|
170
|
+
end
|
171
|
+
|
172
|
+
# @api private
|
173
|
+
def render
|
174
|
+
@input = ""
|
175
|
+
until @done
|
176
|
+
question = render_question
|
177
|
+
@prompt.print(question)
|
178
|
+
read_input
|
179
|
+
@prompt.print(refresh(question.lines.count))
|
180
|
+
end
|
181
|
+
@prompt.print(render_question) unless @quiet
|
182
|
+
answer
|
183
|
+
end
|
184
|
+
|
185
|
+
# @api private
|
186
|
+
def answer
|
187
|
+
@selected.value
|
188
|
+
end
|
189
|
+
|
190
|
+
# Render message with options
|
191
|
+
#
|
192
|
+
# @return [String]
|
193
|
+
#
|
194
|
+
# @api private
|
195
|
+
def render_header
|
196
|
+
header = ["#{@prefix}#{@message} "]
|
197
|
+
if @done
|
198
|
+
selected_item = @selected.name.to_s
|
199
|
+
header << @prompt.decorate(selected_item, @active_color)
|
200
|
+
elsif collapsed?
|
201
|
+
header << %[(enter "h" for help) ]
|
202
|
+
header << "[#{possible_keys}] "
|
203
|
+
header << @input
|
204
|
+
end
|
205
|
+
header.join
|
206
|
+
end
|
207
|
+
|
208
|
+
# Show hint for selected option key
|
209
|
+
#
|
210
|
+
# return [String]
|
211
|
+
#
|
212
|
+
# @api private
|
213
|
+
def render_hint
|
214
|
+
"\n" + @prompt.decorate(">> ", @active_color) +
|
215
|
+
@hint +
|
216
|
+
@prompt.cursor.prev_line +
|
217
|
+
@prompt.cursor.forward(@prompt.strip(render_header).size)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Render question with menu
|
221
|
+
#
|
222
|
+
# @return [String]
|
223
|
+
#
|
224
|
+
# @api private
|
225
|
+
def render_question
|
226
|
+
load_auto_hint if @auto_hint
|
227
|
+
header = render_header
|
228
|
+
header << render_hint if @hint
|
229
|
+
header << "\n" if @done
|
230
|
+
|
231
|
+
if !@done && expanded?
|
232
|
+
header << render_menu
|
233
|
+
header << render_footer
|
234
|
+
end
|
235
|
+
header
|
236
|
+
end
|
237
|
+
|
238
|
+
def load_auto_hint
|
239
|
+
if @hint.nil? && collapsed?
|
240
|
+
if @selected
|
241
|
+
@hint = @selected.name
|
242
|
+
else
|
243
|
+
if @input.empty?
|
244
|
+
@hint = @choices[@default - 1].name
|
245
|
+
else
|
246
|
+
@hint = "invalid option"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def render_footer
|
253
|
+
" Choice [#{@choices[@default - 1].key}]: #{@input}"
|
254
|
+
end
|
255
|
+
|
256
|
+
def read_input
|
257
|
+
@prompt.read_keypress
|
258
|
+
end
|
259
|
+
|
260
|
+
# Refresh the current input
|
261
|
+
#
|
262
|
+
# @param [Integer] lines
|
263
|
+
#
|
264
|
+
# @return [String]
|
265
|
+
#
|
266
|
+
# @api private
|
267
|
+
def refresh(lines)
|
268
|
+
if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?)
|
269
|
+
@hint = nil
|
270
|
+
@prompt.clear_lines(lines, :down) +
|
271
|
+
@prompt.cursor.prev_line
|
272
|
+
elsif expanded?
|
273
|
+
@prompt.clear_lines(lines)
|
274
|
+
else
|
275
|
+
@prompt.clear_line
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Render help menu
|
280
|
+
#
|
281
|
+
# @api private
|
282
|
+
def render_menu
|
283
|
+
output = ["\n"]
|
284
|
+
@choices.each do |choice|
|
285
|
+
chosen = %(#{choice.key} - #{choice.name})
|
286
|
+
if @selected && @selected.key == choice.key
|
287
|
+
chosen = @prompt.decorate(chosen, @active_color)
|
288
|
+
end
|
289
|
+
output << " " + chosen + "\n"
|
290
|
+
end
|
291
|
+
output.join
|
292
|
+
end
|
293
|
+
|
294
|
+
def setup_defaults
|
295
|
+
validate_choices
|
296
|
+
end
|
297
|
+
|
298
|
+
def validate_choices
|
299
|
+
errors = []
|
300
|
+
keys = []
|
301
|
+
@choices.each do |choice|
|
302
|
+
if choice.key.nil?
|
303
|
+
errors << "Choice #{choice.name} is missing a :key attribute"
|
304
|
+
next
|
305
|
+
end
|
306
|
+
if choice.key.length != 1
|
307
|
+
errors << "Choice key `#{choice.key}` is more than one character long."
|
308
|
+
end
|
309
|
+
if choice.key.to_s == "h"
|
310
|
+
errors << "Choice key `#{choice.key}` is reserved for help menu."
|
311
|
+
end
|
312
|
+
if keys.include?(choice.key)
|
313
|
+
errors << "Choice key `#{choice.key}` is a duplicate."
|
314
|
+
end
|
315
|
+
keys << choice.key if choice.key
|
316
|
+
end
|
317
|
+
errors.each { |err| raise ConfigurationError, err }
|
318
|
+
end
|
319
|
+
end # Expander
|
320
|
+
end # Prompt
|
321
|
+
end # TTY2
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "question"
|
4
|
+
require_relative "timer"
|
5
|
+
|
6
|
+
module TTY2
|
7
|
+
class Prompt
|
8
|
+
class Keypress < Question
|
9
|
+
# Create keypress question
|
10
|
+
#
|
11
|
+
# @param [Prompt] prompt
|
12
|
+
# @param [Hash] options
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def initialize(prompt, **options)
|
16
|
+
super
|
17
|
+
@echo = options.fetch(:echo) { false }
|
18
|
+
@keys = options.fetch(:keys) { UndefinedSetting }
|
19
|
+
@timeout = options.fetch(:timeout) { UndefinedSetting }
|
20
|
+
@interval = options.fetch(:interval) {
|
21
|
+
(@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1
|
22
|
+
}
|
23
|
+
@decimals = (@interval.to_s.split(".")[1] || []).size
|
24
|
+
@countdown = @timeout
|
25
|
+
time = timeout? ? Float(@timeout) : nil
|
26
|
+
@timer = Timer.new(time, Float(@interval))
|
27
|
+
|
28
|
+
@prompt.subscribe(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def countdown(value = (not_set = true))
|
32
|
+
return @countdown if not_set
|
33
|
+
|
34
|
+
@countdown = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if any specific keys are set
|
38
|
+
def any_key?
|
39
|
+
@keys == UndefinedSetting
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if timeout is set
|
43
|
+
def timeout?
|
44
|
+
@timeout != UndefinedSetting
|
45
|
+
end
|
46
|
+
|
47
|
+
def keypress(event)
|
48
|
+
if any_key?
|
49
|
+
@done = true
|
50
|
+
elsif @keys.is_a?(Array) && @keys.include?(event.key.name)
|
51
|
+
@done = true
|
52
|
+
else
|
53
|
+
@done = false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_question
|
58
|
+
header = super
|
59
|
+
if timeout?
|
60
|
+
header.gsub!(/:countdown/, format("%.#{@decimals}f", countdown))
|
61
|
+
end
|
62
|
+
header
|
63
|
+
end
|
64
|
+
|
65
|
+
def interval_handler(time)
|
66
|
+
return if @done
|
67
|
+
|
68
|
+
question = render_question
|
69
|
+
line_size = question.size
|
70
|
+
total_lines = @prompt.count_screen_lines(line_size)
|
71
|
+
@prompt.print(refresh(question.lines.count, total_lines))
|
72
|
+
countdown(time)
|
73
|
+
@prompt.print(render_question)
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_input(question)
|
77
|
+
@prompt.print(render_question)
|
78
|
+
|
79
|
+
@timer.on_tick do |time|
|
80
|
+
interval_handler(time)
|
81
|
+
end
|
82
|
+
|
83
|
+
@timer.while_remaining do |remaining|
|
84
|
+
break if @done
|
85
|
+
|
86
|
+
@input = @prompt.read_keypress(nonblock: true)
|
87
|
+
end
|
88
|
+
countdown(0) unless @done
|
89
|
+
|
90
|
+
@evaluator.(@input)
|
91
|
+
end
|
92
|
+
|
93
|
+
def refresh(lines, lines_to_clear)
|
94
|
+
@prompt.clear_lines(lines)
|
95
|
+
end
|
96
|
+
end # Keypress
|
97
|
+
end # Prompt
|
98
|
+
end # TTY2
|