tty-prompt 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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