tty-prompt 0.4.0 → 0.5.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 +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
|