progress_bar_none_overload_3000 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +250 -0
- data/lib/cockpit3000.rb +1 -0
- data/lib/progressbarnone.rb +1 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/ansi/360/237/246/253.rb +426 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/bar/360/237/246/253.rb +234 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/download_thread_state/360/237/246/253.rb +249 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/enumerable_extension/360/237/246/253.rb +115 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/frames/360/237/246/253.rb +224 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/gantt/360/237/246/253.rb +356 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/graphics/360/237/246/253.rb +315 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/metrics/360/237/246/253.rb +150 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/multi_bar/360/237/246/253.rb +379 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/progressbar_compat/360/237/246/253.rb +132 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/renderer/360/237/246/253.rb +473 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/sparkline/360/237/246/253.rb +158 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none/version/360/237/246/253.rb +5 -0
- data/lib/progress/360/237/246/253bar/360/237/246/253none.rb +108 -0
- metadata +139 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ProgressBarNone
|
|
4
|
+
# Tracks custom metrics reported by work items
|
|
5
|
+
class Metrics
|
|
6
|
+
# Individual metric tracker
|
|
7
|
+
class Metric
|
|
8
|
+
attr_reader :name, :values, :sum, :min, :max, :count
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
@values = []
|
|
13
|
+
@sum = 0.0
|
|
14
|
+
@min = Float::INFINITY
|
|
15
|
+
@max = -Float::INFINITY
|
|
16
|
+
@count = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add(value)
|
|
20
|
+
value = value.to_f
|
|
21
|
+
@values << value
|
|
22
|
+
@sum += value
|
|
23
|
+
@min = value if value < @min
|
|
24
|
+
@max = value if value > @max
|
|
25
|
+
@count += 1
|
|
26
|
+
|
|
27
|
+
# Keep only last 100 values for sparklines
|
|
28
|
+
@values.shift if @values.length > 100
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def avg
|
|
32
|
+
return 0.0 if @count.zero?
|
|
33
|
+
@sum / @count
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def last
|
|
37
|
+
@values.last || 0.0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Recent values for sparkline (last n)
|
|
41
|
+
def recent(n = 20)
|
|
42
|
+
@values.last(n)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
avg: avg.round(2),
|
|
48
|
+
min: @min == Float::INFINITY ? 0 : @min.round(2),
|
|
49
|
+
max: @max == -Float::INFINITY ? 0 : @max.round(2),
|
|
50
|
+
sum: @sum.round(2),
|
|
51
|
+
count: @count,
|
|
52
|
+
last: last.round(2),
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :metrics
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
@metrics = {}
|
|
61
|
+
@mutex = Mutex.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Record metrics from a hash
|
|
65
|
+
# @param data [Hash] Key-value pairs of metric names and values
|
|
66
|
+
def record(data)
|
|
67
|
+
return unless data.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
data.each do |key, value|
|
|
71
|
+
next unless value.is_a?(Numeric)
|
|
72
|
+
|
|
73
|
+
key_sym = key.to_sym
|
|
74
|
+
@metrics[key_sym] ||= Metric.new(key.to_s)
|
|
75
|
+
@metrics[key_sym].add(value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get a specific metric
|
|
81
|
+
def [](name)
|
|
82
|
+
@metrics[name.to_sym]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get all metric names
|
|
86
|
+
def names
|
|
87
|
+
@metrics.keys
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if we have any metrics
|
|
91
|
+
def any?
|
|
92
|
+
@metrics.any?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Format metrics for display
|
|
96
|
+
# @param width [Integer] Available width for each metric
|
|
97
|
+
# @param palette [Symbol] Color palette to use
|
|
98
|
+
# @return [Array<String>] Formatted metric lines
|
|
99
|
+
def format_all(width: 60, palette: :crystal, sparkline_width: 15)
|
|
100
|
+
@mutex.synchronize do
|
|
101
|
+
@metrics.map do |name, metric|
|
|
102
|
+
format_metric(name, metric, width: width, palette: palette, sparkline_width: sparkline_width)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Format a single metric with sparkline
|
|
108
|
+
def format_metric(name, metric, width: 60, palette: :crystal, sparkline_width: 15)
|
|
109
|
+
# Build the metric display
|
|
110
|
+
sparkline = Sparkline.generate_colored(
|
|
111
|
+
metric.recent(sparkline_width),
|
|
112
|
+
width: sparkline_width,
|
|
113
|
+
palette: palette
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Colorized label
|
|
117
|
+
label_color = ANSI.palette_color(palette, 0.3)
|
|
118
|
+
value_color = ANSI.palette_color(palette, 0.7)
|
|
119
|
+
dim = ANSI::DIM
|
|
120
|
+
|
|
121
|
+
# Format numbers nicely
|
|
122
|
+
avg_str = format_number(metric.avg)
|
|
123
|
+
min_str = format_number(metric.min == Float::INFINITY ? 0 : metric.min)
|
|
124
|
+
max_str = format_number(metric.max == -Float::INFINITY ? 0 : metric.max)
|
|
125
|
+
sum_str = format_number(metric.sum)
|
|
126
|
+
|
|
127
|
+
# Build the line
|
|
128
|
+
"#{label_color}#{name}#{ANSI::RESET} " \
|
|
129
|
+
"#{sparkline} " \
|
|
130
|
+
"#{dim}avg:#{ANSI::RESET}#{value_color}#{avg_str}#{ANSI::RESET} " \
|
|
131
|
+
"#{dim}min:#{ANSI::RESET}#{value_color}#{min_str}#{ANSI::RESET} " \
|
|
132
|
+
"#{dim}max:#{ANSI::RESET}#{value_color}#{max_str}#{ANSI::RESET} " \
|
|
133
|
+
"#{dim}Σ:#{ANSI::RESET}#{value_color}#{sum_str}#{ANSI::RESET}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def format_number(num)
|
|
139
|
+
if num.abs >= 1_000_000
|
|
140
|
+
"#{(num / 1_000_000.0).round(1)}M"
|
|
141
|
+
elsif num.abs >= 1_000
|
|
142
|
+
"#{(num / 1_000.0).round(1)}K"
|
|
143
|
+
elsif num == num.to_i
|
|
144
|
+
num.to_i.to_s
|
|
145
|
+
else
|
|
146
|
+
num.round(2).to_s
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ProgressBarNone
|
|
4
|
+
# Multi-bar progress display with nested/hierarchical bars.
|
|
5
|
+
# Sub-bars compose into parent bars. Bars can start indeterminate
|
|
6
|
+
# and become specific once the total is discovered.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# multi = ProgressBarNone::MultiBar.new
|
|
10
|
+
# multi.add(:total, title: "TOTAL", style: :fire, palette: :neon, rainbow_mode: true)
|
|
11
|
+
# multi.add(:search, title: "Searching", parent: :total, style: :electric, palette: :ocean)
|
|
12
|
+
# multi.add(:import, title: "Importing", parent: :total, style: :crystal, palette: :crystal)
|
|
13
|
+
# multi.start
|
|
14
|
+
#
|
|
15
|
+
# multi.set_total(:search, 3)
|
|
16
|
+
# 3.times { multi.increment(:search) }
|
|
17
|
+
#
|
|
18
|
+
# multi.set_total(:import, 57)
|
|
19
|
+
# 57.times { multi.increment(:import) }
|
|
20
|
+
#
|
|
21
|
+
# multi.finish
|
|
22
|
+
#
|
|
23
|
+
class MultiBar
|
|
24
|
+
PALETTES_CYCLE = %i[neon fire ocean synthwave acid crystal vaporwave matrix lava ice galaxy toxic].freeze
|
|
25
|
+
STYLES_CYCLE = %i[fire electric crystal plasma wave nyan matrix glitch rocket cyberpunk stars skull].freeze
|
|
26
|
+
|
|
27
|
+
BarState = Struct.new(
|
|
28
|
+
:name, :title, :parent, :children,
|
|
29
|
+
:total, :current, :started_at, :finished,
|
|
30
|
+
:style, :palette, :spinner, :rainbow_mode, :glow,
|
|
31
|
+
:indeterminate, :weight,
|
|
32
|
+
keyword_init: true
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def initialize(output: $stderr, fps: 12, width: 40)
|
|
36
|
+
@bars = {}
|
|
37
|
+
@bar_order = []
|
|
38
|
+
@output = output
|
|
39
|
+
@fps = fps
|
|
40
|
+
@width = width
|
|
41
|
+
@mutex = Mutex.new
|
|
42
|
+
@lines_rendered = 0
|
|
43
|
+
@render_thread = nil
|
|
44
|
+
@started = false
|
|
45
|
+
@finished = false
|
|
46
|
+
@frame = 0
|
|
47
|
+
@start_time = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add a bar to the display.
|
|
51
|
+
# @param name [Symbol] Unique identifier
|
|
52
|
+
# @param title [String] Display title
|
|
53
|
+
# @param total [Integer, nil] Total items (nil = indeterminate)
|
|
54
|
+
# @param parent [Symbol, nil] Parent bar name (for nesting)
|
|
55
|
+
# @param style [Symbol] Bar style
|
|
56
|
+
# @param palette [Symbol] Color palette
|
|
57
|
+
# @param weight [Float] How much this bar contributes to parent (auto-calculated if not set)
|
|
58
|
+
def add(name, title:, total: nil, parent: nil, style: nil, palette: nil,
|
|
59
|
+
spinner: :braille, rainbow_mode: false, glow: false, weight: nil)
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
depth = parent ? depth_of(parent) + 1 : 0
|
|
62
|
+
style ||= STYLES_CYCLE[depth % STYLES_CYCLE.size]
|
|
63
|
+
palette ||= PALETTES_CYCLE[depth % PALETTES_CYCLE.size]
|
|
64
|
+
|
|
65
|
+
bar = BarState.new(
|
|
66
|
+
name: name, title: title, parent: parent, children: [],
|
|
67
|
+
total: total, current: 0, started_at: nil, finished: false,
|
|
68
|
+
style: style, palette: palette, spinner: spinner,
|
|
69
|
+
rainbow_mode: rainbow_mode, glow: glow,
|
|
70
|
+
indeterminate: total.nil?, weight: weight
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@bars[name] = bar
|
|
74
|
+
@bar_order << name
|
|
75
|
+
|
|
76
|
+
# Register as child of parent
|
|
77
|
+
if parent && @bars[parent]
|
|
78
|
+
@bars[parent].children << name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Set or update the total for a bar (transitions from indeterminate to determinate)
|
|
84
|
+
def set_total(name, total)
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
bar = @bars[name]
|
|
87
|
+
return unless bar
|
|
88
|
+
bar.total = total
|
|
89
|
+
bar.indeterminate = false
|
|
90
|
+
end
|
|
91
|
+
maybe_render
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Increment a bar's progress. Propagates to parent bars.
|
|
95
|
+
def increment(name, amount = 1, metrics: nil)
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
bar = @bars[name]
|
|
98
|
+
return unless bar
|
|
99
|
+
bar.current += amount
|
|
100
|
+
bar.current = [bar.current, bar.total].min if bar.total
|
|
101
|
+
bar.started_at ||= Time.now
|
|
102
|
+
|
|
103
|
+
# Propagate to parent
|
|
104
|
+
propagate_to_parent(bar)
|
|
105
|
+
end
|
|
106
|
+
maybe_render
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Log a status message for a bar (shown as subtitle)
|
|
110
|
+
def log(name, message)
|
|
111
|
+
@mutex.synchronize do
|
|
112
|
+
bar = @bars[name]
|
|
113
|
+
return unless bar
|
|
114
|
+
bar.title = message
|
|
115
|
+
end
|
|
116
|
+
maybe_render
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Mark a bar as finished
|
|
120
|
+
def finish_bar(name)
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
bar = @bars[name]
|
|
123
|
+
return unless bar
|
|
124
|
+
bar.current = bar.total if bar.total
|
|
125
|
+
bar.finished = true
|
|
126
|
+
propagate_to_parent(bar)
|
|
127
|
+
end
|
|
128
|
+
maybe_render
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Start the multi-bar display with a background render thread
|
|
132
|
+
def start
|
|
133
|
+
@start_time = Time.now
|
|
134
|
+
@started = true
|
|
135
|
+
@output.print ANSI::HIDE_CURSOR
|
|
136
|
+
render(force: true)
|
|
137
|
+
|
|
138
|
+
@render_thread = Thread.new do
|
|
139
|
+
loop do
|
|
140
|
+
break if @finished
|
|
141
|
+
sleep(1.0 / @fps)
|
|
142
|
+
render
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Stop rendering and clean up
|
|
149
|
+
def finish
|
|
150
|
+
@finished = true
|
|
151
|
+
@render_thread&.join
|
|
152
|
+
render(force: true)
|
|
153
|
+
@output.puts
|
|
154
|
+
@output.print ANSI::SHOW_CURSOR
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def depth_of(name)
|
|
161
|
+
bar = @bars[name]
|
|
162
|
+
return 0 unless bar&.parent
|
|
163
|
+
1 + depth_of(bar.parent)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def propagate_to_parent(bar)
|
|
167
|
+
return unless bar.parent
|
|
168
|
+
parent = @bars[bar.parent]
|
|
169
|
+
return unless parent
|
|
170
|
+
|
|
171
|
+
# Recalculate parent progress from children
|
|
172
|
+
children = parent.children.map { |c| @bars[c] }.compact
|
|
173
|
+
return if children.empty?
|
|
174
|
+
|
|
175
|
+
# Total = sum of child totals (known ones)
|
|
176
|
+
known_children = children.select { |c| c.total && c.total > 0 }
|
|
177
|
+
if known_children.any?
|
|
178
|
+
parent.total = known_children.sum(&:total)
|
|
179
|
+
parent.current = known_children.sum(&:current)
|
|
180
|
+
parent.indeterminate = children.any?(&:indeterminate)
|
|
181
|
+
else
|
|
182
|
+
parent.indeterminate = true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
propagate_to_parent(parent)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def maybe_render
|
|
189
|
+
render
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def render(force: false)
|
|
193
|
+
@mutex.synchronize do
|
|
194
|
+
@frame += 1
|
|
195
|
+
|
|
196
|
+
clear_rendered_lines
|
|
197
|
+
|
|
198
|
+
lines = []
|
|
199
|
+
time = Time.now - (@start_time || Time.now)
|
|
200
|
+
|
|
201
|
+
@bar_order.each do |name|
|
|
202
|
+
bar = @bars[name]
|
|
203
|
+
depth = depth_of(name)
|
|
204
|
+
indent = " " * depth
|
|
205
|
+
|
|
206
|
+
# Title line
|
|
207
|
+
title_color = ANSI.palette_color(bar.palette, 0.5)
|
|
208
|
+
spinner_char = spinning_char(bar, time)
|
|
209
|
+
|
|
210
|
+
if bar.finished
|
|
211
|
+
status = "#{ANSI::BOLD}#{ANSI::GREEN}✓#{ANSI::RESET}"
|
|
212
|
+
elsif bar.indeterminate
|
|
213
|
+
status = spinner_char
|
|
214
|
+
else
|
|
215
|
+
status = spinner_char
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
count_str = if bar.total && bar.total > 0
|
|
219
|
+
count_color = ANSI.palette_color(bar.palette, 0.7)
|
|
220
|
+
"#{count_color}#{bar.current}#{ANSI::RESET}#{ANSI::DIM}/#{bar.total}#{ANSI::RESET}"
|
|
221
|
+
elsif bar.current > 0
|
|
222
|
+
count_color = ANSI.palette_color(bar.palette, 0.7)
|
|
223
|
+
"#{count_color}#{bar.current}#{ANSI::RESET}#{ANSI::DIM}/?#{ANSI::RESET}"
|
|
224
|
+
else
|
|
225
|
+
""
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
title_line = "#{indent}#{status} #{ANSI::BOLD}#{title_color}#{bar.title}#{ANSI::RESET}"
|
|
229
|
+
title_line += " #{count_str}" unless count_str.empty?
|
|
230
|
+
|
|
231
|
+
# ETA/rate
|
|
232
|
+
if bar.started_at && bar.total && bar.total > 0 && bar.current > 0 && !bar.finished
|
|
233
|
+
elapsed = Time.now - bar.started_at
|
|
234
|
+
rate = bar.current / elapsed
|
|
235
|
+
eta = (bar.total - bar.current) / rate
|
|
236
|
+
title_line += " #{ANSI::DIM}#{format_rate(rate)} → #{format_time(eta)}#{ANSI::RESET}" if rate > 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
lines << "#{ANSI::CLEAR_LINE}#{title_line}"
|
|
240
|
+
|
|
241
|
+
# Bar line
|
|
242
|
+
bar_line = render_bar_line(bar, depth, time)
|
|
243
|
+
lines << "#{ANSI::CLEAR_LINE}#{bar_line}" if bar_line
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Output
|
|
247
|
+
lines.each_with_index do |line, i|
|
|
248
|
+
@output.print line
|
|
249
|
+
@output.print "\n" if i < lines.length - 1
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
@lines_rendered = lines.length
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def render_bar_line(bar, depth, time)
|
|
257
|
+
indent = " " * depth
|
|
258
|
+
width = @width - (depth * 2)
|
|
259
|
+
width = [width, 10].max
|
|
260
|
+
|
|
261
|
+
if bar.indeterminate
|
|
262
|
+
# Pulsing/bouncing bar for unknown total
|
|
263
|
+
render_indeterminate(bar, indent, width, time)
|
|
264
|
+
elsif bar.total && bar.total > 0
|
|
265
|
+
render_determinate(bar, indent, width, time)
|
|
266
|
+
else
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def render_indeterminate(bar, indent, width, time)
|
|
272
|
+
# Bouncing highlight
|
|
273
|
+
pos = ((Math.sin(time * 2.5) + 1) / 2 * (width - 4)).round
|
|
274
|
+
chars = ""
|
|
275
|
+
width.times do |i|
|
|
276
|
+
dist = (i - pos).abs
|
|
277
|
+
if dist < 3
|
|
278
|
+
intensity = 1.0 - dist / 3.0
|
|
279
|
+
color = ANSI.palette_color(bar.palette, intensity)
|
|
280
|
+
chars += "#{ANSI::BOLD}#{color}█#{ANSI::RESET}"
|
|
281
|
+
else
|
|
282
|
+
chars += "#{ANSI::DIM}░#{ANSI::RESET}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
"#{indent} #{ANSI::DIM}⟨#{ANSI::RESET}#{chars}#{ANSI::DIM}⟩#{ANSI::RESET}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def render_determinate(bar, indent, width, time)
|
|
289
|
+
progress = bar.current.to_f / bar.total
|
|
290
|
+
progress = [[progress, 0.0].max, 1.0].min
|
|
291
|
+
|
|
292
|
+
filled_count = (progress * width).floor
|
|
293
|
+
remaining = width - filled_count
|
|
294
|
+
|
|
295
|
+
# Build gradient bar
|
|
296
|
+
filled = ""
|
|
297
|
+
style_def = Renderer::STYLES[bar.style] || Renderer::STYLES[:crystal]
|
|
298
|
+
|
|
299
|
+
filled_count.times do |i|
|
|
300
|
+
char_progress = i.to_f / [width, 1].max
|
|
301
|
+
color = if bar.rainbow_mode
|
|
302
|
+
ANSI.rainbow_cycle(char_progress, time, 0.5)
|
|
303
|
+
else
|
|
304
|
+
ANSI.palette_color(bar.palette, char_progress)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Shimmer wave
|
|
308
|
+
wave_pos = (time * 3.0 * width) % (width * 2)
|
|
309
|
+
dist = (i - wave_pos).abs
|
|
310
|
+
bold = dist < 5 ? ANSI::BOLD : ""
|
|
311
|
+
|
|
312
|
+
filled += "#{bold}#{color}#{style_def[:filled]}#{ANSI::RESET}"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Partial block at edge
|
|
316
|
+
if remaining > 0 && progress > 0
|
|
317
|
+
partial_progress = (progress * width) - filled_count
|
|
318
|
+
partial_idx = (partial_progress * (style_def[:partial].length - 1)).round
|
|
319
|
+
partial_char = style_def[:partial][[partial_idx, 0].max] || style_def[:partial].last
|
|
320
|
+
edge_color = ANSI.palette_color(bar.palette, progress)
|
|
321
|
+
filled += "#{ANSI::BOLD}#{edge_color}#{partial_char}#{ANSI::RESET}"
|
|
322
|
+
remaining -= 1
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Empty portion
|
|
326
|
+
empty = ""
|
|
327
|
+
remaining.times { empty += "#{ANSI::DIM}#{style_def[:empty]}#{ANSI::RESET}" }
|
|
328
|
+
|
|
329
|
+
# Percentage
|
|
330
|
+
pct = (progress * 100).round(1)
|
|
331
|
+
pct_color = ANSI.palette_color(bar.palette, progress)
|
|
332
|
+
pct_str = "#{pct_color}#{format("%5.1f", pct)}%#{ANSI::RESET}"
|
|
333
|
+
|
|
334
|
+
left = "#{ANSI::DIM}#{style_def[:left]}#{ANSI::RESET}"
|
|
335
|
+
right = "#{ANSI::DIM}#{style_def[:right]}#{ANSI::RESET}"
|
|
336
|
+
|
|
337
|
+
"#{indent} #{pct_str} #{left}#{filled}#{empty}#{right}"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def spinning_char(bar, time)
|
|
341
|
+
return "" if bar.finished
|
|
342
|
+
spinners = ANSI::SPINNERS[bar.spinner] || ANSI::SPINNERS[:braille]
|
|
343
|
+
idx = (@frame % spinners.length)
|
|
344
|
+
color = ANSI.palette_color(bar.palette, (time * 2) % 1.0)
|
|
345
|
+
"#{color}#{spinners[idx]}#{ANSI::RESET}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def clear_rendered_lines
|
|
349
|
+
return if @lines_rendered.zero?
|
|
350
|
+
(@lines_rendered - 1).times do
|
|
351
|
+
@output.print ANSI.up(1)
|
|
352
|
+
@output.print ANSI::CLEAR_LINE
|
|
353
|
+
end
|
|
354
|
+
@output.print "\r#{ANSI::CLEAR_LINE}"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def format_rate(rate)
|
|
358
|
+
if rate >= 1000
|
|
359
|
+
"#{(rate / 1000.0).round(1)}K/s"
|
|
360
|
+
elsif rate >= 1
|
|
361
|
+
"#{rate.round(1)}/s"
|
|
362
|
+
else
|
|
363
|
+
"#{(rate * 60).round(1)}/min"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def format_time(seconds)
|
|
368
|
+
return "∞" if seconds == Float::INFINITY || seconds.nan?
|
|
369
|
+
return "0s" if seconds <= 0
|
|
370
|
+
if seconds < 60
|
|
371
|
+
"#{seconds.round(0)}s"
|
|
372
|
+
elsif seconds < 3600
|
|
373
|
+
"#{(seconds / 60).floor}m#{(seconds % 60).round}s"
|
|
374
|
+
else
|
|
375
|
+
"#{(seconds / 3600).floor}h#{((seconds % 3600) / 60).round}m"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
data/lib/progress/360/237/246/253bar/360/237/246/253none/progressbar_compat/360/237/246/253.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ProgressBarNone
|
|
4
|
+
module ProgressbarCompat
|
|
5
|
+
class Bar
|
|
6
|
+
DEFAULT_TOTAL = 100
|
|
7
|
+
|
|
8
|
+
def self.create(**options)
|
|
9
|
+
new(**options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(**options)
|
|
13
|
+
@output = options.fetch(:output, $stderr)
|
|
14
|
+
@title = options[:title]
|
|
15
|
+
@total = normalize_total(options[:total] || options[:length])
|
|
16
|
+
@progress = normalize_progress(options[:starting_at] || options[:progress] || 0)
|
|
17
|
+
|
|
18
|
+
@bar = ProgressBarNone::Bar.new(
|
|
19
|
+
total: @total,
|
|
20
|
+
title: @title,
|
|
21
|
+
output: @output,
|
|
22
|
+
width: options[:width] || options[:length] || 40,
|
|
23
|
+
style: options[:style] || :crystal,
|
|
24
|
+
palette: options[:palette] || :crystal,
|
|
25
|
+
spinner: options[:spinner] || :braille,
|
|
26
|
+
rainbow_mode: !!options[:rainbow_mode],
|
|
27
|
+
celebration: options[:celebration] || :confetti,
|
|
28
|
+
glow: !!options[:glow],
|
|
29
|
+
fps: options[:fps] || 15
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@bar.start
|
|
33
|
+
@bar.set(@progress)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :output
|
|
37
|
+
|
|
38
|
+
def total
|
|
39
|
+
@total
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def total=(value)
|
|
43
|
+
@total = normalize_total(value)
|
|
44
|
+
@bar.instance_variable_set(:@total, @total)
|
|
45
|
+
@progress = [@progress, @total].min
|
|
46
|
+
@bar.set(@progress)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def progress
|
|
50
|
+
@progress
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def progress=(value)
|
|
54
|
+
@progress = normalize_progress(value)
|
|
55
|
+
@bar.set(@progress)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def title
|
|
59
|
+
@title
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def title=(value)
|
|
63
|
+
@title = value.to_s
|
|
64
|
+
@bar.instance_variable_set(:@title, @title)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def increment
|
|
68
|
+
increment_progress(1)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def decrement
|
|
72
|
+
increment_progress(-1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def log(message)
|
|
76
|
+
output.puts(message)
|
|
77
|
+
output.flush
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def pause; end
|
|
81
|
+
|
|
82
|
+
def resume; end
|
|
83
|
+
|
|
84
|
+
def stopped?
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def finished?
|
|
89
|
+
@progress >= @total
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reset
|
|
93
|
+
self.progress = 0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def finish
|
|
97
|
+
@progress = @total
|
|
98
|
+
@bar.finish
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def normalize_total(value)
|
|
104
|
+
n = value.to_i
|
|
105
|
+
n = DEFAULT_TOTAL if n <= 0
|
|
106
|
+
n
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_progress(value)
|
|
110
|
+
n = value.to_i
|
|
111
|
+
n = 0 if n.negative?
|
|
112
|
+
[n, @total || DEFAULT_TOTAL].min
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def increment_progress(by)
|
|
116
|
+
next_value = @progress + by.to_i
|
|
117
|
+
self.progress = next_value
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Top-level compatibility constants matching the common progressbar API.
|
|
124
|
+
unless defined?(::ProgressBar)
|
|
125
|
+
module ProgressBar
|
|
126
|
+
class << self
|
|
127
|
+
def create(**options)
|
|
128
|
+
ProgressBarNone::ProgressbarCompat::Bar.create(**options)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|