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.
@@ -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
@@ -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
@@ -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)
@@ -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'
@@ -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
- def_delegators :@pastel, :decorate
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? (Y/n)') # => true
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?(question, *args, &block)
232
- options = Utils.extract_options!(args)
233
- options.merge!(convert: :bool)
234
- args << options
235
- ask(question, *args, &block)
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
@@ -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
- new("#{val.keys.first}", val.values.first)
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
@@ -85,7 +85,23 @@ module TTY
85
85
  #
86
86
  # @api public
87
87
  def pluck(name)
88
- find { |choice| choice.name == name }
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