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,317 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'choices'
|
4
|
+
require_relative 'paginator'
|
5
|
+
require_relative 'symbols'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Prompt
|
9
|
+
# A class responsible for rendering select list menu
|
10
|
+
# Used by {Prompt} to display interactive menu.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class List
|
14
|
+
include Symbols
|
15
|
+
|
16
|
+
HELP = '(Use arrow%s keys, press Enter to select)'
|
17
|
+
|
18
|
+
PAGE_HELP = '(Move up or down to reveal more choices)'
|
19
|
+
|
20
|
+
# Create instance of TTY::Prompt::List menu.
|
21
|
+
#
|
22
|
+
# @param Hash options
|
23
|
+
# the configuration options
|
24
|
+
# @option options [Symbol] :default
|
25
|
+
# the default active choice, defaults to 1
|
26
|
+
# @option options [Symbol] :color
|
27
|
+
# the color for the selected item, defualts to :green
|
28
|
+
# @option options [Symbol] :marker
|
29
|
+
# the marker for the selected item
|
30
|
+
# @option options [String] :enum
|
31
|
+
# the delimiter for the item index
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def initialize(prompt, options = {})
|
35
|
+
@prompt = prompt
|
36
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
37
|
+
@enum = options.fetch(:enum) { nil }
|
38
|
+
@default = Array[options.fetch(:default) { 1 }]
|
39
|
+
@active = @default.first
|
40
|
+
@choices = Choices.new
|
41
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
42
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
43
|
+
@marker = options.fetch(:marker) { symbols[:pointer] }
|
44
|
+
@help = options[:help]
|
45
|
+
@first_render = true
|
46
|
+
@done = false
|
47
|
+
@per_page = options[:per_page]
|
48
|
+
@page_help = options[:page_help] || PAGE_HELP
|
49
|
+
@paginator = Paginator.new
|
50
|
+
|
51
|
+
@prompt.subscribe(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Set marker
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def marker(value)
|
58
|
+
@marker = value
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set default option selected
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def default(*default_values)
|
65
|
+
@default = default_values
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set number of items per page
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def per_page(value)
|
72
|
+
@per_page = value
|
73
|
+
end
|
74
|
+
|
75
|
+
def page_size
|
76
|
+
(@per_page || Paginator::DEFAULT_PAGE_SIZE)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check if list is paginated
|
80
|
+
#
|
81
|
+
# @return [Boolean]
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
def paginated?
|
85
|
+
@choices.size > page_size
|
86
|
+
end
|
87
|
+
|
88
|
+
# @param [String] text
|
89
|
+
# the help text to display per page
|
90
|
+
# @api pbulic
|
91
|
+
def page_help(text)
|
92
|
+
@page_help = text
|
93
|
+
end
|
94
|
+
|
95
|
+
# Provide help information
|
96
|
+
#
|
97
|
+
# @param [String] value
|
98
|
+
# the new help text
|
99
|
+
#
|
100
|
+
# @return [String]
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def help(value = (not_set = true))
|
104
|
+
return @help if !@help.nil? && not_set
|
105
|
+
|
106
|
+
@help = (@help.nil? && !not_set) ? value : default_help
|
107
|
+
end
|
108
|
+
|
109
|
+
# Default help text
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
def default_help
|
113
|
+
self.class::HELP % [enumerate? ? " or number (1-#{@choices.size})" : '']
|
114
|
+
end
|
115
|
+
|
116
|
+
# Set selecting active index using number pad
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def enum(value)
|
120
|
+
@enum = value
|
121
|
+
end
|
122
|
+
|
123
|
+
# Add a single choice
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def choice(*value, &block)
|
127
|
+
if block
|
128
|
+
@choices << (value << block)
|
129
|
+
else
|
130
|
+
@choices << value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Add multiple choices
|
135
|
+
#
|
136
|
+
# @param [Array[Object]] values
|
137
|
+
# the values to add as choices
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
def choices(values)
|
141
|
+
Array(values).each { |val| choice(*val) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Call the list menu by passing question and choices
|
145
|
+
#
|
146
|
+
# @param [String] question
|
147
|
+
#
|
148
|
+
# @param
|
149
|
+
# @api public
|
150
|
+
def call(question, possibilities, &block)
|
151
|
+
choices(possibilities)
|
152
|
+
@question = question
|
153
|
+
block.call(self) if block
|
154
|
+
setup_defaults
|
155
|
+
render
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check if list is enumerated
|
159
|
+
#
|
160
|
+
# @return [Boolean]
|
161
|
+
def enumerate?
|
162
|
+
!@enum.nil?
|
163
|
+
end
|
164
|
+
|
165
|
+
def keynum(event)
|
166
|
+
return unless enumerate?
|
167
|
+
value = event.value.to_i
|
168
|
+
return unless (1..@choices.count).cover?(value)
|
169
|
+
@active = value
|
170
|
+
end
|
171
|
+
|
172
|
+
def keyspace(*)
|
173
|
+
@done = true
|
174
|
+
end
|
175
|
+
|
176
|
+
def keyreturn(*)
|
177
|
+
@done = true
|
178
|
+
end
|
179
|
+
|
180
|
+
def keyup(*)
|
181
|
+
@active = (@active == 1) ? @choices.length : @active - 1
|
182
|
+
end
|
183
|
+
|
184
|
+
def keydown(*)
|
185
|
+
@active = (@active == @choices.length) ? 1 : @active + 1
|
186
|
+
end
|
187
|
+
alias keytab keydown
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
# Setup default option and active selection
|
192
|
+
#
|
193
|
+
# @api private
|
194
|
+
def setup_defaults
|
195
|
+
validate_defaults
|
196
|
+
@active = @default.first
|
197
|
+
end
|
198
|
+
|
199
|
+
# Validate default indexes to be within range
|
200
|
+
#
|
201
|
+
# @api private
|
202
|
+
def validate_defaults
|
203
|
+
@default.each do |d|
|
204
|
+
if d.nil? || d.to_s.empty?
|
205
|
+
raise ConfigurationError,
|
206
|
+
"default index must be an integer in range (1 - #{@choices.size})"
|
207
|
+
end
|
208
|
+
if d < 1 || d > @choices.size
|
209
|
+
raise ConfigurationError,
|
210
|
+
"default index `#{d}` out of range (1 - #{@choices.size})"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Render a selection list.
|
216
|
+
#
|
217
|
+
# By default the result is printed out.
|
218
|
+
#
|
219
|
+
# @return [Object] value
|
220
|
+
# return the selected value
|
221
|
+
#
|
222
|
+
# @api private
|
223
|
+
def render
|
224
|
+
@prompt.print(@prompt.hide)
|
225
|
+
until @done
|
226
|
+
question = render_question
|
227
|
+
@prompt.print(question)
|
228
|
+
@prompt.read_keypress
|
229
|
+
@prompt.print(refresh(question.lines.count))
|
230
|
+
end
|
231
|
+
@prompt.print(render_question)
|
232
|
+
answer
|
233
|
+
ensure
|
234
|
+
@prompt.print(@prompt.show)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Find value for the choice selected
|
238
|
+
#
|
239
|
+
# @return [nil, Object]
|
240
|
+
#
|
241
|
+
# @api private
|
242
|
+
def answer
|
243
|
+
@choices[@active - 1].value
|
244
|
+
end
|
245
|
+
|
246
|
+
# Clear screen lines
|
247
|
+
#
|
248
|
+
# @param [String]
|
249
|
+
#
|
250
|
+
# @api private
|
251
|
+
def refresh(lines)
|
252
|
+
@prompt.clear_lines(lines)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Render question with instructions and menu
|
256
|
+
#
|
257
|
+
# @return [String]
|
258
|
+
#
|
259
|
+
# @api private
|
260
|
+
def render_question
|
261
|
+
header = "#{@prefix}#{@question} #{render_header}\n"
|
262
|
+
@first_render = false
|
263
|
+
rendered_menu = render_menu
|
264
|
+
rendered_menu << render_footer
|
265
|
+
header << rendered_menu unless @done
|
266
|
+
header
|
267
|
+
end
|
268
|
+
|
269
|
+
# Render initial help and selected choice
|
270
|
+
#
|
271
|
+
# @return [String]
|
272
|
+
#
|
273
|
+
# @api private
|
274
|
+
def render_header
|
275
|
+
if @done
|
276
|
+
selected_item = "#{@choices[@active - 1].name}"
|
277
|
+
@prompt.decorate(selected_item, @active_color)
|
278
|
+
elsif @first_render
|
279
|
+
@prompt.decorate(help, @help_color)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Render menu with choices to select from
|
284
|
+
#
|
285
|
+
# @return [String]
|
286
|
+
#
|
287
|
+
# @api private
|
288
|
+
def render_menu
|
289
|
+
output = ''
|
290
|
+
@paginator.paginate(@choices, @active, @per_page) do |choice, index|
|
291
|
+
num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
|
292
|
+
message = if index + 1 == @active
|
293
|
+
selected = @marker + ' ' + num + choice.name
|
294
|
+
@prompt.decorate("#{selected}", @active_color)
|
295
|
+
else
|
296
|
+
' ' * 2 + num + choice.name
|
297
|
+
end
|
298
|
+
max_index = paginated? ? @paginator.max_index : @choices.size - 1
|
299
|
+
newline = (index == max_index) ? '' : "\n"
|
300
|
+
output << (message + newline)
|
301
|
+
end
|
302
|
+
output
|
303
|
+
end
|
304
|
+
|
305
|
+
# Render page info footer
|
306
|
+
#
|
307
|
+
# @return [String]
|
308
|
+
#
|
309
|
+
# @api private
|
310
|
+
def render_footer
|
311
|
+
return '' unless paginated?
|
312
|
+
colored_footer = @prompt.decorate(@page_help, @help_color)
|
313
|
+
"\n" << colored_footer
|
314
|
+
end
|
315
|
+
end # List
|
316
|
+
end # Prompt
|
317
|
+
end # TTY
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'question'
|
4
|
+
require_relative 'symbols'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Prompt
|
8
|
+
class MaskQuestion < Question
|
9
|
+
# Create masked question
|
10
|
+
#
|
11
|
+
# @param [Hash] options
|
12
|
+
# @option options [String] :mask
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def initialize(prompt, options = {})
|
16
|
+
super
|
17
|
+
@mask = options.fetch(:mask) { Symbols.symbols[:dot] }
|
18
|
+
@done_masked = false
|
19
|
+
@failure = false
|
20
|
+
@prompt.subscribe(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set character for masking the STDIN input
|
24
|
+
#
|
25
|
+
# @param [String] char
|
26
|
+
#
|
27
|
+
# @return [self]
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def mask(char = (not_set = true))
|
31
|
+
return @mask if not_set
|
32
|
+
@mask = char
|
33
|
+
end
|
34
|
+
|
35
|
+
def keyreturn(event)
|
36
|
+
@done_masked = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def keyenter(event)
|
40
|
+
@done_masked = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def keypress(event)
|
44
|
+
if [:backspace, :delete].include?(event.key.name)
|
45
|
+
@input.chop! unless @input.empty?
|
46
|
+
elsif event.value =~ /^[^\e\n\r]/
|
47
|
+
@input += event.value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Render question and input replaced with masked character
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
def render_question
|
55
|
+
header = "#{@prefix}#{message} "
|
56
|
+
if echo?
|
57
|
+
masked = "#{@mask * "#{@input}".length}"
|
58
|
+
if @done_masked && !@failure
|
59
|
+
masked = @prompt.decorate(masked, @active_color)
|
60
|
+
elsif @done_masked && @failure
|
61
|
+
masked = @prompt.decorate(masked, @error_color)
|
62
|
+
end
|
63
|
+
header += masked
|
64
|
+
end
|
65
|
+
header << "\n" if @done
|
66
|
+
header
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_error(errors)
|
70
|
+
@failure = !errors.empty?
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
# Read input from user masked by character
|
75
|
+
#
|
76
|
+
# @private
|
77
|
+
def read_input(question)
|
78
|
+
@done_masked = false
|
79
|
+
@failure = false
|
80
|
+
@input = ''
|
81
|
+
until @done_masked
|
82
|
+
@prompt.read_keypress
|
83
|
+
@prompt.print(@prompt.clear_line)
|
84
|
+
@prompt.print(render_question)
|
85
|
+
end
|
86
|
+
@prompt.puts
|
87
|
+
@input
|
88
|
+
end
|
89
|
+
end # MaskQuestion
|
90
|
+
end # Prompt
|
91
|
+
end # TTY
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'list'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
# A class responsible for rendering multi select list menu.
|
8
|
+
# Used by {Prompt} to display interactive choice menu.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class MultiList < List
|
12
|
+
HELP = '(Use arrow%s keys, press Space to select and Enter to finish)'.freeze
|
13
|
+
|
14
|
+
# Create instance of TTY::Prompt::MultiList menu.
|
15
|
+
#
|
16
|
+
# @param [Prompt] :prompt
|
17
|
+
# @param [Hash] options
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def initialize(prompt, options)
|
21
|
+
super
|
22
|
+
@selected = []
|
23
|
+
@help = options[:help]
|
24
|
+
@default = Array(options[:default])
|
25
|
+
@echo = options.fetch(:echo, true)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Callback fired when space key is pressed
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
def keyspace(*)
|
32
|
+
active_choice = @choices[@active - 1]
|
33
|
+
if @selected.include?(active_choice)
|
34
|
+
@selected.delete(active_choice)
|
35
|
+
else
|
36
|
+
@selected << active_choice
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Setup default options and active selection
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
def setup_defaults
|
46
|
+
validate_defaults
|
47
|
+
@selected = @choices.values_at(*@default.map { |d| d - 1 })
|
48
|
+
@active = @default.last unless @selected.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generate selected items names
|
52
|
+
#
|
53
|
+
# @return [String]
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
def selected_names
|
57
|
+
@selected.map(&:name).join(', ')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Render initial help text and then currently selected choices
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
def render_header
|
64
|
+
instructions = @prompt.decorate(help, :bright_black)
|
65
|
+
if @done && @echo
|
66
|
+
@prompt.decorate(selected_names, @active_color)
|
67
|
+
elsif @selected.size.nonzero? && @echo
|
68
|
+
selected_names + (@first_render ? " #{instructions}" : '')
|
69
|
+
elsif @first_render
|
70
|
+
instructions
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# All values for the choices selected
|
75
|
+
#
|
76
|
+
# @return [Array[nil,Object]]
|
77
|
+
#
|
78
|
+
# @api private
|
79
|
+
def answer
|
80
|
+
@selected.map(&:value)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Render menu with choices to select from
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
#
|
87
|
+
# @api private
|
88
|
+
def render_menu
|
89
|
+
output = ''
|
90
|
+
@paginator.paginate(@choices, @active, @per_page) do |choice, index|
|
91
|
+
num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
|
92
|
+
indicator = (index + 1 == @active) ? @marker : ' '
|
93
|
+
indicator += ' '
|
94
|
+
message = if @selected.include?(choice)
|
95
|
+
selected = @prompt.decorate(symbols[:radio_on], @active_color)
|
96
|
+
selected + ' ' + num + choice.name
|
97
|
+
else
|
98
|
+
symbols[:radio_off] + ' ' + num + choice.name
|
99
|
+
end
|
100
|
+
max_index = paginated? ? @paginator.max_index : @choices.size - 1
|
101
|
+
newline = (index == max_index) ? '' : "\n"
|
102
|
+
output << indicator + message + newline
|
103
|
+
end
|
104
|
+
output
|
105
|
+
end
|
106
|
+
end # MultiList
|
107
|
+
end # Prompt
|
108
|
+
end # TTY
|