popro 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +1 -0
- data/LICENSE +7 -0
- data/README.md +321 -0
- data/bin/popro +5 -0
- data/lib/popro.rb +31 -0
- data/lib/popro/context.rb +97 -0
- data/lib/popro/formatter.rb +99 -0
- data/lib/popro/indicator.rb +65 -0
- data/lib/popro/indicator/rails.rb +43 -0
- data/lib/popro/info.rb +68 -0
- data/lib/popro/progress.rb +41 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 23e8aecacb06e0bfe98467ac74b74f0b5e90ddc65f57069ae4bf29bdc7073b8a
|
4
|
+
data.tar.gz: 6a220ecf6de03a768aeed0d4998983ada21d037859a9deafe6263b4604b201d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cb458d4d2a5169342ede984048be23c86d101db7546585e408e7470fe799f3344df4d2c691e42e43ff5ddd915e9af284a44ae59a7780890b06340a70a5e56419
|
7
|
+
data.tar.gz: 6a8fc7aef7feec2b0c39af0f1478f1cebaf4ccb8667edcc0842d119019dec709998e2d0a3930869646b51a210f4758eaa2b721894b7a1b79d554f44294996ce3
|
data/CHANGELOG
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1 initial version
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2020 MikeSmithEU
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
# Po'Pro: The Poor-Man's Progress Indicator
|
2
|
+
|
3
|
+
## Why?
|
4
|
+
|
5
|
+
Easier and cleaner progress indication.
|
6
|
+
|
7
|
+
## How?
|
8
|
+
|
9
|
+
TODO: properly update documentation, simplify some stuff. 100% code coverage
|
10
|
+
|
11
|
+
### Basic usage
|
12
|
+
|
13
|
+
The invocation `Popro#did(yielded)` is used to signify that one step in progress has been finished.
|
14
|
+
|
15
|
+
1. Create a `Progress` object
|
16
|
+
```ruby
|
17
|
+
progress = Popro.new(500)
|
18
|
+
```
|
19
|
+
|
20
|
+
2. Use the progress indicator
|
21
|
+
```ruby
|
22
|
+
(0..499).each { progress.did }
|
23
|
+
```
|
24
|
+
|
25
|
+
Will result in a default progress outputted to STDOUT, e.g.
|
26
|
+
|
27
|
+
```bash
|
28
|
+
irb> (0..499).each { |n| sleep(0.1) & "Just did #{n}" }
|
29
|
+
[221/500] 44.2% Just did 220
|
30
|
+
```
|
31
|
+
|
32
|
+
Note, `Popro#did` can be passed as a `Proc` using the shortcut `&progress`.
|
33
|
+
So below code would be equivalent and slightly more concise:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
(0.499).each &progress
|
37
|
+
```
|
38
|
+
|
39
|
+
Block notation is usually preferable (cleaner) to using the `Progress` instance directly (see below).
|
40
|
+
The functionality is the same though.
|
41
|
+
|
42
|
+
### Using block context
|
43
|
+
|
44
|
+
`Popro.new` can be called with a block. This block will then be executed after `Progress` has initialized
|
45
|
+
and will receive a `Context` instance as the first parameter. On this instance you can call e.g.
|
46
|
+
`each`, `did`, `will` and `formatter`.
|
47
|
+
|
48
|
+
E.g.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Popro.new(500) do |p|
|
52
|
+
(1..500).each &p
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
or
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Popro.new do |progress|
|
60
|
+
progress.each(1..500)
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Using `Proc` shortcut notation
|
65
|
+
|
66
|
+
`&Popro.new` can be used for a more concise syntax. This is just syntactical sugar for calling
|
67
|
+
`Popro#did` directly.
|
68
|
+
|
69
|
+
For example:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
def job(size=100)
|
73
|
+
(1..size).each do
|
74
|
+
sleep 0.01
|
75
|
+
yield
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Below invocations are all equivalent.
|
80
|
+
|
81
|
+
puts "\n job(&progress)"
|
82
|
+
progress = Popro.new(100)
|
83
|
+
job(&progress)
|
84
|
+
|
85
|
+
puts "\n job { progress.did }"
|
86
|
+
progress = Popro.new(100)
|
87
|
+
job { progress.did }
|
88
|
+
|
89
|
+
puts "\n using Popro.new(100)"
|
90
|
+
|
91
|
+
# below code is a more concise equivalent to:
|
92
|
+
# `progress = Popro.new(100); job &progress`
|
93
|
+
# or
|
94
|
+
# `progress = Popro.new(100); job { |*args| progress.did(*args) }`
|
95
|
+
|
96
|
+
job(&Popro.new(100))
|
97
|
+
```
|
98
|
+
|
99
|
+
### Using each notation
|
100
|
+
|
101
|
+
`Popro.each` or `Popro#each` can be used to loop over enumerables while providing progress
|
102
|
+
feedback.
|
103
|
+
|
104
|
+
`Popro.each(enumerable, size=nil)` will use `enumerable.size` to determine the total amount of elements
|
105
|
+
only if the `size` argument is `nil`.
|
106
|
+
If this method is not available you should manually provide a `size` argument, i.e. `Popro.each(enumerable, 50)`.
|
107
|
+
|
108
|
+
Shortcut methods `Popro.each0(enumerable)` and `Popro#each0(enumerable)` are available for enumerables
|
109
|
+
that are used if you do not want to update the total with `enumerable.size`.
|
110
|
+
I.e. this is equivalent to `Popro.each(enumerable, 0)` and `Popro#each(enumerable, 0)` respectively.
|
111
|
+
|
112
|
+
E.g. below are all equivalent.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
sleeper = proc { sleep 0.05 }
|
116
|
+
|
117
|
+
Popro.each(1..50) { sleep 0.05 }
|
118
|
+
Popro.new.each(1..50, 50) { sleep 0.05 }
|
119
|
+
Popro.new(50).each(1..50, 0) { sleep 0.05 }
|
120
|
+
Popro.new(50).each0(1..50) { sleep 0.05 }
|
121
|
+
|
122
|
+
Popro.each(1..50, &sleeper)
|
123
|
+
Popro.new(50).each(1..50, 0, &sleeper)
|
124
|
+
Popro.new(50).each0(1..50, &sleeper)
|
125
|
+
|
126
|
+
Popro.each(1..50, 50, &sleeper)
|
127
|
+
Popro.new.each(1..50, 50, &sleeper)
|
128
|
+
```
|
129
|
+
|
130
|
+
It is easy to chain multiple items, the total will be updated at the start of each `each` block. It
|
131
|
+
might be preferable to provide a total sum in advance, and passing a `size` argument of `0` to
|
132
|
+
`Popro#each` (or using `Popro#each0`), e.g.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
|
136
|
+
Popro.new(200)
|
137
|
+
.each0(1..50) { |n| sleep(0.1) && "#{n} of first 50" }
|
138
|
+
.each0(1..100) { |n| sleep(0.1) && "#{n} of second 100" }
|
139
|
+
.each0(1..50) { |n| sleep(0.1) && "#{n} of last 50" }
|
140
|
+
|
141
|
+
```
|
142
|
+
|
143
|
+
Or, using a block:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
|
147
|
+
Popro.new(200) do |p|
|
148
|
+
p.each0(1..50) { |n| sleep(0.1) && "#{n} of first 50" }
|
149
|
+
p.each0(1..100) { |n| sleep(0.1) && "#{n} of second 100" }
|
150
|
+
p.each0(1..50) { |n| sleep(0.1) && "#{n} of last 50" }
|
151
|
+
end
|
152
|
+
|
153
|
+
```
|
154
|
+
|
155
|
+
Note that when using `Popro.each(enumberable, &block)` or `Popro#each(enumerable, &block)` the arguments
|
156
|
+
passed to `block` on invocation are not the same as for `Popro.new`.
|
157
|
+
|
158
|
+
`Popro.each` invokes `enumerable.each` and the argument signature of the `Popro.each` `&block` is the
|
159
|
+
same as the signature that `enumerable.each` uses to call the block. Only the named argument `progress` is
|
160
|
+
added (as opposed to being the first argument as for `Popro.new`).
|
161
|
+
|
162
|
+
E.g.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
|
166
|
+
Popro.each(1..100) do |number, progress:|
|
167
|
+
progress.will "count sheep ##{number} and sleep for a bit" do
|
168
|
+
sleep 0.1
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
```
|
173
|
+
|
174
|
+
would be equivalent to:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
|
178
|
+
Popro.new(100) do |p|
|
179
|
+
p.each(1..100) do |number, progress:|
|
180
|
+
# note, `p == progress`, only added `progress:` to `each` call for example purposes
|
181
|
+
p.will "count sheep ##{number} and sleep for a bit" do
|
182
|
+
sleep 0.1
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
```
|
188
|
+
|
189
|
+
### Using `Popro#will`
|
190
|
+
|
191
|
+
The helper `Popro#will(message, &block)` can be used to signify intention. When given a block, it will mark
|
192
|
+
this intention as done when the block has finished (using a call to `Popro#did`). If no block is passed,
|
193
|
+
`Popro#did` needs to be invoked manually once the intention has been fulfilled.
|
194
|
+
|
195
|
+
This method sends the `message` passed to `Popro#will` to the `Indicator` so it can handle it (usually output to
|
196
|
+
screen).
|
197
|
+
|
198
|
+
Thus `Popro#will` can be used to signify the action about to take place instead of the action that has just finished.
|
199
|
+
If the script encounters some kind of error, we can now see in which context this error occured instead of seeing
|
200
|
+
the last invocation that did not cause an error.
|
201
|
+
|
202
|
+
E.g.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Popro.each(1..200) do |progress:|
|
206
|
+
progress.will "sleep a bit" do
|
207
|
+
sleep 0.1
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# equivalent to
|
212
|
+
|
213
|
+
Popro.new(200) do |p|
|
214
|
+
200.times do
|
215
|
+
p.will "sleep a bit" do
|
216
|
+
sleep 0.01
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# equivalent to
|
222
|
+
|
223
|
+
Popro.new(200) do |p|
|
224
|
+
200.times do
|
225
|
+
p.will "sleep a bit"
|
226
|
+
sleep 0.01
|
227
|
+
p.done "sleep a bit: DONE"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
### Using `Popro.each_will`
|
233
|
+
|
234
|
+
`Popro.each_will(enumerator, titler, size = nil, &block)` is a shortcut for:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
Popro.each(enumerator, size) do |*args, **kwargs, progress:|
|
238
|
+
progress.will titler.call(*args) do
|
239
|
+
block.call(*args, **kwargs)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
So instead of e.g.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
Popro.each(SomeModel.all) do |model, progress:|
|
248
|
+
progress.will "Delete #{model.id}" do
|
249
|
+
model.destroy
|
250
|
+
end
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
A more concise syntax is available:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
titler = ->(model) { "Delete #{model.id}" }
|
258
|
+
Popro.each_will(SomeModel.all, titler) do |model|
|
259
|
+
model.destroy
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
## Formatters
|
264
|
+
|
265
|
+
You can set your own formatters using `Popro#formatter(&block)`. Each formatter can be a `Proc`, `block` or
|
266
|
+
class implementing the `call` method (e.g. the `PoproFormatter::*` classes).
|
267
|
+
|
268
|
+
The formatter will be invoked with 2 arguments:
|
269
|
+
|
270
|
+
1. `info`, an instance of `PoproInfo` containing e.g. `current` and `total`.
|
271
|
+
2. `yielded`, whatever was passed to the `Popro#did` method.
|
272
|
+
|
273
|
+
It can also be used inside blocks.
|
274
|
+
|
275
|
+
E.g.
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
|
279
|
+
progress = Popro.new
|
280
|
+
progress.formatter do |info, yielded|
|
281
|
+
"#{info.current}, yielded #{yielded}\n"
|
282
|
+
end
|
283
|
+
progress.each(1..8) { |i| i**2 }
|
284
|
+
progress.formatter { "." } # output a dot
|
285
|
+
progress.each(1..8) { |i| i**2 }
|
286
|
+
|
287
|
+
# or equivalent, more concise:
|
288
|
+
|
289
|
+
Popro.new do |p|
|
290
|
+
p.formatter { |info, yielded| "#{info.current}, yielded #{yielded}\n" }
|
291
|
+
p.each(1..8) { |i| i**2 }
|
292
|
+
|
293
|
+
p.formatter { "." } # output a dot
|
294
|
+
p.each(1..8) { |i| i**2 }
|
295
|
+
end
|
296
|
+
|
297
|
+
```
|
298
|
+
|
299
|
+
would output:
|
300
|
+
|
301
|
+
```
|
302
|
+
1, yielded 1
|
303
|
+
2, yielded 4
|
304
|
+
3, yielded 9
|
305
|
+
4, yielded 16
|
306
|
+
5, yielded 25
|
307
|
+
6, yielded 36
|
308
|
+
7, yielded 49
|
309
|
+
8, yielded 64
|
310
|
+
........
|
311
|
+
```
|
312
|
+
|
313
|
+
|
314
|
+
## Indicator Classes
|
315
|
+
|
316
|
+
Indicator classes are responsible for communicating the progress (or not, as the case may be).
|
317
|
+
|
318
|
+
It is possible to provide your own "indicators" or use one of the provided ones (see `PoproIndicator` module).
|
319
|
+
|
320
|
+
The default `PoproIndicator` class is `PoproIndicator.default` (which returns an instance of `PoproIndicator::Stream`), which outputs the progress to STDOUT each time `Popro#did` or `Popro#will` is called.
|
321
|
+
|
data/bin/popro
ADDED
data/lib/popro.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# shortcut methods to Popro::Progress usage
|
4
|
+
module Popro
|
5
|
+
class OutOfSyncError < StandardError; end
|
6
|
+
class ConfigError < StandardError; end
|
7
|
+
|
8
|
+
require_relative 'popro/progress'
|
9
|
+
|
10
|
+
def self.new(total, **options, &block)
|
11
|
+
raise ConfigError, 'using :total is not supported in new' if options.key?(:total) && (options[:total] != total)
|
12
|
+
|
13
|
+
options[:total] = total
|
14
|
+
Progress.new(**options, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.each(obj, _total = nil, **options, &block)
|
18
|
+
new(0, **options).each(obj, total, &block).done
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.each0(obj, **options, &block)
|
22
|
+
raise ConfigError, 'using :step is not supported in each0' if options.key?(:step)
|
23
|
+
|
24
|
+
options[:step] = 0
|
25
|
+
each(obj, 0, **options, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.command_line(*_args)
|
29
|
+
raise 'TODO: implement a `ps` style progress indicator for command line'
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
# the progress context passed as first argument to blocks (or named argument for `Progress#each` and `Popro.each`)
|
5
|
+
|
6
|
+
WILL_CHECK_MARKS ||= '☐☑'
|
7
|
+
|
8
|
+
class Context
|
9
|
+
def initialize(progress:, info:, indicator:, step: 1)
|
10
|
+
@progress = progress
|
11
|
+
@indicator = indicator
|
12
|
+
@info = info
|
13
|
+
@step = step
|
14
|
+
end
|
15
|
+
|
16
|
+
def each(obj, total = nil, &block)
|
17
|
+
total = obj.size if total.nil?
|
18
|
+
@info.total += total if total.positive?
|
19
|
+
block = proc { |d| d } unless block_given?
|
20
|
+
|
21
|
+
obj.each do |*args|
|
22
|
+
did block.call(*args, progress: self)
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def each0(obj, &block)
|
29
|
+
each(obj, 0, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def each_will(obj, titler, total = nil)
|
33
|
+
each(obj, total) do |*args, **kwargs|
|
34
|
+
kwargs[:progress].will titler.call(*args) do
|
35
|
+
yield(*args, **kwargs)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def start
|
41
|
+
raise OutOfSyncError, 'trying to start when already running' if @info.running?
|
42
|
+
|
43
|
+
@info.start
|
44
|
+
end
|
45
|
+
|
46
|
+
def done
|
47
|
+
raise OutOfSyncError, 'done while not started' unless @info.running?
|
48
|
+
|
49
|
+
@info.finish
|
50
|
+
@indicator.finish if @indicator.respond_to? :finish
|
51
|
+
end
|
52
|
+
|
53
|
+
def formatter(&block)
|
54
|
+
unless @indicator.respond_to?(:formatter=)
|
55
|
+
raise ConfigError, "seems formatter is not available for #{@indicator.class}"
|
56
|
+
end
|
57
|
+
|
58
|
+
@indicator.formatter = block
|
59
|
+
block
|
60
|
+
end
|
61
|
+
|
62
|
+
def did(yielded = nil, amount = 1)
|
63
|
+
@info.start unless @info.running?
|
64
|
+
inc amount, yielded
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def will(title = nil, use_block_result_as_title = nil, step = nil)
|
70
|
+
@info.start unless @info.running?
|
71
|
+
inc 0, "#{WILL_CHECK_MARKS[0]} #{title}"
|
72
|
+
|
73
|
+
return self unless block_given?
|
74
|
+
|
75
|
+
step = @step if step.nil?
|
76
|
+
yielded = yield @context
|
77
|
+
yielded = "#{WILL_CHECK_MARKS[1]} #{title}" unless title.nil? || use_block_result_as_title
|
78
|
+
|
79
|
+
# no need to communicate to Indicator if we are not advancing (avoid double calls)
|
80
|
+
did yielded, step unless step.zero?
|
81
|
+
yielded
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_proc
|
85
|
+
proc { |yielded| did(yielded) }
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def inc(amount, yielded = nil)
|
91
|
+
raise TypeError('expected an integer') unless amount.is_a? Integer
|
92
|
+
|
93
|
+
@info.current += amount unless amount.zero?
|
94
|
+
@indicator.call(@info, yielded)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
module Formatter
|
5
|
+
class Aggregate
|
6
|
+
def initialize(*formatters, &block)
|
7
|
+
@formatters = formatters
|
8
|
+
@join = if block_given?
|
9
|
+
block
|
10
|
+
else
|
11
|
+
proc(&:join)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(info, *args)
|
16
|
+
@join.call(
|
17
|
+
@formatters.collect do |formatter|
|
18
|
+
formatter.call(info, *args)
|
19
|
+
end
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class RewriteLine
|
25
|
+
def initialize(formatter)
|
26
|
+
@formatter = formatter
|
27
|
+
@longest = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(info, *args)
|
31
|
+
result = @formatter.call(info, *args)
|
32
|
+
@longest = [@longest, result.size].max
|
33
|
+
"\r" + result.ljust(@longest, ' ')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module Concat
|
38
|
+
# Factory for calling Aggregate with a join block
|
39
|
+
def self.new(*formatters, separator: '')
|
40
|
+
Aggregate.new(*formatters) do |results|
|
41
|
+
results.join separator
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Sprintf
|
47
|
+
def initialize(format_string = nil)
|
48
|
+
@format_string = format_string
|
49
|
+
end
|
50
|
+
|
51
|
+
def call(info, *_args)
|
52
|
+
string_params = Hash.new { |_, k| info.public_send(k) }
|
53
|
+
format_string.gsub('{n}', info.total_length.to_s) % string_params
|
54
|
+
end
|
55
|
+
|
56
|
+
def format_string
|
57
|
+
@format_string ||= '[%<current>{n}s/%<total>-{n}s] %<pct_formatted>4s%%'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Looper
|
62
|
+
def initialize(enumerable = nil)
|
63
|
+
enumerable = '.' if enumerable.nil?
|
64
|
+
enumerable = enumerable.split '' if enumerable.is_a? String
|
65
|
+
|
66
|
+
@enumerator = Enumerator.new do |e|
|
67
|
+
loop do
|
68
|
+
enumerable.each do |item|
|
69
|
+
e.yield item
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def call(...)
|
76
|
+
@enumerator.next
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Spinner < Looper
|
81
|
+
STYLES ||= {
|
82
|
+
slashes: '-\\|/',
|
83
|
+
hbar: '▁▂▃▄▅▆▇█',
|
84
|
+
vbar: '▉▊▋▌▍▎',
|
85
|
+
heartbeat: '♥♡',
|
86
|
+
dots: '⣀⣄⣤⣦⣶⣷⣿',
|
87
|
+
block: '░▒▓█',
|
88
|
+
circle: '◜◝◞◟'
|
89
|
+
}.freeze
|
90
|
+
|
91
|
+
def initialize(style_type = nil, bounce: false, reverse: false)
|
92
|
+
style = STYLES[style_type] || STYLES[:slashes]
|
93
|
+
style = style.reverse if reverse
|
94
|
+
style += style.reverse if bounce
|
95
|
+
super style
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
module Indicator
|
5
|
+
require_relative 'formatter'
|
6
|
+
|
7
|
+
class Aggregate
|
8
|
+
def initialize(*indicators)
|
9
|
+
@indicators = indicators
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*args)
|
13
|
+
@indicators.each do |indicator|
|
14
|
+
indicator.call(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def finish
|
19
|
+
@indicators.each(&:finish)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Stream
|
24
|
+
attr_accessor :formatter
|
25
|
+
|
26
|
+
def initialize(stream: nil, formatter: nil)
|
27
|
+
formatter = self.class.default_formatter(formatter) if formatter.nil? || formatter.is_a?(String)
|
28
|
+
|
29
|
+
@formatter = formatter
|
30
|
+
@stream = stream || STDOUT
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(*args)
|
34
|
+
@stream << @formatter.call(*args)
|
35
|
+
@stream.flush
|
36
|
+
end
|
37
|
+
|
38
|
+
def finish
|
39
|
+
@stream << "\n"
|
40
|
+
@stream.flush
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.default_formatter(format_string = nil)
|
44
|
+
::Popro::Formatter::Sprintf.new(format_string)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.default_formatter
|
49
|
+
::Popro::Formatter::RewriteLine.new(
|
50
|
+
::Popro::Formatter::Concat.new(
|
51
|
+
::Popro::Formatter::Spinner.new(:dots, bounce: true),
|
52
|
+
::Popro::Formatter::Sprintf.new,
|
53
|
+
(proc do |_, yielded = nil|
|
54
|
+
yielded if yielded.is_a?(String) || yielded.is_a?(Numeric)
|
55
|
+
end),
|
56
|
+
separator: ' '
|
57
|
+
)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.default
|
62
|
+
Stream.new(formatter: default_formatter)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
module Indicator
|
5
|
+
module Rails
|
6
|
+
class ServerSentEvent
|
7
|
+
DEFAULT_HEADERS ||= {
|
8
|
+
'Last-Modified': 0,
|
9
|
+
'ETag': '',
|
10
|
+
'Cache-Control': 'no-cache, must-revalidate',
|
11
|
+
'X-Accel-Buffering': 'no',
|
12
|
+
'Content-Type': 'text/event-stream'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(response, **options)
|
16
|
+
response.status = 200
|
17
|
+
response.headers.merge!(options.delete(:headers) || DEFAULT_HEADERS)
|
18
|
+
formatter = options.delete(:formatter) || self.class.method(:default_formatter)
|
19
|
+
|
20
|
+
@stream = ::ActionController::Live::SSE.new(response.stream, **options)
|
21
|
+
@formatter = formatter
|
22
|
+
@response = response
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(*args)
|
26
|
+
@stream.write(@formatter.call(*args))
|
27
|
+
rescue ::ActionController::Live::ClientDisconnected
|
28
|
+
# ignore disconnections
|
29
|
+
end
|
30
|
+
|
31
|
+
def finish
|
32
|
+
@stream.close
|
33
|
+
rescue ::ActionController::Live::ClientDisconnected
|
34
|
+
# ignore disconnections
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.default_formatter(info, yielded)
|
38
|
+
info.to_h.merge(yielded: yielded)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/popro/info.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
class Info
|
5
|
+
attr_accessor :total, :current
|
6
|
+
|
7
|
+
def initialize(total: nil, current: nil)
|
8
|
+
@total = total
|
9
|
+
@current = current || 0
|
10
|
+
@started = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def running?
|
14
|
+
@started
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@started = true
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def finish
|
23
|
+
@started = false
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def pct
|
28
|
+
return 0 if @total.nil? || @total.zero?
|
29
|
+
|
30
|
+
@current.to_f / @total
|
31
|
+
end
|
32
|
+
|
33
|
+
def pct_formatted
|
34
|
+
percentage = pct
|
35
|
+
return nil if percentage.nil?
|
36
|
+
|
37
|
+
format('%<percent>.1f', percent: pct * 100)
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_length
|
41
|
+
num = [@total, @current].max
|
42
|
+
return 1 if num.zero?
|
43
|
+
|
44
|
+
Math.log10(num + 1).ceil
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_f
|
48
|
+
pct
|
49
|
+
end
|
50
|
+
|
51
|
+
def next(num = 1)
|
52
|
+
raise TypeError, 'expected an integer' unless num.is_a? Integer
|
53
|
+
|
54
|
+
@current += num
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_h
|
59
|
+
{
|
60
|
+
started: @started,
|
61
|
+
pct: pct,
|
62
|
+
pct_formatted: pct_formatted,
|
63
|
+
current: @current,
|
64
|
+
total: @total
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Popro
|
4
|
+
class Progress
|
5
|
+
require_relative 'context'
|
6
|
+
require_relative 'info'
|
7
|
+
require_relative 'indicator'
|
8
|
+
|
9
|
+
attr_reader :context
|
10
|
+
|
11
|
+
def initialize(**options)
|
12
|
+
@started = false
|
13
|
+
@info = Info.new(total: options.delete(:total), current: options.delete(:current))
|
14
|
+
|
15
|
+
options[:step] ||= (block_given? ? 0 : 1)
|
16
|
+
options[:progress] = self
|
17
|
+
options[:info] = @info
|
18
|
+
options[:indicator] = Indicator.default unless options.key? :indicator
|
19
|
+
|
20
|
+
@context = Context.new(**options)
|
21
|
+
|
22
|
+
register_aliases
|
23
|
+
return unless block_given?
|
24
|
+
|
25
|
+
yield self
|
26
|
+
done
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def register_aliases
|
32
|
+
class << self
|
33
|
+
%i[each each0 each_will to_proc did will formatter start done].each do |method_name|
|
34
|
+
define_method method_name do |*args, &block|
|
35
|
+
@context.public_send(method_name, *args, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: popro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- MikeSmithEU
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: The Poor-Man's Progress Indicator
|
14
|
+
email:
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- CHANGELOG
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- bin/popro
|
23
|
+
- lib/popro.rb
|
24
|
+
- lib/popro/context.rb
|
25
|
+
- lib/popro/formatter.rb
|
26
|
+
- lib/popro/indicator.rb
|
27
|
+
- lib/popro/indicator/rails.rb
|
28
|
+
- lib/popro/info.rb
|
29
|
+
- lib/popro/progress.rb
|
30
|
+
homepage: https://rubygems.org/gems/popro
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubygems_version: 3.1.2
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: Po'Pro
|
53
|
+
test_files: []
|