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 +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]
|
2
2
|
[][gem]
|
3
3
|
[][travis]
|
4
|
+
[][appveyor]
|
4
5
|
[][codeclimate]
|
5
6
|
[][coverage]
|
6
7
|
[][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:
|