tty-prompt 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +8 -4
- data/README.md +229 -32
- data/benchmarks/speed.rb +27 -0
- data/examples/collect.rb +19 -0
- data/examples/expand.rb +29 -0
- data/lib/tty-prompt.rb +3 -0
- data/lib/tty/prompt.rb +82 -21
- data/lib/tty/prompt/answers_collector.rb +59 -0
- data/lib/tty/prompt/choice.rb +9 -2
- data/lib/tty/prompt/choices.rb +17 -1
- data/lib/tty/prompt/confirm_question.rb +140 -0
- data/lib/tty/prompt/enum_list.rb +15 -12
- data/lib/tty/prompt/expander.rb +288 -0
- data/lib/tty/prompt/list.rb +14 -8
- data/lib/tty/prompt/mask_question.rb +2 -2
- data/lib/tty/prompt/multi_list.rb +14 -4
- data/lib/tty/prompt/question.rb +8 -10
- data/lib/tty/prompt/slider.rb +9 -7
- data/lib/tty/prompt/version.rb +1 -1
- data/spec/unit/ask_spec.rb +68 -0
- data/spec/unit/choice/from_spec.rb +7 -1
- data/spec/unit/choices/find_by_spec.rb +10 -0
- data/spec/unit/choices/pluck_spec.rb +4 -4
- data/spec/unit/collect_spec.rb +33 -0
- data/spec/unit/converters/convert_bool_spec.rb +1 -1
- data/spec/unit/enum_select_spec.rb +20 -1
- data/spec/unit/expand_spec.rb +198 -0
- data/spec/unit/multi_select_spec.rb +39 -2
- data/spec/unit/select_spec.rb +28 -5
- data/spec/unit/slider_spec.rb +15 -0
- data/spec/unit/yes_no_spec.rb +149 -14
- data/tty-prompt.gemspec +1 -2
- metadata +15 -17
data/benchmarks/speed.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
require 'benchmark/ips'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
input = ::StringIO.new
|
8
|
+
output = ::StringIO.new
|
9
|
+
prompt = TTY::Prompt.new(input: input, output: output)
|
10
|
+
|
11
|
+
Benchmark.ips do |r|
|
12
|
+
|
13
|
+
r.report("Ruby #puts") do
|
14
|
+
output.puts "What is your name?"
|
15
|
+
end
|
16
|
+
|
17
|
+
r.report("TTY::Prompt #ask") do
|
18
|
+
prompt.ask("What is your name?")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Calculating -------------------------------------
|
23
|
+
# Ruby #puts 34601 i/100ms
|
24
|
+
# TTY::Prompt #ask 12 i/100ms
|
25
|
+
# -------------------------------------------------
|
26
|
+
# Ruby #puts 758640.5 (±14.9%) i/s - 3736908 in 5.028562s
|
27
|
+
# TTY::Prompt #ask 63.1 (±7.9%) i/s - 324 in 5.176857s
|
data/examples/collect.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
|
5
|
+
prompt = TTY::Prompt.new(prefix: '[?] ')
|
6
|
+
|
7
|
+
result = prompt.collect do
|
8
|
+
key(:name).ask('Name?')
|
9
|
+
|
10
|
+
key(:age).ask('Age?', convert: :int)
|
11
|
+
|
12
|
+
key(:address) do
|
13
|
+
key(:street).ask('Street?', required: true)
|
14
|
+
key(:city).ask('City?')
|
15
|
+
key(:zip).ask('Zip?', validate: /\A\d{3}\Z/)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
puts result
|
data/examples/expand.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
|
5
|
+
choices = [{
|
6
|
+
key: 'y',
|
7
|
+
name: 'overwrite this file',
|
8
|
+
value: :yes
|
9
|
+
}, {
|
10
|
+
key: 'n',
|
11
|
+
name: 'do not overwrite this file',
|
12
|
+
value: :no
|
13
|
+
}, {
|
14
|
+
key: 'a',
|
15
|
+
name: 'overwrite this file and all later files',
|
16
|
+
value: :all
|
17
|
+
}, {
|
18
|
+
key: 'd',
|
19
|
+
name: 'show diff',
|
20
|
+
value: :diff
|
21
|
+
}, {
|
22
|
+
key: 'q',
|
23
|
+
name: 'quit; do not overwrite this file ',
|
24
|
+
value: :quit
|
25
|
+
}]
|
26
|
+
|
27
|
+
prompt = TTY::Prompt.new
|
28
|
+
|
29
|
+
prompt.expand('Overwrite Gemfile?', choices, default: 3)
|
data/lib/tty-prompt.rb
CHANGED
@@ -9,15 +9,18 @@ require 'tty/prompt'
|
|
9
9
|
require 'tty/prompt/choice'
|
10
10
|
require 'tty/prompt/choices'
|
11
11
|
require 'tty/prompt/enum_list'
|
12
|
+
require 'tty/prompt/expander'
|
12
13
|
require 'tty/prompt/evaluator'
|
13
14
|
require 'tty/prompt/list'
|
14
15
|
require 'tty/prompt/multi_list'
|
15
16
|
require 'tty/prompt/question'
|
16
17
|
require 'tty/prompt/mask_question'
|
18
|
+
require 'tty/prompt/confirm_question'
|
17
19
|
require 'tty/prompt/reader'
|
18
20
|
require 'tty/prompt/slider'
|
19
21
|
require 'tty/prompt/statement'
|
20
22
|
require 'tty/prompt/suggestion'
|
23
|
+
require 'tty/prompt/answers_collector'
|
21
24
|
require 'tty/prompt/symbols'
|
22
25
|
require 'tty/prompt/test'
|
23
26
|
require 'tty/prompt/utils'
|
data/lib/tty/prompt.rb
CHANGED
@@ -31,10 +31,20 @@ module TTY
|
|
31
31
|
|
32
32
|
# Prompt prefix
|
33
33
|
#
|
34
|
+
# @example
|
35
|
+
# prompt = TTY::Prompt.new(prefix: [?])
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
#
|
34
39
|
# @api private
|
35
40
|
attr_reader :prefix
|
36
41
|
|
37
|
-
|
42
|
+
# Theme colors
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
attr_reader :active_color, :help_color, :error_color
|
46
|
+
|
47
|
+
def_delegators :@pastel, :decorate, :strip
|
38
48
|
|
39
49
|
def_delegators :@cursor, :clear_lines, :clear_line,
|
40
50
|
:show, :hide
|
@@ -60,6 +70,9 @@ module TTY
|
|
60
70
|
@input = options.fetch(:input) { $stdin }
|
61
71
|
@output = options.fetch(:output) { $stdout }
|
62
72
|
@prefix = options.fetch(:prefix) { '' }
|
73
|
+
@active_color = options.fetch(:active_color) { :green }
|
74
|
+
@help_color = options.fetch(:help_color) { :bright_black }
|
75
|
+
@error_color = options.fetch(:error_color) { :red }
|
63
76
|
|
64
77
|
@cursor = TTY::Cursor
|
65
78
|
@pastel = Pastel.new
|
@@ -223,16 +236,62 @@ module TTY
|
|
223
236
|
#
|
224
237
|
# @example
|
225
238
|
# prompt = TTY::Prompt.new
|
226
|
-
# prompt.yes?('Are you human?
|
239
|
+
# prompt.yes?('Are you human?')
|
240
|
+
# # => Are you human? (Y/n)
|
227
241
|
#
|
228
242
|
# @return [Boolean]
|
229
243
|
#
|
230
244
|
# @api public
|
231
|
-
def yes?(
|
232
|
-
|
233
|
-
options.
|
234
|
-
|
235
|
-
|
245
|
+
def yes?(message, *args, &block)
|
246
|
+
defaults = { default: true }
|
247
|
+
options = Utils.extract_options!(args)
|
248
|
+
options.merge!(defaults.reject { |k, _| options.key?(k) })
|
249
|
+
|
250
|
+
question = ConfirmQuestion.new(self, options)
|
251
|
+
question.call(message, &block)
|
252
|
+
end
|
253
|
+
|
254
|
+
# A shortcut method to ask the user negative question and return
|
255
|
+
# true for 'no' reply.
|
256
|
+
#
|
257
|
+
# @example
|
258
|
+
# prompt = TTY::Prompt.new
|
259
|
+
# prompt.no?('Are you alien?') # => true
|
260
|
+
# # => Are you human? (y/N)
|
261
|
+
#
|
262
|
+
# @return [Boolean]
|
263
|
+
#
|
264
|
+
# @api public
|
265
|
+
def no?(message, *args, &block)
|
266
|
+
defaults = { default: false, type: :no }
|
267
|
+
options = Utils.extract_options!(args)
|
268
|
+
options.merge!(defaults.reject { |k, _| options.key?(k) })
|
269
|
+
|
270
|
+
question = ConfirmQuestion.new(self, options)
|
271
|
+
!question.call(message, &block)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Expand available options
|
275
|
+
#
|
276
|
+
# @example
|
277
|
+
# prompt = TTY::Prompt.new
|
278
|
+
# choices = [{
|
279
|
+
# key: 'Y',
|
280
|
+
# name: 'Overwrite',
|
281
|
+
# value: :yes
|
282
|
+
# }, {
|
283
|
+
# key: 'n',
|
284
|
+
# name: 'Skip',
|
285
|
+
# value: :no
|
286
|
+
# }]
|
287
|
+
# prompt.expand('Overwirte Gemfile?', choices)
|
288
|
+
#
|
289
|
+
# @return [Object]
|
290
|
+
# the user specified value
|
291
|
+
#
|
292
|
+
# @api public
|
293
|
+
def expand(message, *args, &block)
|
294
|
+
invoke_select(Expander, message, *args, &block)
|
236
295
|
end
|
237
296
|
|
238
297
|
# Ask a question with a range slider
|
@@ -253,20 +312,6 @@ module TTY
|
|
253
312
|
slider.call(question, &block)
|
254
313
|
end
|
255
314
|
|
256
|
-
# A shortcut method to ask the user negative question and return
|
257
|
-
# true for 'no' reply.
|
258
|
-
#
|
259
|
-
# @example
|
260
|
-
# prompt = TTY::Prompt.new
|
261
|
-
# prompt.no?('Are you alien? (y/N)') # => true
|
262
|
-
#
|
263
|
-
# @return [Boolean]
|
264
|
-
#
|
265
|
-
# @api public
|
266
|
-
def no?(question, *args, &block)
|
267
|
-
!yes?(question, *args, &block)
|
268
|
-
end
|
269
|
-
|
270
315
|
# Print statement out. If the supplied message ends with a space or
|
271
316
|
# tab character, a new line will not be appended.
|
272
317
|
#
|
@@ -361,6 +406,22 @@ module TTY
|
|
361
406
|
say(suggestion.suggest(message, possibilities))
|
362
407
|
end
|
363
408
|
|
409
|
+
# Gathers more than one aswer
|
410
|
+
#
|
411
|
+
# @example
|
412
|
+
# prompt.collect do
|
413
|
+
# key(:name).ask('Name?')
|
414
|
+
# end
|
415
|
+
#
|
416
|
+
# @return [Hash]
|
417
|
+
# the collection of answers
|
418
|
+
#
|
419
|
+
# @api public
|
420
|
+
def collect(options = {}, &block)
|
421
|
+
collector = AnswersCollector.new(self, options)
|
422
|
+
collector.call(&block)
|
423
|
+
end
|
424
|
+
|
364
425
|
# Check if outputing to terminal
|
365
426
|
#
|
366
427
|
# @return [Boolean]
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class AnswersCollector
|
6
|
+
# Initialize answer collector
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def initialize(prompt, options = {})
|
10
|
+
@prompt = prompt
|
11
|
+
@answers = options.fetch(:answers) { {} }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Start gathering answers
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
# the collection of all answers
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def call(&block)
|
21
|
+
instance_eval(&block)
|
22
|
+
@answers
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create answer entry
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# key(:name).ask('Name?')
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def key(name, &block)
|
32
|
+
@name = name
|
33
|
+
if block
|
34
|
+
answer = create_collector.(&block)
|
35
|
+
add_answer(answer)
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# @api public
|
41
|
+
def create_collector
|
42
|
+
self.class.new(@prompt)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api public
|
46
|
+
def add_answer(answer)
|
47
|
+
@answers[@name] = answer
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
answer = @prompt.public_send(method, *args, &block)
|
55
|
+
add_answer(answer)
|
56
|
+
end
|
57
|
+
end # AnswersCollector
|
58
|
+
end # Prompt
|
59
|
+
end # TTY
|
data/lib/tty/prompt/choice.rb
CHANGED
@@ -11,12 +11,15 @@ module TTY
|
|
11
11
|
# @api public
|
12
12
|
attr_reader :name
|
13
13
|
|
14
|
+
attr_reader :key
|
15
|
+
|
14
16
|
# Create a Choice instance
|
15
17
|
#
|
16
18
|
# @api public
|
17
|
-
def initialize(name, value)
|
19
|
+
def initialize(name, value, key = nil)
|
18
20
|
@name = name
|
19
21
|
@value = value
|
22
|
+
@key = key
|
20
23
|
end
|
21
24
|
|
22
25
|
# Create choice from value
|
@@ -42,7 +45,11 @@ module TTY
|
|
42
45
|
when Array
|
43
46
|
new("#{val.first}", val.last)
|
44
47
|
when Hash
|
45
|
-
|
48
|
+
if val.key?(:name)
|
49
|
+
new("#{val[:name]}", val[:value], val[:key])
|
50
|
+
else
|
51
|
+
new("#{val.keys.first}", val.values.first)
|
52
|
+
end
|
46
53
|
else
|
47
54
|
raise ArgumentError, "#{val} cannot be coerced into Choice"
|
48
55
|
end
|
data/lib/tty/prompt/choices.rb
CHANGED
@@ -85,7 +85,23 @@ module TTY
|
|
85
85
|
#
|
86
86
|
# @api public
|
87
87
|
def pluck(name)
|
88
|
-
|
88
|
+
map { |choice| choice.public_send(name) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Find a matching choice
|
92
|
+
#
|
93
|
+
# @exmaple
|
94
|
+
# choices.find_by(:name, 'small')
|
95
|
+
#
|
96
|
+
# @param [Symbol] attr
|
97
|
+
# the attribute name
|
98
|
+
# @param [Object] value
|
99
|
+
#
|
100
|
+
# @return [Choice]
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def find_by(attr, value)
|
104
|
+
find { |choice| choice.public_send(attr) == value }
|
89
105
|
end
|
90
106
|
end # Choices
|
91
107
|
end # Prompt
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class ConfirmQuestion < Question
|
6
|
+
# Create confirmation question
|
7
|
+
#
|
8
|
+
# @param [Hash] options
|
9
|
+
# @option options [String] :suffix
|
10
|
+
# @option options [String] :positive
|
11
|
+
# @option options [String] :negative
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def initialize(prompt, options = {})
|
15
|
+
super
|
16
|
+
|
17
|
+
@suffix = options.fetch(:suffix) { UndefinedSetting }
|
18
|
+
@positive = options.fetch(:positive) { UndefinedSetting }
|
19
|
+
@negative = options.fetch(:negative) { UndefinedSetting }
|
20
|
+
@type = options.fetch(:type) { :yes }
|
21
|
+
end
|
22
|
+
|
23
|
+
def positive?
|
24
|
+
@positive != UndefinedSetting
|
25
|
+
end
|
26
|
+
|
27
|
+
def negative?
|
28
|
+
@negative != UndefinedSetting
|
29
|
+
end
|
30
|
+
|
31
|
+
def suffix?
|
32
|
+
@suffix != UndefinedSetting
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set question suffix
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def suffix(value)
|
39
|
+
@suffix = value
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set value for matching positive choice
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def positive(value)
|
46
|
+
@positive = value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set value for matching negative choice
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def negative(value)
|
53
|
+
@negative = value
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(message, &block)
|
57
|
+
return if Utils.blank?(message)
|
58
|
+
@message = message
|
59
|
+
block.call(self) if block
|
60
|
+
setup_defaults
|
61
|
+
render
|
62
|
+
end
|
63
|
+
|
64
|
+
# Render confirmation question
|
65
|
+
#
|
66
|
+
# @api private
|
67
|
+
def render_question
|
68
|
+
header = "#{@prefix}#{message} "
|
69
|
+
|
70
|
+
if !@done
|
71
|
+
header += @prompt.decorate("(#{@suffix})", @help_color) + ' '
|
72
|
+
else
|
73
|
+
answer = convert_result(@input)
|
74
|
+
label = answer ? @positive : @negative
|
75
|
+
header += @prompt.decorate(label, @active_color)
|
76
|
+
end
|
77
|
+
@prompt.print(header)
|
78
|
+
@prompt.print("\n") if @done
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def is?(type)
|
85
|
+
@type == type
|
86
|
+
end
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
def setup_defaults
|
90
|
+
return if suffix? && positive?
|
91
|
+
|
92
|
+
if suffix? && !positive?
|
93
|
+
parts = @suffix.split('/')
|
94
|
+
@positive = parts[0]
|
95
|
+
@negative = parts[1]
|
96
|
+
@convert = conversion
|
97
|
+
elsif !suffix? && positive?
|
98
|
+
@suffix = create_suffix
|
99
|
+
@convert = conversion
|
100
|
+
else
|
101
|
+
create_default_labels
|
102
|
+
@convert = :bool
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_default_labels
|
107
|
+
if is?(:yes)
|
108
|
+
@suffix = default? ? 'Y/n' : 'y/N'
|
109
|
+
@positive = default? ? 'Yes' : 'yes'
|
110
|
+
@negative = default? ? 'no' : 'No'
|
111
|
+
else
|
112
|
+
@suffix = default? ? 'y/N' : 'Y/n'
|
113
|
+
@positive = default? ? 'Yes' : 'yes'
|
114
|
+
@negative = default? ? 'No' : 'no'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
def create_suffix
|
120
|
+
result = ''
|
121
|
+
if is?(:yes)
|
122
|
+
result << "#{default? ? @positive.capitalize : @positive.downcase}"
|
123
|
+
result << '/'
|
124
|
+
result << "#{default? ? @negative.downcase : @negative.capitalize}"
|
125
|
+
else
|
126
|
+
result << "#{default? ? @positive.downcase : @positive.capitalize}"
|
127
|
+
result << '/'
|
128
|
+
result << "#{default? ? @negative.capitalize : @negative.downcase}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Create custom conversion
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
def conversion
|
136
|
+
proc { |input| !input.match(/^#{@positive}|#{@positive[0]}$/i).nil? }
|
137
|
+
end
|
138
|
+
end # ConfirmQuestion
|
139
|
+
end # Prompt
|
140
|
+
end # TTY
|