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,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'question'
|
4
|
+
require_relative 'symbols'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Prompt
|
8
|
+
# A prompt responsible for multi line user input
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Multiline < Question
|
12
|
+
HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze
|
13
|
+
|
14
|
+
def initialize(prompt, options = {})
|
15
|
+
super
|
16
|
+
@help = options[:help] || self.class::HELP
|
17
|
+
@first_render = true
|
18
|
+
@lines_count = 0
|
19
|
+
|
20
|
+
@prompt.subscribe(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Provide help information
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def help(value = (not_set = true))
|
29
|
+
return @help if not_set
|
30
|
+
@help = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_input
|
34
|
+
@prompt.read_multiline
|
35
|
+
end
|
36
|
+
|
37
|
+
def keyreturn(*)
|
38
|
+
@lines_count += 1
|
39
|
+
end
|
40
|
+
alias keyenter keyreturn
|
41
|
+
|
42
|
+
def render_question
|
43
|
+
header = "#{@prefix}#{message} "
|
44
|
+
if !echo?
|
45
|
+
header
|
46
|
+
elsif @done
|
47
|
+
header += @prompt.decorate("#{@input}", @active_color)
|
48
|
+
elsif @first_render
|
49
|
+
header += @prompt.decorate(help, @help_color)
|
50
|
+
@first_render = false
|
51
|
+
end
|
52
|
+
header += "\n"
|
53
|
+
header
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_input(question)
|
57
|
+
@lines = read_input
|
58
|
+
@input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
|
59
|
+
if Utils.blank?(@input)
|
60
|
+
@input = default? ? default : nil
|
61
|
+
end
|
62
|
+
@evaluator.(@lines)
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh(lines)
|
66
|
+
size = @lines_count + lines + 1
|
67
|
+
@prompt.clear_lines(size)
|
68
|
+
end
|
69
|
+
end # Multiline
|
70
|
+
end # Prompt
|
71
|
+
end # TTY
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Paginator
|
6
|
+
DEFAULT_PAGE_SIZE = 6
|
7
|
+
|
8
|
+
# Create a Paginator
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
def initialize(options = {})
|
12
|
+
@last_index = Array(options[:default]).flatten.first || 0
|
13
|
+
@per_page = options[:per_page]
|
14
|
+
@lower_index = Array(options[:default]).flatten.first
|
15
|
+
end
|
16
|
+
|
17
|
+
# Maximum index for current pagination
|
18
|
+
#
|
19
|
+
# @return [Integer]
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
def max_index
|
23
|
+
raise ArgumentError, 'no max index' unless @per_page
|
24
|
+
@lower_index + @per_page - 1
|
25
|
+
end
|
26
|
+
|
27
|
+
# Paginate collection given an active index
|
28
|
+
#
|
29
|
+
# @param [Array[Choice]] list
|
30
|
+
# a collection of choice items
|
31
|
+
# @param [Integer] active
|
32
|
+
# current choice active index
|
33
|
+
# @param [Integer] per_page
|
34
|
+
# number of choice items per page
|
35
|
+
#
|
36
|
+
# @return [Enumerable]
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def paginate(list, active, per_page = nil, &block)
|
40
|
+
current_index = active - 1
|
41
|
+
default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
|
42
|
+
@per_page = @per_page || per_page || default_size
|
43
|
+
@lower_index ||= current_index
|
44
|
+
@upper_index ||= max_index
|
45
|
+
|
46
|
+
# Don't paginate short lists
|
47
|
+
if list.size <= @per_page
|
48
|
+
@lower_index = 0
|
49
|
+
@upper_index = list.size - 1
|
50
|
+
if block
|
51
|
+
return list.each_with_index(&block)
|
52
|
+
else
|
53
|
+
return list.each_with_index.to_enum
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if current_index > @last_index # going up
|
58
|
+
if current_index > @upper_index && current_index < list.size - 1
|
59
|
+
@lower_index += 1
|
60
|
+
end
|
61
|
+
elsif current_index < @last_index # going down
|
62
|
+
if current_index < @lower_index && current_index > 0
|
63
|
+
@lower_index -= 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Cycle list
|
68
|
+
if current_index.zero?
|
69
|
+
@lower_index = 0
|
70
|
+
elsif current_index == list.size - 1
|
71
|
+
@lower_index = list.size - 1 - (@per_page - 1)
|
72
|
+
end
|
73
|
+
|
74
|
+
@upper_index = @lower_index + (@per_page - 1)
|
75
|
+
@last_index = current_index
|
76
|
+
|
77
|
+
sliced_list = list[@lower_index..@upper_index]
|
78
|
+
indices = (@lower_index..@upper_index)
|
79
|
+
|
80
|
+
return sliced_list.zip(indices).to_enum unless block_given?
|
81
|
+
|
82
|
+
sliced_list.each_with_index do |item, index|
|
83
|
+
block[item, @lower_index + index]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end # Paginator
|
87
|
+
end # Prompt
|
88
|
+
end # TTY
|
@@ -0,0 +1,333 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'converters'
|
4
|
+
require_relative 'evaluator'
|
5
|
+
require_relative 'question/modifier'
|
6
|
+
require_relative 'question/validation'
|
7
|
+
require_relative 'question/checks'
|
8
|
+
require_relative 'utils'
|
9
|
+
|
10
|
+
module TTY
|
11
|
+
# A class responsible for shell prompt interactions.
|
12
|
+
class Prompt
|
13
|
+
# A class responsible for gathering user input
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
class Question
|
17
|
+
include Checks
|
18
|
+
|
19
|
+
UndefinedSetting = Class.new do
|
20
|
+
def to_s
|
21
|
+
"undefined"
|
22
|
+
end
|
23
|
+
alias inspect to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
# Store question message
|
27
|
+
# @api public
|
28
|
+
attr_reader :message
|
29
|
+
|
30
|
+
attr_reader :modifier
|
31
|
+
|
32
|
+
attr_reader :validation
|
33
|
+
|
34
|
+
# Initialize a Question
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def initialize(prompt, options = {})
|
38
|
+
@prompt = prompt
|
39
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
40
|
+
@default = options.fetch(:default) { UndefinedSetting }
|
41
|
+
@required = options.fetch(:required) { false }
|
42
|
+
@echo = options.fetch(:echo) { true }
|
43
|
+
@in = options.fetch(:in) { UndefinedSetting }
|
44
|
+
@modifier = options.fetch(:modifier) { [] }
|
45
|
+
@validation = options.fetch(:validation) { UndefinedSetting }
|
46
|
+
@convert = options.fetch(:convert) { UndefinedSetting }
|
47
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
48
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
49
|
+
@error_color = options.fetch(:error_color) { :red }
|
50
|
+
@messages = Utils.deep_copy(options.fetch(:messages) { { } })
|
51
|
+
@done = false
|
52
|
+
@input = nil
|
53
|
+
|
54
|
+
@evaluator = Evaluator.new(self)
|
55
|
+
|
56
|
+
@evaluator << CheckRequired
|
57
|
+
@evaluator << CheckDefault
|
58
|
+
@evaluator << CheckRange
|
59
|
+
@evaluator << CheckValidation
|
60
|
+
@evaluator << CheckModifier
|
61
|
+
end
|
62
|
+
|
63
|
+
# Stores all the error messages displayed to user
|
64
|
+
# The currently supported messages are:
|
65
|
+
# * :range?
|
66
|
+
# * :required?
|
67
|
+
# * :valid?
|
68
|
+
attr_reader :messages
|
69
|
+
|
70
|
+
# Retrieve message based on the key
|
71
|
+
#
|
72
|
+
# @param [Symbol] name
|
73
|
+
# the name of message key
|
74
|
+
#
|
75
|
+
# @param [Hash] tokens
|
76
|
+
# the tokens to evaluate
|
77
|
+
#
|
78
|
+
# @return [Array[String]]
|
79
|
+
#
|
80
|
+
# @api private
|
81
|
+
def message_for(name, tokens = nil)
|
82
|
+
template = @messages[name]
|
83
|
+
if template && !template.match(/\%\{/).nil?
|
84
|
+
[template % tokens]
|
85
|
+
else
|
86
|
+
[template || '']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Call the question
|
91
|
+
#
|
92
|
+
# @param [String] message
|
93
|
+
#
|
94
|
+
# @return [self]
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
def call(message, &block)
|
98
|
+
return if Utils.blank?(message)
|
99
|
+
@message = message
|
100
|
+
block.call(self) if block
|
101
|
+
render
|
102
|
+
end
|
103
|
+
|
104
|
+
# Read answer and convert to type
|
105
|
+
#
|
106
|
+
# @api private
|
107
|
+
def render
|
108
|
+
@errors = []
|
109
|
+
until @done
|
110
|
+
question = render_question
|
111
|
+
@prompt.print(question)
|
112
|
+
result = process_input(question)
|
113
|
+
if result.failure?
|
114
|
+
@errors = result.errors
|
115
|
+
@prompt.print(render_error(result.errors))
|
116
|
+
else
|
117
|
+
@done = true
|
118
|
+
end
|
119
|
+
@prompt.print(refresh(question.lines.count))
|
120
|
+
end
|
121
|
+
@prompt.print(render_question)
|
122
|
+
convert_result(result.value)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Render question
|
126
|
+
#
|
127
|
+
# @return [String]
|
128
|
+
#
|
129
|
+
# @api private
|
130
|
+
def render_question
|
131
|
+
header = "#{@prefix}#{message} "
|
132
|
+
if !echo?
|
133
|
+
header
|
134
|
+
elsif @done
|
135
|
+
header += @prompt.decorate("#{@input}", @active_color)
|
136
|
+
elsif default? && !Utils.blank?(@default)
|
137
|
+
header += @prompt.decorate("(#{default})", @help_color) + ' '
|
138
|
+
end
|
139
|
+
header << "\n" if @done
|
140
|
+
header
|
141
|
+
end
|
142
|
+
|
143
|
+
# Decide how to handle input from user
|
144
|
+
#
|
145
|
+
# @api private
|
146
|
+
def process_input(question)
|
147
|
+
@input = read_input(question)
|
148
|
+
if Utils.blank?(@input)
|
149
|
+
@input = default? ? default : nil
|
150
|
+
end
|
151
|
+
@evaluator.(@input)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Process input
|
155
|
+
#
|
156
|
+
# @api private
|
157
|
+
def read_input(question)
|
158
|
+
@prompt.read_line(question, echo: echo).chomp
|
159
|
+
end
|
160
|
+
|
161
|
+
# Handle error condition
|
162
|
+
#
|
163
|
+
# @return [String]
|
164
|
+
#
|
165
|
+
# @api private
|
166
|
+
def render_error(errors)
|
167
|
+
errors.reduce('') do |acc, err|
|
168
|
+
newline = (@echo ? '' : "\n")
|
169
|
+
acc << newline + @prompt.decorate('>>', :red) + ' ' + err
|
170
|
+
acc
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Determine area of the screen to clear
|
175
|
+
#
|
176
|
+
# @param [Integer] lines
|
177
|
+
# number of lines to clear
|
178
|
+
#
|
179
|
+
# @return [String]
|
180
|
+
#
|
181
|
+
# @api private
|
182
|
+
def refresh(lines)
|
183
|
+
output = ''
|
184
|
+
if @done
|
185
|
+
if @errors.count.zero? && @echo
|
186
|
+
output << @prompt.cursor.up(lines)
|
187
|
+
else
|
188
|
+
lines += @errors.count
|
189
|
+
end
|
190
|
+
else
|
191
|
+
output << @prompt.cursor.up(lines)
|
192
|
+
end
|
193
|
+
output + @prompt.clear_lines(lines)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Convert value to expected type
|
197
|
+
#
|
198
|
+
# @param [Object] value
|
199
|
+
#
|
200
|
+
# @api private
|
201
|
+
def convert_result(value)
|
202
|
+
if convert? & !Utils.blank?(value)
|
203
|
+
Converters.convert(@convert, value)
|
204
|
+
else
|
205
|
+
value
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Specify answer conversion
|
210
|
+
#
|
211
|
+
# @api public
|
212
|
+
def convert(value)
|
213
|
+
@convert = value
|
214
|
+
end
|
215
|
+
|
216
|
+
# Check if conversion is set
|
217
|
+
#
|
218
|
+
# @return [Boolean]
|
219
|
+
#
|
220
|
+
# @api public
|
221
|
+
def convert?
|
222
|
+
@convert != UndefinedSetting
|
223
|
+
end
|
224
|
+
|
225
|
+
# Set default value.
|
226
|
+
#
|
227
|
+
# @api public
|
228
|
+
def default(value = (not_set = true))
|
229
|
+
return @default if not_set
|
230
|
+
@default = value
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check if default value is set
|
234
|
+
#
|
235
|
+
# @return [Boolean]
|
236
|
+
#
|
237
|
+
# @api public
|
238
|
+
def default?
|
239
|
+
@default != UndefinedSetting
|
240
|
+
end
|
241
|
+
|
242
|
+
# Ensure that passed argument is present or not
|
243
|
+
#
|
244
|
+
# @return [Boolean]
|
245
|
+
#
|
246
|
+
# @api public
|
247
|
+
def required(value = (not_set = true), message = nil)
|
248
|
+
messages[:required?] = message if message
|
249
|
+
return @required if not_set
|
250
|
+
@required = value
|
251
|
+
end
|
252
|
+
alias_method :required?, :required
|
253
|
+
|
254
|
+
# Set validation rule for an argument
|
255
|
+
#
|
256
|
+
# @param [Object] value
|
257
|
+
#
|
258
|
+
# @return [Question]
|
259
|
+
#
|
260
|
+
# @api public
|
261
|
+
def validate(value = nil, message = nil, &block)
|
262
|
+
messages[:valid?] = message if message
|
263
|
+
@validation = (value || block)
|
264
|
+
end
|
265
|
+
|
266
|
+
def validation?
|
267
|
+
@validation != UndefinedSetting
|
268
|
+
end
|
269
|
+
|
270
|
+
# Modify string according to the rule given.
|
271
|
+
#
|
272
|
+
# @param [Symbol] rule
|
273
|
+
#
|
274
|
+
# @api public
|
275
|
+
def modify(*rules)
|
276
|
+
@modifier = rules
|
277
|
+
end
|
278
|
+
|
279
|
+
# Turn terminal echo on or off. This is used to secure the display so
|
280
|
+
# that the entered characters are not echoed back to the screen.
|
281
|
+
#
|
282
|
+
# @api public
|
283
|
+
def echo(value = nil)
|
284
|
+
return @echo if value.nil?
|
285
|
+
@echo = value
|
286
|
+
end
|
287
|
+
alias_method :echo?, :echo
|
288
|
+
|
289
|
+
# Turn raw mode on or off. This enables character-based input.
|
290
|
+
#
|
291
|
+
# @api public
|
292
|
+
def raw(value = nil)
|
293
|
+
return @raw if value.nil?
|
294
|
+
@raw = value
|
295
|
+
end
|
296
|
+
alias_method :raw?, :raw
|
297
|
+
|
298
|
+
# Set expected range of values
|
299
|
+
#
|
300
|
+
# @param [String] value
|
301
|
+
#
|
302
|
+
# @api public
|
303
|
+
def in(value = (not_set = true), message = nil)
|
304
|
+
messages[:range?] = message if message
|
305
|
+
if in? && !@in.is_a?(Range)
|
306
|
+
@in = Converters.convert(:range, @in)
|
307
|
+
end
|
308
|
+
return @in if not_set
|
309
|
+
@in = Converters.convert(:range, value)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Check if range is set
|
313
|
+
#
|
314
|
+
# @return [Boolean]
|
315
|
+
#
|
316
|
+
# @api public
|
317
|
+
def in?
|
318
|
+
@in != UndefinedSetting
|
319
|
+
end
|
320
|
+
|
321
|
+
# @api public
|
322
|
+
def to_s
|
323
|
+
"#{message}"
|
324
|
+
end
|
325
|
+
|
326
|
+
# String representation of this question
|
327
|
+
# @api public
|
328
|
+
def inspect
|
329
|
+
"#<#{self.class.name} @message=#{message}, @input=#{@input}>"
|
330
|
+
end
|
331
|
+
end # Question
|
332
|
+
end # Prompt
|
333
|
+
end # TTY
|