popro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'popro'
5
+ Popro.command_line(*ARGV)
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: []