tty-spinner 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: