tty-prompt 0.10.1 → 0.11.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +39 -9
  5. data/examples/echo.rb +5 -1
  6. data/examples/inputs.rb +10 -0
  7. data/examples/mask.rb +6 -2
  8. data/examples/multi_select.rb +1 -1
  9. data/examples/multi_select_paged.rb +9 -0
  10. data/examples/select.rb +5 -5
  11. data/examples/slider.rb +1 -1
  12. data/lib/tty-prompt.rb +2 -36
  13. data/lib/tty/prompt.rb +49 -8
  14. data/lib/tty/prompt/choices.rb +2 -0
  15. data/lib/tty/prompt/confirm_question.rb +6 -1
  16. data/lib/tty/prompt/converter_dsl.rb +9 -6
  17. data/lib/tty/prompt/converter_registry.rb +27 -19
  18. data/lib/tty/prompt/converters.rb +16 -22
  19. data/lib/tty/prompt/enum_list.rb +8 -4
  20. data/lib/tty/prompt/enum_paginator.rb +2 -0
  21. data/lib/tty/prompt/evaluator.rb +1 -1
  22. data/lib/tty/prompt/expander.rb +1 -1
  23. data/lib/tty/prompt/list.rb +21 -11
  24. data/lib/tty/prompt/mask_question.rb +15 -6
  25. data/lib/tty/prompt/multi_list.rb +12 -10
  26. data/lib/tty/prompt/question.rb +38 -36
  27. data/lib/tty/prompt/question/modifier.rb +2 -0
  28. data/lib/tty/prompt/question/validation.rb +5 -4
  29. data/lib/tty/prompt/reader.rb +104 -58
  30. data/lib/tty/prompt/reader/codes.rb +103 -63
  31. data/lib/tty/prompt/reader/console.rb +57 -0
  32. data/lib/tty/prompt/reader/key_event.rb +51 -88
  33. data/lib/tty/prompt/reader/mode.rb +5 -5
  34. data/lib/tty/prompt/reader/win_api.rb +29 -0
  35. data/lib/tty/prompt/reader/win_console.rb +49 -0
  36. data/lib/tty/prompt/slider.rb +10 -6
  37. data/lib/tty/prompt/suggestion.rb +1 -1
  38. data/lib/tty/prompt/symbols.rb +52 -10
  39. data/lib/tty/prompt/version.rb +1 -1
  40. data/lib/tty/{prompt/test.rb → test_prompt.rb} +2 -1
  41. data/spec/unit/ask_spec.rb +8 -16
  42. data/spec/unit/converters/convert_bool_spec.rb +1 -2
  43. data/spec/unit/converters/on_error_spec.rb +9 -0
  44. data/spec/unit/enum_paginator_spec.rb +16 -0
  45. data/spec/unit/enum_select_spec.rb +69 -25
  46. data/spec/unit/expand_spec.rb +14 -14
  47. data/spec/unit/mask_spec.rb +66 -29
  48. data/spec/unit/multi_select_spec.rb +120 -74
  49. data/spec/unit/new_spec.rb +5 -3
  50. data/spec/unit/paginator_spec.rb +16 -0
  51. data/spec/unit/question/default_spec.rb +2 -4
  52. data/spec/unit/question/echo_spec.rb +2 -3
  53. data/spec/unit/question/in_spec.rb +9 -14
  54. data/spec/unit/question/modifier/letter_case_spec.rb +32 -11
  55. data/spec/unit/question/modifier/whitespace_spec.rb +41 -15
  56. data/spec/unit/question/required_spec.rb +9 -13
  57. data/spec/unit/question/validate_spec.rb +7 -10
  58. data/spec/unit/reader/key_event_spec.rb +36 -50
  59. data/spec/unit/reader/publish_keypress_event_spec.rb +5 -3
  60. data/spec/unit/reader/read_keypress_spec.rb +8 -7
  61. data/spec/unit/reader/read_line_spec.rb +9 -9
  62. data/spec/unit/reader/read_multiline_spec.rb +8 -7
  63. data/spec/unit/select_spec.rb +85 -25
  64. data/spec/unit/slider_spec.rb +43 -16
  65. data/spec/unit/yes_no_spec.rb +14 -28
  66. data/tasks/console.rake +1 -0
  67. data/tty-prompt.gemspec +2 -2
  68. metadata +14 -7
@@ -2,27 +2,34 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
+ # Immutable collection of converters for type transformation
6
+ #
7
+ # @api private
5
8
  class ConverterRegistry
6
- def initialize
7
- @_registry = {}
9
+ # Create a registry of conversions
10
+ #
11
+ # @param [Hash] registry
12
+ #
13
+ # @api private
14
+ def initialize(registry = {})
15
+ @_registry = registry.dup.freeze
16
+ freeze
8
17
  end
9
18
 
10
19
  # Register converter
11
20
  #
21
+ # @param [Symbol] name
22
+ # the converter name
23
+ #
12
24
  # @api public
13
- def register(key, contents = nil, &block)
14
- if block_given?
15
- item = block
16
- else
17
- item = contents
18
- end
25
+ def register(name, contents = nil, &block)
26
+ item = block_given? ? block : contents
19
27
 
20
- if key?(key)
21
- fail ArgumentError, "Converter for #{key.inspect} already registered"
22
- else
23
- @_registry[key] = item
28
+ if key?(name)
29
+ raise ArgumentError,
30
+ "Converter for #{name.inspect} already registered"
24
31
  end
25
- self
32
+ self.class.new(@_registry.merge(name => item))
26
33
  end
27
34
 
28
35
  # Check if converter is registered
@@ -37,16 +44,17 @@ module TTY
37
44
  # Execute converter
38
45
  #
39
46
  # @api public
40
- def call(key, input)
41
- if key.respond_to?(:call)
42
- converter = key
47
+ def call(name, input)
48
+ if name.respond_to?(:call)
49
+ converter = name
43
50
  else
44
- converter = @_registry.fetch(key) do
45
- fail ArgumentError, "#{key.inspect} is not registered"
51
+ converter = @_registry.fetch(name) do
52
+ raise ArgumentError, "#{name.inspect} is not registered"
46
53
  end
47
54
  end
48
- converter.call(input)
55
+ converter[input]
49
56
  end
57
+ alias [] call
50
58
 
51
59
  def inspect
52
60
  @_registry.inspect
@@ -2,30 +2,29 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'necromancer'
5
- require 'tty/prompt/converter_dsl'
5
+
6
+ require_relative 'converter_dsl'
6
7
 
7
8
  module TTY
8
9
  class Prompt
9
10
  module Converters
10
11
  extend ConverterDSL
11
12
 
12
- def self.included(base)
13
- base.class_eval do
14
- def converter_registry
15
- Converters.converter_registry
16
- end
17
- end
18
- end
19
-
13
+ # Delegate Necromancer errors
14
+ #
15
+ # @api private
20
16
  def self.on_error
21
- yield
17
+ if block_given?
18
+ yield
19
+ else
20
+ raise ArgumentError, 'You need to provide a block argument.'
21
+ end
22
22
  rescue Necromancer::ConversionTypeError => e
23
23
  raise ConversionError, e.message
24
24
  end
25
25
 
26
26
  converter(:bool) do |input|
27
- converter = Necromancer.new
28
- on_error { converter.convert(input).to(:boolean, strict: true) }
27
+ on_error { Necromancer.convert(input).to(:boolean, strict: true) }
29
28
  end
30
29
 
31
30
  converter(:string) do |input|
@@ -37,28 +36,23 @@ module TTY
37
36
  end
38
37
 
39
38
  converter(:date) do |input|
40
- converter = Necromancer.new
41
- on_error { converter.convert(input).to(:date, strict: true) }
39
+ on_error { Necromancer.convert(input).to(:date, strict: true) }
42
40
  end
43
41
 
44
42
  converter(:datetime) do |input|
45
- converter = Necromancer.new
46
- on_error { converter.convert(input).to(:datetime, strict: true) }
43
+ on_error { Necromancer.convert(input).to(:datetime, strict: true) }
47
44
  end
48
45
 
49
46
  converter(:int) do |input|
50
- converter = Necromancer.new
51
- on_error { converter.convert(input).to(:integer, strict: true) }
47
+ on_error { Necromancer.convert(input).to(:integer, strict: true) }
52
48
  end
53
49
 
54
50
  converter(:float) do |input|
55
- converter = Necromancer.new
56
- on_error { converter.convert(input).to(:float, strict: true) }
51
+ on_error { Necromancer.convert(input).to(:float, strict: true) }
57
52
  end
58
53
 
59
54
  converter(:range) do |input|
60
- converter = Necromancer.new
61
- on_error { converter.convert(input).to(:range, strict: true) }
55
+ on_error { Necromancer.convert(input).to(:range, strict: true) }
62
56
  end
63
57
 
64
58
  converter(:regexp) do |input|
@@ -1,5 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require_relative 'choices'
4
+ require_relative 'enum_paginator'
5
+ require_relative 'paginator'
6
+
3
7
  module TTY
4
8
  class Prompt
5
9
  # A class reponsible for rendering enumerated list menu.
@@ -58,7 +62,7 @@ module TTY
58
62
  #
59
63
  # @api private
60
64
  def paginated?
61
- @choices.size >= page_size
65
+ @choices.size > page_size
62
66
  end
63
67
 
64
68
  # @param [String] text
@@ -157,7 +161,7 @@ module TTY
157
161
  #
158
162
  # @api private
159
163
  def mark_choice_as_active
160
- if (@input.to_i > 1) && !@choices[@input.to_i - 1].nil?
164
+ if (@input.to_i > 0) && !@choices[@input.to_i - 1].nil?
161
165
  @active = @input.to_i
162
166
  else
163
167
  @active = @default
@@ -308,8 +312,8 @@ module TTY
308
312
  def render_menu
309
313
  output = ''
310
314
  @paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
311
- num = (index + 1).to_s + @enum + Symbols::SPACE
312
- selected = Symbols::SPACE * 2 + num + choice.name
315
+ num = (index + 1).to_s + @enum + ' '
316
+ selected = ' ' * 2 + num + choice.name
313
317
  output << if index + 1 == @active
314
318
  @prompt.decorate(selected.to_s, @active_color)
315
319
  else
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require_relative 'paginator'
4
+
3
5
  module TTY
4
6
  class Prompt
5
7
  class EnumPaginator < Paginator
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/result'
3
+ require_relative 'result'
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/question'
3
+ require_relative 'choices'
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -1,5 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require_relative 'choices'
4
+ require_relative 'paginator'
5
+ require_relative 'symbols'
6
+
3
7
  module TTY
4
8
  class Prompt
5
9
  # A class responsible for rendering select list menu
@@ -7,6 +11,8 @@ module TTY
7
11
  #
8
12
  # @api private
9
13
  class List
14
+ include Symbols
15
+
10
16
  HELP = '(Use arrow%s keys, press Enter to select)'
11
17
 
12
18
  PAGE_HELP = '(Move up or down to reveal more choices)'
@@ -34,7 +40,7 @@ module TTY
34
40
  @choices = Choices.new
35
41
  @active_color = options.fetch(:active_color) { @prompt.active_color }
36
42
  @help_color = options.fetch(:help_color) { @prompt.help_color }
37
- @marker = options.fetch(:marker) { Symbols::ITEM_SELECTED }
43
+ @marker = options.fetch(:marker) { symbols[:pointer] }
38
44
  @help = options[:help]
39
45
  @first_render = true
40
46
  @done = false
@@ -66,13 +72,17 @@ module TTY
66
72
  @per_page = value
67
73
  end
68
74
 
75
+ def page_size
76
+ (@per_page || Paginator::DEFAULT_PAGE_SIZE)
77
+ end
78
+
69
79
  # Check if list is paginated
70
80
  #
71
81
  # @return [Boolean]
72
82
  #
73
83
  # @api private
74
84
  def paginated?
75
- @choices.size >= (@per_page || Paginator::DEFAULT_PAGE_SIZE)
85
+ @choices.size > page_size
76
86
  end
77
87
 
78
88
  # @param [String] text
@@ -134,7 +144,7 @@ module TTY
134
144
  def keynum(event)
135
145
  return unless enumerate?
136
146
  value = event.value.to_i
137
- return unless (1..@choices.count).include?(value)
147
+ return unless (1..@choices.count).cover?(value)
138
148
  @active = value
139
149
  end
140
150
 
@@ -171,11 +181,11 @@ module TTY
171
181
  def validate_defaults
172
182
  @default.each do |d|
173
183
  if d.nil? || d.to_s.empty?
174
- fail ConfigurationError,
184
+ raise ConfigurationError,
175
185
  "default index must be an integer in range (1 - #{@choices.size})"
176
186
  end
177
187
  if d < 1 || d > @choices.size
178
- fail ConfigurationError,
188
+ raise ConfigurationError,
179
189
  "default index `#{d}` out of range (1 - #{@choices.size})"
180
190
  end
181
191
  end
@@ -197,10 +207,9 @@ module TTY
197
207
  refresh(lines)
198
208
  end
199
209
  render_question
200
- answer = render_answer
210
+ render_answer
201
211
  ensure
202
212
  @prompt.print(@prompt.show)
203
- answer
204
213
  end
205
214
 
206
215
  # Find value for the choice selected
@@ -266,14 +275,15 @@ module TTY
266
275
  def render_menu
267
276
  output = ''
268
277
  @paginator.paginate(@choices, @active, @per_page) do |choice, index|
269
- num = enumerate? ? (index + 1).to_s + @enum + Symbols::SPACE : ''
278
+ num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
270
279
  message = if index + 1 == @active
271
- selected = @marker + Symbols::SPACE + num + choice.name
280
+ selected = @marker + ' ' + num + choice.name
272
281
  @prompt.decorate("#{selected}", @active_color)
273
282
  else
274
- Symbols::SPACE * 2 + num + choice.name
283
+ ' ' * 2 + num + choice.name
275
284
  end
276
- newline = (index == @paginator.max_index) ? '' : "\n"
285
+ max_index = paginated? ? @paginator.max_index : @choices.size - 1
286
+ newline = (index == max_index) ? '' : "\n"
277
287
  output << (message + newline)
278
288
  end
279
289
  output
@@ -1,5 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require_relative 'question'
4
+ require_relative 'symbols'
5
+
3
6
  module TTY
4
7
  class Prompt
5
8
  class MaskQuestion < Question
@@ -11,7 +14,7 @@ module TTY
11
14
  # @api public
12
15
  def initialize(prompt, options = {})
13
16
  super
14
- @mask = options.fetch(:mask) { Symbols::ITEM_SECURE }
17
+ @mask = options.fetch(:mask) { Symbols.symbols[:dot] }
15
18
  @done_masked = false
16
19
  @failure = false
17
20
  @prompt.subscribe(self)
@@ -54,15 +57,19 @@ module TTY
54
57
  masked = "#{@mask * "#{@input}".length}"
55
58
  if @done_masked && !@failure
56
59
  masked = @prompt.decorate(masked, @active_color)
60
+ elsif @done_masked && @failure
61
+ masked = @prompt.decorate(masked, @error_color)
57
62
  end
58
63
  header += masked
59
64
  end
60
65
  @prompt.print(header)
61
- @prompt.print("\n") if @done
66
+ @prompt.puts if @done
67
+
68
+ header.lines.count + (@done ? 1 : 0)
62
69
  end
63
70
 
64
- def render_error_or_finish(result)
65
- @failure = result.failure?
71
+ def render_error(errors)
72
+ @failure = !errors.empty?
66
73
  super
67
74
  end
68
75
 
@@ -71,13 +78,15 @@ module TTY
71
78
  # @private
72
79
  def read_input
73
80
  @done_masked = false
81
+ @failure = false
74
82
  @input = ''
83
+
75
84
  until @done_masked
76
- @prompt.read_keypress(echo?)
85
+ @prompt.read_keypress
77
86
  @prompt.print(@prompt.clear_line)
78
87
  render_question
79
88
  end
80
- @prompt.print("\n")
89
+ @prompt.puts
81
90
  @input
82
91
  end
83
92
  end # MaskQuestion
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/list'
3
+ require_relative 'list'
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -22,6 +22,7 @@ module TTY
22
22
  @selected = []
23
23
  @help = options[:help]
24
24
  @default = Array(options[:default])
25
+ @echo = options.fetch(:echo, true)
25
26
  end
26
27
 
27
28
  # Callback fired when space key is pressed
@@ -61,9 +62,9 @@ module TTY
61
62
  # @api private
62
63
  def render_header
63
64
  instructions = @prompt.decorate(help, :bright_black)
64
- if @done
65
+ if @done && @echo
65
66
  @prompt.decorate(selected_names, @active_color)
66
- elsif @selected.size.nonzero?
67
+ elsif @selected.size.nonzero? && @echo
67
68
  selected_names + (@first_render ? " #{instructions}" : '')
68
69
  elsif @first_render
69
70
  instructions
@@ -85,16 +86,17 @@ module TTY
85
86
  def render_menu
86
87
  output = ''
87
88
  @paginator.paginate(@choices, @active, @per_page) do |choice, index|
88
- num = enumerate? ? (index + 1).to_s + @enum + Symbols::SPACE : ''
89
- indicator = (index + 1 == @active) ? @marker : Symbols::SPACE
90
- indicator += Symbols::SPACE
89
+ num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
90
+ indicator = (index + 1 == @active) ? @marker : ' '
91
+ indicator += ' '
91
92
  message = if @selected.include?(choice)
92
- selected = @prompt.decorate(Symbols::RADIO_CHECKED, @active_color)
93
- selected + Symbols::SPACE + num + choice.name
93
+ selected = @prompt.decorate(symbols[:radio_on], @active_color)
94
+ selected + ' ' + num + choice.name
94
95
  else
95
- Symbols::RADIO_UNCHECKED + Symbols::SPACE + num + choice.name
96
+ symbols[:radio_off] + ' ' + num + choice.name
96
97
  end
97
- newline = (index == @paginator.max_index) ? '' : "\n"
98
+ max_index = paginated? ? @paginator.max_index : @choices.size - 1
99
+ newline = (index == max_index) ? '' : "\n"
98
100
  output << indicator + message + newline
99
101
  end
100
102
  output
@@ -1,10 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/question/modifier'
4
- require 'tty/prompt/question/validation'
5
- require 'tty/prompt/question/checks'
6
- require 'tty/prompt/converter_dsl'
7
- require 'tty/prompt/converters'
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'
8
9
 
9
10
  module TTY
10
11
  # A class responsible for shell prompt interactions.
@@ -14,7 +15,6 @@ module TTY
14
15
  # @api public
15
16
  class Question
16
17
  include Checks
17
- include Converters
18
18
 
19
19
  UndefinedSetting = Module.new
20
20
 
@@ -42,6 +42,7 @@ module TTY
42
42
  @convert = options.fetch(:convert) { UndefinedSetting }
43
43
  @active_color = options.fetch(:active_color) { @prompt.active_color }
44
44
  @help_color = options.fetch(:help_color) { @prompt.help_color }
45
+ @error_color = options.fetch(:error_color) { :red }
45
46
  @messages = Utils.deep_copy(options.fetch(:messages) { { } })
46
47
  @done = false
47
48
  @input = nil
@@ -100,12 +101,17 @@ module TTY
100
101
  #
101
102
  # @api private
102
103
  def render
104
+ @errors = []
103
105
  until @done
104
- render_question
106
+ lines = render_question
105
107
  result = process_input
106
- errors = result.errors
107
- render_error_or_finish(result)
108
- refresh(errors.count)
108
+ if result.failure?
109
+ @errors = result.errors
110
+ render_error(result.errors)
111
+ else
112
+ @done = true
113
+ end
114
+ refresh(lines)
109
115
  end
110
116
  render_question
111
117
  convert_result(result.value)
@@ -124,7 +130,9 @@ module TTY
124
130
  header += @prompt.decorate("(#{default})", @help_color) + ' '
125
131
  end
126
132
  @prompt.print(header)
127
- @prompt.print("\n") if @done
133
+ @prompt.puts if @done
134
+
135
+ header.lines.count + (@done ? 1 : 0)
128
136
  end
129
137
 
130
138
  # Decide how to handle input from user
@@ -146,43 +154,37 @@ module TTY
146
154
  when :keypress
147
155
  @prompt.read_keypress
148
156
  when :multiline
149
- @prompt.read_multiline
157
+ @prompt.read_multiline.each(&:chomp!)
150
158
  else
151
- @prompt.read_line(echo)
159
+ @prompt.read_line(echo: echo).chomp
152
160
  end
153
161
  end
154
162
 
155
163
  # Handle error condition
156
164
  #
157
165
  # @api private
158
- def render_error_or_finish(result)
159
- if result.failure?
160
- result.errors.each do |err|
161
- @prompt.print(@prompt.clear_line)
162
- @prompt.print(@prompt.decorate('>>', :red) + ' ' + err)
163
- end
164
- @prompt.print(@prompt.cursor.up(result.errors.count))
165
- else
166
- @done = true
167
- if result.errors.count.nonzero?
168
- @prompt.print(@prompt.cursor.down(result.errors.count))
169
- end
166
+ def render_error(errors)
167
+ errors.each do |err|
168
+ newline = (@echo ? '' : "\n")
169
+ @prompt.print(newline + @prompt.decorate('>>', :red) + ' ' + err)
170
170
  end
171
171
  end
172
172
 
173
173
  # Determine area of the screen to clear
174
174
  #
175
- # @param [Integer] errors
175
+ # @param [Array[String]] errors
176
176
  #
177
177
  # @api private
178
- def refresh(errors = nil)
179
- lines = @message.scan("\n").length
180
- lines += ((!echo? || errors.nonzero?) ? 1 : 2) # clear user enter
181
-
182
- if errors.nonzero? && @done
183
- lines += errors
178
+ def refresh(lines)
179
+ if @done
180
+ if @errors.count.zero? && @echo
181
+ @prompt.print(@prompt.cursor.up(lines))
182
+ else
183
+ lines += @errors.count
184
+ end
185
+ else
186
+ @prompt.print(@prompt.cursor.up(lines))
184
187
  end
185
-
186
188
  @prompt.print(@prompt.clear_lines(lines))
187
189
  end
188
190
 
@@ -193,7 +195,7 @@ module TTY
193
195
  # @api private
194
196
  def convert_result(value)
195
197
  if convert? & !Utils.blank?(value)
196
- converter_registry.(@convert, value)
198
+ Converters.convert(@convert, value)
197
199
  else
198
200
  value
199
201
  end
@@ -303,10 +305,10 @@ module TTY
303
305
  def in(value = (not_set = true), message = nil)
304
306
  messages[:range?] = message if message
305
307
  if in? && !@in.is_a?(Range)
306
- @in = converter_registry.(:range, @in)
308
+ @in = Converters.convert(:range, @in)
307
309
  end
308
310
  return @in if not_set
309
- @in = converter_registry.(:range, value)
311
+ @in = Converters.convert(:range, value)
310
312
  end
311
313
 
312
314
  # Check if range is set