cli-ui 2.3.1 → 2.6.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.
- checksums.yaml +4 -4
- data/lib/cli/ui/ansi.rb +23 -27
- data/lib/cli/ui/color.rb +8 -13
- data/lib/cli/ui/formatter.rb +24 -23
- data/lib/cli/ui/frame/frame_stack.rb +9 -21
- data/lib/cli/ui/frame/frame_style/box.rb +11 -10
- data/lib/cli/ui/frame/frame_style/bracket.rb +11 -10
- data/lib/cli/ui/frame/frame_style.rb +31 -22
- data/lib/cli/ui/frame.rb +12 -43
- data/lib/cli/ui/glyph.rb +8 -14
- data/lib/cli/ui/os.rb +9 -10
- data/lib/cli/ui/printer.rb +1 -15
- data/lib/cli/ui/progress.rb +20 -23
- data/lib/cli/ui/progress_reporter.rb +209 -0
- data/lib/cli/ui/prompt/interactive_options.rb +85 -53
- data/lib/cli/ui/prompt/options_handler.rb +4 -6
- data/lib/cli/ui/prompt.rb +22 -45
- data/lib/cli/ui/spinner/async.rb +3 -7
- data/lib/cli/ui/spinner/spin_group.rb +205 -135
- data/lib/cli/ui/spinner.rb +3 -16
- data/lib/cli/ui/stdout_router.rb +38 -55
- data/lib/cli/ui/table.rb +3 -7
- data/lib/cli/ui/terminal.rb +4 -8
- data/lib/cli/ui/truncater.rb +5 -6
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui/widgets/base.rb +13 -14
- data/lib/cli/ui/widgets/status.rb +10 -10
- data/lib/cli/ui/widgets.rb +7 -15
- data/lib/cli/ui/work_queue.rb +23 -27
- data/lib/cli/ui/wrap.rb +3 -5
- data/lib/cli/ui.rb +42 -97
- metadata +4 -7
- data/lib/cli/ui/sorbet_runtime_stub.rb +0 -169
@@ -0,0 +1,209 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CLI
|
5
|
+
module UI
|
6
|
+
# Handles terminal progress bar reporting using ConEmu OSC 9;4 sequences
|
7
|
+
# Supports:
|
8
|
+
# - Numerical progress (0-100%)
|
9
|
+
# - Indeterminate/pulsing progress
|
10
|
+
# - Success/error states
|
11
|
+
# - Paused state
|
12
|
+
module ProgressReporter
|
13
|
+
# Progress reporter instance that manages the lifecycle of progress reporting
|
14
|
+
class Reporter
|
15
|
+
# OSC (Operating System Command) escape sequences
|
16
|
+
OSC = "\e]"
|
17
|
+
ST = "\a" # String Terminator (BEL)
|
18
|
+
|
19
|
+
# Progress states
|
20
|
+
REMOVE = 0
|
21
|
+
SET_PROGRESS = 1
|
22
|
+
ERROR = 2
|
23
|
+
INDETERMINATE = 3
|
24
|
+
PAUSED = 4
|
25
|
+
|
26
|
+
#: (Symbol mode, ?io_like to, ?parent: Reporter?, ?delay_start: bool) -> void
|
27
|
+
def initialize(mode, to = $stdout, parent: nil, delay_start: false)
|
28
|
+
@mode = mode
|
29
|
+
@to = to
|
30
|
+
@parent = parent
|
31
|
+
@children = []
|
32
|
+
@active = ProgressReporter.supports_progress? && @parent.nil?
|
33
|
+
|
34
|
+
# Register with parent if nested
|
35
|
+
@parent&.add_child(self)
|
36
|
+
|
37
|
+
return unless @active
|
38
|
+
return if delay_start # Don't emit initial OSC if delayed
|
39
|
+
|
40
|
+
case mode
|
41
|
+
when :indeterminate
|
42
|
+
set_indeterminate
|
43
|
+
when :progress
|
44
|
+
set_progress(0)
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Unknown progress mode: #{mode}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#: (Reporter child) -> void
|
51
|
+
def add_child(child)
|
52
|
+
@children << child
|
53
|
+
end
|
54
|
+
|
55
|
+
#: (Reporter child) -> void
|
56
|
+
def remove_child(child)
|
57
|
+
@children.delete(child)
|
58
|
+
end
|
59
|
+
|
60
|
+
#: -> bool
|
61
|
+
def has_active_children?
|
62
|
+
@children.any?
|
63
|
+
end
|
64
|
+
|
65
|
+
#: (Integer percentage) -> void
|
66
|
+
def set_progress(percentage) # rubocop:disable Naming/AccessorMethodName
|
67
|
+
# Don't emit progress if we have active children (they own the progress)
|
68
|
+
return if has_active_children?
|
69
|
+
return unless @active
|
70
|
+
|
71
|
+
@mode = :progress # Update mode when switching to progress
|
72
|
+
percentage = percentage.clamp(0, 100)
|
73
|
+
@to.print("#{OSC}9;4;#{SET_PROGRESS};#{percentage}#{ST}")
|
74
|
+
end
|
75
|
+
|
76
|
+
#: -> void
|
77
|
+
def set_indeterminate
|
78
|
+
# Don't emit progress if we have active children
|
79
|
+
return if has_active_children?
|
80
|
+
return unless @active
|
81
|
+
|
82
|
+
@mode = :indeterminate # Update mode when switching to indeterminate
|
83
|
+
@to.print("#{OSC}9;4;#{INDETERMINATE};#{ST}")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Force progress mode even if there are children - used by SpinGroup
|
87
|
+
# when a task needs to show deterministic progress
|
88
|
+
#: (Integer percentage) -> void
|
89
|
+
def force_set_progress(percentage)
|
90
|
+
return unless @active
|
91
|
+
|
92
|
+
@mode = :progress
|
93
|
+
percentage = percentage.clamp(0, 100)
|
94
|
+
@to.print("#{OSC}9;4;#{SET_PROGRESS};#{percentage}#{ST}")
|
95
|
+
end
|
96
|
+
|
97
|
+
# Force indeterminate mode even if there are children
|
98
|
+
#: -> void
|
99
|
+
def force_set_indeterminate
|
100
|
+
return unless @active
|
101
|
+
|
102
|
+
@mode = :indeterminate
|
103
|
+
@to.print("#{OSC}9;4;#{INDETERMINATE};#{ST}")
|
104
|
+
end
|
105
|
+
|
106
|
+
#: -> void
|
107
|
+
def set_error
|
108
|
+
# Error state can be set even with children
|
109
|
+
return unless @active
|
110
|
+
|
111
|
+
@to.print("#{OSC}9;4;#{ERROR};#{ST}")
|
112
|
+
end
|
113
|
+
|
114
|
+
#: (?Integer? percentage) -> void
|
115
|
+
def set_paused(percentage = nil)
|
116
|
+
return if has_active_children?
|
117
|
+
return unless @active
|
118
|
+
|
119
|
+
if percentage
|
120
|
+
percentage = percentage.clamp(0, 100)
|
121
|
+
@to.print("#{OSC}9;4;#{PAUSED};#{percentage}#{ST}")
|
122
|
+
else
|
123
|
+
@to.print("#{OSC}9;4;#{PAUSED};#{ST}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#: -> void
|
128
|
+
def clear
|
129
|
+
# Only clear if we're the root reporter and have no active children
|
130
|
+
return unless @active
|
131
|
+
return if has_active_children?
|
132
|
+
|
133
|
+
@to.print("#{OSC}9;4;#{REMOVE};#{ST}")
|
134
|
+
end
|
135
|
+
|
136
|
+
#: -> void
|
137
|
+
def cleanup
|
138
|
+
# Remove self from parent's children list
|
139
|
+
@parent&.remove_child(self)
|
140
|
+
|
141
|
+
# If parent exists and has no more children, restore its progress state
|
142
|
+
if @parent && !@parent.has_active_children?
|
143
|
+
case @parent.instance_variable_get(:@mode)
|
144
|
+
when :indeterminate
|
145
|
+
@parent.set_indeterminate
|
146
|
+
when :progress
|
147
|
+
# Parent progress bar should maintain its last state
|
148
|
+
# The parent will handle re-emitting its progress on next tick
|
149
|
+
end
|
150
|
+
elsif !@parent
|
151
|
+
# We're the root, clear progress
|
152
|
+
clear
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class << self
|
158
|
+
# Thread-local storage for the current reporter stack
|
159
|
+
#: -> Array[Reporter]
|
160
|
+
def reporter_stack
|
161
|
+
Thread.current[:progress_reporter_stack] ||= []
|
162
|
+
end
|
163
|
+
|
164
|
+
#: -> Reporter?
|
165
|
+
def current_reporter
|
166
|
+
reporter_stack.last
|
167
|
+
end
|
168
|
+
|
169
|
+
# Block-based API that ensures progress is cleared
|
170
|
+
#: [T] (?mode: Symbol, ?to: io_like, ?delay_start: bool) { (Reporter reporter) -> T } -> T
|
171
|
+
def with_progress(mode: :indeterminate, to: $stdout, delay_start: false, &block)
|
172
|
+
parent = current_reporter
|
173
|
+
reporter = Reporter.new(mode, to, parent: parent, delay_start: delay_start)
|
174
|
+
|
175
|
+
reporter_stack.push(reporter)
|
176
|
+
yield(reporter)
|
177
|
+
ensure
|
178
|
+
reporter_stack.pop
|
179
|
+
reporter&.cleanup
|
180
|
+
end
|
181
|
+
|
182
|
+
#: -> bool
|
183
|
+
def supports_progress?
|
184
|
+
# Check if terminal supports ConEmu OSC sequences
|
185
|
+
# This is supported by:
|
186
|
+
# - ConEmu on Windows
|
187
|
+
# - Windows Terminal
|
188
|
+
# - Ghostty
|
189
|
+
# - Various terminals on Linux (with OSC 9;4 support)
|
190
|
+
|
191
|
+
# Check common environment variables that indicate support
|
192
|
+
return true if ENV['ConEmuPID']
|
193
|
+
return true if ENV['WT_SESSION'] # Windows Terminal
|
194
|
+
return true if ENV['GHOSTTY_RESOURCES_DIR'] # Ghostty
|
195
|
+
|
196
|
+
# Check TERM_PROGRAM for known supporting terminals
|
197
|
+
term_program = ENV['TERM_PROGRAM']
|
198
|
+
return true if term_program == 'ghostty'
|
199
|
+
|
200
|
+
# For now, we'll be conservative and only enable for known terminals
|
201
|
+
# Users can force-enable with an environment variable
|
202
|
+
return true if ENV['CLI_UI_ENABLE_PROGRESS'] == '1'
|
203
|
+
|
204
|
+
false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -8,14 +8,10 @@ module CLI
|
|
8
8
|
module UI
|
9
9
|
module Prompt
|
10
10
|
class InteractiveOptions
|
11
|
-
extend T::Sig
|
12
|
-
|
13
11
|
DONE = 'Done'
|
14
12
|
CHECKBOX_ICON = { false => '☐', true => '☑' }
|
15
13
|
|
16
14
|
class << self
|
17
|
-
extend T::Sig
|
18
|
-
|
19
15
|
# Prompts the user with options
|
20
16
|
# Uses an interactive session to allow the user to pick an answer
|
21
17
|
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
@@ -30,18 +26,17 @@ module CLI
|
|
30
26
|
# Ask an interactive question
|
31
27
|
# CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
|
32
28
|
#
|
33
|
-
|
34
|
-
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
|
35
|
-
.returns(T.any(String, T::Array[String]))
|
36
|
-
end
|
29
|
+
#: (Array[String] options, ?multiple: bool, ?default: (String | Array[String])?) -> (String | Array[String])
|
37
30
|
def call(options, multiple: false, default: nil)
|
38
31
|
list = new(options, multiple: multiple, default: default)
|
39
32
|
selected = list.call
|
40
33
|
case selected
|
41
34
|
when Array
|
42
|
-
selected.map
|
35
|
+
selected.map do |s|
|
36
|
+
options[s - 1] #: as !nil
|
37
|
+
end
|
43
38
|
else
|
44
|
-
|
39
|
+
options[selected - 1] #: as !nil
|
45
40
|
end
|
46
41
|
end
|
47
42
|
end
|
@@ -53,10 +48,7 @@ module CLI
|
|
53
48
|
#
|
54
49
|
# CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
|
55
50
|
#
|
56
|
-
|
57
|
-
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
|
58
|
-
.void
|
59
|
-
end
|
51
|
+
#: (Array[String] options, ?multiple: bool, ?default: (String | Array[String])?) -> void
|
60
52
|
def initialize(options, multiple: false, default: nil)
|
61
53
|
@options = options
|
62
54
|
@active = if default && (i = options.index(default))
|
@@ -82,13 +74,13 @@ module CLI
|
|
82
74
|
end
|
83
75
|
end
|
84
76
|
@redraw = true
|
85
|
-
@presented_options =
|
77
|
+
@presented_options = [] #: Array[[String, Integer?]]
|
86
78
|
end
|
87
79
|
|
88
80
|
# Calls the +InteractiveOptions+ and asks the question
|
89
81
|
# Usually used from +self.call+
|
90
82
|
#
|
91
|
-
|
83
|
+
#: -> (Integer | Array[Integer])
|
92
84
|
def call
|
93
85
|
calculate_option_line_lengths
|
94
86
|
CLI::UI.raw { print(ANSI.hide_cursor) }
|
@@ -108,7 +100,7 @@ module CLI
|
|
108
100
|
|
109
101
|
private
|
110
102
|
|
111
|
-
|
103
|
+
#: -> void
|
112
104
|
def calculate_option_line_lengths
|
113
105
|
@terminal_width_at_calculation_time = CLI::UI::Terminal.width
|
114
106
|
# options will be an array of questions but each option can be multi-line
|
@@ -138,7 +130,7 @@ module CLI
|
|
138
130
|
end
|
139
131
|
end
|
140
132
|
|
141
|
-
|
133
|
+
#: (?Integer number_of_lines) -> void
|
142
134
|
def reset_position(number_of_lines = num_lines)
|
143
135
|
# This will put us back at the beginning of the options
|
144
136
|
# When we redraw the options, they will be overwritten
|
@@ -147,7 +139,7 @@ module CLI
|
|
147
139
|
end
|
148
140
|
end
|
149
141
|
|
150
|
-
|
142
|
+
#: (?Integer number_of_lines) -> void
|
151
143
|
def clear_output(number_of_lines = num_lines)
|
152
144
|
CLI::UI.raw do
|
153
145
|
# Write over all lines with whitespace
|
@@ -163,12 +155,12 @@ module CLI
|
|
163
155
|
|
164
156
|
# Don't use this in place of +@displaying_metadata+, this updates too
|
165
157
|
# quickly to be useful when drawing to the screen.
|
166
|
-
|
158
|
+
#: -> bool
|
167
159
|
def display_metadata?
|
168
160
|
filtering? || selecting? || has_filter?
|
169
161
|
end
|
170
162
|
|
171
|
-
|
163
|
+
#: -> Integer
|
172
164
|
def num_lines
|
173
165
|
calculate_option_line_lengths if terminal_width_changed?
|
174
166
|
|
@@ -182,7 +174,7 @@ module CLI
|
|
182
174
|
option_length + (@displaying_metadata ? 1 : 0)
|
183
175
|
end
|
184
176
|
|
185
|
-
|
177
|
+
#: -> bool
|
186
178
|
def terminal_width_changed?
|
187
179
|
@terminal_width_at_calculation_time != CLI::UI::Terminal.width
|
188
180
|
end
|
@@ -192,7 +184,7 @@ module CLI
|
|
192
184
|
CTRL_C = "\u0003"
|
193
185
|
CTRL_D = "\u0004"
|
194
186
|
|
195
|
-
|
187
|
+
#: -> void
|
196
188
|
def up
|
197
189
|
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
198
190
|
|
@@ -203,7 +195,7 @@ module CLI
|
|
203
195
|
@redraw = true
|
204
196
|
end
|
205
197
|
|
206
|
-
|
198
|
+
#: -> void
|
207
199
|
def down
|
208
200
|
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
209
201
|
|
@@ -214,10 +206,45 @@ module CLI
|
|
214
206
|
@redraw = true
|
215
207
|
end
|
216
208
|
|
209
|
+
#: -> void
|
210
|
+
def first_option
|
211
|
+
@active = @filtered_options.first&.last || -1
|
212
|
+
@redraw = true
|
213
|
+
end
|
214
|
+
|
215
|
+
#: -> void
|
216
|
+
def last_option
|
217
|
+
@active = @filtered_options.last&.last || -1
|
218
|
+
@redraw = true
|
219
|
+
end
|
220
|
+
|
221
|
+
#: -> void
|
222
|
+
def next_page
|
223
|
+
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
224
|
+
|
225
|
+
previous_visible = @filtered_options[active_index + max_lines]
|
226
|
+
previous_visible ||= @filtered_options.last
|
227
|
+
|
228
|
+
@active = previous_visible ? previous_visible.last : -1
|
229
|
+
@redraw = true
|
230
|
+
end
|
231
|
+
|
232
|
+
#: -> void
|
233
|
+
def previous_page
|
234
|
+
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
235
|
+
|
236
|
+
# do not jump into the end of the options if the subtraction result is non-positive
|
237
|
+
previous_visible = @filtered_options[active_index - max_lines] if active_index - max_lines >= 0
|
238
|
+
previous_visible ||= @filtered_options.first
|
239
|
+
|
240
|
+
@active = previous_visible ? previous_visible.last : -1
|
241
|
+
@redraw = true
|
242
|
+
end
|
243
|
+
|
217
244
|
# n is 1-indexed selection
|
218
245
|
# n == 0 if "Done" was selected in @multiple mode
|
219
|
-
|
220
|
-
def select_n(n)
|
246
|
+
#: (Integer n, ?final: bool) -> void
|
247
|
+
def select_n(n, final: false)
|
221
248
|
if @multiple
|
222
249
|
if n == 0
|
223
250
|
@answer = []
|
@@ -230,7 +257,7 @@ module CLI
|
|
230
257
|
end
|
231
258
|
elsif n == 0
|
232
259
|
# Ignore pressing "0" when not in multiple mode
|
233
|
-
elsif should_enter_select_mode?(n)
|
260
|
+
elsif !final && should_enter_select_mode?(n)
|
234
261
|
# When we have more than 9 options, we need to enter select mode
|
235
262
|
# to avoid pre-selecting (e.g) 1 when the user wanted 10.
|
236
263
|
# This also applies to 2 and 20+ options, 3/30+, etc.
|
@@ -243,7 +270,7 @@ module CLI
|
|
243
270
|
@redraw = true
|
244
271
|
end
|
245
272
|
|
246
|
-
|
273
|
+
#: (Integer n) -> bool
|
247
274
|
def should_enter_select_mode?(n)
|
248
275
|
# If we have less than 10 options, we don't need to enter select mode
|
249
276
|
# and we can just select the option directly. This just keeps the code easier
|
@@ -257,29 +284,29 @@ module CLI
|
|
257
284
|
@options.length >= (n * 10)
|
258
285
|
end
|
259
286
|
|
260
|
-
|
287
|
+
#: (String char) -> void
|
261
288
|
def select_bool(char)
|
262
289
|
return unless (@options - ['yes', 'no']).empty?
|
263
290
|
|
264
|
-
index =
|
291
|
+
index = @options.index { |o| o.start_with?(char) } #: as !nil
|
265
292
|
@active = index + 1
|
266
293
|
@answer = index + 1
|
267
294
|
@redraw = true
|
268
295
|
end
|
269
296
|
|
270
|
-
|
297
|
+
#: (String char) -> void
|
271
298
|
def build_selection(char)
|
272
299
|
@active = (@active.to_s + char).to_i
|
273
300
|
@redraw = true
|
274
301
|
end
|
275
302
|
|
276
|
-
|
303
|
+
#: -> void
|
277
304
|
def chop_selection
|
278
305
|
@active = @active.to_s.chop.to_i
|
279
306
|
@redraw = true
|
280
307
|
end
|
281
308
|
|
282
|
-
|
309
|
+
#: (String char) -> void
|
283
310
|
def update_search(char)
|
284
311
|
@redraw = true
|
285
312
|
|
@@ -297,22 +324,22 @@ module CLI
|
|
297
324
|
end
|
298
325
|
end
|
299
326
|
|
300
|
-
|
327
|
+
#: -> void
|
301
328
|
def select_current
|
302
329
|
# Prevent selection of invisible options
|
303
330
|
return unless presented_options.any? { |_, num| num == @active }
|
304
331
|
|
305
|
-
select_n(@active)
|
332
|
+
select_n(@active, final: true)
|
306
333
|
end
|
307
334
|
|
308
|
-
|
335
|
+
#: -> void
|
309
336
|
def process_input_until_redraw_required
|
310
337
|
@redraw = false
|
311
338
|
wait_for_user_input until @redraw
|
312
339
|
end
|
313
340
|
|
314
341
|
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
315
|
-
|
342
|
+
#: -> void
|
316
343
|
def wait_for_user_input
|
317
344
|
char = Prompt.read_char
|
318
345
|
@last_char = char
|
@@ -363,48 +390,53 @@ module CLI
|
|
363
390
|
when 'B' ; down
|
364
391
|
when 'C' ; # Ignore right key
|
365
392
|
when 'D' ; # Ignore left key
|
393
|
+
when '3' ; print("\a")
|
394
|
+
when '5' ; previous_page
|
395
|
+
when '6' ; next_page
|
396
|
+
when 'H' ; first_option
|
397
|
+
when 'F' ; last_option
|
366
398
|
else ; raise Interrupt # unhandled escape sequence.
|
367
399
|
end
|
368
400
|
end
|
369
401
|
end
|
370
402
|
# rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
371
403
|
|
372
|
-
|
404
|
+
#: -> bool
|
373
405
|
def selecting?
|
374
406
|
@state == :line_select
|
375
407
|
end
|
376
408
|
|
377
|
-
|
409
|
+
#: -> bool
|
378
410
|
def filtering?
|
379
411
|
@state == :filter
|
380
412
|
end
|
381
413
|
|
382
|
-
|
414
|
+
#: -> bool
|
383
415
|
def has_filter?
|
384
416
|
!@filter.empty?
|
385
417
|
end
|
386
418
|
|
387
|
-
|
419
|
+
#: -> void
|
388
420
|
def start_filter
|
389
421
|
@state = :filter
|
390
422
|
@redraw = true
|
391
423
|
end
|
392
424
|
|
393
|
-
|
425
|
+
#: -> void
|
394
426
|
def start_line_select
|
395
427
|
@state = :line_select
|
396
428
|
@active = 0
|
397
429
|
@redraw = true
|
398
430
|
end
|
399
431
|
|
400
|
-
|
432
|
+
#: -> void
|
401
433
|
def stop_line_select
|
402
434
|
@state = :root
|
403
435
|
@active = 1 if @active.zero?
|
404
436
|
@redraw = true
|
405
437
|
end
|
406
438
|
|
407
|
-
|
439
|
+
#: (?recalculate: bool) -> Array[[String, Integer?]]
|
408
440
|
def presented_options(recalculate: false)
|
409
441
|
return @presented_options unless recalculate
|
410
442
|
|
@@ -447,44 +479,44 @@ module CLI
|
|
447
479
|
@presented_options
|
448
480
|
end
|
449
481
|
|
450
|
-
|
482
|
+
#: -> void
|
451
483
|
def ensure_visible_is_active
|
452
484
|
unless presented_options.any? { |_, num| num == @active }
|
453
485
|
@active = presented_options.first&.last.to_i
|
454
486
|
end
|
455
487
|
end
|
456
488
|
|
457
|
-
|
489
|
+
#: -> Integer
|
458
490
|
def distance_from_selection_to_end
|
459
491
|
@presented_options.count - index_of_active_option
|
460
492
|
end
|
461
493
|
|
462
|
-
|
494
|
+
#: -> Integer
|
463
495
|
def distance_from_start_to_selection
|
464
496
|
index_of_active_option
|
465
497
|
end
|
466
498
|
|
467
|
-
|
499
|
+
#: -> Integer
|
468
500
|
def index_of_active_option
|
469
501
|
@presented_options.index { |_, num| num == @active }.to_i
|
470
502
|
end
|
471
503
|
|
472
|
-
|
504
|
+
#: -> void
|
473
505
|
def ensure_last_item_is_continuation_marker
|
474
506
|
@presented_options.push(['...', nil]) if @presented_options.last&.last
|
475
507
|
end
|
476
508
|
|
477
|
-
|
509
|
+
#: -> void
|
478
510
|
def ensure_first_item_is_continuation_marker
|
479
511
|
@presented_options.unshift(['...', nil]) if @presented_options.first&.last
|
480
512
|
end
|
481
513
|
|
482
|
-
|
514
|
+
#: -> Integer
|
483
515
|
def max_lines
|
484
516
|
CLI::UI::Terminal.height - (@displaying_metadata ? 3 : 2) # Keeps a one line question visible
|
485
517
|
end
|
486
518
|
|
487
|
-
|
519
|
+
#: -> void
|
488
520
|
def render_options
|
489
521
|
previously_displayed_lines = num_lines
|
490
522
|
|
@@ -535,7 +567,7 @@ module CLI
|
|
535
567
|
end
|
536
568
|
end
|
537
569
|
|
538
|
-
|
570
|
+
#: (String format, String choice) -> String
|
539
571
|
def format_choice(format, choice)
|
540
572
|
eol = CLI::UI::ANSI.clear_to_end_of_line
|
541
573
|
lines = choice.split("\n")
|
@@ -6,24 +6,22 @@ module CLI
|
|
6
6
|
module Prompt
|
7
7
|
# A class that handles the various options of an InteractivePrompt and their callbacks
|
8
8
|
class OptionsHandler
|
9
|
-
|
10
|
-
|
11
|
-
sig { void }
|
9
|
+
#: -> void
|
12
10
|
def initialize
|
13
11
|
@options = {}
|
14
12
|
end
|
15
13
|
|
16
|
-
|
14
|
+
#: -> Array[String]
|
17
15
|
def options
|
18
16
|
@options.keys
|
19
17
|
end
|
20
18
|
|
21
|
-
|
19
|
+
#: (String option) { (String option) -> String } -> void
|
22
20
|
def option(option, &handler)
|
23
21
|
@options[option] = handler
|
24
22
|
end
|
25
23
|
|
26
|
-
|
24
|
+
#: ((Array[String] | String) options) -> String
|
27
25
|
def call(options)
|
28
26
|
case options
|
29
27
|
when Array
|