cli-ui 1.5.1 → 2.2.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.
- checksums.yaml +4 -4
- data/README.md +28 -17
- data/lib/cli/ui/ansi.rb +172 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +46 -21
- data/lib/cli/ui/frame/frame_stack.rb +30 -50
- data/lib/cli/ui/frame/frame_style/box.rb +16 -5
- data/lib/cli/ui/frame/frame_style/bracket.rb +19 -8
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +79 -32
- data/lib/cli/ui/glyph.rb +44 -31
- data/lib/cli/ui/os.rb +44 -48
- data/lib/cli/ui/printer.rb +65 -47
- data/lib/cli/ui/progress.rb +50 -33
- data/lib/cli/ui/prompt/interactive_options.rb +114 -68
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +168 -58
- data/lib/cli/ui/sorbet_runtime_stub.rb +169 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +174 -36
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +229 -47
- data/lib/cli/ui/terminal.rb +37 -25
- data/lib/cli/ui/truncater.rb +7 -2
- data/lib/cli/ui/version.rb +3 -1
- data/lib/cli/ui/widgets/base.rb +23 -4
- data/lib/cli/ui/widgets/status.rb +19 -1
- data/lib/cli/ui/widgets.rb +42 -23
- data/lib/cli/ui/wrap.rb +8 -1
- data/lib/cli/ui.rb +336 -188
- data/vendor/reentrant_mutex.rb +78 -0
- metadata +11 -9
data/lib/cli/ui/prompt.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
2
5
|
require 'cli/ui'
|
3
6
|
require 'readline'
|
4
7
|
|
@@ -22,9 +25,26 @@ module CLI
|
|
22
25
|
module Prompt
|
23
26
|
autoload :InteractiveOptions, 'cli/ui/prompt/interactive_options'
|
24
27
|
autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
|
25
|
-
private_constant :InteractiveOptions, :OptionsHandler
|
26
28
|
|
27
29
|
class << self
|
30
|
+
extend T::Sig
|
31
|
+
|
32
|
+
sig { returns(Color) }
|
33
|
+
def instructions_color
|
34
|
+
@instructions_color ||= Color::YELLOW
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the instructions color.
|
38
|
+
#
|
39
|
+
# ==== Attributes
|
40
|
+
#
|
41
|
+
# * +color+ - the color to use for prompt instructions
|
42
|
+
#
|
43
|
+
sig { params(color: Colorable).void }
|
44
|
+
def instructions_color=(color)
|
45
|
+
@instructions_color = CLI::UI.resolve_color(color)
|
46
|
+
end
|
47
|
+
|
28
48
|
# Ask a user a question with either free form answer or a set of answers (multiple choice)
|
29
49
|
# Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
|
30
50
|
# Do not use this method for yes/no questions. Use +confirm+
|
@@ -92,26 +112,52 @@ module CLI
|
|
92
112
|
# handler.option('python') { |selection| selection }
|
93
113
|
# end
|
94
114
|
#
|
115
|
+
sig do
|
116
|
+
params(
|
117
|
+
question: String,
|
118
|
+
options: T.nilable(T::Array[String]),
|
119
|
+
default: T.nilable(T.any(String, T::Array[String])),
|
120
|
+
is_file: T::Boolean,
|
121
|
+
allow_empty: T::Boolean,
|
122
|
+
multiple: T::Boolean,
|
123
|
+
filter_ui: T::Boolean,
|
124
|
+
select_ui: T::Boolean,
|
125
|
+
options_proc: T.nilable(T.proc.params(handler: OptionsHandler).void),
|
126
|
+
).returns(T.any(String, T::Array[String]))
|
127
|
+
end
|
95
128
|
def ask(
|
96
129
|
question,
|
97
130
|
options: nil,
|
98
131
|
default: nil,
|
99
|
-
is_file:
|
132
|
+
is_file: false,
|
100
133
|
allow_empty: true,
|
101
134
|
multiple: false,
|
102
135
|
filter_ui: true,
|
103
136
|
select_ui: true,
|
104
137
|
&options_proc
|
105
138
|
)
|
106
|
-
|
107
|
-
|
139
|
+
has_options = !!(options || block_given?)
|
140
|
+
if has_options && default && !multiple
|
141
|
+
raise(ArgumentError, 'conflicting arguments: default may not be provided with options when not multiple')
|
108
142
|
end
|
109
143
|
|
110
|
-
if
|
144
|
+
if has_options && is_file
|
145
|
+
raise(ArgumentError, 'conflicting arguments: is_file is only useful when options are not provided')
|
146
|
+
end
|
147
|
+
|
148
|
+
if options && multiple && default && !(Array(default) - options).empty?
|
111
149
|
raise(ArgumentError, 'conflicting arguments: default should only include elements present in options')
|
112
150
|
end
|
113
151
|
|
114
|
-
if
|
152
|
+
if multiple && !has_options
|
153
|
+
raise(ArgumentError, 'conflicting arguments: options must be provided when multiple is true')
|
154
|
+
end
|
155
|
+
|
156
|
+
if !multiple && default.is_a?(Array)
|
157
|
+
raise(ArgumentError, 'conflicting arguments: multiple defaults may only be provided when multiple is true')
|
158
|
+
end
|
159
|
+
|
160
|
+
if has_options
|
115
161
|
ask_interactive(
|
116
162
|
question,
|
117
163
|
options,
|
@@ -122,7 +168,7 @@ module CLI
|
|
122
168
|
&options_proc
|
123
169
|
)
|
124
170
|
else
|
125
|
-
ask_free_form(question, default, is_file, allow_empty)
|
171
|
+
ask_free_form(question, T.cast(default, T.nilable(String)), is_file, allow_empty)
|
126
172
|
end
|
127
173
|
end
|
128
174
|
|
@@ -133,21 +179,22 @@ module CLI
|
|
133
179
|
#
|
134
180
|
# The password, without a trailing newline.
|
135
181
|
# If the user simply presses "Enter" without typing any password, this will return an empty string.
|
182
|
+
sig { params(question: String).returns(String) }
|
136
183
|
def ask_password(question)
|
137
184
|
require 'io/console'
|
138
185
|
|
139
|
-
CLI::UI.
|
140
|
-
|
186
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
187
|
+
$stdout.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
|
141
188
|
|
142
189
|
# noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
|
143
190
|
# No fancy Readline integration (like echoing back) is required for a password prompt anyway.
|
144
|
-
password =
|
191
|
+
password = $stdin.noecho do
|
145
192
|
# Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
|
146
193
|
# " 123 \n".chomp => " 123 "
|
147
|
-
|
194
|
+
$stdin.gets.to_s.chomp
|
148
195
|
end
|
149
196
|
|
150
|
-
|
197
|
+
$stdout.puts # Complete the line
|
151
198
|
|
152
199
|
password
|
153
200
|
end
|
@@ -163,38 +210,85 @@ module CLI
|
|
163
210
|
#
|
164
211
|
# CLI::UI::Prompt.confirm('Do a dangerous thing?', default: false)
|
165
212
|
#
|
213
|
+
sig { params(question: String, default: T::Boolean).returns(T::Boolean) }
|
166
214
|
def confirm(question, default: true)
|
167
|
-
ask_interactive(question, default ?
|
215
|
+
ask_interactive(question, default ? ['yes', 'no'] : ['no', 'yes'], filter_ui: false) == 'yes'
|
216
|
+
end
|
217
|
+
|
218
|
+
# Present the user with a message and wait for any key to be pressed, returning the pressed key.
|
219
|
+
#
|
220
|
+
# ==== Example Usage:
|
221
|
+
#
|
222
|
+
# CLI::UI::Prompt.any_key # Press any key to continue...
|
223
|
+
#
|
224
|
+
# CLI::UI::Prompt.any_key('Press RETURN to continue...') # Then check if that's what they pressed
|
225
|
+
sig { params(prompt: String).returns(T.nilable(String)) }
|
226
|
+
def any_key(prompt = 'Press any key to continue...')
|
227
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
228
|
+
puts_question(prompt)
|
229
|
+
read_char
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Wait for any key to be pressed, returning the pressed key.
|
234
|
+
sig { returns(T.nilable(String)) }
|
235
|
+
def read_char
|
236
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
237
|
+
if $stdin.tty? && !ENV['TEST']
|
238
|
+
require 'io/console'
|
239
|
+
$stdin.getch # raw mode for tty
|
240
|
+
else
|
241
|
+
$stdin.getc # returns nil at end of input
|
242
|
+
end
|
243
|
+
end
|
244
|
+
rescue Errno::EIO, Errno::EPIPE, IOError
|
245
|
+
"\e"
|
168
246
|
end
|
169
247
|
|
170
248
|
private
|
171
249
|
|
250
|
+
sig do
|
251
|
+
params(question: String, default: T.nilable(String), is_file: T::Boolean, allow_empty: T::Boolean)
|
252
|
+
.returns(String)
|
253
|
+
end
|
172
254
|
def ask_free_form(question, default, is_file, allow_empty)
|
173
255
|
if default && !allow_empty
|
174
256
|
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
|
175
257
|
end
|
176
258
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
259
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
260
|
+
if default
|
261
|
+
puts_question("#{question} (empty = #{default})")
|
262
|
+
else
|
263
|
+
puts_question(question)
|
264
|
+
end
|
182
265
|
|
183
|
-
|
184
|
-
|
185
|
-
|
266
|
+
# Ask a free form question
|
267
|
+
loop do
|
268
|
+
line = readline(is_file: is_file)
|
186
269
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
270
|
+
if line.empty? && default
|
271
|
+
write_default_over_empty_input(default)
|
272
|
+
return default
|
273
|
+
end
|
191
274
|
|
192
|
-
|
193
|
-
|
275
|
+
if !line.empty? || allow_empty
|
276
|
+
return line
|
277
|
+
end
|
194
278
|
end
|
195
279
|
end
|
196
280
|
end
|
197
281
|
|
282
|
+
sig do
|
283
|
+
params(
|
284
|
+
question: String,
|
285
|
+
options: T.nilable(T::Array[String]),
|
286
|
+
multiple: T::Boolean,
|
287
|
+
default: T.nilable(T.any(String, T::Array[String])),
|
288
|
+
filter_ui: T::Boolean,
|
289
|
+
select_ui: T::Boolean,
|
290
|
+
).returns(T.any(String, T::Array[String]))
|
291
|
+
end
|
198
292
|
def ask_interactive(question, options = nil, multiple: false, default: nil, filter_ui: true, select_ui: true)
|
199
293
|
raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
|
200
294
|
|
@@ -205,7 +299,8 @@ module CLI
|
|
205
299
|
end
|
206
300
|
|
207
301
|
raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
|
208
|
-
|
302
|
+
|
303
|
+
navigate_text = if CLI::UI::OS.current.suggest_arrow_keys?
|
209
304
|
'Choose with ↑ ↓ ⏎'
|
210
305
|
else
|
211
306
|
"Navigate up with 'k' and down with 'j', press Enter to select"
|
@@ -214,55 +309,70 @@ module CLI
|
|
214
309
|
instructions = (multiple ? 'Toggle options. ' : '') + navigate_text
|
215
310
|
instructions += ", filter with 'f'" if filter_ui
|
216
311
|
instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
resp_text = case resp
|
229
|
-
when
|
230
|
-
|
231
|
-
|
232
|
-
|
312
|
+
|
313
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
314
|
+
puts_question("#{question} " + instructions_color.code + "(#{instructions})" + Color::RESET.code)
|
315
|
+
resp = interactive_prompt(options, multiple: multiple, default: default)
|
316
|
+
|
317
|
+
# Clear the line
|
318
|
+
print(ANSI.previous_line + ANSI.clear_to_end_of_line)
|
319
|
+
# Force StdoutRouter to prefix
|
320
|
+
print(ANSI.previous_line + "\n")
|
321
|
+
|
322
|
+
# reset the question to include the answer
|
323
|
+
resp_text = case resp
|
324
|
+
when Array
|
325
|
+
case resp.size
|
326
|
+
when 0
|
327
|
+
'<nothing>'
|
328
|
+
when 1..2
|
329
|
+
resp.join(' and ')
|
330
|
+
else
|
331
|
+
"#{resp.size} items"
|
332
|
+
end
|
233
333
|
else
|
234
|
-
|
334
|
+
resp
|
235
335
|
end
|
236
|
-
|
237
|
-
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
336
|
+
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
238
337
|
|
239
|
-
|
240
|
-
|
338
|
+
if block_given?
|
339
|
+
T.must(handler).call(resp)
|
340
|
+
else
|
341
|
+
resp
|
342
|
+
end
|
343
|
+
end
|
241
344
|
end
|
242
345
|
|
243
346
|
# Useful for stubbing in tests
|
347
|
+
sig do
|
348
|
+
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(T::Array[String], String)))
|
349
|
+
.returns(T.any(T::Array[String], String))
|
350
|
+
end
|
244
351
|
def interactive_prompt(options, multiple: false, default: nil)
|
245
|
-
|
352
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
353
|
+
InteractiveOptions.call(options, multiple: multiple, default: default)
|
354
|
+
end
|
246
355
|
end
|
247
356
|
|
357
|
+
sig { params(default: String).void }
|
248
358
|
def write_default_over_empty_input(default)
|
249
359
|
CLI::UI.raw do
|
250
|
-
|
360
|
+
$stderr.puts(
|
251
361
|
CLI::UI::ANSI.cursor_up(1) +
|
252
362
|
"\r" +
|
253
363
|
CLI::UI::ANSI.cursor_forward(4) + # TODO: width
|
254
364
|
default +
|
255
|
-
CLI::UI::Color::RESET.code
|
365
|
+
CLI::UI::Color::RESET.code,
|
256
366
|
)
|
257
367
|
end
|
258
368
|
end
|
259
369
|
|
370
|
+
sig { params(str: String).void }
|
260
371
|
def puts_question(str)
|
261
|
-
CLI::UI.
|
262
|
-
STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
|
263
|
-
end
|
372
|
+
$stdout.puts(CLI::UI.fmt('{{?}} ' + str))
|
264
373
|
end
|
265
374
|
|
375
|
+
sig { params(is_file: T::Boolean).returns(String) }
|
266
376
|
def readline(is_file: false)
|
267
377
|
if is_file
|
268
378
|
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
@@ -276,18 +386,18 @@ module CLI
|
|
276
386
|
# work. We could work around this by having CLI::UI use a pipe and a
|
277
387
|
# thread to manage output, but the current strategy feels like a
|
278
388
|
# better tradeoff.
|
279
|
-
prefix = CLI::UI
|
389
|
+
prefix = CLI::UI::Frame.prefix
|
280
390
|
# If a prompt is interrupted on Windows it locks the colour of the terminal from that point on, so we should
|
281
391
|
# not change the colour here.
|
282
392
|
prompt = prefix + CLI::UI.fmt('{{blue:> }}')
|
283
|
-
prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.
|
393
|
+
prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.use_color_prompt?
|
284
394
|
|
285
395
|
begin
|
286
396
|
line = Readline.readline(prompt, true)
|
287
397
|
print(CLI::UI::Color::RESET.code)
|
288
398
|
line.to_s.chomp
|
289
399
|
rescue Interrupt
|
290
|
-
CLI::UI.raw {
|
400
|
+
CLI::UI.raw { $stderr.puts('^C' + CLI::UI::Color::RESET.code) }
|
291
401
|
raise
|
292
402
|
end
|
293
403
|
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# typed: ignore
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module T
|
5
|
+
class << self
|
6
|
+
def absurd(value); end
|
7
|
+
def all(type_a, type_b, *types); end
|
8
|
+
def any(type_a, type_b, *types); end
|
9
|
+
def attached_class; end
|
10
|
+
def class_of(klass); end
|
11
|
+
def enum(values); end
|
12
|
+
def nilable(type); end
|
13
|
+
def noreturn; end
|
14
|
+
def self_type; end
|
15
|
+
def type_alias(type = nil, &_blk); end
|
16
|
+
def type_parameter(name); end
|
17
|
+
def untyped; end
|
18
|
+
|
19
|
+
def assert_type!(value, _type, _checked: true)
|
20
|
+
value
|
21
|
+
end
|
22
|
+
|
23
|
+
def cast(value, _type, _checked: true)
|
24
|
+
value
|
25
|
+
end
|
26
|
+
|
27
|
+
def let(value, _type, _checked: true)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
def must(arg, _msg = nil)
|
32
|
+
arg
|
33
|
+
end
|
34
|
+
|
35
|
+
def proc
|
36
|
+
T::Proc.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def reveal_type(value)
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
43
|
+
def unsafe(value)
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Sig
|
49
|
+
def sig(arg0 = nil, &blk); end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Helpers
|
53
|
+
def abstract!; end
|
54
|
+
def interface!; end
|
55
|
+
def final!; end
|
56
|
+
def sealed!; end
|
57
|
+
def mixes_in_class_methods(mod); end
|
58
|
+
end
|
59
|
+
|
60
|
+
module Generic
|
61
|
+
include(T::Helpers)
|
62
|
+
|
63
|
+
def type_parameters(*params); end
|
64
|
+
def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
65
|
+
def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
66
|
+
|
67
|
+
def [](*types)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Array
|
73
|
+
class << self
|
74
|
+
def [](type); end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Boolean = Object.new.freeze
|
79
|
+
|
80
|
+
module Configuration
|
81
|
+
class << self
|
82
|
+
def call_validation_error_handler(signature, opts); end
|
83
|
+
def call_validation_error_handler=(value); end
|
84
|
+
def default_checked_level=(default_checked_level); end
|
85
|
+
def enable_checking_for_sigs_marked_checked_tests; end
|
86
|
+
def enable_final_checks_on_hooks; end
|
87
|
+
def enable_legacy_t_enum_migration_mode; end
|
88
|
+
def reset_final_checks_on_hooks; end
|
89
|
+
def hard_assert_handler(str, extra); end
|
90
|
+
def hard_assert_handler=(value); end
|
91
|
+
def inline_type_error_handler(error); end
|
92
|
+
def inline_type_error_handler=(value); end
|
93
|
+
def log_info_handler(str, extra); end
|
94
|
+
def log_info_handler=(value); end
|
95
|
+
def scalar_types; end
|
96
|
+
def scalar_types=(values); end
|
97
|
+
# rubocop:disable Naming/InclusiveLanguage
|
98
|
+
def sealed_violation_whitelist; end
|
99
|
+
def sealed_violation_whitelist=(sealed_violation_whitelist); end
|
100
|
+
# rubocop:enable Naming/InclusiveLanguage
|
101
|
+
def sig_builder_error_handler(error, location); end
|
102
|
+
def sig_builder_error_handler=(value); end
|
103
|
+
def sig_validation_error_handler(error, opts); end
|
104
|
+
def sig_validation_error_handler=(value); end
|
105
|
+
def soft_assert_handler(str, extra); end
|
106
|
+
def soft_assert_handler=(value); end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module Enumerable
|
111
|
+
class << self
|
112
|
+
def [](type); end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module Enumerator
|
117
|
+
class << self
|
118
|
+
def [](type); end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module Hash
|
123
|
+
class << self
|
124
|
+
def [](keys, values); end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Proc
|
129
|
+
def bind(*_)
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
def params(*_param)
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
def void
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def returns(_type)
|
142
|
+
self
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
module Range
|
147
|
+
class << self
|
148
|
+
def [](type); end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
module Set
|
153
|
+
class << self
|
154
|
+
def [](type); end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class << self
|
159
|
+
def const_added(name)
|
160
|
+
super
|
161
|
+
raise 'When using both cli-ui and sorbet, you must require sorbet before cli-ui'
|
162
|
+
end
|
163
|
+
|
164
|
+
def method_added(name)
|
165
|
+
super
|
166
|
+
raise 'When using both cli-ui and sorbet, you must require sorbet before cli-ui'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/cli/ui/spinner/async.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
module CLI
|
2
4
|
module UI
|
3
5
|
module Spinner
|
4
6
|
class Async
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Convenience method for +initialize+
|
13
|
+
#
|
14
|
+
sig { params(title: String).returns(Async) }
|
15
|
+
def start(title)
|
16
|
+
new(title)
|
17
|
+
end
|
9
18
|
end
|
10
19
|
|
11
20
|
# Initializes a new asynchronous spinner with no specific end.
|
@@ -19,6 +28,7 @@ module CLI
|
|
19
28
|
#
|
20
29
|
# CLI::UI::Spinner::Async.new('Title')
|
21
30
|
#
|
31
|
+
sig { params(title: String).void }
|
22
32
|
def initialize(title)
|
23
33
|
require 'thread'
|
24
34
|
sg = CLI::UI::Spinner::SpinGroup.new
|
@@ -30,6 +40,7 @@ module CLI
|
|
30
40
|
|
31
41
|
# Stops an asynchronous spinner
|
32
42
|
#
|
43
|
+
sig { returns(T::Boolean) }
|
33
44
|
def stop
|
34
45
|
@m.synchronize { @cv.signal }
|
35
46
|
@t.value
|