gorails 0.1.0 → 0.1.3

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +3 -1
  4. data/Gemfile.lock +65 -0
  5. data/README.md +41 -12
  6. data/bin/update-deps +95 -0
  7. data/exe/gorails +18 -0
  8. data/gorails.gemspec +4 -3
  9. data/lib/gorails/commands/episodes.rb +25 -0
  10. data/lib/gorails/commands/example.rb +19 -0
  11. data/lib/gorails/commands/help.rb +21 -0
  12. data/lib/gorails/commands/jobs.rb +25 -0
  13. data/lib/gorails/commands/jumpstart.rb +29 -0
  14. data/lib/gorails/commands/railsbytes.rb +67 -0
  15. data/lib/gorails/commands.rb +19 -0
  16. data/lib/gorails/entry_point.rb +10 -0
  17. data/lib/gorails/version.rb +1 -1
  18. data/lib/gorails.rb +22 -1
  19. data/vendor/deps/cli-kit/REVISION +1 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  39. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  40. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  41. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  42. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  43. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  44. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  45. data/vendor/deps/cli-ui/REVISION +1 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  68. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  69. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  70. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  71. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  72. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  73. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  74. metadata +114 -5
@@ -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