tty-spinner 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c26cc2927383414d21521728ed1f422220ba6a6
4
- data.tar.gz: 3249e7dbba98b16a202ab085bd686994d4abbbde
3
+ metadata.gz: 4fcc097a53cf0eecf886ee4784e264033b035e5f
4
+ data.tar.gz: 749c0703578a4f0fd260c930eee3f1adf7e6c502
5
5
  SHA512:
6
- metadata.gz: 124f9ca4848567f0c79def88f9db19afd25a6844624867292c89013959a96cf31fa710f342b48248aa249e7e5484a8d30da71d919c9277cd162c78adff929e48
7
- data.tar.gz: 58417c89c98f29482eca0b33cf1b285bab9d45260458284e00f934c57d8153a3be0baca2ad64622e5c730592cd15331ddd6827f26597daeac28d03c5a747ba57
6
+ metadata.gz: eb546b547b98b0630006660873fe0224ac839c82c67481d0b3711a453898bee0601f037fdbac596cbff54f1a33d62efb951f084c15a8100459a57ac08023aefb
7
+ data.tar.gz: c8b6121dccdbfbe8d3f0e1f04f662df51940d1ef2b3d42e227835e2c6394f95399bd9ed30db759be67bd615be19c81bbbb812781619f3db26ff5ee4dd0ac45a7
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # TTY::Spinner [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
2
2
  [![Gem Version](https://badge.fury.io/rb/tty-spinner.svg)][gem]
3
3
  [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-spinner.svg?branch=master)][travis]
4
+ [![Build status](https://ci.appveyor.com/api/projects/status/2i5lx3tvyi5l8x3j?svg=true)][appveyor]
4
5
  [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-spinner/badges/gpa.png)][codeclimate]
5
6
  [![Coverage Status](https://coveralls.io/repos/piotrmurach/tty-spinner/badge.svg)][coverage]
6
7
  [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-spinner.svg?branch=master)][inchpages]
@@ -8,6 +9,7 @@
8
9
  [gitter]: https://gitter.im/piotrmurach/tty
9
10
  [gem]: http://badge.fury.io/rb/tty-spinner
10
11
  [travis]: http://travis-ci.org/piotrmurach/tty-spinner
12
+ [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-spinner
11
13
  [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-spinner
12
14
  [coverage]: https://coveralls.io/r/piotrmurach/tty-spinner
13
15
  [inchpages]: http://inch-ci.org/github/piotrmurach/tty-spinner
@@ -37,9 +39,11 @@ Or install it yourself as:
37
39
  ## Contents
38
40
 
39
41
  * [1. Usage](#1-usage)
40
- * [2. API](#2-api)
42
+ * [2. TTY::Spinner API](#2-ttyspinner-api)
41
43
  * [2.1 spin](#21-spin)
42
44
  * [2.2 auto_spin](#22-auto_spin)
45
+ * [2.2.1 pause](#221-pause)
46
+ * [2.2.2 resume](#222-resume)
43
47
  * [2.3 run](#23-run)
44
48
  * [2.4 start](#24-start)
45
49
  * [2.5 stop](#25-stop)
@@ -61,6 +65,15 @@ Or install it yourself as:
61
65
  * [4.1 done](#41-done)
62
66
  * [4.2 success](#42-success)
63
67
  * [4.3 error](#43-error)
68
+ * [5. TTY::Spinner::Multi API](#5-ttyspinnermulti-api)
69
+ * [5.1 reigster](#51-register)
70
+ * [5.2 auto_spin](#52-auto_spin)
71
+ * [5.2.1 manual async](#521-manual-async)
72
+ * [5.2.2 auto async tasks](#522-auto-async-tasks)
73
+ * [5.3 stop](#53-stop)
74
+ * [5.3.1 success](#531-success)
75
+ * [5.3.2 error](#532-error)
76
+ * [5.4 :style](#54-style)
64
77
 
65
78
  ## 1. Usage
66
79
 
@@ -74,11 +87,12 @@ In addition you can provide a message with `:spinner` token and format type you
74
87
 
75
88
  ```ruby
76
89
  spinner = TTY::Spinner.new("[:spinner] Loading ...", format: :pulse_2)
77
- 30.times do
78
- spinner.spin
79
- sleep(0.1)
80
- end
81
- spinner.stop('Done!')
90
+
91
+ spinner.auto_spin # Automatic animation with default interval
92
+
93
+ sleep(2) # Perform task
94
+
95
+ spinner.stop('Done!') # Stop animation
82
96
  ```
83
97
 
84
98
  This would produce animation in your terminal:
@@ -93,9 +107,34 @@ and when finished output:
93
107
  _ Loading ... Done!
94
108
  ```
95
109
 
110
+ Use **TTY::Spinner::Multi** to synchornize multiple spinners:
111
+
112
+ ```ruby
113
+ spinners = TTY::Spinner::Multi.new("[:spinner] top")
114
+
115
+ sp1 = spinners.register "[:spinner] one"
116
+ sp2 = spinners.register "[:spinner] two"
117
+
118
+ sp1.auto_spin
119
+ sp2.auto_spin
120
+
121
+ sleep(2) # Perform work
122
+
123
+ sp1.success
124
+ sp2.success
125
+ ```
126
+
127
+ which when done will display:
128
+
129
+ ```ruby
130
+ ┌[✔]] top
131
+ ├──[✔] one
132
+ └──[✔] two
133
+ ```
134
+
96
135
  For more usage examples please see [examples directory](https://github.com/piotrmurach/tty-spinner/tree/master/examples)
97
136
 
98
- ## 2. API
137
+ ## 2. TTY::Spinner API
99
138
 
100
139
  ### 2.1 spin
101
140
 
@@ -119,6 +158,22 @@ spinner.auto_spin
119
158
 
120
159
  The speed with which the spinning happens is determined by the `:interval` parameter. All the spinner formats have their default intervals specified ([see](https://github.com/piotrmurach/tty-spinner/blob/master/lib/tty/spinner/formats.rb)).
121
160
 
161
+ ### 2.2.1 pause
162
+
163
+ After calling `auto_spin` you can pause spinner execution:
164
+
165
+ ```ruby
166
+ spinner.pause
167
+ ```
168
+
169
+ ### 2.2.2 resume
170
+
171
+ You can continue any paused spinner:
172
+
173
+ ```ruby
174
+ spinner.resume
175
+ ```
176
+
122
177
  ### 2.3 run
123
178
 
124
179
  Use `run` with a code block that will automatically display spinning animation while the block executes and finish animation when the block terminates. Optionally you can provide a stop message to display when animation is finished.
@@ -179,7 +234,6 @@ This will produce:
179
234
  [✖] Task name (error)
180
235
  ```
181
236
 
182
-
183
237
  ### 2.6 update
184
238
 
185
239
  Use `update` call to dynamically change label name(s).
@@ -338,6 +392,131 @@ This event is fired when `error` completion is called. In order to respond to th
338
392
  spinner.on(:error) { ... }
339
393
  ```
340
394
 
395
+ ## 5. TTY::Spinner::Multi API
396
+
397
+ ### 5.1 register
398
+
399
+ Create and register a `TTY::Spinner` under the multispinner
400
+
401
+ ```ruby
402
+ new_spinner = multi_spinner.register("[:spinner] Task 1 name", options)
403
+ ```
404
+
405
+ If no options are given it will use the options given to the multi_spinner when it was initialized to create the new spinner.
406
+ If options are passed, they will override any options given to the multi spinner.
407
+
408
+ ### 5.2 auto_spin
409
+
410
+ The multispinner has to have been given a message on initialization.
411
+ To perform automatic spinning animation use `auto_spin` method like so:
412
+
413
+ ```ruby
414
+ multi_spinner = TTY::Spinner::Multi.new("[:spinner] Top level spinner")
415
+ multi_spinner.auto_spin
416
+ ```
417
+
418
+ If you register spinners without any tasks then you will have to manually control when the `multi_spinner` finishes by calling `stop`, `success` or `error` (see [manual](#521-manual-async)).
419
+
420
+ Alternatively, you can register spinners with tasks that will automatically animate and finish spinners when respective tasks are done (see [async tasks](#522-auto-async-tasks)).
421
+
422
+ The speed with which the spinning happens is determined by the `:interval` parameter. All the spinner formats have their default intervals specified ([see](https://github.com/piotrmurach/tty-spinner/blob/master/lib/tty/spinner/formats.rb)).
423
+
424
+ #### 5.2.1 manual async
425
+
426
+ In case when you wish to have full control over multiple spinners, you will need to perform all actions manually.
427
+
428
+ For example, create a multi spinner that will track status of all registered spinners:
429
+
430
+ ```ruby
431
+ multi_spinner = TTY::Spinner::Multi.new("[:spinner] top")
432
+ ```
433
+
434
+ and then register spinners with their formats:
435
+
436
+ ```
437
+ spinner_1 = spinners.register "[:spinner] one"
438
+ spinner_2 = spinners.register "[:spinner] two"
439
+ ```
440
+
441
+ Once registered, you can set spinners running in separate threads:
442
+
443
+ ```ruby
444
+ multi_spinner.auto_spin
445
+ spinner_1.auto_spin
446
+ spinner_2.auto_spin
447
+ ```
448
+
449
+ Finnally, you need to stop each spinner manually, in our case we mark the multi spinner as failure as one of its children has been marked as failure:
450
+
451
+ ```ruby
452
+ spinner_1.success
453
+ spinner_2.error
454
+ multi_spinner.error
455
+ ```
456
+
457
+ #### 5.2.2 auto async tasks
458
+
459
+ In case when you wish to execute async tasks and update individual spinners automatically, in any order, about their task status use `#register` and pass additional block parameter with the job to be executed.
460
+
461
+ For example, create a multi spinner that will track status of all registered spinners:
462
+
463
+ ```ruby
464
+ multi_spinner = TTY::Spinner::Multi.new("[:spinner] top")
465
+ ```
466
+
467
+ and then register spinners with their respective tasks:
468
+
469
+ ```ruby
470
+ multi_spinner.register("[:spinner] one") { |sp| sleep(2); sp.success('yes 2') }
471
+ multi_spinner.register("[:spinner] two") { |sp| sleep(3); sp.error('no 2') }
472
+ ```
473
+
474
+ Finally, call `#auto_spin` to kick things off:
475
+
476
+ ```ruby
477
+ multi_spinner.auto_spin
478
+ ```
479
+
480
+ If any of the child spinner stops with error then the top level spinner will be marked as failure.
481
+
482
+ ### 5.3 stop
483
+
484
+ In order to stop the multi spinner call `stop`. This will stop the top level spinner, if it exists, and any sub-spinners still spinning.
485
+
486
+ ```ruby
487
+ multi_spinner.stop
488
+ ```
489
+
490
+ #### 5.3.1 success
491
+
492
+ Use `success` call to stop the spinning animation and replace the spinning symbol with checkmark character to indicate successful completion.
493
+ This will also call `#success` on any sub-spinners that are still spinning.
494
+
495
+ ```ruby
496
+ multi_spinner.success
497
+ ```
498
+
499
+ #### 5.3.2 error
500
+
501
+ Use `error` call to stop the spining animation and replace the spinning symbol with cross character to indicate error completion.
502
+ This will also call `#error` on any sub-spinners that are still spinning.
503
+
504
+ ```ruby
505
+ multi_spinner.error
506
+ ```
507
+
508
+ ### 5.4 :style
509
+
510
+ In addition to all [configuration options](#3-configuration) you can style multi spinner like so:
511
+
512
+ ```ruby
513
+ multi_spinner = TTY::Spinner::Multi.new("[:spinner] parent", style: {
514
+ top: '. '
515
+ middle: '|-> '
516
+ bottom: '|__ '
517
+ })
518
+ ```
519
+
341
520
  ## Contributing
342
521
 
343
522
  1. Fork it ( https://github.com/piotrmurach/tty-spinner/fork )
@@ -348,4 +527,4 @@ spinner.on(:error) { ... }
348
527
 
349
528
  ## Copyright
350
529
 
351
- Copyright (c) 2014-2016 Piotr Murach. See LICENSE for further details.
530
+ Copyright (c) 2014-2017 Piotr Murach. See LICENSE for further details.
@@ -1 +1,4 @@
1
- require 'tty/spinner'
1
+ # encoding: utf-8
2
+
3
+ require_relative 'tty/spinner'
4
+ require_relative 'tty/spinner/multi'
@@ -1,7 +1,11 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
- require 'tty/spinner/version'
4
- require 'tty/spinner/formats'
4
+ require 'monitor'
5
+ require 'tty-cursor'
6
+
7
+ require_relative 'spinner/version'
8
+ require_relative 'spinner/formats'
5
9
 
6
10
  module TTY
7
11
  # Used for creating terminal spinner
@@ -9,23 +13,19 @@ module TTY
9
13
  # @api public
10
14
  class Spinner
11
15
  include Formats
16
+ include MonitorMixin
12
17
 
13
18
  # @raised when attempting to join dead thread
14
19
  NotSpinningError = Class.new(StandardError)
15
20
 
16
- ECMA_ESC = "\x1b".freeze
17
21
  ECMA_CSI = "\x1b[".freeze
18
- ECMA_CHA = 'G'.freeze
19
- ECMA_CLR = 'K'.freeze
20
-
21
- DEC_RST = 'l'.freeze
22
- DEC_SET = 'h'.freeze
23
- DEC_TCEM = '?25'.freeze
24
22
 
25
23
  MATCHER = /:spinner/
26
24
  TICK = '✔'.freeze
27
25
  CROSS = '✖'.freeze
28
26
 
27
+ CURSOR_USAGE_LOCK = Monitor.new
28
+
29
29
  # The object that responds to print call defaulting to stderr
30
30
  #
31
31
  # @api public
@@ -83,6 +83,7 @@ module TTY
83
83
  #
84
84
  # @api public
85
85
  def initialize(*args)
86
+ super()
86
87
  options = args.last.is_a?(::Hash) ? args.pop : {}
87
88
  @message = args.empty? ? ':spinner' : args.pop
88
89
  @tokens = {}
@@ -106,25 +107,69 @@ module TTY
106
107
  @done = false
107
108
  @state = :stopped
108
109
  @thread = nil
110
+ @job = nil
111
+ @multispinner= nil
112
+ @index = nil
113
+ @succeeded = false
114
+ @first_run = true
115
+ end
116
+
117
+ # Notifies the TTY::Spinner that it is running under a multispinner
118
+ #
119
+ # @param [TTY::Spinner::Multi] the multispinner that it is running under
120
+ # @param [Integer] the index of this spinner in the multispinner
121
+ #
122
+ # @api private
123
+ def add_multispinner(multispinner, index)
124
+ @multispinner = multispinner
125
+ @index = index
126
+ end
127
+
128
+ # Whether the spinner has completed spinning
129
+ #
130
+ # @return [Boolean] whether or not the spinner has finished
131
+ #
132
+ # @api public
133
+ def done?
134
+ @done
109
135
  end
110
136
 
137
+ # Whether the spinner is spinning
138
+ #
139
+ # @return [Boolean] whether or not the spinner is spinning
140
+ #
141
+ # @api public
111
142
  def spinning?
112
143
  @state == :spinning
113
144
  end
114
145
 
146
+ # Whether the spinner is in the success state.
147
+ # When true the spinner is marked with a success mark.
148
+ #
149
+ # @return [Boolean] whether or not the spinner succeeded
150
+ #
151
+ # @api public
115
152
  def success?
116
- @state == :success
153
+ @succeeded == :success
117
154
  end
118
155
 
156
+ # Whether the spinner is in the error state. This is only true
157
+ # temporarily while it is being marked with a failure mark.
158
+ #
159
+ # @return [Boolean] whether or not the spinner is erroring
160
+ #
161
+ # @api public
119
162
  def error?
120
- @state == :error
163
+ @succeeded == :error
121
164
  end
122
165
 
123
166
  # Register callback
124
167
  #
125
168
  # @api public
126
169
  def on(name, &block)
127
- @callbacks[name] << block
170
+ synchronize do
171
+ @callbacks[name] << block
172
+ end
128
173
  self
129
174
  end
130
175
 
@@ -137,21 +182,91 @@ module TTY
137
182
  reset
138
183
  end
139
184
 
185
+ # Add job to this spinner
186
+ #
187
+ # @api public
188
+ def job(&work)
189
+ synchronize do
190
+ if block_given?
191
+ @job = work
192
+ else
193
+ @job
194
+ end
195
+ end
196
+ end
197
+
198
+ # Execute this spinner job
199
+ #
200
+ # @api public
201
+ def execute_job
202
+ if job.arity.zero?
203
+ instance_eval(&job)
204
+ else
205
+ job.(self)
206
+ end
207
+ end
208
+
209
+ # Check if this spinner has a scheduled job
210
+ #
211
+ # @return [Boolean]
212
+ #
213
+ # @api public
214
+ def job?
215
+ !@job.nil?
216
+ end
217
+
140
218
  # Start automatic spinning animation
141
219
  #
142
220
  # @api public
143
221
  def auto_spin
144
- start
145
- sleep_time = 1.0 / @interval
222
+ CURSOR_USAGE_LOCK.synchronize do
223
+ start
224
+ sleep_time = 1.0 / @interval
146
225
 
147
- @thread = Thread.new do
148
- while @started_at
149
- spin
226
+ spin
227
+ @thread = Thread.new do
150
228
  sleep(sleep_time)
229
+ while @started_at
230
+ if Thread.current['pause']
231
+ Thread.stop
232
+ Thread.current['pause'] = false
233
+ end
234
+ spin
235
+ sleep(sleep_time)
236
+ end
151
237
  end
152
238
  end
153
239
  end
154
240
 
241
+ # Checked if current spinner is paused
242
+ #
243
+ # @return [Boolean]
244
+ #
245
+ # @api public
246
+ def paused?
247
+ !!(@thread && @thread['pause'])
248
+ end
249
+
250
+ # Pause spinner automatic animation
251
+ #
252
+ # @api public
253
+ def pause
254
+ return if paused?
255
+
256
+ synchronize do
257
+ @thread['pause'] = true if @thread
258
+ end
259
+ end
260
+
261
+ # Resume spinner automatic animation
262
+ #
263
+ # @api public
264
+ def resume
265
+ return unless paused?
266
+
267
+ @thread.wakeup if @thread
268
+ end
269
+
155
270
  # Run spinner while executing job
156
271
  #
157
272
  # @param [String] stop_message
@@ -164,9 +279,10 @@ module TTY
164
279
  #
165
280
  # @api public
166
281
  def run(stop_message = '', &block)
282
+ job(&block)
167
283
  auto_spin
168
284
 
169
- @work = Thread.new(&block)
285
+ @work = Thread.new { execute_job }
170
286
  @work.join
171
287
  ensure
172
288
  stop(stop_message)
@@ -199,7 +315,9 @@ module TTY
199
315
  #
200
316
  # @api public
201
317
  def kill
202
- @thread.kill if @thread
318
+ synchronize do
319
+ @thread.kill if @thread
320
+ end
203
321
  end
204
322
 
205
323
  # Perform a spin
@@ -209,18 +327,31 @@ module TTY
209
327
  #
210
328
  # @api public
211
329
  def spin
212
- return if @done
330
+ synchronize do
331
+ return if @done
332
+
333
+ if @hide_cursor && !spinning?
334
+ write(TTY::Cursor.hide)
335
+ end
213
336
 
337
+ data = message.gsub(MATCHER, @frames[@current])
338
+ data = replace_tokens(data)
339
+ write(data, true)
340
+ @current = (@current + 1) % @length
341
+ @state = :spinning
342
+ data
343
+ end
344
+ end
345
+
346
+ # Redraw the indent for this spinner, if it exists
347
+ #
348
+ # @api private
349
+ def redraw_indent
214
350
  if @hide_cursor && !spinning?
215
351
  write(ECMA_CSI + DEC_TCEM + DEC_RST)
216
352
  end
217
353
 
218
- data = message.gsub(MATCHER, @frames[@current])
219
- data = replace_tokens(data)
220
- write(data, true)
221
- @current = (@current + 1) % @length
222
- @state = :spinning
223
- data
354
+ write("", false)
224
355
  end
225
356
 
226
357
  # Finish spining
@@ -230,10 +361,11 @@ module TTY
230
361
  #
231
362
  # @api public
232
363
  def stop(stop_message = '')
233
- return if @done
364
+ mon_enter
365
+ return if done?
234
366
 
235
367
  if @hide_cursor
236
- write(ECMA_CSI + DEC_TCEM + DEC_SET, false)
368
+ write(TTY::Cursor.show, false)
237
369
  end
238
370
  return clear_line if @clear
239
371
 
@@ -244,13 +376,14 @@ module TTY
244
376
  end
245
377
 
246
378
  write(data, true)
247
- write("\n", false) unless @clear
379
+ write("\n", false) unless @clear || @multispinner
248
380
  ensure
249
381
  @state = :stopped
250
382
  @done = true
251
383
  @started_at = nil
252
384
  emit(:done)
253
385
  kill
386
+ mon_exit
254
387
  end
255
388
 
256
389
  # Retrieve next character
@@ -272,25 +405,33 @@ module TTY
272
405
  #
273
406
  # @api public
274
407
  def success(stop_message = '')
275
- @state = :success
276
- stop(stop_message)
277
- emit(:success)
408
+ return if done?
409
+
410
+ synchronize do
411
+ @succeeded = :success
412
+ stop(stop_message)
413
+ emit(:success)
414
+ end
278
415
  end
279
416
 
280
417
  # Finish spinning and set state to :error
281
418
  #
282
419
  # @api public
283
420
  def error(stop_message = '')
284
- @state = :error
285
- stop(stop_message)
286
- emit(:error)
421
+ return if done?
422
+
423
+ synchronize do
424
+ @succeeded = :error
425
+ stop(stop_message)
426
+ emit(:error)
427
+ end
287
428
  end
288
429
 
289
430
  # Clear current line
290
431
  #
291
432
  # @api public
292
433
  def clear_line
293
- output.print(ECMA_CSI + '0m' + ECMA_CSI + '1000D' + ECMA_CSI + ECMA_CLR)
434
+ write(ECMA_CSI + '0m' + TTY::Cursor.clear_line)
294
435
  end
295
436
 
296
437
  # Update string formatting tokens
@@ -300,28 +441,64 @@ module TTY
300
441
  #
301
442
  # @api public
302
443
  def update(tokens)
303
- clear_line if spinning?
304
- @tokens.merge!(tokens)
444
+ synchronize do
445
+ clear_line if spinning?
446
+ @tokens.merge!(tokens)
447
+ end
305
448
  end
306
449
 
307
450
  # Reset the spinner to initial frame
308
451
  #
309
452
  # @api public
310
453
  def reset
311
- @current = 0
454
+ synchronize do
455
+ @current = 0
456
+ @first_run = true
457
+ end
312
458
  end
313
459
 
314
460
  private
315
461
 
462
+ # Execute a block on the proper terminal line if the spinner is running
463
+ # under a multispinner. Otherwise, execute the block on the current line.
464
+ #
465
+ # @api private
466
+ def execute_on_line
467
+ if @multispinner
468
+ CURSOR_USAGE_LOCK.synchronize do
469
+ lines_up = @multispinner.count_line_offset(@index)
470
+
471
+ if @first_run
472
+ yield if block_given?
473
+ output.print "\n"
474
+ @first_run = false
475
+ else
476
+ output.print TTY::Cursor.save
477
+ output.print TTY::Cursor.up(lines_up)
478
+ yield if block_given?
479
+ output.print TTY::Cursor.restore
480
+ end
481
+ end
482
+ else
483
+ yield if block_given?
484
+ end
485
+ end
486
+
316
487
  # Write data out to output
317
488
  #
318
489
  # @return [nil]
319
490
  #
320
491
  # @api private
321
492
  def write(data, clear_first = false)
322
- output.print(ECMA_CSI + '1' + ECMA_CHA) if clear_first
323
- output.print(data)
324
- output.flush
493
+ execute_on_line do
494
+ output.print(TTY::Cursor.column(1)) if clear_first
495
+
496
+ # If there's a top level spinner, print with inset
497
+ characters_in = @multispinner.nil? ? "" : @multispinner.line_inset(self)
498
+
499
+ output.print(characters_in + data)
500
+ output.flush
501
+ end
325
502
  end
326
503
 
327
504
  # Emit callback
@@ -0,0 +1,259 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'forwardable'
5
+
6
+ require_relative '../spinner'
7
+
8
+ module TTY
9
+ class Spinner
10
+ # Used for managing multiple terminal spinners
11
+ #
12
+ # @api public
13
+ class Multi
14
+ include Enumerable
15
+
16
+ extend Forwardable
17
+
18
+ def_delegators :@spinners, :each, :empty?, :length
19
+
20
+ DEFAULT_INSET = {
21
+ top: Gem.win_platform? ? '+ ' : "\u250c ",
22
+ middle: Gem.win_platform? ? '|-- ' : "\u251c\u2500\u2500",
23
+ bottom: Gem.win_platform? ? '|__ ' : "\u2514\u2500\u2500"
24
+ }.freeze
25
+
26
+ # Initialize a multispinner
27
+ #
28
+ # @example
29
+ # spinner = TTY::Spinner::Multi.new
30
+ #
31
+ # @param [String] message
32
+ # the optional message to print in front of the top level spinner
33
+ #
34
+ # @param [Hash] options
35
+ # @option options [Hash] :style
36
+ # keys :top :middle and :bottom can contain Strings that are used to
37
+ # indent the spinners. Ignored if message is blank
38
+ # @option options [Object] :output
39
+ # the object that responds to print call defaulting to stderr
40
+ # @option options [Boolean] :hide_cursor
41
+ # display or hide cursor
42
+ # @option options [Boolean] :clear
43
+ # clear ouptut when finished
44
+ # @option options [Float] :interval
45
+ # the interval for auto spinning
46
+ #
47
+ # @api public
48
+ def initialize(*args)
49
+ @options = args.last.is_a?(::Hash) ? args.pop : {}
50
+ message = args.empty? ? nil : args.pop
51
+ @inset_opts = @options.delete(:style) { DEFAULT_INSET }
52
+ @create_spinner_lock = Mutex.new
53
+ @spinners = []
54
+ @top_spinner = nil
55
+ @top_spinner = register(message) unless message.nil?
56
+
57
+ @callbacks = {
58
+ success: [],
59
+ error: [],
60
+ done: []
61
+ }
62
+ end
63
+
64
+ # Register a new spinner
65
+ #
66
+ # @param [String] pattern
67
+ # the pattern used for creating spinner
68
+ #
69
+ # @api public
70
+ def register(pattern, options = {}, &job)
71
+ spinner = TTY::Spinner.new(pattern, @options.merge(options))
72
+
73
+ @create_spinner_lock.synchronize do
74
+ spinner.add_multispinner(self, @spinners.length)
75
+ spinner.job(&job) if block_given?
76
+ observe_events(spinner) if @top_spinner
77
+ @spinners << spinner
78
+ if @top_spinner
79
+ @spinners.each { |sp| sp.redraw_indent if sp.spinning? || sp.done? }
80
+ end
81
+ end
82
+
83
+ spinner
84
+ end
85
+
86
+ # Observe all child events to notify top spinner of current state
87
+ #
88
+ # @param [TTY::Spinner] spinner
89
+ # the spinner to listen to for events
90
+ #
91
+ # @api private
92
+ def observe_events(spinner)
93
+ spinner.on(:success) { @top_spinner.success if success? }
94
+ .on(:error) { @top_spinner.error if error? }
95
+ .on(:done) { @top_spinner.stop if done? && !success? && !error? }
96
+ end
97
+
98
+ # Get the top level spinner if it exists
99
+ #
100
+ # @return [TTY::Spinner] the top level spinner
101
+ #
102
+ # @api public
103
+ def top_spinner
104
+ raise "No top level spinner" if @top_spinner.nil?
105
+
106
+ @top_spinner
107
+ end
108
+
109
+ # Auto spin the top level spinner & all child spinners
110
+ # that have scheduled jobs
111
+ #
112
+ # @api public
113
+ def auto_spin
114
+ raise "No top level spinner" if @top_spinner.nil?
115
+
116
+ @top_spinner.auto_spin
117
+ jobs = []
118
+ @spinners.each do |spinner|
119
+ if spinner.job?
120
+ spinner.auto_spin
121
+ jobs << Thread.new { spinner.execute_job }
122
+ end
123
+ end
124
+ jobs.each(&:join)
125
+ end
126
+
127
+ # Pause all spinners
128
+ #
129
+ # @api public
130
+ def pause
131
+ @spinners.dup.each(&:pause)
132
+ end
133
+
134
+ # Resume all spinners
135
+ #
136
+ # @api public
137
+ def resume
138
+ @spinners.dup.each(&:resume)
139
+ end
140
+
141
+ # Find relative offset position to which to move the current cursor
142
+ #
143
+ # The position is found among the registered spinners given the current
144
+ # position the spinner is at provided its index
145
+ #
146
+ # @param [Integer] index
147
+ # the position to search from
148
+ #
149
+ # @return [Integer]
150
+ # the current position
151
+ #
152
+ # @api public
153
+ def count_line_offset(index)
154
+ Array(@spinners[index..-1]).reduce(0) do |acc, spinner|
155
+ if spinner.spinning? || spinner.done?
156
+ acc += 1
157
+ end
158
+ acc
159
+ end
160
+ end
161
+
162
+ # Find the number of characters to move into the line
163
+ # before printing the spinner
164
+ #
165
+ # @param [TTY::Spinner] spinner
166
+ # the spinner for which line inset is calculated
167
+ #
168
+ # @return [String]
169
+ # the inset
170
+ #
171
+ # @api public
172
+ def line_inset(spinner)
173
+ return '' if @top_spinner.nil?
174
+
175
+ case spinner
176
+ when @top_spinner
177
+ @inset_opts[:top]
178
+ when @spinners.last
179
+ @inset_opts[:bottom]
180
+ else
181
+ @inset_opts[:middle]
182
+ end
183
+ end
184
+
185
+ # Check if all spinners are done
186
+ #
187
+ # @return [Boolean]
188
+ #
189
+ # @api public
190
+ def done?
191
+ (@spinners - [@top_spinner]).all?(&:done?)
192
+ end
193
+
194
+ # Check if all spinners succeeded
195
+ #
196
+ # @return [Boolean]
197
+ #
198
+ # @api public
199
+ def success?
200
+ (@spinners - [@top_spinner]).all?(&:success?)
201
+ end
202
+
203
+ # Check if any spinner errored
204
+ #
205
+ # @return [Boolean]
206
+ #
207
+ # @api public
208
+ def error?
209
+ (@spinners - [@top_spinner]).any?(&:error?)
210
+ end
211
+
212
+ # Stop all spinners
213
+ #
214
+ # @api public
215
+ def stop
216
+ @spinners.dup.each(&:stop)
217
+ emit :done
218
+ end
219
+
220
+ # Stop all spinners with success status
221
+ #
222
+ # @api public
223
+ def success
224
+ @top_spinner.success if @top_spinner
225
+ @spinners.dup.each(&:success)
226
+ emit :success
227
+ end
228
+
229
+ # Stop all spinners with error status
230
+ #
231
+ # @api public
232
+ def error
233
+ @top_spinner.error if @top_spinner
234
+ @spinners.dup.each(&:error)
235
+ emit :error
236
+ end
237
+
238
+ # Listen on event
239
+ #
240
+ # @api public
241
+ def on(key, &callback)
242
+ unless @callbacks.key?(key)
243
+ raise ArgumentError, "The event #{key} does not exist. "\
244
+ " Use :success, :error, or :done instead"
245
+ end
246
+ @callbacks[key] << callback
247
+ self
248
+ end
249
+
250
+ private
251
+
252
+ def emit(key, *args)
253
+ @callbacks[key].each do |block|
254
+ block.call(*args)
255
+ end
256
+ end
257
+ end # MultiSpinner
258
+ end # Spinner
259
+ end # TTY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Spinner
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end # Spinner
7
7
  end # TTY
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-spinner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-07 00:00:00.000000000 Z
11
+ date: 2017-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tty-cursor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -56,6 +70,7 @@ files:
56
70
  - lib/tty-spinner.rb
57
71
  - lib/tty/spinner.rb
58
72
  - lib/tty/spinner/formats.rb
73
+ - lib/tty/spinner/multi.rb
59
74
  - lib/tty/spinner/version.rb
60
75
  homepage: https://github.com/piotrmurach/tty-spinner
61
76
  licenses: