toys-core 0.9.4 → 0.10.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +47 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +3 -3
  6. data/lib/toys-core.rb +14 -21
  7. data/lib/toys/acceptor.rb +0 -21
  8. data/lib/toys/arg_parser.rb +1 -22
  9. data/lib/toys/cli.rb +102 -70
  10. data/lib/toys/compat.rb +54 -41
  11. data/lib/toys/completion.rb +0 -21
  12. data/lib/toys/context.rb +0 -23
  13. data/lib/toys/core.rb +1 -22
  14. data/lib/toys/dsl/flag.rb +0 -21
  15. data/lib/toys/dsl/flag_group.rb +0 -21
  16. data/lib/toys/dsl/positional_arg.rb +0 -21
  17. data/lib/toys/dsl/tool.rb +135 -51
  18. data/lib/toys/errors.rb +0 -21
  19. data/lib/toys/flag.rb +0 -21
  20. data/lib/toys/flag_group.rb +0 -21
  21. data/lib/toys/input_file.rb +0 -21
  22. data/lib/toys/loader.rb +41 -78
  23. data/lib/toys/middleware.rb +146 -77
  24. data/lib/toys/mixin.rb +0 -21
  25. data/lib/toys/module_lookup.rb +3 -26
  26. data/lib/toys/positional_arg.rb +0 -21
  27. data/lib/toys/source_info.rb +49 -38
  28. data/lib/toys/standard_middleware/add_verbosity_flags.rb +0 -23
  29. data/lib/toys/standard_middleware/apply_config.rb +42 -0
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +7 -28
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +0 -23
  32. data/lib/toys/standard_middleware/show_help.rb +0 -23
  33. data/lib/toys/standard_middleware/show_root_version.rb +0 -23
  34. data/lib/toys/standard_mixins/bundler.rb +113 -0
  35. data/lib/toys/standard_mixins/exec.rb +478 -128
  36. data/lib/toys/standard_mixins/fileutils.rb +0 -21
  37. data/lib/toys/standard_mixins/gems.rb +2 -24
  38. data/lib/toys/standard_mixins/highline.rb +0 -21
  39. data/lib/toys/standard_mixins/terminal.rb +0 -21
  40. data/lib/toys/template.rb +0 -21
  41. data/lib/toys/tool.rb +22 -34
  42. data/lib/toys/utils/completion_engine.rb +0 -21
  43. data/lib/toys/utils/exec.rb +142 -71
  44. data/lib/toys/utils/gems.rb +221 -67
  45. data/lib/toys/utils/gems/gemfile.rb +6 -0
  46. data/lib/toys/utils/help_text.rb +0 -21
  47. data/lib/toys/utils/terminal.rb +47 -38
  48. data/lib/toys/wrappable_string.rb +0 -21
  49. metadata +7 -116
@@ -1,30 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
3
+ require "monitor"
4
+ require "rubygems"
23
5
 
24
6
  module Toys
25
7
  module Utils
26
8
  ##
27
- # A helper module that activates and installs gems.
9
+ # A helper class that activates and installs gems and sets up bundler.
28
10
  #
29
11
  # This class is not loaded by default. Before using it directly, you should
30
12
  # `require "toys/utils/gems"`
@@ -58,6 +40,30 @@ module Toys
58
40
  end
59
41
  end
60
42
 
43
+ ##
44
+ # Failed to run Bundler
45
+ #
46
+ class BundlerFailedError < ::StandardError
47
+ end
48
+
49
+ ##
50
+ # Could not find a Gemfile
51
+ #
52
+ class GemfileNotFoundError < BundlerFailedError
53
+ end
54
+
55
+ ##
56
+ # The bundle is not and could not be installed
57
+ #
58
+ class BundleNotInstalledError < BundlerFailedError
59
+ end
60
+
61
+ ##
62
+ # Bundler has already been run; cannot do so again
63
+ #
64
+ class AlreadyBundledError < BundlerFailedError
65
+ end
66
+
61
67
  ##
62
68
  # Activate the given gem. If it is not present, attempt to install it (or
63
69
  # inform the user to update the bundle).
@@ -73,24 +79,48 @@ module Toys
73
79
  ##
74
80
  # Create a new gem activator.
75
81
  #
76
- # @param input [IO] Input IO
77
- # @param output [IO] Output IO
78
- # @param suppress_confirm [Boolean] Suppress the confirmation prompt and
79
- # just use the given `default_confirm` value. Default is false,
80
- # indicating the confirmation prompt appears by default.
81
- # @param default_confirm [Boolean] Default response for the confirmation
82
- # prompt. Default is true.
83
- #
84
- def initialize(input: $stdin,
85
- output: $stderr,
86
- suppress_confirm: false,
87
- default_confirm: true)
88
- require "toys/utils/terminal"
89
- require "toys/utils/exec"
90
- @terminal = Utils::Terminal.new(input: input, output: output)
91
- @exec = Utils::Exec.new
92
- @suppress_confirm = suppress_confirm ? true : false
93
- @default_confirm = default_confirm ? true : false
82
+ # @param on_missing [:confirm,:error,:install] What to do if a needed gem
83
+ # is not installed. Possible values:
84
+ #
85
+ # * `:confirm` - prompt the user on whether to install
86
+ # * `:error` - raise an exception
87
+ # * `:install` - just install the gem
88
+ #
89
+ # The default is `:confirm`.
90
+ #
91
+ # @param on_conflict [:error,:warn,:ignore] What to do if bundler has
92
+ # already been run with a different Gemfile. Possible values:
93
+ #
94
+ # * `:error` - raise an exception
95
+ # * `:ignore` - just silently proceed without bundling again
96
+ # * `:warn` - print a warning and proceed without bundling again
97
+ #
98
+ # The default is `:error`.
99
+ #
100
+ # @param terminal [Toys::Utils::Terminal] Terminal to use (optional)
101
+ # @param input [IO] Input IO (optional, defaults to STDIN)
102
+ # @param output [IO] Output IO (optional, defaults to STDOUT)
103
+ # @param suppress_confirm [Boolean] Deprecated. Use `on_missing` instead.
104
+ # @param default_confirm [Boolean] Deprecated. Use `on_missing` instead.
105
+ #
106
+ def initialize(on_missing: nil,
107
+ on_conflict: nil,
108
+ terminal: nil,
109
+ input: nil,
110
+ output: nil,
111
+ suppress_confirm: nil,
112
+ default_confirm: nil)
113
+ @default_confirm = default_confirm || default_confirm.nil? ? true : false
114
+ @on_missing = on_missing ||
115
+ if suppress_confirm
116
+ @default_confirm ? :install : :error
117
+ else
118
+ :confirm
119
+ end
120
+ @on_conflict = on_conflict || :error
121
+ @terminal = terminal
122
+ @input = input || ::STDIN
123
+ @output = output || ::STDOUT
94
124
  end
95
125
 
96
126
  ##
@@ -102,13 +132,57 @@ module Toys
102
132
  # @return [void]
103
133
  #
104
134
  def activate(name, *requirements)
105
- gem(name, *requirements)
106
- rescue ::Gem::LoadError => e
107
- handle_activation_error(e, name, requirements)
135
+ Gems.synchronize do
136
+ begin
137
+ gem(name, *requirements)
138
+ rescue ::Gem::LoadError => e
139
+ handle_activation_error(e, name, requirements)
140
+ end
141
+ end
142
+ end
143
+
144
+ ##
145
+ # Set up the bundle.
146
+ #
147
+ # @param groups [Array<String>] The groups to include in setup
148
+ # @param search_dirs [Array<String>] Directories to search for a Gemfile
149
+ # @return [void]
150
+ #
151
+ def bundle(groups: nil,
152
+ search_dirs: nil)
153
+ Gems.synchronize do
154
+ gemfile_path = find_gemfile(Array(search_dirs))
155
+ if configure_gemfile(gemfile_path)
156
+ activate("bundler", "~> 2.1")
157
+ require "bundler"
158
+ setup_bundle(gemfile_path, groups || [])
159
+ end
160
+ end
161
+ end
162
+
163
+ @global_mutex = ::Monitor.new
164
+
165
+ ## @private
166
+ def self.synchronize(&block)
167
+ @global_mutex.synchronize(&block)
108
168
  end
109
169
 
110
170
  private
111
171
 
172
+ def terminal
173
+ @terminal ||= begin
174
+ require "toys/utils/terminal"
175
+ Utils::Terminal.new(input: @input, output: @output)
176
+ end
177
+ end
178
+
179
+ def exec_util
180
+ @exec_util ||= begin
181
+ require "toys/utils/exec"
182
+ Utils::Exec.new
183
+ end
184
+ end
185
+
112
186
  def handle_activation_error(error, name, requirements)
113
187
  is_missing_spec =
114
188
  if defined?(::Gem::MissingSpecError)
@@ -116,15 +190,15 @@ module Toys
116
190
  else
117
191
  error.message.include?("Could not find")
118
192
  end
119
- unless is_missing_spec
120
- report_error(name, requirements, error)
193
+ if !is_missing_spec || @on_missing == :error
194
+ report_activation_error(name, requirements, error)
121
195
  return
122
196
  end
123
- install_gem(name, requirements)
197
+ confirm_and_install_gem(name, requirements)
124
198
  begin
125
199
  gem(name, *requirements)
126
200
  rescue ::Gem::LoadError => e
127
- report_error(name, requirements, e)
201
+ report_activation_error(name, requirements, e)
128
202
  end
129
203
  end
130
204
 
@@ -132,41 +206,121 @@ module Toys
132
206
  "#{name.inspect}, #{requirements.map(&:inspect).join(', ')}"
133
207
  end
134
208
 
135
- def install_gem(name, requirements)
136
- requirements_text = gem_requirements_text(name, requirements)
137
- response =
138
- if @suppress_confirm
139
- @default_confirm
140
- else
141
- @terminal.confirm("Gem needed: #{requirements_text}. Install? ",
142
- default: @default_confirm)
209
+ def confirm_and_install_gem(name, requirements)
210
+ if @on_missing == :confirm
211
+ requirements_text = gem_requirements_text(name, requirements)
212
+ response = terminal.confirm("Gem needed: #{requirements_text}. Install? ",
213
+ default: @default_confirm)
214
+ unless response
215
+ raise InstallFailedError, "Canceled installation of needed gem: #{requirements_text}"
143
216
  end
144
- unless response
145
- raise InstallFailedError, "Canceled installation of needed gem: #{requirements_text}"
146
- end
147
- perform_install(name, requirements)
148
- end
149
-
150
- def perform_install(name, requirements)
151
- result = @terminal.spinner(leading_text: "Installing gem #{name}... ",
152
- final_text: "Done.\n") do
153
- @exec.exec(["gem", "install", name, "--version", requirements.join(",")],
154
- out: :capture, err: :capture)
155
217
  end
156
- @terminal.puts(result.captured_out + result.captured_err)
218
+ result = exec_util.exec(["gem", "install", name, "--version", requirements.join(",")])
157
219
  if result.error?
158
220
  raise InstallFailedError, "Failed to install gem #{name}"
159
221
  end
160
222
  ::Gem::Specification.reset
161
223
  end
162
224
 
163
- def report_error(name, requirements, err)
225
+ def report_activation_error(name, requirements, err)
164
226
  if ::ENV["BUNDLE_GEMFILE"]
165
227
  raise GemfileUpdateNeededError.new(gem_requirements_text(name, requirements),
166
228
  ::ENV["BUNDLE_GEMFILE"])
167
229
  end
168
230
  raise ActivationFailedError, err.message
169
231
  end
232
+
233
+ def find_gemfile(search_dirs)
234
+ search_dirs.each do |dir|
235
+ gemfile_path = ::File.join(dir, "Gemfile")
236
+ return gemfile_path if ::File.readable?(gemfile_path)
237
+ end
238
+ raise GemfileNotFoundError, "Gemfile not found"
239
+ end
240
+
241
+ def configure_gemfile(gemfile_path)
242
+ old_path = ::ENV["BUNDLE_GEMFILE"]
243
+ if old_path
244
+ if gemfile_path != old_path
245
+ case @on_conflict
246
+ when :warn
247
+ terminal.puts("Warning: could not set up bundler because it is already set up.", :red)
248
+ when :error
249
+ raise AlreadyBundledError, "Could not set up bundler because it is already set up"
250
+ end
251
+ end
252
+ return false
253
+ end
254
+ ::ENV["BUNDLE_GEMFILE"] = gemfile_path
255
+ true
256
+ end
257
+
258
+ def setup_bundle(gemfile_path, groups)
259
+ begin
260
+ modify_bundle_definition(gemfile_path)
261
+ ::Bundler.setup(*groups)
262
+ rescue ::Bundler::GemNotFound
263
+ restore_toys_libs
264
+ install_bundle(gemfile_path)
265
+ ::Bundler.reset!
266
+ modify_bundle_definition(gemfile_path)
267
+ ::Bundler.setup(*groups)
268
+ end
269
+ restore_toys_libs
270
+ end
271
+
272
+ def modify_bundle_definition(gemfile_path)
273
+ builder = ::Bundler::Dsl.new
274
+ builder.eval_gemfile(gemfile_path)
275
+ begin
276
+ builder.eval_gemfile(::File.join(__dir__, "gems", "gemfile.rb"))
277
+ rescue ::Bundler::Dsl::DSLError
278
+ terminal.puts(
279
+ "WARNING: Unable to integrate your Gemfile into the Toys runtime.\n" \
280
+ "When using the Toys Bundler integration features, do NOT list\n" \
281
+ "the toys or toys-core gems directly in your Gemfile. They can be\n" \
282
+ "dependencies of another gem, but cannot be listed directly.",
283
+ :red
284
+ )
285
+ return
286
+ end
287
+ toys_gems = ["toys-core"]
288
+ toys_gems << "toys" if ::Toys.const_defined?(:VERSION)
289
+ definition = builder.to_definition(gemfile_path + ".lock", { gems: toys_gems })
290
+ ::Bundler.instance_variable_set(:@definition, definition)
291
+ end
292
+
293
+ def restore_toys_libs
294
+ $LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
295
+ $LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
296
+ if ::Toys.const_defined?(:LIB_PATH)
297
+ $LOAD_PATH.delete(::Toys::LIB_PATH)
298
+ $LOAD_PATH.unshift(::Toys::LIB_PATH)
299
+ end
300
+ end
301
+
302
+ def permission_to_bundle?
303
+ case @on_missing
304
+ when :install
305
+ true
306
+ when :error
307
+ false
308
+ else
309
+ terminal.confirm("Your bundle requires additional gems. Install? ",
310
+ default: @default_confirm)
311
+ end
312
+ end
313
+
314
+ def install_bundle(gemfile_path)
315
+ gemfile_dir = ::File.dirname(gemfile_path)
316
+ unless permission_to_bundle?
317
+ raise BundleNotInstalledError,
318
+ "Your bundle is not installed. Consider running" \
319
+ " `cd #{gemfile_dir} && bundle install`"
320
+ end
321
+ require "bundler/cli"
322
+ ::Bundler::CLI.start(["install"])
323
+ end
170
324
  end
171
325
  end
172
326
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless defined?(@__toys_dev_gemspec)
4
+ gem("toys-core", ::Toys::Core::VERSION)
5
+ gem("toys", ::Toys::VERSION) if ::Toys.const_defined?(:VERSION)
6
+ end
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  module Utils
26
5
  ##
@@ -1,32 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  require "stringio"
25
4
  require "monitor"
26
5
 
27
6
  begin
28
7
  require "io/console"
29
- rescue ::LoadError # rubocop:disable Lint/SuppressedException
8
+ rescue ::LoadError
30
9
  # TODO: alternate methods of getting terminal size
31
10
  end
32
11
 
@@ -146,6 +125,8 @@ module Toys
146
125
  styled ? true : false
147
126
  end
148
127
  @named_styles = BUILTIN_STYLE_NAMES.dup
128
+ @output_mutex = ::Monitor.new
129
+ @input_mutex = ::Monitor.new
149
130
  end
150
131
 
151
132
  ##
@@ -164,7 +145,7 @@ module Toys
164
145
  # Whether output is styled
165
146
  # @return [Boolean]
166
147
  #
167
- attr_accessor :styled
148
+ attr_reader :styled
168
149
 
169
150
  ##
170
151
  # Write a partial line without appending a newline.
@@ -175,11 +156,41 @@ module Toys
175
156
  # @return [self]
176
157
  #
177
158
  def write(str = "", *styles)
178
- output&.write(apply_styles(str, *styles))
179
- output&.flush
159
+ @output_mutex.synchronize do
160
+ begin
161
+ output&.write(apply_styles(str, *styles))
162
+ output&.flush
163
+ rescue ::IOError
164
+ nil
165
+ end
166
+ end
180
167
  self
181
168
  end
182
169
 
170
+ ##
171
+ # Read a line, blocking until one is available.
172
+ #
173
+ # @return [String] the entire string including the temrinating newline
174
+ # @return [nil] if the input is closed or at eof, or there is no input
175
+ #
176
+ def readline
177
+ @input_mutex.synchronize do
178
+ begin
179
+ input&.gets
180
+ rescue ::IOError
181
+ nil
182
+ end
183
+ end
184
+ end
185
+
186
+ ##
187
+ # This method is defined so that `::Logger` will recognize a terminal as
188
+ # a log device target, but it does not actually close anything.
189
+ #
190
+ def close
191
+ nil
192
+ end
193
+
183
194
  ##
184
195
  # Write a line, appending a newline if one is not already present.
185
196
  #
@@ -233,7 +244,7 @@ module Toys
233
244
  prompt = "#{ptext} #{trailing_text}#{pspaces}"
234
245
  end
235
246
  write(prompt, *styles)
236
- resp = input&.gets.to_s.chomp
247
+ resp = readline.to_s.chomp
237
248
  resp.empty? ? default.to_s : resp
238
249
  end
239
250
 
@@ -296,13 +307,13 @@ module Toys
296
307
  return nil unless block_given?
297
308
  frame_length ||= DEFAULT_SPINNER_FRAME_LENGTH
298
309
  frames ||= DEFAULT_SPINNER_FRAMES
299
- output.write(leading_text) unless leading_text.empty?
310
+ write(leading_text) unless leading_text.empty?
300
311
  spin = SpinDriver.new(self, frames, Array(style), frame_length)
301
312
  begin
302
313
  yield
303
314
  ensure
304
315
  spin.stop
305
- output.write(final_text) unless final_text.empty?
316
+ write(final_text) unless final_text.empty?
306
317
  end
307
318
  end
308
319
 
@@ -312,8 +323,8 @@ module Toys
312
323
  # @return [Array(Integer,Integer)]
313
324
  #
314
325
  def size
315
- if @output.respond_to?(:tty?) && @output.tty? && @output.respond_to?(:winsize)
316
- @output.winsize.reverse
326
+ if output.respond_to?(:tty?) && output.tty? && output.respond_to?(:winsize)
327
+ output.winsize.reverse
317
328
  else
318
329
  [80, 25]
319
330
  end
@@ -422,10 +433,8 @@ module Toys
422
433
 
423
434
  ## @private
424
435
  class SpinDriver
425
- include ::MonitorMixin
426
-
427
436
  def initialize(terminal, frames, style, frame_length)
428
- super()
437
+ @mutex = ::Monitor.new
429
438
  @terminal = terminal
430
439
  @frames = frames.map do |f|
431
440
  [@terminal.apply_styles(f, *style), Terminal.remove_style_escapes(f).size]
@@ -433,12 +442,12 @@ module Toys
433
442
  @frame_length = frame_length
434
443
  @cur_frame = 0
435
444
  @stopping = false
436
- @cond = new_cond
445
+ @cond = @mutex.new_cond
437
446
  @thread = @terminal.output.tty? ? start_thread : nil
438
447
  end
439
448
 
440
449
  def stop
441
- synchronize do
450
+ @mutex.synchronize do
442
451
  @stopping = true
443
452
  @cond.broadcast
444
453
  end
@@ -450,12 +459,12 @@ module Toys
450
459
 
451
460
  def start_thread
452
461
  ::Thread.new do
453
- synchronize do
462
+ @mutex.synchronize do
454
463
  until @stopping
455
- @terminal.output.write(@frames[@cur_frame][0])
464
+ @terminal.write(@frames[@cur_frame][0])
456
465
  @cond.wait(@frame_length)
457
466
  size = @frames[@cur_frame][1]
458
- @terminal.output.write("\b" * size + " " * size + "\b" * size)
467
+ @terminal.write("\b" * size + " " * size + "\b" * size)
459
468
  @cur_frame += 1
460
469
  @cur_frame = 0 if @cur_frame >= @frames.size
461
470
  end