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 +4 -4
- data/README.md +188 -9
- data/lib/tty-spinner.rb +4 -1
- data/lib/tty/spinner.rb +220 -43
- data/lib/tty/spinner/multi.rb +259 -0
- data/lib/tty/spinner/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4fcc097a53cf0eecf886ee4784e264033b035e5f
|
4
|
+
data.tar.gz: 749c0703578a4f0fd260c930eee3f1adf7e6c502
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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-
|
530
|
+
Copyright (c) 2014-2017 Piotr Murach. See LICENSE for further details.
|
data/lib/tty-spinner.rb
CHANGED
data/lib/tty/spinner.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
-
#
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require '
|
4
|
-
require 'tty
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
145
|
-
|
222
|
+
CURSOR_USAGE_LOCK.synchronize do
|
223
|
+
start
|
224
|
+
sleep_time = 1.0 / @interval
|
146
225
|
|
147
|
-
|
148
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
364
|
+
mon_enter
|
365
|
+
return if done?
|
234
366
|
|
235
367
|
if @hide_cursor
|
236
|
-
write(
|
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
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
304
|
-
|
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
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
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
|
data/lib/tty/spinner/version.rb
CHANGED
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
|
+
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:
|
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:
|