tty-prompt 0.19.0 → 0.23.1

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +485 -233
  5. data/lib/tty-prompt.rb +1 -2
  6. data/lib/tty/prompt.rb +159 -147
  7. data/lib/tty/prompt/answers_collector.rb +5 -5
  8. data/lib/tty/prompt/block_paginator.rb +1 -1
  9. data/lib/tty/prompt/choice.rb +31 -13
  10. data/lib/tty/prompt/choices.rb +30 -12
  11. data/lib/tty/prompt/confirm_question.rb +42 -16
  12. data/lib/tty/prompt/const.rb +17 -0
  13. data/lib/tty/prompt/converter_dsl.rb +6 -7
  14. data/lib/tty/prompt/converter_registry.rb +31 -26
  15. data/lib/tty/prompt/converters.rb +139 -32
  16. data/lib/tty/prompt/enum_list.rb +58 -25
  17. data/lib/tty/prompt/errors.rb +31 -0
  18. data/lib/tty/prompt/evaluator.rb +2 -2
  19. data/lib/tty/prompt/expander.rb +27 -15
  20. data/lib/tty/prompt/keypress.rb +5 -3
  21. data/lib/tty/prompt/list.rb +101 -38
  22. data/lib/tty/prompt/mask_question.rb +9 -5
  23. data/lib/tty/prompt/multi_list.rb +108 -29
  24. data/lib/tty/prompt/multiline.rb +9 -7
  25. data/lib/tty/prompt/paginator.rb +1 -1
  26. data/lib/tty/prompt/question.rb +67 -36
  27. data/lib/tty/prompt/question/checks.rb +20 -2
  28. data/lib/tty/prompt/question/modifier.rb +4 -2
  29. data/lib/tty/prompt/question/validation.rb +3 -3
  30. data/lib/tty/prompt/selected_choices.rb +77 -0
  31. data/lib/tty/prompt/slider.rb +110 -23
  32. data/lib/tty/prompt/statement.rb +3 -3
  33. data/lib/tty/prompt/suggestion.rb +7 -6
  34. data/lib/tty/prompt/symbols.rb +58 -58
  35. data/lib/tty/prompt/test.rb +36 -0
  36. data/lib/tty/prompt/utils.rb +1 -3
  37. data/lib/tty/prompt/version.rb +1 -1
  38. metadata +27 -196
  39. data/Rakefile +0 -8
  40. data/examples/ask.rb +0 -7
  41. data/examples/ask_blank.rb +0 -9
  42. data/examples/ask_valid.rb +0 -12
  43. data/examples/collect.rb +0 -21
  44. data/examples/echo.rb +0 -11
  45. data/examples/enum_select.rb +0 -7
  46. data/examples/enum_select_disabled.rb +0 -16
  47. data/examples/enum_select_paged.rb +0 -9
  48. data/examples/enum_select_wrapped.rb +0 -15
  49. data/examples/expand.rb +0 -29
  50. data/examples/expand_auto.rb +0 -29
  51. data/examples/in.rb +0 -9
  52. data/examples/inputs.rb +0 -10
  53. data/examples/key_events.rb +0 -15
  54. data/examples/keypress.rb +0 -9
  55. data/examples/mask.rb +0 -13
  56. data/examples/multi_select.rb +0 -8
  57. data/examples/multi_select_disabled.rb +0 -17
  58. data/examples/multi_select_disabled_paged.rb +0 -22
  59. data/examples/multi_select_paged.rb +0 -9
  60. data/examples/multi_select_wrapped.rb +0 -15
  61. data/examples/multiline.rb +0 -9
  62. data/examples/pause.rb +0 -9
  63. data/examples/select.rb +0 -24
  64. data/examples/select_disabled.rb +0 -18
  65. data/examples/select_disabled_paged.rb +0 -22
  66. data/examples/select_enum.rb +0 -8
  67. data/examples/select_filtered.rb +0 -11
  68. data/examples/select_paginated.rb +0 -11
  69. data/examples/select_wrapped.rb +0 -15
  70. data/examples/slider.rb +0 -6
  71. data/examples/validation.rb +0 -9
  72. data/examples/yes_no.rb +0 -7
  73. data/lib/tty/prompt/messages.rb +0 -49
  74. data/lib/tty/test_prompt.rb +0 -20
  75. data/spec/spec_helper.rb +0 -61
  76. data/spec/unit/ask_spec.rb +0 -173
  77. data/spec/unit/block_paginator_spec.rb +0 -84
  78. data/spec/unit/choice/eql_spec.rb +0 -22
  79. data/spec/unit/choice/from_spec.rb +0 -112
  80. data/spec/unit/choices/add_spec.rb +0 -12
  81. data/spec/unit/choices/each_spec.rb +0 -13
  82. data/spec/unit/choices/find_by_spec.rb +0 -10
  83. data/spec/unit/choices/new_spec.rb +0 -10
  84. data/spec/unit/choices/pluck_spec.rb +0 -9
  85. data/spec/unit/collect_spec.rb +0 -96
  86. data/spec/unit/converters/convert_bool_spec.rb +0 -58
  87. data/spec/unit/converters/convert_char_spec.rb +0 -11
  88. data/spec/unit/converters/convert_custom_spec.rb +0 -14
  89. data/spec/unit/converters/convert_date_spec.rb +0 -34
  90. data/spec/unit/converters/convert_file_spec.rb +0 -18
  91. data/spec/unit/converters/convert_number_spec.rb +0 -39
  92. data/spec/unit/converters/convert_path_spec.rb +0 -15
  93. data/spec/unit/converters/convert_range_spec.rb +0 -22
  94. data/spec/unit/converters/convert_regex_spec.rb +0 -12
  95. data/spec/unit/converters/convert_string_spec.rb +0 -21
  96. data/spec/unit/converters/on_error_spec.rb +0 -9
  97. data/spec/unit/distance/distance_spec.rb +0 -73
  98. data/spec/unit/enum_select_spec.rb +0 -518
  99. data/spec/unit/error_spec.rb +0 -20
  100. data/spec/unit/evaluator_spec.rb +0 -67
  101. data/spec/unit/expand_spec.rb +0 -290
  102. data/spec/unit/keypress_spec.rb +0 -66
  103. data/spec/unit/mask_spec.rb +0 -140
  104. data/spec/unit/multi_select_spec.rb +0 -741
  105. data/spec/unit/multiline_spec.rb +0 -77
  106. data/spec/unit/new_spec.rb +0 -20
  107. data/spec/unit/ok_spec.rb +0 -10
  108. data/spec/unit/paginator_spec.rb +0 -92
  109. data/spec/unit/question/checks_spec.rb +0 -97
  110. data/spec/unit/question/default_spec.rb +0 -31
  111. data/spec/unit/question/echo_spec.rb +0 -38
  112. data/spec/unit/question/in_spec.rb +0 -115
  113. data/spec/unit/question/initialize_spec.rb +0 -12
  114. data/spec/unit/question/modifier/apply_to_spec.rb +0 -24
  115. data/spec/unit/question/modifier/letter_case_spec.rb +0 -41
  116. data/spec/unit/question/modifier/whitespace_spec.rb +0 -51
  117. data/spec/unit/question/modify_spec.rb +0 -41
  118. data/spec/unit/question/required_spec.rb +0 -92
  119. data/spec/unit/question/validate_spec.rb +0 -115
  120. data/spec/unit/question/validation/call_spec.rb +0 -31
  121. data/spec/unit/question/validation/coerce_spec.rb +0 -30
  122. data/spec/unit/result_spec.rb +0 -40
  123. data/spec/unit/say_spec.rb +0 -67
  124. data/spec/unit/select_spec.rb +0 -942
  125. data/spec/unit/slider_spec.rb +0 -142
  126. data/spec/unit/statement/initialize_spec.rb +0 -15
  127. data/spec/unit/subscribe_spec.rb +0 -22
  128. data/spec/unit/suggest_spec.rb +0 -28
  129. data/spec/unit/timer_spec.rb +0 -29
  130. data/spec/unit/warn_spec.rb +0 -21
  131. data/spec/unit/yes_no_spec.rb +0 -251
  132. data/tasks/console.rake +0 -11
  133. data/tasks/coverage.rake +0 -11
  134. data/tasks/spec.rake +0 -29
  135. data/tty-prompt.gemspec +0 -31
@@ -6,7 +6,7 @@ module TTY
6
6
  # Initialize answer collector
7
7
  #
8
8
  # @api public
9
- def initialize(prompt, options = {})
9
+ def initialize(prompt, **options)
10
10
  @prompt = prompt
11
11
  @answers = options.fetch(:answers) { {} }
12
12
  end
@@ -25,7 +25,7 @@ module TTY
25
25
  # Create answer entry
26
26
  #
27
27
  # @example
28
- # key(:name).ask('Name?')
28
+ # key(:name).ask("Name?")
29
29
  #
30
30
  # @api public
31
31
  def key(name, &block)
@@ -40,7 +40,7 @@ module TTY
40
40
  # Change to collect all values for a key
41
41
  #
42
42
  # @example
43
- # key(:colors).values.ask('Color?')
43
+ # key(:colors).values.ask("Color?")
44
44
  #
45
45
  # @api public
46
46
  def values(&block)
@@ -69,8 +69,8 @@ module TTY
69
69
  private
70
70
 
71
71
  # @api private
72
- def method_missing(method, *args, &block)
73
- answer = @prompt.public_send(method, *args, &block)
72
+ def method_missing(method, *args, **options, &block)
73
+ answer = @prompt.public_send(method, *args, **options, &block)
74
74
  add_answer(answer)
75
75
  end
76
76
  end # AnswersCollector
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'paginator'
3
+ require_relative "paginator"
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -15,7 +15,7 @@ module TTY
15
15
  # Choice.from([:foo, 1])
16
16
  # # => <TTY::Prompt::Choice @key=nil @name="foo" @value=1 @disabled=false>
17
17
  #
18
- # Choice.from({name: :foo, value: 1, key: 'f'}
18
+ # Choice.from({name: :foo, value: 1, key: "f"}
19
19
  # # => <TTY::Prompt::Choice @key="f" @name="foo" @value=1 @disabled=false>
20
20
  #
21
21
  # @param [Object] val
@@ -31,12 +31,7 @@ module TTY
31
31
  when Choice
32
32
  val
33
33
  when Array
34
- name, value, options = *val
35
- if name.is_a?(Hash)
36
- convert_hash(name)
37
- else
38
- new(name.to_s, value || name.to_s, options || {})
39
- end
34
+ convert_array(val)
40
35
  when Hash
41
36
  convert_hash(val)
42
37
  else
@@ -44,14 +39,36 @@ module TTY
44
39
  end
45
40
  end
46
41
 
47
- # Convert hash into choice
42
+ # Convert an array into choice
43
+ #
44
+ # @param [Array<Object>]
45
+ #
46
+ # @return [Choice]
47
+ #
48
+ # @api public
49
+ def self.convert_array(val)
50
+ name, value, options = *val
51
+ if name.is_a?(Hash)
52
+ convert_hash(name)
53
+ elsif val.size == 1
54
+ new(name.to_s, name.to_s)
55
+ else
56
+ new(name.to_s, value, **(options || {}))
57
+ end
58
+ end
59
+
60
+ # Convert a hash into choice
61
+ #
62
+ # @param [Hash<Symbol,Object>]
63
+ #
64
+ # @return [Choice]
48
65
  #
49
66
  # @api public
50
67
  def self.convert_hash(val)
51
68
  if val.key?(:name) && val.key?(:value)
52
- new(val[:name].to_s, val[:value], val)
69
+ new(val[:name].to_s, val[:value], **val)
53
70
  elsif val.key?(:name)
54
- new(val[:name].to_s, val[:name].to_s, val)
71
+ new(val[:name].to_s, val[:name].to_s, **val)
55
72
  else
56
73
  new(val.keys.first.to_s, val.values.first)
57
74
  end
@@ -111,9 +128,10 @@ module TTY
111
128
  # @api public
112
129
  def ==(other)
113
130
  return false unless other.is_a?(self.class)
131
+
114
132
  name == other.name &&
115
- value == other.value &&
116
- key == other.key
133
+ value == other.value &&
134
+ key == other.key
117
135
  end
118
136
 
119
137
  # Object string representation
@@ -122,7 +140,7 @@ module TTY
122
140
  #
123
141
  # @api public
124
142
  def to_s
125
- "#{name}"
143
+ name.to_s
126
144
  end
127
145
  end # Choice
128
146
  end # Prompt
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
4
 
5
- require_relative 'choice'
5
+ require_relative "choice"
6
6
 
7
7
  module TTY
8
8
  class Prompt
@@ -13,15 +13,8 @@ module TTY
13
13
  include Enumerable
14
14
  extend Forwardable
15
15
 
16
- # The actual collection choices
17
- #
18
- # @return [Array[Choice]]
19
- #
20
- # @api public
21
- attr_reader :choices
22
-
23
16
  def_delegators :choices, :length, :size, :to_ary, :empty?,
24
- :values_at, :index
17
+ :values_at, :index, :==
25
18
 
26
19
  # Convenience for creating choices
27
20
  #
@@ -48,6 +41,20 @@ module TTY
48
41
  end
49
42
  end
50
43
 
44
+ # Scope of choices which are not disabled
45
+ #
46
+ # @api public
47
+ def enabled
48
+ reject(&:disabled?)
49
+ end
50
+
51
+ def enabled_indexes
52
+ each_with_index.reduce([]) do |acc, (choice, idx)|
53
+ acc << idx unless choice.disabled?
54
+ acc
55
+ end
56
+ end
57
+
51
58
  # Iterate over all choices in the collection
52
59
  #
53
60
  # @yield [Choice]
@@ -55,6 +62,7 @@ module TTY
55
62
  # @api public
56
63
  def each(&block)
57
64
  return to_enum unless block_given?
65
+
58
66
  choices.each(&block)
59
67
  end
60
68
 
@@ -93,8 +101,8 @@ module TTY
93
101
 
94
102
  # Find a matching choice
95
103
  #
96
- # @exmaple
97
- # choices.find_by(:name, 'small')
104
+ # @example
105
+ # choices.find_by(:name, "small")
98
106
  #
99
107
  # @param [Symbol] attr
100
108
  # the attribute name
@@ -106,6 +114,16 @@ module TTY
106
114
  def find_by(attr, value)
107
115
  find { |choice| choice.public_send(attr) == value }
108
116
  end
117
+
118
+ protected
119
+
120
+ # The actual collection choices
121
+ #
122
+ # @return [Array[Choice]]
123
+ #
124
+ # @api private
125
+
126
+ attr_reader :choices
109
127
  end # Choices
110
128
  end # Prompt
111
129
  end # TTY
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'question'
4
- require_relative 'utils'
3
+ require_relative "question"
4
+ require_relative "utils"
5
5
 
6
6
  module TTY
7
7
  class Prompt
@@ -14,7 +14,7 @@ module TTY
14
14
  # @option options [String] :negative
15
15
  #
16
16
  # @api public
17
- def initialize(prompt, options = {})
17
+ def initialize(prompt, **options)
18
18
  super
19
19
  @suffix = options.fetch(:suffix) { UndefinedSetting }
20
20
  @positive = options.fetch(:positive) { UndefinedSetting }
@@ -38,6 +38,7 @@ module TTY
38
38
  # @api public
39
39
  def suffix(value = (not_set = true))
40
40
  return @negative if not_set
41
+
41
42
  @suffix = value
42
43
  end
43
44
 
@@ -46,6 +47,7 @@ module TTY
46
47
  # @api public
47
48
  def positive(value = (not_set = true))
48
49
  return @positive if not_set
50
+
49
51
  @positive = value
50
52
  end
51
53
 
@@ -54,11 +56,13 @@ module TTY
54
56
  # @api public
55
57
  def negative(value = (not_set = true))
56
58
  return @negative if not_set
59
+
57
60
  @negative = value
58
61
  end
59
62
 
60
63
  def call(message, &block)
61
64
  return if Utils.blank?(message)
65
+
62
66
  @message = message
63
67
  block.call(self) if block
64
68
  setup_defaults
@@ -73,9 +77,9 @@ module TTY
73
77
  def render_question
74
78
  header = "#{@prefix}#{message} "
75
79
  if !@done
76
- header += @prompt.decorate("(#{@suffix})", @help_color) + ' '
80
+ header += @prompt.decorate("(#{@suffix})", @help_color) + " "
77
81
  else
78
- answer = convert_result(@input)
82
+ answer = conversion.call(@input)
79
83
  label = answer ? @positive : @negative
80
84
  header += @prompt.decorate(label, @active_color)
81
85
  end
@@ -85,34 +89,56 @@ module TTY
85
89
 
86
90
  protected
87
91
 
92
+ # Decide how to handle input from user
93
+ #
94
+ # @api private
95
+ def process_input(question)
96
+ @input = read_input(question)
97
+ if Utils.blank?(@input)
98
+ @input = default ? positive : negative
99
+ end
100
+ @evaluator.call(@input)
101
+ end
102
+
88
103
  # @api private
89
104
  def setup_defaults
105
+ infer_default
106
+ @convert = conversion
90
107
  return if suffix? && positive?
91
108
 
92
109
  if suffix? && (!positive? || !negative?)
93
- parts = @suffix.split('/')
110
+ parts = @suffix.split("/")
94
111
  @positive = parts[0]
95
112
  @negative = parts[1]
96
- @convert = conversion
97
113
  elsif !suffix? && positive?
98
114
  @suffix = create_suffix
99
- @convert = conversion
100
115
  else
101
116
  create_default_labels
102
- @convert = :bool
117
+ end
118
+ end
119
+
120
+ # @api private
121
+ def infer_default
122
+ converted = Converters.convert(:bool, default.to_s)
123
+ if converted == Const::Undefined
124
+ raise InvalidArgument, "default needs to be `true` or `false`"
125
+ else
126
+ default(converted)
103
127
  end
104
128
  end
105
129
 
106
130
  # @api private
107
131
  def create_default_labels
108
- @suffix = default ? 'Y/n' : 'y/N'
109
- @positive = default ? 'Yes' : 'yes'
110
- @negative = default ? 'no' : 'No'
132
+ @suffix = default ? "Y/n" : "y/N"
133
+ @positive = default ? "Yes" : "yes"
134
+ @negative = default ? "no" : "No"
135
+ @validation = /^(y(es)?|no?)$/i
136
+ @messages[:valid?] = "Invalid input."
111
137
  end
112
138
 
113
139
  # @api private
114
140
  def create_suffix
115
- (default ? positive.capitalize : positive.downcase) + '/' +
141
+ (default ? positive.capitalize : positive.downcase) + "/" +
116
142
  (default ? negative.downcase : negative.capitalize)
117
143
  end
118
144
 
@@ -120,12 +146,12 @@ module TTY
120
146
  #
121
147
  # @api private
122
148
  def conversion
123
- proc { |input|
149
+ ->(input) do
124
150
  positive_word = Regexp.escape(positive)
125
151
  positive_letter = Regexp.escape(positive[0])
126
- pattern = Regexp.new("^#{positive_word}|#{positive_letter}$", true)
152
+ pattern = Regexp.new("^(#{positive_word}|#{positive_letter})$", true)
127
153
  !input.match(pattern).nil?
128
- }
154
+ end
129
155
  end
130
156
  end # ConfirmQuestion
131
157
  end # Prompt
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Prompt
5
+ module Const
6
+ Undefined = Object.new.tap do |obj|
7
+ def obj.to_s
8
+ "undefined"
9
+ end
10
+
11
+ def obj.inspect
12
+ "undefined".inspect
13
+ end
14
+ end
15
+ end # Const
16
+ end # Prompt
17
+ end # TTY
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'converter_registry'
3
+ require_relative "converter_registry"
4
4
 
5
5
  module TTY
6
6
  class Prompt
7
7
  module ConverterDSL
8
8
  def converter_registry
9
- @converter_registry ||= ConverterRegistry.new
9
+ @__converter_registry ||= ConverterRegistry.new
10
10
  end
11
11
 
12
- def converter(name, &block)
13
- @converter_registry = converter_registry.register(name, &block)
14
- self
12
+ def converter(*names, &block)
13
+ converter_registry.register(*names, &block)
15
14
  end
16
15
 
17
- def convert(name, data)
18
- @converter_registry[name, data]
16
+ def convert(name, input)
17
+ converter_registry[name].call(input)
19
18
  end
20
19
  end # ConverterDSL
21
20
  end # Prompt
@@ -1,60 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module TTY
4
6
  class Prompt
5
7
  # Immutable collection of converters for type transformation
6
8
  #
7
9
  # @api private
8
10
  class ConverterRegistry
11
+ extend Forwardable
12
+
13
+ def_delegators "@__registry", :keys
14
+
9
15
  # Create a registry of conversions
10
16
  #
11
17
  # @param [Hash] registry
12
18
  #
13
19
  # @api private
14
20
  def initialize(registry = {})
15
- @_registry = registry.dup.freeze
16
- freeze
21
+ @__registry = registry.dup
17
22
  end
18
23
 
19
- # Register converter
24
+ # Check if conversion is available
20
25
  #
21
- # @param [Symbol] name
22
- # the converter name
26
+ # @param [String] name
27
+ #
28
+ # @return [Boolean]
23
29
  #
24
30
  # @api public
25
- def register(name, contents = nil, &block)
26
- item = block_given? ? block : contents
27
-
28
- if key?(name)
29
- raise ArgumentError,
30
- "Converter for #{name.inspect} already registered"
31
- end
32
- self.class.new(@_registry.merge(name => item))
31
+ def contain?(name)
32
+ conv_name = name.to_s.downcase.to_sym
33
+ @__registry.key?(conv_name)
33
34
  end
34
35
 
35
- # Check if converter is registered
36
+ # Register a conversion
36
37
  #
37
- # @return [Boolean]
38
+ # @param [Symbol] name
39
+ # the converter name
38
40
  #
39
41
  # @api public
40
- def key?(key)
41
- @_registry.key?(key)
42
+ def register(*names, &block)
43
+ names.each do |name|
44
+ if contain?(name)
45
+ raise ConversionAlreadyDefined,
46
+ "converter for #{name.inspect} is already registered"
47
+ end
48
+ @__registry[name] = block
49
+ end
42
50
  end
43
51
 
44
52
  # Execute converter
45
53
  #
46
54
  # @api public
47
- def call(name, input)
48
- if name.respond_to?(:call)
49
- converter = name
50
- else
51
- converter = @_registry.fetch(name) do
52
- raise ArgumentError, "#{name.inspect} is not registered"
53
- end
55
+ def [](name)
56
+ conv_name = name.to_s.downcase.to_sym
57
+ @__registry.fetch(conv_name) do
58
+ raise UnsupportedConversion,
59
+ "converter #{conv_name.inspect} is not registered"
54
60
  end
55
- converter[input]
56
61
  end
57
- alias [] call
62
+ alias fetch []
58
63
 
59
64
  def inspect
60
65
  @_registry.inspect