tapsoob 0.6.1-java → 0.6.2-java
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/lib/tapsoob/operation.rb +1 -2
- data/lib/tapsoob/progress/bar.rb +245 -0
- data/lib/tapsoob/progress/multi_bar.rb +158 -0
- data/lib/tapsoob/progress/thread_safe_bar.rb +90 -0
- data/lib/tapsoob/progress.rb +11 -0
- data/lib/tapsoob/version.rb +1 -1
- metadata +5 -3
- data/lib/tapsoob/multi_progress_bar.rb +0 -234
- data/lib/tapsoob/progress_bar.rb +0 -237
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a60a9f569ac31e7a9390328af8974ee95a056a3d35b510eedafb13fd9ab3331
|
|
4
|
+
data.tar.gz: 5f6a3ca22a9b95bff31adf8a9921bfc79e3768047fbe769a0c9c3adbfe72c7f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7f62ebad5ce2449ae283a22f5ae0e63faaa84cdce808537eda8e35f60346ff782be1826c3614fd4636787b67df8e1cc78f1ce1d74103d33c78b2ff41146dbec
|
|
7
|
+
data.tar.gz: 91911b166a9bae36b504dfa2112088fc11033f36f8b100ded1e9e9d94ad409c9bfb23a7dffe5afb5ec0d6e1adcc619a7923cf774bc9856c0013e6a78b2ae16fd
|
data/lib/tapsoob/operation.rb
CHANGED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Ruby/ProgressBar - a text progress bar library
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
|
|
6
|
+
# All rights reserved.
|
|
7
|
+
# This is free software with ABSOLUTELY NO WARRANTY.
|
|
8
|
+
#
|
|
9
|
+
# You can redistribute it and/or modify it under the terms
|
|
10
|
+
# of Ruby's license.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
module Tapsoob
|
|
14
|
+
module Progress
|
|
15
|
+
class Bar
|
|
16
|
+
VERSION = "0.9"
|
|
17
|
+
|
|
18
|
+
def initialize (title, total, out = STDOUT, title_width = nil)
|
|
19
|
+
@title = title
|
|
20
|
+
@total = total
|
|
21
|
+
@out = out
|
|
22
|
+
@terminal_width = 80
|
|
23
|
+
@bar_mark = "="
|
|
24
|
+
@current = 0
|
|
25
|
+
@previous = 0
|
|
26
|
+
@finished_p = false
|
|
27
|
+
@start_time = ::Time.now
|
|
28
|
+
@previous_time = @start_time
|
|
29
|
+
# Set title width: use provided width, or accommodate the title, with a minimum of 14
|
|
30
|
+
@title_width = title_width || [title.length, 14].max
|
|
31
|
+
@format = "%-#{@title_width}s %3d%% %s %s"
|
|
32
|
+
@format_arguments = [:title, :percentage, :bar, :stat]
|
|
33
|
+
clear
|
|
34
|
+
show
|
|
35
|
+
end
|
|
36
|
+
attr_reader :title
|
|
37
|
+
attr_reader :current
|
|
38
|
+
attr_reader :total
|
|
39
|
+
attr_accessor :start_time
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
def fmt_bar
|
|
43
|
+
bar_width = do_percentage * @terminal_width / 100
|
|
44
|
+
sprintf("|%s%s|",
|
|
45
|
+
@bar_mark * bar_width,
|
|
46
|
+
" " * (@terminal_width - bar_width))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fmt_percentage
|
|
50
|
+
do_percentage
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fmt_stat
|
|
54
|
+
if @finished_p then elapsed else eta end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fmt_stat_for_file_transfer
|
|
58
|
+
if @finished_p then
|
|
59
|
+
sprintf("%s %s %s", bytes, transfer_rate, elapsed)
|
|
60
|
+
else
|
|
61
|
+
sprintf("%s %s %s", bytes, transfer_rate, eta)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fmt_title
|
|
66
|
+
@title[0,(@title_width - 1)] + ":"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def convert_bytes (bytes)
|
|
70
|
+
if bytes < 1024
|
|
71
|
+
sprintf("%6dB", bytes)
|
|
72
|
+
elsif bytes < 1024 * 1000 # 1000kb
|
|
73
|
+
sprintf("%5.1fKB", bytes.to_f / 1024)
|
|
74
|
+
elsif bytes < 1024 * 1024 * 1000 # 1000mb
|
|
75
|
+
sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
|
|
76
|
+
else
|
|
77
|
+
sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def transfer_rate
|
|
82
|
+
bytes_per_second = @current.to_f / (::Time.now - @start_time)
|
|
83
|
+
sprintf("%s/s", convert_bytes(bytes_per_second))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def bytes
|
|
87
|
+
convert_bytes(@current)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def format_time (t)
|
|
91
|
+
t = t.to_i
|
|
92
|
+
sec = t % 60
|
|
93
|
+
min = (t / 60) % 60
|
|
94
|
+
hour = t / 3600
|
|
95
|
+
sprintf("%02d:%02d:%02d", hour, min, sec);
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ETA stands for Estimated Time of Arrival.
|
|
99
|
+
def eta
|
|
100
|
+
if @current == 0
|
|
101
|
+
"ETA: --:--:--"
|
|
102
|
+
else
|
|
103
|
+
elapsed = ::Time.now - @start_time
|
|
104
|
+
eta = elapsed * @total / @current - elapsed;
|
|
105
|
+
sprintf("ETA: %s", format_time(eta))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def elapsed
|
|
110
|
+
elapsed = ::Time.now - @start_time
|
|
111
|
+
sprintf("Time: %s", format_time(elapsed))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def eol
|
|
115
|
+
if @finished_p then "\n" else "\r" end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def do_percentage
|
|
119
|
+
if @total.zero?
|
|
120
|
+
100
|
|
121
|
+
else
|
|
122
|
+
@current * 100 / @total
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_width
|
|
127
|
+
# FIXME: I don't know how portable it is.
|
|
128
|
+
default_width = 80
|
|
129
|
+
begin
|
|
130
|
+
tiocgwinsz = 0x5413
|
|
131
|
+
data = [0, 0, 0, 0].pack("SSSS")
|
|
132
|
+
if @out.ioctl(tiocgwinsz, data) >= 0 then
|
|
133
|
+
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
|
134
|
+
if cols > 0 then cols else default_width end
|
|
135
|
+
else
|
|
136
|
+
default_width
|
|
137
|
+
end
|
|
138
|
+
rescue Exception
|
|
139
|
+
default_width
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def show
|
|
144
|
+
arguments = @format_arguments.map {|method|
|
|
145
|
+
method = sprintf("fmt_%s", method)
|
|
146
|
+
send(method)
|
|
147
|
+
}
|
|
148
|
+
line = sprintf(@format, *arguments)
|
|
149
|
+
|
|
150
|
+
width = get_width
|
|
151
|
+
if line.length == width - 1
|
|
152
|
+
@out.print(line + eol)
|
|
153
|
+
@out.flush
|
|
154
|
+
elsif line.length >= width
|
|
155
|
+
@terminal_width = [@terminal_width - (line.length - width + 1), 0].max
|
|
156
|
+
if @terminal_width == 0 then @out.print(line + eol) else show end
|
|
157
|
+
else # line.length < width - 1
|
|
158
|
+
@terminal_width += width - line.length + 1
|
|
159
|
+
show
|
|
160
|
+
end
|
|
161
|
+
@previous_time = ::Time.now
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def show_if_needed
|
|
165
|
+
if @total.zero?
|
|
166
|
+
cur_percentage = 100
|
|
167
|
+
prev_percentage = 0
|
|
168
|
+
else
|
|
169
|
+
cur_percentage = (@current * 100 / @total).to_i
|
|
170
|
+
prev_percentage = (@previous * 100 / @total).to_i
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Use "!=" instead of ">" to support negative changes
|
|
174
|
+
if cur_percentage != prev_percentage ||
|
|
175
|
+
::Time.now - @previous_time >= 1 || @finished_p
|
|
176
|
+
show
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
public
|
|
181
|
+
def clear
|
|
182
|
+
@out.print "\r"
|
|
183
|
+
@out.print(" " * (get_width - 1))
|
|
184
|
+
@out.print "\r"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def finish
|
|
188
|
+
@current = @total
|
|
189
|
+
@finished_p = true
|
|
190
|
+
show
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def finished?
|
|
194
|
+
@finished_p
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def file_transfer_mode
|
|
198
|
+
@format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def format= (format)
|
|
202
|
+
@format = format
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def format_arguments= (arguments)
|
|
206
|
+
@format_arguments = arguments
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def halt
|
|
210
|
+
@finished_p = true
|
|
211
|
+
show
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def inc (step = 1)
|
|
215
|
+
@current += step
|
|
216
|
+
@current = @total if @current > @total
|
|
217
|
+
show_if_needed
|
|
218
|
+
@previous = @current
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def set (count)
|
|
222
|
+
if count < 0 || count > @total
|
|
223
|
+
raise "invalid count: #{count} (total: #{@total})"
|
|
224
|
+
end
|
|
225
|
+
@current = count
|
|
226
|
+
show_if_needed
|
|
227
|
+
@previous = @current
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def inspect
|
|
231
|
+
"#<Tapsoob::Progress::Bar:#{@current}/#{@total}>"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
class ReversedBar < Bar
|
|
236
|
+
def do_percentage
|
|
237
|
+
100 - super
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Backward compatibility aliases
|
|
244
|
+
ProgressBar = Tapsoob::Progress::Bar
|
|
245
|
+
ReversedProgressBar = Tapsoob::Progress::ReversedBar
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
module Tapsoob
|
|
4
|
+
module Progress
|
|
5
|
+
# MultiBar manages multiple progress bars in parallel
|
|
6
|
+
# Each bar gets its own line in the terminal
|
|
7
|
+
class MultiBar
|
|
8
|
+
def initialize(max_bars = 4)
|
|
9
|
+
@max_bars = max_bars
|
|
10
|
+
@bars = []
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
@active = true
|
|
13
|
+
@out = STDOUT
|
|
14
|
+
@last_update = Time.now
|
|
15
|
+
@reserved_lines = 0 # Track how many lines we've actually reserved
|
|
16
|
+
@max_title_width = 14 # Minimum width, will grow with longer titles
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Create a new progress bar and return it
|
|
20
|
+
def create_bar(title, total)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
# Remove any existing bar with the same title to prevent duplicates
|
|
23
|
+
@bars.reject! { |b| b.title == title }
|
|
24
|
+
|
|
25
|
+
# Update max title width to accommodate longer titles
|
|
26
|
+
@max_title_width = [@max_title_width, title.length].max
|
|
27
|
+
|
|
28
|
+
bar = ThreadSafeBar.new(title, total, self)
|
|
29
|
+
|
|
30
|
+
# Reserve a line for this new bar during active updates
|
|
31
|
+
# Cap at 2 * max_bars to show active workers + some recent finished bars
|
|
32
|
+
if @reserved_lines < @max_bars * 2
|
|
33
|
+
@out.print "\n"
|
|
34
|
+
@out.flush
|
|
35
|
+
@reserved_lines += 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@bars << bar
|
|
39
|
+
bar
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the current maximum title width for alignment
|
|
44
|
+
# Note: Always called from within synchronized methods, so no mutex needed
|
|
45
|
+
def max_title_width
|
|
46
|
+
@max_title_width
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Called by individual bars when they update
|
|
50
|
+
def update
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
return unless @active
|
|
53
|
+
return unless should_redraw?
|
|
54
|
+
|
|
55
|
+
@last_update = Time.now
|
|
56
|
+
redraw_all
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Finish a specific bar - mark it as completed
|
|
61
|
+
def finish_bar(bar)
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
return unless @active
|
|
64
|
+
|
|
65
|
+
bar.mark_finished
|
|
66
|
+
|
|
67
|
+
# Respect throttle when finishing to avoid spamming redraws
|
|
68
|
+
if should_redraw?
|
|
69
|
+
@last_update = Time.now
|
|
70
|
+
redraw_all
|
|
71
|
+
end
|
|
72
|
+
# If throttled, the next regular update will show the finished state
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Stop all progress bars and keep them visible
|
|
77
|
+
def stop
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
@active = false
|
|
80
|
+
|
|
81
|
+
# Final cleanup: remove any duplicate titles (keep the last occurrence of each unique title)
|
|
82
|
+
@bars = @bars.reverse.uniq { |bar| bar.title }.reverse
|
|
83
|
+
|
|
84
|
+
# Final redraw to show completed state (skip active check)
|
|
85
|
+
redraw_all(true)
|
|
86
|
+
# Move cursor past all bars
|
|
87
|
+
@out.print "\n"
|
|
88
|
+
@out.flush
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Check if enough time has passed to redraw (throttle to 10 updates/sec)
|
|
95
|
+
def should_redraw?
|
|
96
|
+
Time.now - @last_update >= 0.1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def redraw_all(force = false)
|
|
100
|
+
return unless force || @active
|
|
101
|
+
return if @bars.empty?
|
|
102
|
+
|
|
103
|
+
if force && !@active
|
|
104
|
+
render_final_display
|
|
105
|
+
else
|
|
106
|
+
render_active_display
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Final display: show all completed bars
|
|
111
|
+
def render_final_display
|
|
112
|
+
# Clear the reserved lines first
|
|
113
|
+
if @reserved_lines > 0
|
|
114
|
+
@out.print "\r\e[#{@reserved_lines}A"
|
|
115
|
+
@reserved_lines.times { @out.print "\r\e[K\n" }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Print all bars (adds new lines as needed)
|
|
119
|
+
@bars.each do |bar|
|
|
120
|
+
@out.print "\r\e[K"
|
|
121
|
+
bar.render_to(@out)
|
|
122
|
+
@out.print "\n"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@out.flush
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Normal operation: show active bars + recent finished in reserved space
|
|
129
|
+
def render_active_display
|
|
130
|
+
return if @reserved_lines == 0
|
|
131
|
+
|
|
132
|
+
# Partition bars in a single pass for efficiency
|
|
133
|
+
active_bars, finished_bars = @bars.partition { |b| !b.finished? }
|
|
134
|
+
|
|
135
|
+
# Build display: active bars first, then recent finished to fill remaining space
|
|
136
|
+
# Ensure we don't request negative count from .last()
|
|
137
|
+
remaining_space = [@reserved_lines - active_bars.length, 0].max
|
|
138
|
+
bars_to_draw = active_bars + finished_bars.last(remaining_space)
|
|
139
|
+
|
|
140
|
+
# If we have more bars than reserved lines, show only the most recent
|
|
141
|
+
bars_to_draw = bars_to_draw.last(@reserved_lines) if bars_to_draw.length > @reserved_lines
|
|
142
|
+
|
|
143
|
+
# Move up and redraw in reserved space
|
|
144
|
+
@out.print "\r\e[#{@reserved_lines}A"
|
|
145
|
+
@reserved_lines.times do |i|
|
|
146
|
+
@out.print "\r\e[K"
|
|
147
|
+
bars_to_draw[i].render_to(@out) if i < bars_to_draw.length
|
|
148
|
+
@out.print "\n"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@out.flush
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Backward compatibility alias
|
|
158
|
+
MultiProgressBar = Tapsoob::Progress::MultiBar
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
module Tapsoob
|
|
4
|
+
module Progress
|
|
5
|
+
# Thread-safe progress bar that reports to a MultiBar
|
|
6
|
+
class ThreadSafeBar < Bar
|
|
7
|
+
attr_reader :title
|
|
8
|
+
|
|
9
|
+
def initialize(title, total, multi_progress_bar)
|
|
10
|
+
@multi_progress_bar = multi_progress_bar
|
|
11
|
+
@out = STDOUT # Need this for get_width to work
|
|
12
|
+
# Don't call parent initialize, we'll manage output ourselves
|
|
13
|
+
@title = title
|
|
14
|
+
@total = total
|
|
15
|
+
@terminal_width = 80
|
|
16
|
+
@bar_mark = "="
|
|
17
|
+
@current = 0
|
|
18
|
+
@previous = 0
|
|
19
|
+
@finished_p = false
|
|
20
|
+
@start_time = ::Time.now
|
|
21
|
+
@previous_time = @start_time
|
|
22
|
+
@format_arguments = [:title, :percentage, :bar, :stat]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Override show to notify multi-progress instead of direct output
|
|
26
|
+
def show
|
|
27
|
+
@previous_time = ::Time.now # Update to prevent time-based refresh spam
|
|
28
|
+
@multi_progress_bar.update
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Render this bar to the given output stream
|
|
32
|
+
def render_to(out)
|
|
33
|
+
# Get dynamic title width from MultiBar for consistent alignment
|
|
34
|
+
# Store as instance variable so parent class fmt_* methods can use it
|
|
35
|
+
@title_width = @multi_progress_bar.max_title_width
|
|
36
|
+
|
|
37
|
+
# Recalculate terminal width to handle resizes and use full width
|
|
38
|
+
width = get_width
|
|
39
|
+
# Calculate bar width: total_width - fixed_elements - padding
|
|
40
|
+
# Fixed: title(variable) + " "(1) + percentage(4) + " "(1) + "|"(1) + "|"(1) + " "(1) + timer(15) = title_width + 25
|
|
41
|
+
# Padding: +3 for timer fluctuations and safety
|
|
42
|
+
fixed_chars = @title_width + 28
|
|
43
|
+
@terminal_width = [width - fixed_chars, 20].max
|
|
44
|
+
|
|
45
|
+
# Build format string with dynamic title width
|
|
46
|
+
format = "%-#{@title_width}s %3d%% %s %s"
|
|
47
|
+
arguments = @format_arguments.map { |method| send("fmt_#{method}") }
|
|
48
|
+
line = sprintf(format, *arguments)
|
|
49
|
+
|
|
50
|
+
# Ensure line doesn't exceed terminal width to prevent wrapping
|
|
51
|
+
# Leave 2 chars margin for safety
|
|
52
|
+
line = line[0, width - 2] if line.length > width - 2
|
|
53
|
+
|
|
54
|
+
out.print(line)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Override clear to do nothing (managed by MultiBar)
|
|
58
|
+
def clear
|
|
59
|
+
# no-op
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Mark this bar as finished (for tracking)
|
|
63
|
+
def mark_finished
|
|
64
|
+
@finished_p = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Override to use the same @finished_p flag
|
|
68
|
+
def finished?
|
|
69
|
+
@finished_p
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Override finish to notify multi-progress
|
|
73
|
+
def finish
|
|
74
|
+
@current = @total
|
|
75
|
+
@multi_progress_bar.finish_bar(self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Override inc to check if we need to update
|
|
79
|
+
def inc(step = 1)
|
|
80
|
+
@current += step
|
|
81
|
+
@current = @total if @current > @total
|
|
82
|
+
show_if_needed
|
|
83
|
+
@previous = @current
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Backward compatibility alias
|
|
90
|
+
ThreadSafeProgressBar = Tapsoob::Progress::ThreadSafeBar
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Progress bar module for Tapsoob
|
|
4
|
+
# Provides progress tracking for database operations with support for:
|
|
5
|
+
# - Single progress bars (Bar)
|
|
6
|
+
# - Multiple parallel progress bars (MultiBar)
|
|
7
|
+
# - Thread-safe progress bars (ThreadSafeBar)
|
|
8
|
+
|
|
9
|
+
require 'tapsoob/progress/bar'
|
|
10
|
+
require 'tapsoob/progress/multi_bar'
|
|
11
|
+
require 'tapsoob/progress/thread_safe_bar'
|
data/lib/tapsoob/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tapsoob
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Félix Bellanger
|
|
@@ -104,9 +104,11 @@ files:
|
|
|
104
104
|
- lib/tapsoob/data_stream.rb
|
|
105
105
|
- lib/tapsoob/errors.rb
|
|
106
106
|
- lib/tapsoob/log.rb
|
|
107
|
-
- lib/tapsoob/multi_progress_bar.rb
|
|
108
107
|
- lib/tapsoob/operation.rb
|
|
109
|
-
- lib/tapsoob/
|
|
108
|
+
- lib/tapsoob/progress.rb
|
|
109
|
+
- lib/tapsoob/progress/bar.rb
|
|
110
|
+
- lib/tapsoob/progress/multi_bar.rb
|
|
111
|
+
- lib/tapsoob/progress/thread_safe_bar.rb
|
|
110
112
|
- lib/tapsoob/railtie.rb
|
|
111
113
|
- lib/tapsoob/schema.rb
|
|
112
114
|
- lib/tapsoob/utils.rb
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
|
2
|
-
require 'tapsoob/progress_bar'
|
|
3
|
-
|
|
4
|
-
# MultiProgressBar manages multiple progress bars in parallel
|
|
5
|
-
# Each bar gets its own line in the terminal
|
|
6
|
-
class MultiProgressBar
|
|
7
|
-
def initialize(max_bars = 4)
|
|
8
|
-
@max_bars = max_bars
|
|
9
|
-
@bars = []
|
|
10
|
-
@mutex = Mutex.new
|
|
11
|
-
@active = true
|
|
12
|
-
@out = STDOUT
|
|
13
|
-
@last_update = Time.now
|
|
14
|
-
@reserved_lines = 0 # Track how many lines we've actually reserved
|
|
15
|
-
@max_title_width = 14 # Minimum width, will grow with longer titles
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Create a new progress bar and return it
|
|
19
|
-
def create_bar(title, total)
|
|
20
|
-
@mutex.synchronize do
|
|
21
|
-
# Remove any existing bar with the same title to prevent duplicates
|
|
22
|
-
@bars.reject! { |b| b.title == title }
|
|
23
|
-
|
|
24
|
-
# Update max title width to accommodate longer titles
|
|
25
|
-
@max_title_width = [@max_title_width, title.length].max
|
|
26
|
-
|
|
27
|
-
bar = ThreadSafeProgressBar.new(title, total, self)
|
|
28
|
-
|
|
29
|
-
# Reserve a line for this new bar during active updates
|
|
30
|
-
# Cap at 2 * max_bars to show active workers + some recent finished bars
|
|
31
|
-
if @reserved_lines < @max_bars * 2
|
|
32
|
-
@out.print "\n"
|
|
33
|
-
@out.flush
|
|
34
|
-
@reserved_lines += 1
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
@bars << bar
|
|
38
|
-
bar
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Get the current maximum title width for alignment
|
|
43
|
-
# Note: Always called from within synchronized methods, so no mutex needed
|
|
44
|
-
def max_title_width
|
|
45
|
-
@max_title_width
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Called by individual bars when they update
|
|
49
|
-
def update
|
|
50
|
-
@mutex.synchronize do
|
|
51
|
-
return unless @active
|
|
52
|
-
return unless should_redraw?
|
|
53
|
-
|
|
54
|
-
@last_update = Time.now
|
|
55
|
-
redraw_all
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Finish a specific bar - mark it as completed
|
|
60
|
-
def finish_bar(bar)
|
|
61
|
-
@mutex.synchronize do
|
|
62
|
-
return unless @active
|
|
63
|
-
|
|
64
|
-
bar.mark_finished
|
|
65
|
-
|
|
66
|
-
# Respect throttle when finishing to avoid spamming redraws
|
|
67
|
-
if should_redraw?
|
|
68
|
-
@last_update = Time.now
|
|
69
|
-
redraw_all
|
|
70
|
-
end
|
|
71
|
-
# If throttled, the next regular update will show the finished state
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Stop all progress bars and keep them visible
|
|
76
|
-
def stop
|
|
77
|
-
@mutex.synchronize do
|
|
78
|
-
@active = false
|
|
79
|
-
|
|
80
|
-
# Final cleanup: remove any duplicate titles (keep the last occurrence of each unique title)
|
|
81
|
-
@bars = @bars.reverse.uniq { |bar| bar.title }.reverse
|
|
82
|
-
|
|
83
|
-
# Final redraw to show completed state (skip active check)
|
|
84
|
-
redraw_all(true)
|
|
85
|
-
# Move cursor past all bars
|
|
86
|
-
@out.print "\n"
|
|
87
|
-
@out.flush
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
# Check if enough time has passed to redraw (throttle to 10 updates/sec)
|
|
94
|
-
def should_redraw?
|
|
95
|
-
Time.now - @last_update >= 0.1
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def redraw_all(force = false)
|
|
99
|
-
return unless force || @active
|
|
100
|
-
return if @bars.empty?
|
|
101
|
-
|
|
102
|
-
if force && !@active
|
|
103
|
-
render_final_display
|
|
104
|
-
else
|
|
105
|
-
render_active_display
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Final display: show all completed bars
|
|
110
|
-
def render_final_display
|
|
111
|
-
# Clear the reserved lines first
|
|
112
|
-
if @reserved_lines > 0
|
|
113
|
-
@out.print "\r\e[#{@reserved_lines}A"
|
|
114
|
-
@reserved_lines.times { @out.print "\r\e[K\n" }
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Print all bars (adds new lines as needed)
|
|
118
|
-
@bars.each do |bar|
|
|
119
|
-
@out.print "\r\e[K"
|
|
120
|
-
bar.render_to(@out)
|
|
121
|
-
@out.print "\n"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
@out.flush
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Normal operation: show active bars + recent finished in reserved space
|
|
128
|
-
def render_active_display
|
|
129
|
-
return if @reserved_lines == 0
|
|
130
|
-
|
|
131
|
-
# Partition bars in a single pass for efficiency
|
|
132
|
-
active_bars, finished_bars = @bars.partition { |b| !b.finished? }
|
|
133
|
-
|
|
134
|
-
# Build display: active bars first, then recent finished to fill remaining space
|
|
135
|
-
# Ensure we don't request negative count from .last()
|
|
136
|
-
remaining_space = [@reserved_lines - active_bars.length, 0].max
|
|
137
|
-
bars_to_draw = active_bars + finished_bars.last(remaining_space)
|
|
138
|
-
|
|
139
|
-
# If we have more bars than reserved lines, show only the most recent
|
|
140
|
-
bars_to_draw = bars_to_draw.last(@reserved_lines) if bars_to_draw.length > @reserved_lines
|
|
141
|
-
|
|
142
|
-
# Move up and redraw in reserved space
|
|
143
|
-
@out.print "\r\e[#{@reserved_lines}A"
|
|
144
|
-
@reserved_lines.times do |i|
|
|
145
|
-
@out.print "\r\e[K"
|
|
146
|
-
bars_to_draw[i].render_to(@out) if i < bars_to_draw.length
|
|
147
|
-
@out.print "\n"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
@out.flush
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Thread-safe progress bar that reports to a MultiProgressBar
|
|
155
|
-
class ThreadSafeProgressBar < ProgressBar
|
|
156
|
-
attr_reader :title
|
|
157
|
-
|
|
158
|
-
def initialize(title, total, multi_progress_bar)
|
|
159
|
-
@multi_progress_bar = multi_progress_bar
|
|
160
|
-
@out = STDOUT # Need this for get_width to work
|
|
161
|
-
# Don't call parent initialize, we'll manage output ourselves
|
|
162
|
-
@title = title
|
|
163
|
-
@total = total
|
|
164
|
-
@terminal_width = 80
|
|
165
|
-
@bar_mark = "="
|
|
166
|
-
@current = 0
|
|
167
|
-
@previous = 0
|
|
168
|
-
@finished_p = false
|
|
169
|
-
@start_time = ::Time.now
|
|
170
|
-
@previous_time = @start_time
|
|
171
|
-
@format_arguments = [:title, :percentage, :bar, :stat]
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Override show to notify multi-progress instead of direct output
|
|
175
|
-
def show
|
|
176
|
-
@previous_time = ::Time.now # Update to prevent time-based refresh spam
|
|
177
|
-
@multi_progress_bar.update
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Render this bar to the given output stream
|
|
181
|
-
def render_to(out)
|
|
182
|
-
# Get dynamic title width from MultiProgressBar for consistent alignment
|
|
183
|
-
# Store as instance variable so parent class fmt_* methods can use it
|
|
184
|
-
@title_width = @multi_progress_bar.max_title_width
|
|
185
|
-
|
|
186
|
-
# Recalculate terminal width to handle resizes and use full width
|
|
187
|
-
width = get_width
|
|
188
|
-
# Calculate bar width: total_width - fixed_elements - padding
|
|
189
|
-
# Fixed: title(variable) + " "(1) + percentage(4) + " "(1) + "|"(1) + "|"(1) + " "(1) + timer(15) = title_width + 25
|
|
190
|
-
# Padding: +3 for timer fluctuations and safety
|
|
191
|
-
fixed_chars = @title_width + 28
|
|
192
|
-
@terminal_width = [width - fixed_chars, 20].max
|
|
193
|
-
|
|
194
|
-
# Build format string with dynamic title width
|
|
195
|
-
format = "%-#{@title_width}s %3d%% %s %s"
|
|
196
|
-
arguments = @format_arguments.map { |method| send("fmt_#{method}") }
|
|
197
|
-
line = sprintf(format, *arguments)
|
|
198
|
-
|
|
199
|
-
# Ensure line doesn't exceed terminal width to prevent wrapping
|
|
200
|
-
# Leave 2 chars margin for safety
|
|
201
|
-
line = line[0, width - 2] if line.length > width - 2
|
|
202
|
-
|
|
203
|
-
out.print(line)
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Override clear to do nothing (managed by MultiProgressBar)
|
|
207
|
-
def clear
|
|
208
|
-
# no-op
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Mark this bar as finished (for tracking)
|
|
212
|
-
def mark_finished
|
|
213
|
-
@finished_p = true
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Override to use the same @finished_p flag
|
|
217
|
-
def finished?
|
|
218
|
-
@finished_p
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Override finish to notify multi-progress
|
|
222
|
-
def finish
|
|
223
|
-
@current = @total
|
|
224
|
-
@multi_progress_bar.finish_bar(self)
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Override inc to check if we need to update
|
|
228
|
-
def inc(step = 1)
|
|
229
|
-
@current += step
|
|
230
|
-
@current = @total if @current > @total
|
|
231
|
-
show_if_needed
|
|
232
|
-
@previous = @current
|
|
233
|
-
end
|
|
234
|
-
end
|
data/lib/tapsoob/progress_bar.rb
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# Ruby/ProgressBar - a text progress bar library
|
|
4
|
-
#
|
|
5
|
-
# Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
|
|
6
|
-
# All rights reserved.
|
|
7
|
-
# This is free software with ABSOLUTELY NO WARRANTY.
|
|
8
|
-
#
|
|
9
|
-
# You can redistribute it and/or modify it under the terms
|
|
10
|
-
# of Ruby's license.
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
class ProgressBar
|
|
14
|
-
VERSION = "0.9"
|
|
15
|
-
|
|
16
|
-
def initialize (title, total, out = STDOUT, title_width = nil)
|
|
17
|
-
@title = title
|
|
18
|
-
@total = total
|
|
19
|
-
@out = out
|
|
20
|
-
@terminal_width = 80
|
|
21
|
-
@bar_mark = "="
|
|
22
|
-
@current = 0
|
|
23
|
-
@previous = 0
|
|
24
|
-
@finished_p = false
|
|
25
|
-
@start_time = ::Time.now
|
|
26
|
-
@previous_time = @start_time
|
|
27
|
-
# Set title width: use provided width, or accommodate the title, with a minimum of 14
|
|
28
|
-
@title_width = title_width || [title.length, 14].max
|
|
29
|
-
@format = "%-#{@title_width}s %3d%% %s %s"
|
|
30
|
-
@format_arguments = [:title, :percentage, :bar, :stat]
|
|
31
|
-
clear
|
|
32
|
-
show
|
|
33
|
-
end
|
|
34
|
-
attr_reader :title
|
|
35
|
-
attr_reader :current
|
|
36
|
-
attr_reader :total
|
|
37
|
-
attr_accessor :start_time
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
def fmt_bar
|
|
41
|
-
bar_width = do_percentage * @terminal_width / 100
|
|
42
|
-
sprintf("|%s%s|",
|
|
43
|
-
@bar_mark * bar_width,
|
|
44
|
-
" " * (@terminal_width - bar_width))
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def fmt_percentage
|
|
48
|
-
do_percentage
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def fmt_stat
|
|
52
|
-
if @finished_p then elapsed else eta end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def fmt_stat_for_file_transfer
|
|
56
|
-
if @finished_p then
|
|
57
|
-
sprintf("%s %s %s", bytes, transfer_rate, elapsed)
|
|
58
|
-
else
|
|
59
|
-
sprintf("%s %s %s", bytes, transfer_rate, eta)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def fmt_title
|
|
64
|
-
@title[0,(@title_width - 1)] + ":"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def convert_bytes (bytes)
|
|
68
|
-
if bytes < 1024
|
|
69
|
-
sprintf("%6dB", bytes)
|
|
70
|
-
elsif bytes < 1024 * 1000 # 1000kb
|
|
71
|
-
sprintf("%5.1fKB", bytes.to_f / 1024)
|
|
72
|
-
elsif bytes < 1024 * 1024 * 1000 # 1000mb
|
|
73
|
-
sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
|
|
74
|
-
else
|
|
75
|
-
sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def transfer_rate
|
|
80
|
-
bytes_per_second = @current.to_f / (::Time.now - @start_time)
|
|
81
|
-
sprintf("%s/s", convert_bytes(bytes_per_second))
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def bytes
|
|
85
|
-
convert_bytes(@current)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def format_time (t)
|
|
89
|
-
t = t.to_i
|
|
90
|
-
sec = t % 60
|
|
91
|
-
min = (t / 60) % 60
|
|
92
|
-
hour = t / 3600
|
|
93
|
-
sprintf("%02d:%02d:%02d", hour, min, sec);
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# ETA stands for Estimated Time of Arrival.
|
|
97
|
-
def eta
|
|
98
|
-
if @current == 0
|
|
99
|
-
"ETA: --:--:--"
|
|
100
|
-
else
|
|
101
|
-
elapsed = ::Time.now - @start_time
|
|
102
|
-
eta = elapsed * @total / @current - elapsed;
|
|
103
|
-
sprintf("ETA: %s", format_time(eta))
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def elapsed
|
|
108
|
-
elapsed = ::Time.now - @start_time
|
|
109
|
-
sprintf("Time: %s", format_time(elapsed))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def eol
|
|
113
|
-
if @finished_p then "\n" else "\r" end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def do_percentage
|
|
117
|
-
if @total.zero?
|
|
118
|
-
100
|
|
119
|
-
else
|
|
120
|
-
@current * 100 / @total
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def get_width
|
|
125
|
-
# FIXME: I don't know how portable it is.
|
|
126
|
-
default_width = 80
|
|
127
|
-
begin
|
|
128
|
-
tiocgwinsz = 0x5413
|
|
129
|
-
data = [0, 0, 0, 0].pack("SSSS")
|
|
130
|
-
if @out.ioctl(tiocgwinsz, data) >= 0 then
|
|
131
|
-
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
|
132
|
-
if cols > 0 then cols else default_width end
|
|
133
|
-
else
|
|
134
|
-
default_width
|
|
135
|
-
end
|
|
136
|
-
rescue Exception
|
|
137
|
-
default_width
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def show
|
|
142
|
-
arguments = @format_arguments.map {|method|
|
|
143
|
-
method = sprintf("fmt_%s", method)
|
|
144
|
-
send(method)
|
|
145
|
-
}
|
|
146
|
-
line = sprintf(@format, *arguments)
|
|
147
|
-
|
|
148
|
-
width = get_width
|
|
149
|
-
if line.length == width - 1
|
|
150
|
-
@out.print(line + eol)
|
|
151
|
-
@out.flush
|
|
152
|
-
elsif line.length >= width
|
|
153
|
-
@terminal_width = [@terminal_width - (line.length - width + 1), 0].max
|
|
154
|
-
if @terminal_width == 0 then @out.print(line + eol) else show end
|
|
155
|
-
else # line.length < width - 1
|
|
156
|
-
@terminal_width += width - line.length + 1
|
|
157
|
-
show
|
|
158
|
-
end
|
|
159
|
-
@previous_time = ::Time.now
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def show_if_needed
|
|
163
|
-
if @total.zero?
|
|
164
|
-
cur_percentage = 100
|
|
165
|
-
prev_percentage = 0
|
|
166
|
-
else
|
|
167
|
-
cur_percentage = (@current * 100 / @total).to_i
|
|
168
|
-
prev_percentage = (@previous * 100 / @total).to_i
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Use "!=" instead of ">" to support negative changes
|
|
172
|
-
if cur_percentage != prev_percentage ||
|
|
173
|
-
::Time.now - @previous_time >= 1 || @finished_p
|
|
174
|
-
show
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
public
|
|
179
|
-
def clear
|
|
180
|
-
@out.print "\r"
|
|
181
|
-
@out.print(" " * (get_width - 1))
|
|
182
|
-
@out.print "\r"
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def finish
|
|
186
|
-
@current = @total
|
|
187
|
-
@finished_p = true
|
|
188
|
-
show
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def finished?
|
|
192
|
-
@finished_p
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def file_transfer_mode
|
|
196
|
-
@format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def format= (format)
|
|
200
|
-
@format = format
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def format_arguments= (arguments)
|
|
204
|
-
@format_arguments = arguments
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def halt
|
|
208
|
-
@finished_p = true
|
|
209
|
-
show
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def inc (step = 1)
|
|
213
|
-
@current += step
|
|
214
|
-
@current = @total if @current > @total
|
|
215
|
-
show_if_needed
|
|
216
|
-
@previous = @current
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def set (count)
|
|
220
|
-
if count < 0 || count > @total
|
|
221
|
-
raise "invalid count: #{count} (total: #{@total})"
|
|
222
|
-
end
|
|
223
|
-
@current = count
|
|
224
|
-
show_if_needed
|
|
225
|
-
@previous = @current
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def inspect
|
|
229
|
-
"#<ProgressBar:#{@current}/#{@total}>"
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
class ReversedProgressBar < ProgressBar
|
|
234
|
-
def do_percentage
|
|
235
|
-
100 - super
|
|
236
|
-
end
|
|
237
|
-
end
|