gorails 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile.lock +1 -1
  4. data/bin/update-deps +95 -0
  5. data/exe/gorails +3 -2
  6. data/lib/gorails/commands/railsbytes.rb +10 -10
  7. data/lib/gorails/commands.rb +1 -4
  8. data/lib/gorails/version.rb +1 -1
  9. data/lib/gorails.rb +11 -20
  10. data/vendor/deps/cli-kit/REVISION +1 -0
  11. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  12. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  13. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  14. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  15. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  16. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  17. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  18. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  19. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  36. data/vendor/deps/cli-ui/REVISION +1 -0
  37. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  38. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  39. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  40. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  41. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  42. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  43. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  44. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  45. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  65. metadata +57 -1
@@ -0,0 +1,354 @@
1
+ # coding: utf-8
2
+
3
+ # typed: true
4
+
5
+ require 'cli/ui'
6
+ require 'readline'
7
+
8
+ module Readline
9
+ unless const_defined?(:FILENAME_COMPLETION_PROC)
10
+ FILENAME_COMPLETION_PROC = proc do |input|
11
+ directory = input[-1] == '/' ? input : File.dirname(input)
12
+ filename = input[-1] == '/' ? '' : File.basename(input)
13
+
14
+ (Dir.entries(directory).select do |fp|
15
+ fp.start_with?(filename)
16
+ end - (input[-1] == '.' ? [] : ['.', '..'])).map do |fp|
17
+ File.join(directory, fp).gsub(/\A\.\//, '')
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module CLI
24
+ module UI
25
+ module Prompt
26
+ autoload :InteractiveOptions, 'cli/ui/prompt/interactive_options'
27
+ autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
28
+
29
+ class << self
30
+ extend T::Sig
31
+
32
+ # Ask a user a question with either free form answer or a set of answers (multiple choice)
33
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
34
+ # Do not use this method for yes/no questions. Use +confirm+
35
+ #
36
+ # * Handles free form answers (options are nil)
37
+ # * Handles default answers for free form text
38
+ # * Handles file auto completion for file input
39
+ # * Handles interactively choosing answers using +InteractiveOptions+
40
+ #
41
+ # https://user-images.githubusercontent.com/3074765/33799822-47f23302-dd01-11e7-82f3-9072a5a5f611.png
42
+ #
43
+ # ==== Attributes
44
+ #
45
+ # * +question+ - (required) The question to ask the user
46
+ #
47
+ # ==== Options
48
+ #
49
+ # * +:options+ - Options that the user may select from. Will use +InteractiveOptions+ to do so.
50
+ # * +:default+ - The default answer to the question (e.g. they just press enter and don't input anything)
51
+ # * +:is_file+ - Tells the input to use file auto-completion (tab completion)
52
+ # * +:allow_empty+ - Allows the answer to be empty
53
+ # * +:multiple+ - Allow multiple options to be selected
54
+ # * +:filter_ui+ - Enable option filtering (default: true)
55
+ # * +:select_ui+ - Enable long-form option selection (default: true)
56
+ #
57
+ # Note:
58
+ # * +:options+ or providing a +Block+ conflicts with +:default+ and +:is_file+,
59
+ # you cannot set options with either of these keywords
60
+ # * +:default+ conflicts with +:allow_empty:, you cannot set these together
61
+ # * +:options+ conflicts with providing a +Block+ , you may only set one
62
+ # * +:multiple+ can only be used with +:options+ or a +Block+; it is ignored, otherwise.
63
+ #
64
+ # ==== Block (optional)
65
+ #
66
+ # * A Proc that provides a +OptionsHandler+ and uses the public +:option+ method to add options and their
67
+ # respective handlers
68
+ #
69
+ # ==== Return Value
70
+ #
71
+ # * If a +Block+ was not provided, the selected option or response to the free form question will be returned
72
+ # * If a +Block+ was provided, the evaluated value of the +Block+ will be returned
73
+ #
74
+ # ==== Example Usage:
75
+ #
76
+ # Free form question
77
+ # CLI::UI::Prompt.ask('What color is the sky?')
78
+ #
79
+ # Free form question with a file answer
80
+ # CLI::UI::Prompt.ask('Where is your Gemfile located?', is_file: true)
81
+ #
82
+ # Free form question with a default answer
83
+ # CLI::UI::Prompt.ask('What color is the sky?', default: 'blue')
84
+ #
85
+ # Free form question when the answer can be empty
86
+ # CLI::UI::Prompt.ask('What is your opinion on this question?', allow_empty: true)
87
+ #
88
+ # Interactive (multiple choice) question
89
+ # CLI::UI::Prompt.ask('What kind of project is this?', options: %w(rails go ruby python))
90
+ #
91
+ # Interactive (multiple choice) question with defined handlers
92
+ # CLI::UI::Prompt.ask('What kind of project is this?') do |handler|
93
+ # handler.option('rails') { |selection| selection }
94
+ # handler.option('go') { |selection| selection }
95
+ # handler.option('ruby') { |selection| selection }
96
+ # handler.option('python') { |selection| selection }
97
+ # end
98
+ #
99
+ sig do
100
+ params(
101
+ question: String,
102
+ options: T.nilable(T::Array[String]),
103
+ default: T.nilable(T.any(String, T::Array[String])),
104
+ is_file: T::Boolean,
105
+ allow_empty: T::Boolean,
106
+ multiple: T::Boolean,
107
+ filter_ui: T::Boolean,
108
+ select_ui: T::Boolean,
109
+ options_proc: T.nilable(T.proc.params(handler: OptionsHandler).void)
110
+ ).returns(T.any(String, T::Array[String]))
111
+ end
112
+ def ask(
113
+ question,
114
+ options: nil,
115
+ default: nil,
116
+ is_file: false,
117
+ allow_empty: true,
118
+ multiple: false,
119
+ filter_ui: true,
120
+ select_ui: true,
121
+ &options_proc
122
+ )
123
+ has_options = !!(options || block_given?)
124
+ if has_options && default && !multiple
125
+ raise(ArgumentError, 'conflicting arguments: default may not be provided with options when not multiple')
126
+ end
127
+
128
+ if has_options && is_file
129
+ raise(ArgumentError, 'conflicting arguments: is_file is only useful when options are not provided')
130
+ end
131
+
132
+ if options && multiple && default && !(Array(default) - options).empty?
133
+ raise(ArgumentError, 'conflicting arguments: default should only include elements present in options')
134
+ end
135
+
136
+ if multiple && !has_options
137
+ raise(ArgumentError, 'conflicting arguments: options must be provided when multiple is true')
138
+ end
139
+
140
+ if !multiple && default.is_a?(Array)
141
+ raise(ArgumentError, 'conflicting arguments: multiple defaults may only be provided when multiple is true')
142
+ end
143
+
144
+ if has_options
145
+ ask_interactive(
146
+ question,
147
+ options,
148
+ multiple: multiple,
149
+ default: default,
150
+ filter_ui: filter_ui,
151
+ select_ui: select_ui,
152
+ &options_proc
153
+ )
154
+ else
155
+ ask_free_form(question, T.cast(default, T.nilable(String)), is_file, allow_empty)
156
+ end
157
+ end
158
+
159
+ # Asks the user for a single-line answer, without displaying the characters while typing.
160
+ # Typically used for password prompts
161
+ #
162
+ # ==== Return Value
163
+ #
164
+ # The password, without a trailing newline.
165
+ # If the user simply presses "Enter" without typing any password, this will return an empty string.
166
+ sig { params(question: String).returns(String) }
167
+ def ask_password(question)
168
+ require 'io/console'
169
+
170
+ CLI::UI.with_frame_color(:blue) do
171
+ STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
172
+
173
+ # noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
174
+ # No fancy Readline integration (like echoing back) is required for a password prompt anyway.
175
+ password = STDIN.noecho do
176
+ # Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
177
+ # " 123 \n".chomp => " 123 "
178
+ T.must(STDIN.gets).chomp
179
+ end
180
+
181
+ STDOUT.puts # Complete the line
182
+
183
+ password
184
+ end
185
+ end
186
+
187
+ # Asks the user a yes/no question.
188
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
189
+ #
190
+ # ==== Example Usage:
191
+ #
192
+ # Confirmation question
193
+ # CLI::UI::Prompt.confirm('Is the sky blue?')
194
+ #
195
+ # CLI::UI::Prompt.confirm('Do a dangerous thing?', default: false)
196
+ #
197
+ sig { params(question: String, default: T::Boolean).returns(T::Boolean) }
198
+ def confirm(question, default: true)
199
+ ask_interactive(question, default ? ['yes', 'no'] : ['no', 'yes'], filter_ui: false) == 'yes'
200
+ end
201
+
202
+ private
203
+
204
+ sig do
205
+ params(question: String, default: T.nilable(String), is_file: T::Boolean, allow_empty: T::Boolean)
206
+ .returns(String)
207
+ end
208
+ def ask_free_form(question, default, is_file, allow_empty)
209
+ if default && !allow_empty
210
+ raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
211
+ end
212
+
213
+ if default
214
+ puts_question("#{question} (empty = #{default})")
215
+ else
216
+ puts_question(question)
217
+ end
218
+
219
+ # Ask a free form question
220
+ loop do
221
+ line = readline(is_file: is_file)
222
+
223
+ if line.empty? && default
224
+ write_default_over_empty_input(default)
225
+ return default
226
+ end
227
+
228
+ if !line.empty? || allow_empty
229
+ return line
230
+ end
231
+ end
232
+ end
233
+
234
+ sig do
235
+ params(
236
+ question: String,
237
+ options: T.nilable(T::Array[String]),
238
+ multiple: T::Boolean,
239
+ default: T.nilable(T.any(String, T::Array[String])),
240
+ filter_ui: T::Boolean,
241
+ select_ui: T::Boolean
242
+ ).returns(T.any(String, T::Array[String]))
243
+ end
244
+ def ask_interactive(question, options = nil, multiple: false, default: nil, filter_ui: true, select_ui: true)
245
+ raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
246
+
247
+ options ||= if block_given?
248
+ handler = OptionsHandler.new
249
+ yield handler
250
+ handler.options
251
+ end
252
+
253
+ raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
254
+
255
+ navigate_text = if CLI::UI::OS.current.suggest_arrow_keys?
256
+ 'Choose with ↑ ↓ ⏎'
257
+ else
258
+ "Navigate up with 'k' and down with 'j', press Enter to select"
259
+ end
260
+
261
+ instructions = (multiple ? 'Toggle options. ' : '') + navigate_text
262
+ instructions += ", filter with 'f'" if filter_ui
263
+ instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
264
+ puts_question("#{question} {{yellow:(#{instructions})}}")
265
+ resp = interactive_prompt(options, multiple: multiple, default: default)
266
+
267
+ # Clear the line
268
+ print(ANSI.previous_line + ANSI.clear_to_end_of_line)
269
+ # Force StdoutRouter to prefix
270
+ print(ANSI.previous_line + "\n")
271
+
272
+ # reset the question to include the answer
273
+ resp_text = case resp
274
+ when Array
275
+ case resp.size
276
+ when 0
277
+ '<nothing>'
278
+ when 1..2
279
+ resp.join(' and ')
280
+ else
281
+ "#{resp.size} items"
282
+ end
283
+ else
284
+ resp
285
+ end
286
+ puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
287
+
288
+ return T.must(handler).call(resp) if block_given?
289
+
290
+ resp
291
+ end
292
+
293
+ # Useful for stubbing in tests
294
+ sig do
295
+ params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(T::Array[String], String)))
296
+ .returns(T.any(T::Array[String], String))
297
+ end
298
+ def interactive_prompt(options, multiple: false, default: nil)
299
+ InteractiveOptions.call(options, multiple: multiple, default: default)
300
+ end
301
+
302
+ sig { params(default: String).void }
303
+ def write_default_over_empty_input(default)
304
+ CLI::UI.raw do
305
+ STDERR.puts(
306
+ CLI::UI::ANSI.cursor_up(1) +
307
+ "\r" +
308
+ CLI::UI::ANSI.cursor_forward(4) + # TODO: width
309
+ default +
310
+ CLI::UI::Color::RESET.code
311
+ )
312
+ end
313
+ end
314
+
315
+ sig { params(str: String).void }
316
+ def puts_question(str)
317
+ CLI::UI.with_frame_color(:blue) do
318
+ STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
319
+ end
320
+ end
321
+
322
+ sig { params(is_file: T::Boolean).returns(String) }
323
+ def readline(is_file: false)
324
+ if is_file
325
+ Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
326
+ Readline.completion_append_character = ''
327
+ else
328
+ Readline.completion_proc = proc { |*| nil }
329
+ Readline.completion_append_character = ' '
330
+ end
331
+
332
+ # because Readline is a C library, CLI::UI's hooks into $stdout don't
333
+ # work. We could work around this by having CLI::UI use a pipe and a
334
+ # thread to manage output, but the current strategy feels like a
335
+ # better tradeoff.
336
+ prefix = CLI::UI.with_frame_color(:blue) { CLI::UI::Frame.prefix }
337
+ # If a prompt is interrupted on Windows it locks the colour of the terminal from that point on, so we should
338
+ # not change the colour here.
339
+ prompt = prefix + CLI::UI.fmt('{{blue:> }}')
340
+ prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.use_color_prompt?
341
+
342
+ begin
343
+ line = Readline.readline(prompt, true)
344
+ print(CLI::UI::Color::RESET.code)
345
+ line.to_s.chomp
346
+ rescue Interrupt
347
+ CLI::UI.raw { STDERR.puts('^C' + CLI::UI::Color::RESET.code) }
348
+ raise
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,143 @@
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
+ def self.[](type); end
74
+ end
75
+
76
+ Boolean = Object.new.freeze
77
+
78
+ module Configuration
79
+ def self.call_validation_error_handler(signature, opts); end
80
+ def self.call_validation_error_handler=(value); end
81
+ def self.default_checked_level=(default_checked_level); end
82
+ def self.enable_checking_for_sigs_marked_checked_tests; end
83
+ def self.enable_final_checks_on_hooks; end
84
+ def self.enable_legacy_t_enum_migration_mode; end
85
+ def self.reset_final_checks_on_hooks; end
86
+ def self.hard_assert_handler(str, extra); end
87
+ def self.hard_assert_handler=(value); end
88
+ def self.inline_type_error_handler(error); end
89
+ def self.inline_type_error_handler=(value); end
90
+ def self.log_info_handler(str, extra); end
91
+ def self.log_info_handler=(value); end
92
+ def self.scalar_types; end
93
+ def self.scalar_types=(values); end
94
+ # rubocop:disable Naming/InclusiveLanguage
95
+ def self.sealed_violation_whitelist; end
96
+ def self.sealed_violation_whitelist=(sealed_violation_whitelist); end
97
+ # rubocop:enable Naming/InclusiveLanguage
98
+ def self.sig_builder_error_handler(error, location); end
99
+ def self.sig_builder_error_handler=(value); end
100
+ def self.sig_validation_error_handler(error, opts); end
101
+ def self.sig_validation_error_handler=(value); end
102
+ def self.soft_assert_handler(str, extra); end
103
+ def self.soft_assert_handler=(value); end
104
+ end
105
+
106
+ module Enumerable
107
+ def self.[](type); end
108
+ end
109
+
110
+ module Enumerator
111
+ def self.[](type); end
112
+ end
113
+
114
+ module Hash
115
+ def self.[](keys, values); end
116
+ end
117
+
118
+ class Proc
119
+ def bind(*_)
120
+ self
121
+ end
122
+
123
+ def params(*_param)
124
+ self
125
+ end
126
+
127
+ def void
128
+ self
129
+ end
130
+
131
+ def returns(_type)
132
+ self
133
+ end
134
+ end
135
+
136
+ module Range
137
+ def self.[](type); end
138
+ end
139
+
140
+ module Set
141
+ def self.[](type); end
142
+ end
143
+ end
@@ -0,0 +1,46 @@
1
+ # typed: true
2
+ module CLI
3
+ module UI
4
+ module Spinner
5
+ class Async
6
+ extend T::Sig
7
+
8
+ # Convenience method for +initialize+
9
+ #
10
+ sig { params(title: String).returns(Async) }
11
+ def self.start(title)
12
+ new(title)
13
+ end
14
+
15
+ # Initializes a new asynchronous spinner with no specific end.
16
+ # Must call +.stop+ to end the spinner
17
+ #
18
+ # ==== Attributes
19
+ #
20
+ # * +title+ - Title of the spinner to use
21
+ #
22
+ # ==== Example Usage:
23
+ #
24
+ # CLI::UI::Spinner::Async.new('Title')
25
+ #
26
+ sig { params(title: String).void }
27
+ def initialize(title)
28
+ require 'thread'
29
+ sg = CLI::UI::Spinner::SpinGroup.new
30
+ @m = Mutex.new
31
+ @cv = ConditionVariable.new
32
+ sg.add(title) { @m.synchronize { @cv.wait(@m) } }
33
+ @t = Thread.new { sg.wait }
34
+ end
35
+
36
+ # Stops an asynchronous spinner
37
+ #
38
+ sig { returns(T::Boolean) }
39
+ def stop
40
+ @m.synchronize { @cv.signal }
41
+ @t.value
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end