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,77 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'necromancer'
|
5
|
+
|
6
|
+
require_relative 'converter_dsl'
|
7
|
+
|
8
|
+
module TTY
|
9
|
+
class Prompt
|
10
|
+
module Converters
|
11
|
+
extend ConverterDSL
|
12
|
+
|
13
|
+
# Delegate Necromancer errors
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
def self.on_error
|
17
|
+
if block_given?
|
18
|
+
yield
|
19
|
+
else
|
20
|
+
raise ArgumentError, 'You need to provide a block argument.'
|
21
|
+
end
|
22
|
+
rescue Necromancer::ConversionTypeError => e
|
23
|
+
raise ConversionError, e.message
|
24
|
+
end
|
25
|
+
|
26
|
+
converter(:bool) do |input|
|
27
|
+
on_error { Necromancer.convert(input).to(:boolean, strict: true) }
|
28
|
+
end
|
29
|
+
|
30
|
+
converter(:string) do |input|
|
31
|
+
String(input).chomp
|
32
|
+
end
|
33
|
+
|
34
|
+
converter(:symbol) do |input|
|
35
|
+
input.to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
converter(:date) do |input|
|
39
|
+
on_error { Necromancer.convert(input).to(:date, strict: true) }
|
40
|
+
end
|
41
|
+
|
42
|
+
converter(:datetime) do |input|
|
43
|
+
on_error { Necromancer.convert(input).to(:datetime, strict: true) }
|
44
|
+
end
|
45
|
+
|
46
|
+
converter(:int) do |input|
|
47
|
+
on_error { Necromancer.convert(input).to(:integer, strict: true) }
|
48
|
+
end
|
49
|
+
|
50
|
+
converter(:float) do |input|
|
51
|
+
on_error { Necromancer.convert(input).to(:float, strict: true) }
|
52
|
+
end
|
53
|
+
|
54
|
+
converter(:range) do |input|
|
55
|
+
on_error { Necromancer.convert(input).to(:range, strict: true) }
|
56
|
+
end
|
57
|
+
|
58
|
+
converter(:regexp) do |input|
|
59
|
+
Regexp.new(input)
|
60
|
+
end
|
61
|
+
|
62
|
+
converter(:file) do |input|
|
63
|
+
directory = ::File.expand_path(::File.dirname($0))
|
64
|
+
::File.open(::File.join(directory, input))
|
65
|
+
end
|
66
|
+
|
67
|
+
converter(:path) do |input|
|
68
|
+
directory = ::File.expand_path(::File.dirname($0))
|
69
|
+
Pathname.new(::File.join(directory, input))
|
70
|
+
end
|
71
|
+
|
72
|
+
converter(:char) do |input|
|
73
|
+
String(input).chars.to_a[0]
|
74
|
+
end
|
75
|
+
end # Converters
|
76
|
+
end # Prompt
|
77
|
+
end # TTY
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# A class responsible for string comparison
|
6
|
+
class Distance
|
7
|
+
# Calculate the optimal string alignment distance
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
def distance(first, second)
|
11
|
+
distances = []
|
12
|
+
rows = first.to_s.length
|
13
|
+
cols = second.to_s.length
|
14
|
+
|
15
|
+
0.upto(rows) do |index|
|
16
|
+
distances << [index] + [0] * cols
|
17
|
+
end
|
18
|
+
distances[0] = 0.upto(cols).to_a
|
19
|
+
|
20
|
+
1.upto(rows) do |first_index|
|
21
|
+
1.upto(cols) do |second_index|
|
22
|
+
first_char = first[first_index - 1]
|
23
|
+
second_char = second[second_index - 1]
|
24
|
+
cost = first_char == second_char ? 0 : 1
|
25
|
+
|
26
|
+
distances[first_index][second_index] = [
|
27
|
+
distances[first_index - 1][second_index], # deletion
|
28
|
+
distances[first_index][second_index - 1], # insertion
|
29
|
+
distances[first_index - 1][second_index - 1] # substitution
|
30
|
+
].min + cost
|
31
|
+
|
32
|
+
if first_index > 1 && second_index > 1
|
33
|
+
first_previous_char = first[first_index - 2]
|
34
|
+
second_previous_char = second[second_index - 2]
|
35
|
+
if first_char == second_previous_char && second_char == first_previous_char
|
36
|
+
distances[first_index][second_index] = [
|
37
|
+
distances[first_index][second_index],
|
38
|
+
distances[first_index - 2][second_index - 2] + 1 # transposition
|
39
|
+
].min
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
distances[rows][cols]
|
46
|
+
end
|
47
|
+
end # Distance
|
48
|
+
end # Prompt
|
49
|
+
end # TTY
|
@@ -0,0 +1,337 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'choices'
|
4
|
+
require_relative 'enum_paginator'
|
5
|
+
require_relative 'paginator'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Prompt
|
9
|
+
# A class reponsible for rendering enumerated list menu.
|
10
|
+
# Used by {Prompt} to display static choice menu.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class EnumList
|
14
|
+
PAGE_HELP = '(Press tab/right or left to reveal more choices)'.freeze
|
15
|
+
|
16
|
+
# Create instance of EnumList menu.
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(prompt, options = {})
|
20
|
+
@prompt = prompt
|
21
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
22
|
+
@enum = options.fetch(:enum) { ')' }
|
23
|
+
@default = options.fetch(:default) { 1 }
|
24
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
25
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
26
|
+
@error_color = options.fetch(:error_color) { @prompt.error_color }
|
27
|
+
@input = nil
|
28
|
+
@done = false
|
29
|
+
@first_render = true
|
30
|
+
@failure = false
|
31
|
+
@active = @default
|
32
|
+
@choices = Choices.new
|
33
|
+
@per_page = options[:per_page]
|
34
|
+
@page_help = options[:page_help] || PAGE_HELP
|
35
|
+
@paginator = EnumPaginator.new
|
36
|
+
@page_active = @default
|
37
|
+
|
38
|
+
@prompt.subscribe(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set default option selected
|
42
|
+
#
|
43
|
+
# @api public
|
44
|
+
def default(default)
|
45
|
+
@default = default
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set number of items per page
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def per_page(value)
|
52
|
+
@per_page = value
|
53
|
+
end
|
54
|
+
|
55
|
+
def page_size
|
56
|
+
(@per_page || Paginator::DEFAULT_PAGE_SIZE)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if list is paginated
|
60
|
+
#
|
61
|
+
# @return [Boolean]
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
def paginated?
|
65
|
+
@choices.size > page_size
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [String] text
|
69
|
+
# the help text to display per page
|
70
|
+
# @api pbulic
|
71
|
+
def page_help(text)
|
72
|
+
@page_help = text
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set selecting active index using number pad
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def enum(value)
|
79
|
+
@enum = value
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a single choice
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def choice(*value, &block)
|
86
|
+
if block
|
87
|
+
@choices << (value << block)
|
88
|
+
else
|
89
|
+
@choices << value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Add multiple choices
|
94
|
+
#
|
95
|
+
# @param [Array[Object]] values
|
96
|
+
# the values to add as choices
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def choices(values)
|
100
|
+
values.each { |val| choice(*val) }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Call the list menu by passing question and choices
|
104
|
+
#
|
105
|
+
# @param [String] question
|
106
|
+
#
|
107
|
+
# @param
|
108
|
+
# @api public
|
109
|
+
def call(question, possibilities, &block)
|
110
|
+
choices(possibilities)
|
111
|
+
@question = question
|
112
|
+
block[self] if block
|
113
|
+
setup_defaults
|
114
|
+
render
|
115
|
+
end
|
116
|
+
|
117
|
+
def keypress(event)
|
118
|
+
if [:backspace, :delete].include?(event.key.name)
|
119
|
+
return if @input.empty?
|
120
|
+
@input.chop!
|
121
|
+
mark_choice_as_active
|
122
|
+
elsif event.value =~ /^\d+$/
|
123
|
+
@input += event.value
|
124
|
+
mark_choice_as_active
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def keyreturn(*)
|
129
|
+
@failure = false
|
130
|
+
if (@input.to_i > 0 && @input.to_i <= @choices.size) || @input.empty?
|
131
|
+
@done = true
|
132
|
+
else
|
133
|
+
@input = ''
|
134
|
+
@failure = true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
alias keyenter keyreturn
|
138
|
+
|
139
|
+
def keyright(*)
|
140
|
+
if (@page_active + page_size) <= @choices.size
|
141
|
+
@page_active += page_size
|
142
|
+
else
|
143
|
+
@page_active = 1
|
144
|
+
end
|
145
|
+
end
|
146
|
+
alias keytab keyright
|
147
|
+
|
148
|
+
def keyleft(*)
|
149
|
+
if (@page_active - page_size) >= 0
|
150
|
+
@page_active -= page_size
|
151
|
+
else
|
152
|
+
@page_active = @choices.size - 1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
# Find active choice or set to default
|
159
|
+
#
|
160
|
+
# @return [nil]
|
161
|
+
#
|
162
|
+
# @api private
|
163
|
+
def mark_choice_as_active
|
164
|
+
if (@input.to_i > 0) && !@choices[@input.to_i - 1].nil?
|
165
|
+
@active = @input.to_i
|
166
|
+
else
|
167
|
+
@active = @default
|
168
|
+
end
|
169
|
+
@page_active = @active
|
170
|
+
end
|
171
|
+
|
172
|
+
# Validate default indexes to be within range
|
173
|
+
#
|
174
|
+
# @api private
|
175
|
+
def validate_defaults
|
176
|
+
return if @default >= 1 && @default <= @choices.size
|
177
|
+
raise PromptConfigurationError,
|
178
|
+
"default index `#{d}` out of range (1 - #{@choices.size})"
|
179
|
+
end
|
180
|
+
|
181
|
+
# Setup default option and active selection
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
def setup_defaults
|
185
|
+
validate_defaults
|
186
|
+
mark_choice_as_active
|
187
|
+
end
|
188
|
+
|
189
|
+
# Render a selection list.
|
190
|
+
#
|
191
|
+
# By default the result is printed out.
|
192
|
+
#
|
193
|
+
# @return [Object] value
|
194
|
+
# return the selected value
|
195
|
+
#
|
196
|
+
# @api private
|
197
|
+
def render
|
198
|
+
@input = ''
|
199
|
+
until @done
|
200
|
+
question = render_question
|
201
|
+
@prompt.print(question)
|
202
|
+
@prompt.print(render_error) if @failure
|
203
|
+
if paginated? && !@done
|
204
|
+
@prompt.print(render_page_help)
|
205
|
+
end
|
206
|
+
@prompt.read_keypress
|
207
|
+
@prompt.print(refresh(question.lines.count))
|
208
|
+
end
|
209
|
+
@prompt.print(render_question)
|
210
|
+
answer
|
211
|
+
end
|
212
|
+
|
213
|
+
# Find value for the choice selected
|
214
|
+
#
|
215
|
+
# @return [nil, Object]
|
216
|
+
#
|
217
|
+
# @api private
|
218
|
+
def answer
|
219
|
+
@choices[@active - 1].value
|
220
|
+
end
|
221
|
+
|
222
|
+
# Determine area of the screen to clear
|
223
|
+
#
|
224
|
+
# @param [Integer] lines
|
225
|
+
# the lines to clear
|
226
|
+
#
|
227
|
+
# @return [String]
|
228
|
+
#
|
229
|
+
# @api private
|
230
|
+
def refresh(lines)
|
231
|
+
@prompt.clear_lines(lines) +
|
232
|
+
@prompt.cursor.clear_screen_down
|
233
|
+
end
|
234
|
+
|
235
|
+
# Render question with the menu options
|
236
|
+
#
|
237
|
+
# @return [String]
|
238
|
+
#
|
239
|
+
# @api private
|
240
|
+
def render_question
|
241
|
+
header = "#{@prefix}#{@question} #{render_header}\n"
|
242
|
+
unless @done
|
243
|
+
header << render_menu
|
244
|
+
header << render_footer
|
245
|
+
end
|
246
|
+
header
|
247
|
+
end
|
248
|
+
|
249
|
+
# Error message when incorrect index chosen
|
250
|
+
#
|
251
|
+
# @api private
|
252
|
+
def error_message
|
253
|
+
error = 'Please enter a valid number'
|
254
|
+
"\n" + @prompt.decorate('>>', @error_color) + ' ' + error
|
255
|
+
end
|
256
|
+
|
257
|
+
# Render error message and return cursor to position of input
|
258
|
+
#
|
259
|
+
# @return [String]
|
260
|
+
#
|
261
|
+
# @api private
|
262
|
+
def render_error
|
263
|
+
error = error_message.dup
|
264
|
+
if !paginated?
|
265
|
+
error << @prompt.cursor.prev_line
|
266
|
+
error << @prompt.cursor.forward(render_footer.size)
|
267
|
+
end
|
268
|
+
error
|
269
|
+
end
|
270
|
+
|
271
|
+
# Render chosen option
|
272
|
+
#
|
273
|
+
# @return [String]
|
274
|
+
#
|
275
|
+
# @api private
|
276
|
+
def render_header
|
277
|
+
return '' unless @done
|
278
|
+
return '' unless @active
|
279
|
+
selected_item = @choices[@active - 1].name.to_s
|
280
|
+
@prompt.decorate(selected_item, @active_color)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Render footer for the indexed menu
|
284
|
+
#
|
285
|
+
# @return [String]
|
286
|
+
#
|
287
|
+
# @api private
|
288
|
+
def render_footer
|
289
|
+
" Choose 1-#{@choices.size} [#{@default}]: #{@input}"
|
290
|
+
end
|
291
|
+
|
292
|
+
# Pagination help message
|
293
|
+
#
|
294
|
+
# @return [String]
|
295
|
+
#
|
296
|
+
# @api private
|
297
|
+
def page_help_message
|
298
|
+
return '' unless paginated?
|
299
|
+
"\n" + @prompt.decorate(@page_help, @help_color)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Render page help
|
303
|
+
#
|
304
|
+
# @return [String]
|
305
|
+
#
|
306
|
+
# @api private
|
307
|
+
def render_page_help
|
308
|
+
help = page_help_message.dup
|
309
|
+
if @failure
|
310
|
+
help << @prompt.cursor.prev_line
|
311
|
+
end
|
312
|
+
help << @prompt.cursor.prev_line
|
313
|
+
help << @prompt.cursor.forward(render_footer.size)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Render menu with indexed choices to select from
|
317
|
+
#
|
318
|
+
# @return [String]
|
319
|
+
#
|
320
|
+
# @api private
|
321
|
+
def render_menu
|
322
|
+
output = ''
|
323
|
+
@paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
|
324
|
+
num = (index + 1).to_s + @enum + ' '
|
325
|
+
selected = ' ' * 2 + num + choice.name
|
326
|
+
output << if index + 1 == @active
|
327
|
+
@prompt.decorate(selected.to_s, @active_color)
|
328
|
+
else
|
329
|
+
selected
|
330
|
+
end
|
331
|
+
output << "\n"
|
332
|
+
end
|
333
|
+
output
|
334
|
+
end
|
335
|
+
end # EnumList
|
336
|
+
end # Prompt
|
337
|
+
end # TTY
|