gorails 0.1.1 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/Gemfile.lock +1 -6
  4. data/README.md +41 -12
  5. data/bin/update-deps +95 -0
  6. data/exe/gorails +2 -1
  7. data/gorails.gemspec +0 -2
  8. data/lib/gorails/commands/railsbytes.rb +45 -4
  9. data/lib/gorails/commands/version.rb +15 -0
  10. data/lib/gorails/commands.rb +2 -5
  11. data/lib/gorails/version.rb +1 -1
  12. data/lib/gorails.rb +11 -20
  13. data/vendor/deps/cli-kit/REVISION +1 -0
  14. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  15. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  16. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  17. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  18. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  19. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  39. data/vendor/deps/cli-ui/REVISION +1 -0
  40. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  41. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  42. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  43. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  44. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  45. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  68. metadata +59 -30
@@ -0,0 +1,102 @@
1
+ # typed: true
2
+ require 'cli/ui'
3
+
4
+ module CLI
5
+ module UI
6
+ class Progress
7
+ extend T::Sig
8
+
9
+ # A Cyan filled block
10
+ FILLED_BAR = "\e[46m"
11
+ # A bright white block
12
+ UNFILLED_BAR = "\e[1;47m"
13
+
14
+ # Add a progress bar to the terminal output
15
+ #
16
+ # https://user-images.githubusercontent.com/3074765/33799794-cc4c940e-dd00-11e7-9bdc-90f77ec9167c.gif
17
+ #
18
+ # ==== Example Usage:
19
+ #
20
+ # Set the percent to X
21
+ # CLI::UI::Progress.progress do |bar|
22
+ # bar.tick(set_percent: percent)
23
+ # end
24
+ #
25
+ # Increase the percent by 1 percent
26
+ # CLI::UI::Progress.progress do |bar|
27
+ # bar.tick
28
+ # end
29
+ #
30
+ # Increase the percent by X
31
+ # CLI::UI::Progress.progress do |bar|
32
+ # bar.tick(percent: 0.05)
33
+ # end
34
+ sig do
35
+ type_parameters(:T)
36
+ .params(width: Integer, block: T.proc.params(bar: Progress).returns(T.type_parameter(:T)))
37
+ .returns(T.type_parameter(:T))
38
+ end
39
+ def self.progress(width: Terminal.width, &block)
40
+ bar = Progress.new(width: width)
41
+ print(CLI::UI::ANSI.hide_cursor)
42
+ yield(bar)
43
+ ensure
44
+ puts bar.to_s
45
+ CLI::UI.raw do
46
+ print(ANSI.show_cursor)
47
+ end
48
+ end
49
+
50
+ # Initialize a progress bar. Typically used in a +Progress.progress+ block
51
+ #
52
+ # ==== Options
53
+ # One of the follow can be used, but not both together
54
+ #
55
+ # * +:width+ - The width of the terminal
56
+ #
57
+ sig { params(width: Integer).void }
58
+ def initialize(width: Terminal.width)
59
+ @percent_done = T.let(0, Numeric)
60
+ @max_width = width
61
+ end
62
+
63
+ # Set the progress of the bar. Typically used in a +Progress.progress+ block
64
+ #
65
+ # ==== Options
66
+ # One of the follow can be used, but not both together
67
+ #
68
+ # * +:percent+ - Increment progress by a specific percent amount
69
+ # * +:set_percent+ - Set progress to a specific percent
70
+ #
71
+ # *Note:* The +:percent+ and +:set_percent must be between 0.00 and 1.0
72
+ #
73
+ sig { params(percent: T.nilable(Numeric), set_percent: T.nilable(Numeric)).void }
74
+ def tick(percent: nil, set_percent: nil)
75
+ raise ArgumentError, 'percent and set_percent cannot both be specified' if percent && set_percent
76
+
77
+ @percent_done += percent || 0.01
78
+ @percent_done = set_percent if set_percent
79
+ @percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
80
+
81
+ print(to_s)
82
+ print(CLI::UI::ANSI.previous_line + "\n")
83
+ end
84
+
85
+ # Format the progress bar to be printed to terminal
86
+ #
87
+ sig { returns(String) }
88
+ def to_s
89
+ suffix = " #{(@percent_done * 100).floor}%".ljust(5)
90
+ workable_width = @max_width - Frame.prefix_width - suffix.size
91
+ filled = [(@percent_done * workable_width.to_f).ceil, 0].max
92
+ unfilled = [workable_width - filled, 0].max
93
+
94
+ CLI::UI.resolve_text([
95
+ FILLED_BAR + ' ' * filled,
96
+ UNFILLED_BAR + ' ' * unfilled,
97
+ CLI::UI::Color::RESET.code + suffix,
98
+ ].join)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,534 @@
1
+ # coding: utf-8
2
+
3
+ # typed: true
4
+
5
+ require 'io/console'
6
+
7
+ module CLI
8
+ module UI
9
+ module Prompt
10
+ class InteractiveOptions
11
+ extend T::Sig
12
+
13
+ DONE = 'Done'
14
+ CHECKBOX_ICON = { false => '☐', true => '☑' }
15
+
16
+ # Prompts the user with options
17
+ # Uses an interactive session to allow the user to pick an answer
18
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
19
+ # For more than 9 options, hitting 'e', ':', or 'G' will enter select
20
+ # mode allowing the user to type in longer numbers
21
+ # Pressing 'f' or '/' will allow the user to filter the results
22
+ #
23
+ # https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
24
+ #
25
+ # ==== Example Usage:
26
+ #
27
+ # Ask an interactive question
28
+ # CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
29
+ #
30
+ sig do
31
+ params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
32
+ .returns(T.any(String, T::Array[String]))
33
+ end
34
+ def self.call(options, multiple: false, default: nil)
35
+ list = new(options, multiple: multiple, default: default)
36
+ selected = list.call
37
+ if multiple
38
+ selected.map { |s| options[s - 1] }
39
+ else
40
+ options[selected - 1]
41
+ end
42
+ end
43
+
44
+ # Initializes a new +InteractiveOptions+
45
+ # Usually called from +self.call+
46
+ #
47
+ # ==== Example Usage:
48
+ #
49
+ # CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
50
+ #
51
+ sig do
52
+ params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
53
+ .void
54
+ end
55
+ def initialize(options, multiple: false, default: nil)
56
+ @options = options
57
+ @active = 1
58
+ @marker = '>'
59
+ @answer = nil
60
+ @state = :root
61
+ @multiple = multiple
62
+ # Indicate that an extra line (the "metadata" line) is present and
63
+ # the terminal output should be drawn over when processing user input
64
+ @displaying_metadata = false
65
+ @filter = ''
66
+ # 0-indexed array representing if selected
67
+ # @options[0] is selected if @chosen[0]
68
+ if multiple
69
+ @chosen = if default
70
+ @options.map { |option| default.include?(option) }
71
+ else
72
+ Array.new(@options.size) { false }
73
+ end
74
+ end
75
+ @redraw = true
76
+ @presented_options = T.let([], T::Array[[String, T.nilable(Integer)]])
77
+ end
78
+
79
+ # Calls the +InteractiveOptions+ and asks the question
80
+ # Usually used from +self.call+
81
+ #
82
+ sig { returns(T.any(Integer, T::Array[Integer])) }
83
+ def call
84
+ calculate_option_line_lengths
85
+ CLI::UI.raw { print(ANSI.hide_cursor) }
86
+ while @answer.nil?
87
+ render_options
88
+ process_input_until_redraw_required
89
+ reset_position
90
+ end
91
+ clear_output
92
+
93
+ @answer
94
+ ensure
95
+ CLI::UI.raw do
96
+ print(ANSI.show_cursor)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ sig { void }
103
+ def calculate_option_line_lengths
104
+ @terminal_width_at_calculation_time = CLI::UI::Terminal.width
105
+ # options will be an array of questions but each option can be multi-line
106
+ # so to get the # of lines, you need to join then split
107
+
108
+ # since lines may be longer than the terminal is wide, we need to
109
+ # determine how many extra lines would be taken up by them
110
+ max_width = (@terminal_width_at_calculation_time -
111
+ @options.count.to_s.size - # Width of the displayed number
112
+ 5 - # Extra characters added during rendering
113
+ (@multiple ? 1 : 0) # Space for the checkbox, if rendered
114
+ ).to_f
115
+
116
+ @option_lengths = @options.map do |text|
117
+ width = 1 if text.empty?
118
+ width ||= text
119
+ .split("\n")
120
+ .reject(&:empty?)
121
+ .map { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
122
+ .reduce(&:+)
123
+
124
+ width
125
+ end
126
+ end
127
+
128
+ sig { params(number_of_lines: Integer).void }
129
+ def reset_position(number_of_lines = num_lines)
130
+ # This will put us back at the beginning of the options
131
+ # When we redraw the options, they will be overwritten
132
+ CLI::UI.raw do
133
+ number_of_lines.times { print(ANSI.previous_line) }
134
+ end
135
+ end
136
+
137
+ sig { params(number_of_lines: Integer).void }
138
+ def clear_output(number_of_lines = num_lines)
139
+ CLI::UI.raw do
140
+ # Write over all lines with whitespace
141
+ number_of_lines.times { puts(' ' * CLI::UI::Terminal.width) }
142
+ end
143
+ reset_position(number_of_lines)
144
+
145
+ # Update if metadata is being displayed
146
+ # This must be done _after_ the output is cleared or it won't draw over
147
+ # the entire output
148
+ @displaying_metadata = display_metadata?
149
+ end
150
+
151
+ # Don't use this in place of +@displaying_metadata+, this updates too
152
+ # quickly to be useful when drawing to the screen.
153
+ sig { returns(T::Boolean) }
154
+ def display_metadata?
155
+ filtering? || selecting? || has_filter?
156
+ end
157
+
158
+ sig { returns(Integer) }
159
+ def num_lines
160
+ calculate_option_line_lengths if terminal_width_changed?
161
+
162
+ option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
163
+ # Handle continuation markers and "Done" option when multiple is true
164
+ next total_length + 1 if option_number.nil? || option_number.zero?
165
+
166
+ total_length + @option_lengths[option_number - 1]
167
+ end
168
+
169
+ option_length + (@displaying_metadata ? 1 : 0)
170
+ end
171
+
172
+ sig { returns(T::Boolean) }
173
+ def terminal_width_changed?
174
+ @terminal_width_at_calculation_time != CLI::UI::Terminal.width
175
+ end
176
+
177
+ ESC = "\e"
178
+ BACKSPACE = "\u007F"
179
+ CTRL_C = "\u0003"
180
+ CTRL_D = "\u0004"
181
+
182
+ sig { void }
183
+ def up
184
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
185
+
186
+ previous_visible = @filtered_options[active_index - 1]
187
+ previous_visible ||= @filtered_options.last
188
+
189
+ @active = previous_visible ? previous_visible.last : -1
190
+ @redraw = true
191
+ end
192
+
193
+ sig { void }
194
+ def down
195
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
196
+
197
+ next_visible = @filtered_options[active_index + 1]
198
+ next_visible ||= @filtered_options.first
199
+
200
+ @active = next_visible ? next_visible.last : -1
201
+ @redraw = true
202
+ end
203
+
204
+ # n is 1-indexed selection
205
+ # n == 0 if "Done" was selected in @multiple mode
206
+ sig { params(n: Integer).void }
207
+ def select_n(n)
208
+ if @multiple
209
+ if n == 0
210
+ @answer = []
211
+ @chosen.each_with_index do |selected, i|
212
+ @answer << i + 1 if selected
213
+ end
214
+ else
215
+ @active = n
216
+ @chosen[n - 1] = !@chosen[n - 1]
217
+ end
218
+ elsif n == 0
219
+ # Ignore pressing "0" when not in multiple mode
220
+ else
221
+ @active = n
222
+ @answer = n
223
+ end
224
+ @redraw = true
225
+ end
226
+
227
+ sig { params(char: String).void }
228
+ def select_bool(char)
229
+ return unless (@options - ['yes', 'no']).empty?
230
+
231
+ index = T.must(@options.index { |o| o.start_with?(char) })
232
+ @active = index + 1
233
+ @answer = index + 1
234
+ @redraw = true
235
+ end
236
+
237
+ sig { params(char: String).void }
238
+ def build_selection(char)
239
+ @active = (@active.to_s + char).to_i
240
+ @redraw = true
241
+ end
242
+
243
+ sig { void }
244
+ def chop_selection
245
+ @active = @active.to_s.chop.to_i
246
+ @redraw = true
247
+ end
248
+
249
+ sig { params(char: String).void }
250
+ def update_search(char)
251
+ @redraw = true
252
+
253
+ # Control+D or Backspace on empty search closes search
254
+ if (char == CTRL_D) || (@filter.empty? && (char == BACKSPACE))
255
+ @filter = ''
256
+ @state = :root
257
+ return
258
+ end
259
+
260
+ if char == BACKSPACE
261
+ @filter.chop!
262
+ else
263
+ @filter += char
264
+ end
265
+ end
266
+
267
+ sig { void }
268
+ def select_current
269
+ # Prevent selection of invisible options
270
+ return unless presented_options.any? { |_, num| num == @active }
271
+
272
+ select_n(@active)
273
+ end
274
+
275
+ sig { void }
276
+ def process_input_until_redraw_required
277
+ @redraw = false
278
+ wait_for_user_input until @redraw
279
+ end
280
+
281
+ # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
282
+ sig { void }
283
+ def wait_for_user_input
284
+ char = read_char
285
+ @last_char = char
286
+
287
+ case char
288
+ when :timeout ; raise Interrupt # Timeout, use interrupt to simulate
289
+ when CTRL_C ; raise Interrupt
290
+ end
291
+
292
+ max_digit = [@options.size, 9].min.to_s
293
+ case @state
294
+ when :root
295
+ case char
296
+ when ESC ; @state = :esc
297
+ when 'k' ; up
298
+ when 'j' ; down
299
+ when 'e', ':', 'G' ; start_line_select
300
+ when 'f', '/' ; start_filter
301
+ when ('0'..max_digit) ; select_n(char.to_i)
302
+ when 'y', 'n' ; select_bool(char)
303
+ when ' ', "\r", "\n" ; select_current # <enter>
304
+ end
305
+ when :filter
306
+ case char
307
+ when ESC ; @state = :esc
308
+ when "\r", "\n" ; select_current
309
+ when "\b" ; update_search(BACKSPACE) # Happens on Windows
310
+ else ; update_search(char)
311
+ end
312
+ when :line_select
313
+ case char
314
+ when ESC ; @state = :esc
315
+ when 'k' ; up ; @state = :root
316
+ when 'j' ; down ; @state = :root
317
+ when 'e', ':', 'G', 'q' ; stop_line_select
318
+ when '0'..'9' ; build_selection(char)
319
+ when BACKSPACE ; chop_selection # Pop last input on backspace
320
+ when ' ', "\r", "\n" ; select_current
321
+ end
322
+ when :esc
323
+ case char
324
+ when '[' ; @state = :esc_bracket
325
+ else ; raise Interrupt # unhandled escape sequence.
326
+ end
327
+ when :esc_bracket
328
+ @state = has_filter? ? :filter : :root
329
+ case char
330
+ when 'A' ; up
331
+ when 'B' ; down
332
+ when 'C' ; # Ignore right key
333
+ when 'D' ; # Ignore left key
334
+ else ; raise Interrupt # unhandled escape sequence.
335
+ end
336
+ end
337
+ end
338
+ # rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
339
+
340
+ sig { returns(T::Boolean) }
341
+ def selecting?
342
+ @state == :line_select
343
+ end
344
+
345
+ sig { returns(T::Boolean) }
346
+ def filtering?
347
+ @state == :filter
348
+ end
349
+
350
+ sig { returns(T::Boolean) }
351
+ def has_filter?
352
+ !@filter.empty?
353
+ end
354
+
355
+ sig { void }
356
+ def start_filter
357
+ @state = :filter
358
+ @redraw = true
359
+ end
360
+
361
+ sig { void }
362
+ def start_line_select
363
+ @state = :line_select
364
+ @active = 0
365
+ @redraw = true
366
+ end
367
+
368
+ sig { void }
369
+ def stop_line_select
370
+ @state = :root
371
+ @active = 1 if @active.zero?
372
+ @redraw = true
373
+ end
374
+
375
+ sig { returns(String) }
376
+ def read_char
377
+ if $stdin.tty? && !ENV['TEST']
378
+ $stdin.getch # raw mode for tty
379
+ else
380
+ $stdin.getc
381
+ end
382
+ rescue IOError
383
+ "\e"
384
+ end
385
+
386
+ sig { params(recalculate: T::Boolean).returns(T::Array[[String, T.nilable(Integer)]]) }
387
+ def presented_options(recalculate: false)
388
+ return @presented_options unless recalculate
389
+
390
+ @presented_options = @options.zip(1..)
391
+ if has_filter?
392
+ @presented_options.select! { |option, _| option.downcase.include?(@filter.downcase) }
393
+ end
394
+
395
+ # Used for selection purposes
396
+ @presented_options.push([DONE, 0]) if @multiple
397
+ @filtered_options = @presented_options.dup
398
+
399
+ ensure_visible_is_active if has_filter?
400
+
401
+ # Must have more lines before the selection than we can display
402
+ if distance_from_start_to_selection > max_lines
403
+ @presented_options.shift(distance_from_start_to_selection - max_lines)
404
+ ensure_first_item_is_continuation_marker
405
+ end
406
+
407
+ # Must have more lines after the selection than we can display
408
+ if distance_from_selection_to_end > max_lines
409
+ @presented_options.pop(distance_from_selection_to_end - max_lines)
410
+ ensure_last_item_is_continuation_marker
411
+ end
412
+
413
+ while num_lines > max_lines
414
+ # try to keep the selection centered in the window:
415
+ if distance_from_selection_to_end > distance_from_start_to_selection
416
+ # selection is closer to top than bottom, so trim a row from the bottom
417
+ ensure_last_item_is_continuation_marker
418
+ @presented_options.delete_at(-2)
419
+ else
420
+ # selection is closer to bottom than top, so trim a row from the top
421
+ ensure_first_item_is_continuation_marker
422
+ @presented_options.delete_at(1)
423
+ end
424
+ end
425
+
426
+ @presented_options
427
+ end
428
+
429
+ sig { void }
430
+ def ensure_visible_is_active
431
+ unless presented_options.any? { |_, num| num == @active }
432
+ @active = presented_options.first&.last.to_i
433
+ end
434
+ end
435
+
436
+ sig { returns(Integer) }
437
+ def distance_from_selection_to_end
438
+ @presented_options.count - index_of_active_option
439
+ end
440
+
441
+ sig { returns(Integer) }
442
+ def distance_from_start_to_selection
443
+ index_of_active_option
444
+ end
445
+
446
+ sig { returns(Integer) }
447
+ def index_of_active_option
448
+ @presented_options.index { |_, num| num == @active }.to_i
449
+ end
450
+
451
+ sig { void }
452
+ def ensure_last_item_is_continuation_marker
453
+ @presented_options.push(['...', nil]) if @presented_options.last&.last
454
+ end
455
+
456
+ sig { void }
457
+ def ensure_first_item_is_continuation_marker
458
+ @presented_options.unshift(['...', nil]) if @presented_options.first&.last
459
+ end
460
+
461
+ sig { returns(Integer) }
462
+ def max_lines
463
+ CLI::UI::Terminal.height - (@displaying_metadata ? 3 : 2) # Keeps a one line question visible
464
+ end
465
+
466
+ sig { void }
467
+ def render_options
468
+ previously_displayed_lines = num_lines
469
+
470
+ @displaying_metadata = display_metadata?
471
+
472
+ options = presented_options(recalculate: true)
473
+
474
+ clear_output(previously_displayed_lines) if previously_displayed_lines > num_lines
475
+
476
+ max_num_length = (@options.size + 1).to_s.length
477
+
478
+ metadata_text = if selecting?
479
+ select_text = @active
480
+ select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
481
+ "Select: #{select_text}"
482
+ elsif filtering? || has_filter?
483
+ filter_text = @filter
484
+ filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
485
+ "Filter: #{filter_text}"
486
+ end
487
+
488
+ if metadata_text
489
+ CLI::UI.with_frame_color(:blue) do
490
+ puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}")
491
+ end
492
+ end
493
+
494
+ options.each do |choice, num|
495
+ is_chosen = @multiple && num && @chosen[num - 1] && num != 0
496
+
497
+ padding = ' ' * (max_num_length - num.to_s.length)
498
+ message = " #{num}#{num ? "." : " "}#{padding}"
499
+
500
+ format = '%s'
501
+ # If multiple, bold only selected. If not multiple, bold everything
502
+ format = "{{bold:#{format}}}" if !@multiple || is_chosen
503
+ format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
504
+ format = " #{format}"
505
+
506
+ message += format(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
507
+ message += format_choice(format, choice)
508
+
509
+ if num == @active
510
+
511
+ color = filtering? || selecting? ? 'green' : 'blue'
512
+ message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
513
+ end
514
+
515
+ CLI::UI.with_frame_color(:blue) do
516
+ puts CLI::UI.fmt(message)
517
+ end
518
+ end
519
+ end
520
+
521
+ sig { params(format: String, choice: String).returns(String) }
522
+ def format_choice(format, choice)
523
+ eol = CLI::UI::ANSI.clear_to_end_of_line
524
+ lines = choice.split("\n")
525
+
526
+ return eol if lines.empty? # Handle blank options
527
+
528
+ lines.map! { |l| format(format, l) + eol }
529
+ lines.join("\n")
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ module CLI
3
+ module UI
4
+ module Prompt
5
+ # A class that handles the various options of an InteractivePrompt and their callbacks
6
+ class OptionsHandler
7
+ extend T::Sig
8
+
9
+ sig { void }
10
+ def initialize
11
+ @options = {}
12
+ end
13
+
14
+ sig { returns(T::Array[String]) }
15
+ def options
16
+ @options.keys
17
+ end
18
+
19
+ sig { params(option: String, handler: T.proc.params(option: String).returns(String)).void }
20
+ def option(option, &handler)
21
+ @options[option] = handler
22
+ end
23
+
24
+ sig { params(options: T.any(T::Array[String], String)).returns(String) }
25
+ def call(options)
26
+ case options
27
+ when Array
28
+ options.map { |option| @options[option].call(options) }
29
+ else
30
+ @options[options].call(options)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end