austb-tty-prompt 0.13.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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +218 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +1132 -0
- data/Rakefile +8 -0
- data/appveyor.yml +23 -0
- data/benchmarks/speed.rb +27 -0
- data/examples/ask.rb +15 -0
- data/examples/collect.rb +19 -0
- data/examples/echo.rb +11 -0
- data/examples/enum.rb +8 -0
- data/examples/enum_paged.rb +9 -0
- data/examples/enum_select.rb +7 -0
- data/examples/expand.rb +29 -0
- data/examples/in.rb +9 -0
- data/examples/inputs.rb +10 -0
- data/examples/key_events.rb +11 -0
- data/examples/keypress.rb +9 -0
- data/examples/mask.rb +13 -0
- data/examples/multi_select.rb +8 -0
- data/examples/multi_select_paged.rb +9 -0
- data/examples/multiline.rb +9 -0
- data/examples/pause.rb +7 -0
- data/examples/select.rb +18 -0
- data/examples/select_paginated.rb +9 -0
- data/examples/slider.rb +6 -0
- data/examples/validation.rb +9 -0
- data/examples/yes_no.rb +7 -0
- data/lib/tty-prompt.rb +4 -0
- data/lib/tty/prompt.rb +535 -0
- data/lib/tty/prompt/answers_collector.rb +59 -0
- data/lib/tty/prompt/choice.rb +90 -0
- data/lib/tty/prompt/choices.rb +110 -0
- data/lib/tty/prompt/confirm_question.rb +129 -0
- data/lib/tty/prompt/converter_dsl.rb +22 -0
- data/lib/tty/prompt/converter_registry.rb +64 -0
- data/lib/tty/prompt/converters.rb +77 -0
- data/lib/tty/prompt/distance.rb +49 -0
- data/lib/tty/prompt/enum_list.rb +337 -0
- data/lib/tty/prompt/enum_paginator.rb +56 -0
- data/lib/tty/prompt/evaluator.rb +29 -0
- data/lib/tty/prompt/expander.rb +292 -0
- data/lib/tty/prompt/keypress.rb +94 -0
- data/lib/tty/prompt/list.rb +317 -0
- data/lib/tty/prompt/mask_question.rb +91 -0
- data/lib/tty/prompt/multi_list.rb +108 -0
- data/lib/tty/prompt/multiline.rb +71 -0
- data/lib/tty/prompt/paginator.rb +88 -0
- data/lib/tty/prompt/question.rb +333 -0
- data/lib/tty/prompt/question/checks.rb +87 -0
- data/lib/tty/prompt/question/modifier.rb +94 -0
- data/lib/tty/prompt/question/validation.rb +72 -0
- data/lib/tty/prompt/reader.rb +352 -0
- data/lib/tty/prompt/reader/codes.rb +121 -0
- data/lib/tty/prompt/reader/console.rb +57 -0
- data/lib/tty/prompt/reader/history.rb +145 -0
- data/lib/tty/prompt/reader/key_event.rb +91 -0
- data/lib/tty/prompt/reader/line.rb +162 -0
- data/lib/tty/prompt/reader/mode.rb +44 -0
- data/lib/tty/prompt/reader/win_api.rb +29 -0
- data/lib/tty/prompt/reader/win_console.rb +53 -0
- data/lib/tty/prompt/result.rb +42 -0
- data/lib/tty/prompt/slider.rb +182 -0
- data/lib/tty/prompt/statement.rb +55 -0
- data/lib/tty/prompt/suggestion.rb +115 -0
- data/lib/tty/prompt/symbols.rb +61 -0
- data/lib/tty/prompt/timeout.rb +69 -0
- data/lib/tty/prompt/utils.rb +44 -0
- data/lib/tty/prompt/version.rb +7 -0
- data/lib/tty/test_prompt.rb +20 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-prompt.gemspec +32 -0
- metadata +243 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'paginator'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
class EnumPaginator < Paginator
|
8
|
+
# Paginate list of choices based on current active choice.
|
9
|
+
# Move entire pages.
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
def paginate(list, active, per_page = nil, &block)
|
13
|
+
default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
|
14
|
+
@per_page = @per_page || per_page || default_size
|
15
|
+
|
16
|
+
# Don't paginate short lists
|
17
|
+
if list.size <= @per_page
|
18
|
+
@lower_index = 0
|
19
|
+
@upper_index = list.size - 1
|
20
|
+
if block
|
21
|
+
return list.each_with_index(&block)
|
22
|
+
else
|
23
|
+
return list.each_with_index.to_enum
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
unless active.nil? # User may input index out of range
|
28
|
+
@last_index = active
|
29
|
+
end
|
30
|
+
page = (@last_index / @per_page.to_f).ceil
|
31
|
+
pages = (list.size / @per_page.to_f).ceil
|
32
|
+
if page == 0
|
33
|
+
@lower_index = 0
|
34
|
+
@upper_index = @lower_index + @per_page - 1
|
35
|
+
elsif page > 0 && page <= pages
|
36
|
+
@lower_index = (page - 1) * @per_page
|
37
|
+
@upper_index = @lower_index + @per_page - 1
|
38
|
+
else
|
39
|
+
@upper_index = list.size - 1
|
40
|
+
@lower_index = @upper_index - @per_page + 1
|
41
|
+
end
|
42
|
+
|
43
|
+
sliced_list = list[@lower_index..@upper_index]
|
44
|
+
indices = (@lower_index..@upper_index)
|
45
|
+
|
46
|
+
if block
|
47
|
+
sliced_list.each_with_index do |item, index|
|
48
|
+
block[item, @lower_index + index]
|
49
|
+
end
|
50
|
+
else
|
51
|
+
sliced_list.zip(indices).to_enum unless block_given?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end # EnumPaginator
|
55
|
+
end # Prompt
|
56
|
+
end # TTY
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'result'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
# Evaluates provided parameters and stops if any of them fails
|
8
|
+
# @api private
|
9
|
+
class Evaluator
|
10
|
+
attr_reader :results
|
11
|
+
|
12
|
+
def initialize(question, &block)
|
13
|
+
@question = question
|
14
|
+
@results = []
|
15
|
+
instance_eval(&block) if block
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(initial)
|
19
|
+
seed = Result::Success.new(@question, initial)
|
20
|
+
results.reduce(seed, &:with)
|
21
|
+
end
|
22
|
+
|
23
|
+
def check(proc = nil, &block)
|
24
|
+
results << (proc || block)
|
25
|
+
end
|
26
|
+
alias_method :<<, :check
|
27
|
+
end # Evaluator
|
28
|
+
end # Prompt
|
29
|
+
end # TTY
|
@@ -0,0 +1,292 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'choices'
|
4
|
+
|
5
|
+
module TTY
|
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
|
+
}
|
17
|
+
|
18
|
+
# Create instance of Expander
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def initialize(prompt, options = {})
|
22
|
+
@prompt = prompt
|
23
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
24
|
+
@default = options.fetch(:default) { 1 }
|
25
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
26
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
27
|
+
@choices = Choices.new
|
28
|
+
@selected = nil
|
29
|
+
@done = false
|
30
|
+
@status = :collapsed
|
31
|
+
@hint = nil
|
32
|
+
@default_key = false
|
33
|
+
|
34
|
+
@prompt.subscribe(self)
|
35
|
+
end
|
36
|
+
|
37
|
+
def expanded?
|
38
|
+
@status == :expanded
|
39
|
+
end
|
40
|
+
|
41
|
+
def collapsed?
|
42
|
+
@status == :collapsed
|
43
|
+
end
|
44
|
+
|
45
|
+
def expand
|
46
|
+
@status = :expanded
|
47
|
+
end
|
48
|
+
|
49
|
+
# Respond to submit event
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def keyenter(_)
|
53
|
+
if @input.nil? || @input.empty?
|
54
|
+
@input = @choices[@default - 1].key
|
55
|
+
@default_key = true
|
56
|
+
end
|
57
|
+
|
58
|
+
selected = select_choice(@input)
|
59
|
+
|
60
|
+
if selected && selected.key.to_s == 'h'
|
61
|
+
expand
|
62
|
+
@selected = nil
|
63
|
+
@input = ''
|
64
|
+
elsif selected
|
65
|
+
@done = true
|
66
|
+
@selected = selected
|
67
|
+
else
|
68
|
+
@input = ''
|
69
|
+
end
|
70
|
+
end
|
71
|
+
alias_method :keyreturn, :keyenter
|
72
|
+
|
73
|
+
# Respond to key press event
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def keypress(event)
|
77
|
+
if [:backspace, :delete].include?(event.key.name)
|
78
|
+
@input.chop! unless @input.empty?
|
79
|
+
elsif event.value =~ /^[^\e\n\r]/
|
80
|
+
@input += event.value
|
81
|
+
end
|
82
|
+
@selected = select_choice(@input)
|
83
|
+
if @selected && !@default_key && collapsed?
|
84
|
+
@hint = @selected.name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Select choice by given key
|
89
|
+
#
|
90
|
+
# @return [Choice]
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
def select_choice(key)
|
94
|
+
@choices.find_by(:key, key)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Set default value.
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def default(value = (not_set = true))
|
101
|
+
return @default if not_set
|
102
|
+
@default = value
|
103
|
+
end
|
104
|
+
|
105
|
+
# Add a single choice
|
106
|
+
#
|
107
|
+
# @api public
|
108
|
+
def choice(value, &block)
|
109
|
+
if block
|
110
|
+
@choices << value.update(value: block)
|
111
|
+
else
|
112
|
+
@choices << value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Add multiple choices
|
117
|
+
#
|
118
|
+
# @param [Array[Object]] values
|
119
|
+
# the values to add as choices
|
120
|
+
#
|
121
|
+
# @api public
|
122
|
+
def choices(values)
|
123
|
+
values.each { |val| choice(val) }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Execute this prompt
|
127
|
+
#
|
128
|
+
# @api public
|
129
|
+
def call(message, possibilities, &block)
|
130
|
+
choices(possibilities)
|
131
|
+
@message = message
|
132
|
+
block.call(self) if block
|
133
|
+
setup_defaults
|
134
|
+
choice(HELP_CHOICE)
|
135
|
+
render
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# Create possible keys with current choice highlighted
|
141
|
+
#
|
142
|
+
# @return [String]
|
143
|
+
#
|
144
|
+
# @api private
|
145
|
+
def possible_keys
|
146
|
+
keys = @choices.pluck(:key)
|
147
|
+
default_key = keys[@default - 1]
|
148
|
+
if @selected
|
149
|
+
index = keys.index(@selected.key)
|
150
|
+
keys[index] = @prompt.decorate(keys[index], @active_color)
|
151
|
+
elsif @input.to_s.empty? && default_key
|
152
|
+
keys[@default - 1] = @prompt.decorate(default_key, @active_color)
|
153
|
+
end
|
154
|
+
keys.join(',')
|
155
|
+
end
|
156
|
+
|
157
|
+
# @api private
|
158
|
+
def render
|
159
|
+
@input = ''
|
160
|
+
until @done
|
161
|
+
question = render_question
|
162
|
+
@prompt.print(question)
|
163
|
+
read_input
|
164
|
+
@prompt.print(refresh(question.lines.count))
|
165
|
+
end
|
166
|
+
@prompt.print(render_question)
|
167
|
+
answer
|
168
|
+
end
|
169
|
+
|
170
|
+
# @api private
|
171
|
+
def answer
|
172
|
+
@selected.value
|
173
|
+
end
|
174
|
+
|
175
|
+
# Render message with options
|
176
|
+
#
|
177
|
+
# @return [String]
|
178
|
+
#
|
179
|
+
# @api private
|
180
|
+
def render_header
|
181
|
+
header = "#{@prefix}#{@message} "
|
182
|
+
if @done
|
183
|
+
selected_item = "#{@selected.name}"
|
184
|
+
header << @prompt.decorate(selected_item, @active_color)
|
185
|
+
elsif collapsed?
|
186
|
+
header << %[(enter "h" for help) ]
|
187
|
+
header << "[#{possible_keys}] "
|
188
|
+
header << @input
|
189
|
+
end
|
190
|
+
header
|
191
|
+
end
|
192
|
+
|
193
|
+
# Show hint for selected option key
|
194
|
+
#
|
195
|
+
# return [String]
|
196
|
+
#
|
197
|
+
# @api private
|
198
|
+
def render_hint
|
199
|
+
hint = "\n"
|
200
|
+
hint << @prompt.decorate('>> ', @active_color)
|
201
|
+
hint << @hint
|
202
|
+
hint << @prompt.cursor.prev_line
|
203
|
+
hint << @prompt.cursor.forward(@prompt.strip(render_header).size)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Render question with menu
|
207
|
+
#
|
208
|
+
# @return [String]
|
209
|
+
#
|
210
|
+
# @api private
|
211
|
+
def render_question
|
212
|
+
header = render_header
|
213
|
+
header << render_hint if @hint
|
214
|
+
header << "\n" if @done
|
215
|
+
|
216
|
+
if !@done && expanded?
|
217
|
+
header << render_menu
|
218
|
+
header << render_footer
|
219
|
+
end
|
220
|
+
header
|
221
|
+
end
|
222
|
+
|
223
|
+
def render_footer
|
224
|
+
" Choice [#{@choices[@default - 1].key}]: #{@input}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def read_input
|
228
|
+
@prompt.read_keypress
|
229
|
+
end
|
230
|
+
|
231
|
+
# Refresh the current input
|
232
|
+
#
|
233
|
+
# @param [Integer] lines
|
234
|
+
#
|
235
|
+
# @return [String]
|
236
|
+
#
|
237
|
+
# @api private
|
238
|
+
def refresh(lines)
|
239
|
+
if @hint && (!@selected || @done)
|
240
|
+
@hint = nil
|
241
|
+
@prompt.clear_lines(lines, :down) +
|
242
|
+
@prompt.cursor.prev_line
|
243
|
+
elsif expanded?
|
244
|
+
@prompt.clear_lines(lines)
|
245
|
+
else
|
246
|
+
@prompt.clear_line
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Render help menu
|
251
|
+
#
|
252
|
+
# @api private
|
253
|
+
def render_menu
|
254
|
+
output = "\n"
|
255
|
+
@choices.each do |choice|
|
256
|
+
chosen = %(#{choice.key} - #{choice.name})
|
257
|
+
if @selected && @selected.key == choice.key
|
258
|
+
chosen = @prompt.decorate(chosen, @active_color)
|
259
|
+
end
|
260
|
+
output << ' ' + chosen + "\n"
|
261
|
+
end
|
262
|
+
output
|
263
|
+
end
|
264
|
+
|
265
|
+
def setup_defaults
|
266
|
+
validate_choices
|
267
|
+
end
|
268
|
+
|
269
|
+
def validate_choices
|
270
|
+
errors = []
|
271
|
+
keys = []
|
272
|
+
@choices.each do |choice|
|
273
|
+
if choice.key.nil?
|
274
|
+
errors << "Choice #{choice.name} is missing a :key attribute"
|
275
|
+
next
|
276
|
+
end
|
277
|
+
if choice.key.length != 1
|
278
|
+
errors << "Choice key `#{choice.key}` is more than one character long."
|
279
|
+
end
|
280
|
+
if choice.key.to_s == 'h'
|
281
|
+
errors << "Choice key `#{choice.key}` is reserved for help menu."
|
282
|
+
end
|
283
|
+
if keys.include?(choice.key)
|
284
|
+
errors << "Choice key `#{choice.key}` is a duplicate."
|
285
|
+
end
|
286
|
+
keys << choice.key if choice.key
|
287
|
+
end
|
288
|
+
errors.each { |err| fail ConfigurationError, err }
|
289
|
+
end
|
290
|
+
end # Expander
|
291
|
+
end # Prompt
|
292
|
+
end # TTY
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'question'
|
4
|
+
require_relative 'symbols'
|
5
|
+
require_relative 'timeout'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Prompt
|
9
|
+
class Keypress < Question
|
10
|
+
# Create keypress question
|
11
|
+
#
|
12
|
+
# @param [Prompt] prompt
|
13
|
+
# @param [Hash] options
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
def initialize(prompt, options = {})
|
17
|
+
super
|
18
|
+
@echo = options.fetch(:echo) { false }
|
19
|
+
@keys = options.fetch(:keys) { UndefinedSetting }
|
20
|
+
@timeout = options.fetch(:timeout) { UndefinedSetting }
|
21
|
+
@interval = options.fetch(:interval) {
|
22
|
+
(@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1
|
23
|
+
}
|
24
|
+
@pause = true
|
25
|
+
@countdown = @timeout
|
26
|
+
@interval_handler = proc { |time|
|
27
|
+
question = render_question
|
28
|
+
@prompt.print(refresh(question.lines.count))
|
29
|
+
countdown(time)
|
30
|
+
@prompt.print(render_question)
|
31
|
+
}
|
32
|
+
|
33
|
+
@prompt.subscribe(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def countdown(value = (not_set = true))
|
37
|
+
return @countdown if not_set
|
38
|
+
@countdown = value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check if any specific keys are set
|
42
|
+
def any_key?
|
43
|
+
@keys == UndefinedSetting
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check if timeout is set
|
47
|
+
def timeout?
|
48
|
+
@timeout != UndefinedSetting
|
49
|
+
end
|
50
|
+
|
51
|
+
def keypress(event)
|
52
|
+
if any_key?
|
53
|
+
@pause = false
|
54
|
+
elsif @keys.is_a?(Array) && @keys.include?(event.key.name)
|
55
|
+
@pause = false
|
56
|
+
else
|
57
|
+
@pause = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_question
|
62
|
+
header = super
|
63
|
+
header.gsub!(/:countdown/, countdown.to_s)
|
64
|
+
header
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_input(question)
|
68
|
+
time do
|
69
|
+
while @pause
|
70
|
+
@input = @prompt.read_keypress
|
71
|
+
end
|
72
|
+
@pause
|
73
|
+
end
|
74
|
+
@evaluator.(@input)
|
75
|
+
end
|
76
|
+
|
77
|
+
def refresh(lines)
|
78
|
+
@prompt.clear_lines(lines)
|
79
|
+
end
|
80
|
+
|
81
|
+
def time(&block)
|
82
|
+
if timeout?
|
83
|
+
time = Float(@timeout)
|
84
|
+
interval = Float(@interval)
|
85
|
+
scheduler = Timeout.new(interval_handler: @interval_handler)
|
86
|
+
scheduler.timeout(time, interval, &block)
|
87
|
+
else
|
88
|
+
block.()
|
89
|
+
end
|
90
|
+
rescue Timeout::Error
|
91
|
+
end
|
92
|
+
end # Keypress
|
93
|
+
end # Prompt
|
94
|
+
end # TTY
|