tty-progressbar 0.11.0 → 0.12.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +302 -57
  4. data/examples/failure.rb +14 -0
  5. data/examples/iterator.rb +7 -0
  6. data/examples/multi/main_bar.rb +17 -0
  7. data/examples/multi/simple.rb +15 -0
  8. data/examples/threaded.rb +16 -0
  9. data/lib/tty-progressbar.rb +3 -2
  10. data/lib/tty/progressbar.rb +221 -58
  11. data/lib/tty/progressbar/configuration.rb +4 -0
  12. data/lib/tty/progressbar/converter.rb +2 -1
  13. data/lib/tty/progressbar/formatter.rb +2 -1
  14. data/lib/tty/progressbar/formatter/bar.rb +3 -1
  15. data/lib/tty/progressbar/formatter/byte_rate.rb +2 -1
  16. data/lib/tty/progressbar/formatter/current.rb +2 -1
  17. data/lib/tty/progressbar/formatter/current_byte.rb +2 -1
  18. data/lib/tty/progressbar/formatter/elapsed.rb +2 -1
  19. data/lib/tty/progressbar/formatter/estimated.rb +2 -1
  20. data/lib/tty/progressbar/formatter/mean_byte.rb +2 -1
  21. data/lib/tty/progressbar/formatter/mean_rate.rb +2 -1
  22. data/lib/tty/progressbar/formatter/percent.rb +2 -1
  23. data/lib/tty/progressbar/formatter/rate.rb +2 -1
  24. data/lib/tty/progressbar/formatter/total.rb +2 -1
  25. data/lib/tty/progressbar/formatter/total_byte.rb +2 -1
  26. data/lib/tty/progressbar/meter.rb +2 -1
  27. data/lib/tty/progressbar/multi.rb +221 -0
  28. data/lib/tty/progressbar/version.rb +1 -1
  29. data/spec/unit/events_spec.rb +35 -0
  30. data/spec/unit/formatter/elapsed_spec.rb +0 -1
  31. data/spec/unit/head_spec.rb +34 -0
  32. data/spec/unit/inspect_spec.rb +1 -1
  33. data/spec/unit/iterate_spec.rb +48 -0
  34. data/spec/unit/multi/advance_spec.rb +139 -0
  35. data/spec/unit/multi/events_spec.rb +64 -0
  36. data/spec/unit/multi/finish_spec.rb +19 -0
  37. data/spec/unit/multi/line_inset_spec.rb +67 -0
  38. data/spec/unit/multi/register_spec.rb +36 -0
  39. data/spec/unit/multi/stop_spec.rb +17 -0
  40. data/spec/unit/new_spec.rb +6 -0
  41. data/spec/unit/reset_spec.rb +0 -2
  42. data/spec/unit/stop_spec.rb +21 -0
  43. data/spec/unit/update_spec.rb +24 -0
  44. data/tty-progressbar.gemspec +1 -0
  45. metadata +44 -2
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-progressbar'
4
+
5
+ bar = TTY::ProgressBar.new("downloading [:bar] :percent", head: '>', total: 30)
6
+ 30.times do |i|
7
+ if i == 15
8
+ bar.update(head: 'x')
9
+ bar.stop
10
+ break
11
+ end
12
+ sleep(0.1)
13
+ bar.advance
14
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-progressbar'
4
+
5
+ bar = TTY::ProgressBar.new("[:bar]")
6
+
7
+ bar.iterate(30.times) { sleep(0.1) }
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-progressbar'
4
+
5
+ bars = TTY::ProgressBar::Multi.new("main [:bar] :percent")
6
+
7
+ bar1 = bars.register "foo [:bar] :percent", total: 20
8
+ bar2 = bars.register "bar [:bar] :percent", total: 30
9
+ bar3 = bars.register "baz [:bar] :percent", total: 10
10
+
11
+ bars.start
12
+
13
+ th1 = Thread.new { 20.times { sleep(0.2); bar1.advance } }
14
+ th2 = Thread.new { 30.times { sleep(0.1); bar2.advance } }
15
+ th3 = Thread.new { 10.times { sleep(0.3); bar3.advance } }
16
+
17
+ [th1, th2, th3].each(&:join)
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-progressbar'
4
+
5
+ bars = TTY::ProgressBar::Multi.new
6
+
7
+ bar1 = bars.register "foo [:bar] :percent", total: 20
8
+ bar2 = bars.register "bar [:bar] :percent", total: 30
9
+ bar3 = bars.register "baz [:bar] :percent", total: 10
10
+
11
+ th1 = Thread.new { 20.times { sleep(0.2); bar1.advance } }
12
+ th2 = Thread.new { 30.times { sleep(0.1); bar2.advance } }
13
+ th3 = Thread.new { 10.times { sleep(0.3); bar3.advance } }
14
+
15
+ [th1, th2, th3].each(&:join)
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-progressbar'
4
+
5
+ threads = []
6
+
7
+ bar = TTY::ProgressBar.new("[:bar] :percent", total: 30)
8
+
9
+ threads << Thread.new {
10
+ 15.times { sleep(0.1); bar.update(complete: '-', head: '-'); bar.advance(); }
11
+ }
12
+ threads << Thread.new {
13
+ 15.times { sleep(0.1); bar.update(complete: '+', head: '+'); bar.advance(); }
14
+ }
15
+
16
+ threads.map(&:join)
@@ -1,3 +1,4 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
2
 
3
- require 'tty/progressbar'
3
+ require_relative 'tty/progressbar'
4
+ require_relative 'tty/progressbar/multi'
@@ -1,7 +1,10 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'io/console'
4
5
  require 'forwardable'
6
+ require 'monitor'
7
+ require 'tty-cursor'
5
8
  require 'tty-screen'
6
9
 
7
10
  require_relative 'progressbar/configuration'
@@ -15,24 +18,28 @@ module TTY
15
18
  # @api public
16
19
  class ProgressBar
17
20
  extend Forwardable
21
+ include MonitorMixin
18
22
 
19
23
  ECMA_ESC = "\e".freeze
20
24
  ECMA_CSI = "\e[".freeze
21
- ECMA_CHA = 'G'.freeze
22
25
  ECMA_CLR = 'K'.freeze
23
26
 
24
27
  DEC_RST = 'l'.freeze
25
28
  DEC_SET = 'h'.freeze
26
29
  DEC_TCEM = '?25'.freeze
27
30
 
31
+ CURSOR_LOCK = Monitor.new
32
+
28
33
  attr_reader :format
29
34
 
30
35
  attr_reader :current
31
36
 
32
37
  attr_reader :start_at
33
38
 
39
+ attr_reader :row
40
+
34
41
  def_delegators :@configuration, :total, :width, :no_width,
35
- :complete, :incomplete, :hide_cursor, :clear,
42
+ :complete, :incomplete, :head, :hide_cursor, :clear,
36
43
  :output, :frequency, :interval, :width=
37
44
 
38
45
  def_delegators :@meter, :rate, :mean_rate
@@ -65,33 +72,63 @@ module TTY
65
72
  #
66
73
  # @api public
67
74
  def initialize(format, options = {})
68
- @format = format
75
+ super()
76
+ @format = format
77
+ if format.is_a?(Hash)
78
+ raise ArgumentError, "Expected bar formatting string, " \
79
+ "got `#{format}` instead."
80
+ end
69
81
  @configuration = TTY::ProgressBar::Configuration.new(options)
70
82
  yield @configuration if block_given?
71
83
 
84
+ @formatter = TTY::ProgressBar::Formatter.new
85
+ @meter = TTY::ProgressBar::Meter.new(interval)
86
+ @callbacks = Hash.new { |h, k| h[k] = [] }
87
+
88
+ @formatter.load
89
+ reset
90
+ end
91
+
92
+ # Reset progress to default configuration
93
+ #
94
+ # @api public
95
+ def reset
72
96
  @width = 0 if no_width
73
97
  @render_period = frequency == 0 ? 0 : 1.0 / frequency
74
98
  @current = 0
75
- @readings = 0
76
99
  @last_render_time = Time.now
77
100
  @last_render_width = 0
78
101
  @done = false
102
+ @stopped = false
79
103
  @start_at = Time.now
80
104
  @started = false
81
105
  @tokens = {}
82
- @formatter = TTY::ProgressBar::Formatter.new
83
- @meter = TTY::ProgressBar::Meter.new(options.fetch(:interval, 1))
106
+ @multibar = nil
107
+ @row = nil
108
+ @first_render = true
84
109
 
85
- @formatter.load
110
+ @meter.clear
111
+ end
112
+
113
+ # Attach this bar to multi bar
114
+ #
115
+ # @param [TTY::ProgressBar::Multi] multibar
116
+ # the multibar under which this bar is registered
117
+ #
118
+ # @api private
119
+ def attach_to(multibar)
120
+ @multibar = multibar
86
121
  end
87
122
 
88
123
  # Start progression by drawing bar and setting time
89
124
  #
90
125
  # @api public
91
126
  def start
92
- @started = true
93
- @start_at = Time.now
94
- @meter.start
127
+ synchronize do
128
+ @started = true
129
+ @start_at = Time.now
130
+ @meter.start
131
+ end
95
132
 
96
133
  advance(0)
97
134
  end
@@ -102,25 +139,66 @@ module TTY
102
139
  #
103
140
  # @api public
104
141
  def advance(progress = 1, tokens = {})
105
- #handle_signals
106
- return if @done
107
-
108
- if progress.respond_to?(:to_hash)
109
- tokens, progress = progress, 1
142
+ return if done?
143
+
144
+ synchronize do
145
+ if progress.respond_to?(:to_hash)
146
+ tokens, progress = progress, 1
147
+ end
148
+ @start_at = Time.now if @current.zero? && !@started
149
+ @current += progress
150
+ @tokens = tokens
151
+ @meter.sample(Time.now, progress)
152
+
153
+ if !no_width && @current >= total
154
+ finish && return
155
+ end
156
+
157
+ now = Time.now
158
+ return if (now - @last_render_time) < @render_period
159
+ render
160
+ emit(:progress)
110
161
  end
111
- @start_at = Time.now if @current.zero? && !@started
112
- @readings += 1
113
- @current += progress
114
- @tokens = tokens
115
- @meter.sample(Time.now, progress)
116
-
117
- if !no_width && @current >= total
118
- finish && return
162
+ end
163
+
164
+ # Iterate over collection either yielding computation to block
165
+ # or provided Enumerator.
166
+ #
167
+ # @example
168
+ # bar.iterate(30.times) { ... }
169
+ #
170
+ # @param [Enumerable] collection
171
+ # the collection to iterate over
172
+ #
173
+ # @param [Integer] progress
174
+ # the amount to move progress bar by
175
+ #
176
+ # @return [Enumerator]
177
+ #
178
+ # @api public
179
+ def iterate(collection, progress = 1, &block)
180
+ update(total: collection.count)
181
+ prog = Enumerator.new do |iter|
182
+ collection.each do |elem|
183
+ advance(progress)
184
+ iter.yield(elem)
185
+ end
119
186
  end
187
+ block_given? ? prog.each(&block) : prog
188
+ end
120
189
 
121
- now = Time.now
122
- return if (now - @last_render_time) < @render_period
123
- render
190
+ # Update configuration options for this bar
191
+ #
192
+ # @param [Hash[Symbol]] options
193
+ # the configuration options to update
194
+ #
195
+ # @api public
196
+ def update(options = {})
197
+ synchronize do
198
+ options.each do |name, val|
199
+ @configuration.public_send("#{name}=", val)
200
+ end
201
+ end
124
202
  end
125
203
 
126
204
  # Advance the progress bar to the updated value
@@ -152,15 +230,17 @@ module TTY
152
230
  #
153
231
  # @api public
154
232
  def ratio
155
- proportion = total > 0 ? (@current.to_f / total) : 0
156
- [[proportion, 0].max, 1].min
233
+ synchronize do
234
+ proportion = total > 0 ? (@current.to_f / total) : 0
235
+ [[proportion, 0].max, 1].min
236
+ end
157
237
  end
158
238
 
159
239
  # Render progress to the output
160
240
  #
161
241
  # @api private
162
242
  def render
163
- return if @done
243
+ return if done?
164
244
  if hide_cursor && @last_render_width == 0 && !(@current >= total)
165
245
  write(ECMA_CSI + DEC_TCEM + DEC_RST)
166
246
  end
@@ -175,15 +255,43 @@ module TTY
175
255
  @last_render_width = formatted.length
176
256
  end
177
257
 
258
+ # Move cursor to a row of the current bar if the bar is rendered
259
+ # under a multibar. Otherwise, do not move and yield on current row.
260
+ #
261
+ # @api private
262
+ def move_to_row
263
+ if @multibar
264
+ CURSOR_LOCK.synchronize do
265
+ if @first_render
266
+ @row = @multibar.next_row
267
+ yield if block_given?
268
+ output.print "\n"
269
+ @first_render = false
270
+ else
271
+ lines_up = (@multibar.rows + 1) - @row
272
+ output.print TTY::Cursor.save
273
+ output.print TTY::Cursor.up(lines_up)
274
+ yield if block_given?
275
+ output.print TTY::Cursor.restore
276
+ end
277
+ end
278
+ else
279
+ yield if block_given?
280
+ end
281
+ end
282
+
178
283
  # Write out to the output
179
284
  #
180
285
  # @param [String] data
181
286
  #
182
287
  # @api private
183
288
  def write(data, clear_first = false)
184
- output.print(ECMA_CSI + '1' + ECMA_CHA) if clear_first
185
- output.print(data)
186
- output.flush
289
+ move_to_row do
290
+ output.print(TTY::Cursor.column(1)) if clear_first
291
+ characters_in = @multibar.line_inset(self) if @multibar
292
+ output.print("#{characters_in}#{data}")
293
+ output.flush
294
+ end
187
295
  end
188
296
 
189
297
  # Resize progress bar with new configuration
@@ -193,10 +301,12 @@ module TTY
193
301
  #
194
302
  # @api public
195
303
  def resize(new_width = nil)
196
- return if @done
197
- clear_line
198
- if new_width
199
- self.width = new_width
304
+ return if done?
305
+ synchronize do
306
+ clear_line
307
+ if new_width
308
+ self.width = new_width
309
+ end
200
310
  end
201
311
  end
202
312
 
@@ -208,31 +318,38 @@ module TTY
208
318
  if hide_cursor && @last_render_width != 0
209
319
  write(ECMA_CSI + DEC_TCEM + DEC_SET, false)
210
320
  end
211
- return if @done
321
+ return if done?
212
322
  @current = total unless no_width
213
323
  render
214
324
  clear ? clear_line : write("\n", false)
325
+ ensure
215
326
  @meter.clear
216
327
  @done = true
328
+ emit(:done)
217
329
  end
218
330
 
219
- # Clear current line
331
+ # Stop and cancel the progress at the current position
220
332
  #
221
333
  # @api public
222
- def clear_line
223
- output.print(ECMA_CSI + '0m' + ECMA_CSI + '1000D' + ECMA_CSI + ECMA_CLR)
334
+ def stop
335
+ # reenable cursor if it is turned off
336
+ if hide_cursor && @last_render_width != 0
337
+ write(ECMA_CSI + DEC_TCEM + DEC_SET, false)
338
+ end
339
+ return if done?
340
+ render
341
+ clear ? clear_line : write("\n", false)
342
+ ensure
343
+ @meter.clear
344
+ @stopped = true
345
+ emit(:stopped)
224
346
  end
225
347
 
226
- # Reset progress to default configuration
348
+ # Clear current line
227
349
  #
228
350
  # @api public
229
- def reset
230
- @current = 0
231
- @readings = 0
232
- @done = false
233
- @meter.clear
234
-
235
- advance(0) # rerender with new configuration
351
+ def clear_line
352
+ output.print(ECMA_CSI + '0m' + ECMA_CSI + '1000D' + ECMA_CSI + ECMA_CLR)
236
353
  end
237
354
 
238
355
  # Check if progress is finised
@@ -245,6 +362,39 @@ module TTY
245
362
  @done
246
363
  end
247
364
 
365
+ # Check if progress is stopped
366
+ #
367
+ # @return [Boolean]
368
+ #
369
+ # @api public
370
+ def stopped?
371
+ @stopped
372
+ end
373
+
374
+ # Check if progress is finished or stopped
375
+ #
376
+ # @return [Boolean]
377
+ #
378
+ # @api public
379
+ def done?
380
+ @done || @stopped
381
+ end
382
+
383
+ # Register callback with this bar
384
+ #
385
+ # @param [Symbol] name
386
+ # the name for the event to listen for, e.i. :complete
387
+ #
388
+ # @return [self]
389
+ #
390
+ # @api public
391
+ def on(name, &callback)
392
+ synchronize do
393
+ @callbacks[name] << callback
394
+ end
395
+ self
396
+ end
397
+
248
398
  # Log message above the current progress bar
249
399
  #
250
400
  # @param [String] message
@@ -253,7 +403,7 @@ module TTY
253
403
  # @api public
254
404
  def log(message)
255
405
  sanitized_message = message.gsub(/\r|\n/, ' ')
256
- if @done
406
+ if done?
257
407
  write(sanitized_message + "\n", false)
258
408
  return
259
409
  end
@@ -287,14 +437,15 @@ module TTY
287
437
  #
288
438
  # @api public
289
439
  def inspect
290
- output = "#<#{self.class.name} "
291
- output << "@format=\"#{format}\", "
292
- output << "@current=\"#{@current}\", "
293
- output << "@total=\"#{total}\", "
294
- output << "@width=\"#{width}\", "
295
- output << "@complete=\"#{complete}\", "
296
- output << "@incomplete=\"#{incomplete}\", "
297
- output << "@interval=\"#{interval}\">"
440
+ "#<#{self.class.name} " \
441
+ "@format=\"#{format}\", " \
442
+ "@current=\"#{@current}\", " \
443
+ "@total=\"#{total}\", " \
444
+ "@width=\"#{width}\", " \
445
+ "@complete=\"#{complete}\", " \
446
+ "@head=\"#{head}\", " \
447
+ "@incomplete=\"#{incomplete}\", " \
448
+ "@interval=\"#{interval}\">"
298
449
  end
299
450
 
300
451
  private
@@ -309,5 +460,17 @@ module TTY
309
460
  end
310
461
  message
311
462
  end
463
+
464
+ # Emit callback by name
465
+ #
466
+ # @param [Symbol]
467
+ # the event name
468
+ #
469
+ # @api private
470
+ def emit(name, *args)
471
+ @callbacks[name].each do |callback|
472
+ callback.call(*args)
473
+ end
474
+ end
312
475
  end # ProgressBar
313
476
  end # TTY