tty-prompt 0.21.0 → 0.22.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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'question'
3
+ require_relative "question"
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -75,7 +75,7 @@ module TTY
75
75
  def read_input(question)
76
76
  @done_masked = false
77
77
  @failure = false
78
- @input = ''
78
+ @input = ""
79
79
  @prompt.print(question)
80
80
  until @done_masked
81
81
  @prompt.read_keypress
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'list'
3
+ require_relative "list"
4
+ require_relative "selected_choices"
4
5
 
5
6
  module TTY
6
7
  class Prompt
@@ -9,8 +10,6 @@ module TTY
9
10
  #
10
11
  # @api private
11
12
  class MultiList < List
12
- HELP = '(Use %s arrow%s keys, press Space to select and Enter to finish%s)'
13
-
14
13
  # Create instance of TTY::Prompt::MultiList menu.
15
14
  #
16
15
  # @param [Prompt] :prompt
@@ -19,7 +18,7 @@ module TTY
19
18
  # @api public
20
19
  def initialize(prompt, **options)
21
20
  super
22
- @selected = []
21
+ @selected = SelectedChoices.new
23
22
  @help = options[:help]
24
23
  @echo = options.fetch(:echo, true)
25
24
  @min = options[:min]
@@ -44,11 +43,11 @@ module TTY
44
43
  #
45
44
  # @api private
46
45
  def keyenter(*)
47
- if @min
48
- super if @selected.size >= @min
49
- else
50
- super
51
- end
46
+ valid = true
47
+ valid = @min <= @selected.size if @min
48
+ valid = @selected.size <= @max if @max
49
+
50
+ super if valid
52
51
  end
53
52
  alias keyreturn keyenter
54
53
 
@@ -58,13 +57,33 @@ module TTY
58
57
  def keyspace(*)
59
58
  active_choice = choices[@active - 1]
60
59
  if @selected.include?(active_choice)
61
- @selected.delete(active_choice)
60
+ @selected.delete_at(@active - 1)
62
61
  else
63
62
  return if @max && @selected.size >= @max
64
- @selected << active_choice
63
+ @selected.insert(@active - 1, active_choice)
65
64
  end
66
65
  end
67
66
 
67
+ # Selects all choices when Ctrl+A is pressed
68
+ #
69
+ # @api private
70
+ def keyctrl_a(*)
71
+ return if @max && @max < choices.size
72
+ @selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes)
73
+ end
74
+
75
+ # Revert currently selected choices when Ctrl+I is pressed
76
+ #
77
+ # @api private
78
+ def keyctrl_r(*)
79
+ return if @max && @max < choices.size
80
+ indexes = choices.each_with_index.reduce([]) do |acc, (choice, idx)|
81
+ acc << idx if !choice.disabled? && !@selected.include?(choice)
82
+ acc
83
+ end
84
+ @selected = SelectedChoices.new(choices.enabled - @selected.to_a, indexes)
85
+ end
86
+
68
87
  private
69
88
 
70
89
  # Setup default options and active selection
@@ -73,7 +92,9 @@ module TTY
73
92
  def setup_defaults
74
93
  validate_defaults
75
94
  # At this stage, @choices matches all the visible choices.
76
- @selected = @choices.values_at(*@default.map { |d| d - 1 })
95
+ default_indexes = @default.map { |d| d - 1 }
96
+ @selected = SelectedChoices.new(@choices.values_at(*default_indexes),
97
+ default_indexes)
77
98
 
78
99
  if !@default.empty?
79
100
  @active = @default.last
@@ -88,7 +109,7 @@ module TTY
88
109
  #
89
110
  # @api private
90
111
  def selected_names
91
- @selected.map(&:name).join(', ')
112
+ @selected.map(&:name).join(", ")
92
113
  end
93
114
 
94
115
  # Header part showing the minimum/maximum number of choices
@@ -100,7 +121,28 @@ module TTY
100
121
  help = []
101
122
  help << "min. #{@min}" if @min
102
123
  help << "max. #{@max}" if @max
103
- "(%s) " % [ help.join(' ') ]
124
+ "(%s) " % [help.join(", ")]
125
+ end
126
+
127
+ # Build a default help text
128
+ #
129
+ # @return [String]
130
+ #
131
+ # @api private
132
+ def default_help
133
+ str = []
134
+ str << "(Press "
135
+ str << "#{arrows_help} arrow"
136
+ str << " or 1-#{choices.size} number" if enumerate?
137
+ str << " to move, Space"
138
+ str << "/Ctrl+A|R" if @max.nil?
139
+ str << " to select"
140
+ str << " (all|rev)" if @max.nil?
141
+ str << (filterable? ? "," : " and")
142
+ str << " Enter to finish"
143
+ str << " and letters to filter" if filterable?
144
+ str << ")"
145
+ str.join
104
146
  end
105
147
 
106
148
  # Render initial help text and then currently selected choices
@@ -109,19 +151,21 @@ module TTY
109
151
  def render_header
110
152
  instructions = @prompt.decorate(help, @help_color)
111
153
  minmax_suffix = @min || @max ? minmax_help : ""
154
+ print_selected = @selected.size.nonzero? && @echo
112
155
 
113
156
  if @done && @echo
114
157
  @prompt.decorate(selected_names, @active_color)
115
- elsif @selected.size.nonzero? && @echo
116
- help_suffix = filterable? && @filter.any? ? " #{filter_help}" : ""
117
- minmax_suffix + selected_names +
118
- (@first_render ? " #{instructions}" : help_suffix)
119
- elsif @first_render
120
- minmax_suffix + instructions
158
+ elsif (@first_render && (help_start? || help_always?)) ||
159
+ (help_always? && !@filter.any? && !@done)
160
+ minmax_suffix +
161
+ (print_selected ? "#{selected_names} " : "") +
162
+ instructions
121
163
  elsif filterable? && @filter.any?
122
- minmax_suffix + filter_help
123
- elsif @min || @max
124
- minmax_help
164
+ minmax_suffix +
165
+ (print_selected ? "#{selected_names} " : "") +
166
+ @prompt.decorate(filter_help, @help_color)
167
+ else
168
+ minmax_suffix + (print_selected ? selected_names : "")
125
169
  end
126
170
  end
127
171
 
@@ -144,9 +188,9 @@ module TTY
144
188
 
145
189
  sync_paginators if @paging_changed
146
190
  paginator.paginate(choices, @active, @per_page) do |choice, index|
147
- num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
148
- indicator = (index + 1 == @active) ? @symbols[:marker] : ' '
149
- indicator += ' '
191
+ num = enumerate? ? (index + 1).to_s + @enum + " " : ""
192
+ indicator = (index + 1 == @active) ? @symbols[:marker] : " "
193
+ indicator += " "
150
194
  message = if @selected.include?(choice) && !choice.disabled?
151
195
  selected = @prompt.decorate(@symbols[:radio_on], @active_color)
152
196
  "#{selected} #{num}#{choice.name}"
@@ -157,7 +201,7 @@ module TTY
157
201
  "#{@symbols[:radio_off]} #{num}#{choice.name}"
158
202
  end
159
203
  end_index = paginated? ? paginator.end_index : choices.size - 1
160
- newline = (index == end_index) ? '' : "\n"
204
+ newline = (index == end_index) ? "" : "\n"
161
205
  output << indicator + message + newline
162
206
  end
163
207
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'question'
4
- require_relative 'symbols'
3
+ require_relative "question"
4
+ require_relative "symbols"
5
5
 
6
6
  module TTY
7
7
  class Prompt
@@ -9,7 +9,7 @@ module TTY
9
9
  #
10
10
  # @api private
11
11
  class Multiline < Question
12
- HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze
12
+ HELP = "(Press Ctrl+D or Ctrl+Z to finish)".freeze
13
13
 
14
14
  def initialize(prompt, **options)
15
15
  super
@@ -55,8 +55,9 @@ module TTY
55
55
  @prompt.print(question)
56
56
  @lines = read_input
57
57
  @input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
58
- if Utils.blank?(@input)
59
- @input = default? ? default : nil
58
+ if Utils.blank?(@input) && default?
59
+ @input = default
60
+ @lines = default
60
61
  end
61
62
  @evaluator.(@lines)
62
63
  end
@@ -40,7 +40,7 @@ module TTY
40
40
  #
41
41
  # @api private
42
42
  def check_page_size!
43
- raise InvalidArgument, 'per_page must be > 0' if @per_page < 1
43
+ raise InvalidArgument, "per_page must be > 0" if @per_page < 1
44
44
  end
45
45
 
46
46
  # Paginate collection given an active index
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
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'
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"
9
9
 
10
10
  module TTY
11
11
  # A class responsible for shell prompt interactions.
@@ -35,23 +35,30 @@ module TTY
35
35
  #
36
36
  # @api public
37
37
  def initialize(prompt, **options)
38
- @prompt = prompt
39
- @prefix = options.fetch(:prefix) { @prompt.prefix }
40
- @default = options.fetch(:default) { UndefinedSetting }
41
- @required = options.fetch(:required) { false }
42
- @echo = options.fetch(:echo) { true }
43
- @in = options.fetch(:in) { UndefinedSetting }
44
- @modifier = options.fetch(:modifier) { [] }
45
- @validation = options.fetch(:validation) { UndefinedSetting }
46
- @convert = options.fetch(:convert) { UndefinedSetting }
38
+ # Option deprecation
39
+ if options[:validation]
40
+ warn "[DEPRECATION] The `:validation` option is deprecated. Use `:validate` instead."
41
+ options[:validate] = options[:validation]
42
+ end
43
+
44
+ @prompt = prompt
45
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
46
+ @default = options.fetch(:default) { UndefinedSetting }
47
+ @required = options.fetch(:required) { false }
48
+ @echo = options.fetch(:echo) { true }
49
+ @in = options.fetch(:in) { UndefinedSetting }
50
+ @modifier = options.fetch(:modifier) { [] }
51
+ @validation = options.fetch(:validate) { UndefinedSetting }
52
+ @convert = options.fetch(:convert) { UndefinedSetting }
47
53
  @active_color = options.fetch(:active_color) { @prompt.active_color }
48
- @help_color = options.fetch(:help_color) { @prompt.help_color }
49
- @error_color = options.fetch(:error_color) { :red }
50
- @value = options.fetch(:value) { UndefinedSetting }
51
- @messages = Utils.deep_copy(options.fetch(:messages) { { } })
52
- @done = false
54
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
55
+ @error_color = options.fetch(:error_color) { :red }
56
+ @value = options.fetch(:value) { UndefinedSetting }
57
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
58
+ @messages = Utils.deep_copy(options.fetch(:messages) { { } })
59
+ @done = false
53
60
  @first_render = true
54
- @input = nil
61
+ @input = nil
55
62
 
56
63
  @evaluator = Evaluator.new(self)
57
64
 
@@ -60,6 +67,7 @@ module TTY
60
67
  @evaluator << CheckRange
61
68
  @evaluator << CheckValidation
62
69
  @evaluator << CheckModifier
70
+ @evaluator << CheckConversion
63
71
  end
64
72
 
65
73
  # Stores all the error messages displayed to user
@@ -85,7 +93,7 @@ module TTY
85
93
  if template && !template.match(/\%\{/).nil?
86
94
  [template % tokens]
87
95
  else
88
- [template || '']
96
+ [template || ""]
89
97
  end
90
98
  end
91
99
 
@@ -96,7 +104,7 @@ module TTY
96
104
  # @return [self]
97
105
  #
98
106
  # @api public
99
- def call(message = '', &block)
107
+ def call(message = "", &block)
100
108
  @message = message
101
109
  block.call(self) if block
102
110
  @prompt.subscribe(self) do
@@ -122,8 +130,8 @@ module TTY
122
130
  total_lines = @prompt.count_screen_lines(input_line)
123
131
  @prompt.print(refresh(question.lines.count, total_lines))
124
132
  end
125
- @prompt.print(render_question)
126
- convert_result(result.value)
133
+ @prompt.print(render_question) unless @quiet
134
+ result.value
127
135
  end
128
136
 
129
137
  # Render question
@@ -141,7 +149,7 @@ module TTY
141
149
  elsif @done
142
150
  header << @prompt.decorate(@input.to_s, @active_color)
143
151
  elsif default? && !Utils.blank?(@default)
144
- header << @prompt.decorate("(#{default})", @help_color) + ' '
152
+ header << @prompt.decorate("(#{default})", @help_color) + " "
145
153
  end
146
154
  header << "\n" if @done
147
155
  header.join
@@ -177,7 +185,7 @@ module TTY
177
185
  # @api private
178
186
  def render_error(errors)
179
187
  errors.reduce([]) do |acc, err|
180
- acc << @prompt.decorate('>>', :red) + ' ' + err
188
+ acc << @prompt.decorate(">>", :red) + " " + err
181
189
  acc
182
190
  end.join("\n")
183
191
  end
@@ -211,8 +219,13 @@ module TTY
211
219
  #
212
220
  # @api private
213
221
  def convert_result(value)
214
- if convert? & !Utils.blank?(value)
215
- Converters.convert(@convert, value)
222
+ if convert? && !Utils.blank?(value)
223
+ case @convert
224
+ when Proc
225
+ @convert.call(value)
226
+ else
227
+ Converters.convert(@convert, value)
228
+ end
216
229
  else
217
230
  value
218
231
  end
@@ -221,8 +234,13 @@ module TTY
221
234
  # Specify answer conversion
222
235
  #
223
236
  # @api public
224
- def convert(value)
225
- @convert = value
237
+ def convert(value = (not_set = true), message = nil)
238
+ messages[:convert?] = message if message
239
+ if not_set
240
+ @convert
241
+ else
242
+ @convert = value
243
+ end
226
244
  end
227
245
 
228
246
  # Check if conversion is set
@@ -345,6 +363,13 @@ module TTY
345
363
  @in != UndefinedSetting
346
364
  end
347
365
 
366
+ # Set quiet mode.
367
+ #
368
+ # @api public
369
+ def quiet(value)
370
+ @quiet = value
371
+ end
372
+
348
373
  # @api public
349
374
  def to_s
350
375
  message.to_s
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../const"
4
+
3
5
  module TTY
4
6
  class Prompt
5
7
  class Question
@@ -81,6 +83,22 @@ module TTY
81
83
  end
82
84
  end
83
85
  end
86
+
87
+ class CheckConversion
88
+ def self.call(question, value)
89
+ if question.convert? && !Utils.blank?(value)
90
+ result = question.convert_result(value)
91
+ if result == Const::Undefined
92
+ tokens = {value: value, type: question.convert}
93
+ [value, question.message_for(:convert?, tokens)]
94
+ else
95
+ [result]
96
+ end
97
+ else
98
+ [value]
99
+ end
100
+ end
101
+ end
84
102
  end # Checks
85
103
  end # Question
86
104
  end # Prompt
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Prompt
5
+ # @api private
6
+ class SelectedChoices
7
+ include Enumerable
8
+
9
+ attr_reader :size
10
+
11
+ # Create selected choices
12
+ #
13
+ # @param [Array<Choice>] selected
14
+ # @param [Array<Integer>] indexes
15
+ #
16
+ # @api public
17
+ def initialize(selected = [], indexes = [])
18
+ @selected = selected
19
+ @indexes = indexes
20
+ @size = @selected.size
21
+ end
22
+
23
+ # Clear selected choices
24
+ #
25
+ # @api public
26
+ def clear
27
+ @indexes.clear
28
+ @selected.clear
29
+ @size = 0
30
+ end
31
+
32
+ # Iterate over selected choices
33
+ #
34
+ # @api public
35
+ def each(&block)
36
+ return to_enum unless block_given?
37
+ @selected.each(&block)
38
+ end
39
+
40
+ # Insert choice at index
41
+ #
42
+ # @param [Integer] index
43
+ # @param [Choice] choice
44
+ #
45
+ # @api public
46
+ def insert(index, choice)
47
+ insert_idx = find_index_by { |i| index < @indexes[i] }
48
+ insert_idx ||= -1
49
+ @indexes.insert(insert_idx, index)
50
+ @selected.insert(insert_idx, choice)
51
+ @size += 1
52
+ self
53
+ end
54
+
55
+ # Delete choice at index
56
+ #
57
+ # @return [Choice]
58
+ # the deleted choice
59
+ #
60
+ # @api public
61
+ def delete_at(index)
62
+ delete_idx = @indexes.each_index.find { |i| index == @indexes[i] }
63
+ return nil unless delete_idx
64
+
65
+ @indexes.delete_at(delete_idx)
66
+ choice = @selected.delete_at(delete_idx)
67
+ @size -= 1
68
+ choice
69
+ end
70
+
71
+ def find_index_by(&search)
72
+ (0...@size).bsearch(&search)
73
+ end
74
+ end # SelectedChoices
75
+ end # Prompt
76
+ end # TTY