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,473 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ProgressBarNone
|
|
4
|
+
# Renders the progress bar and metrics display
|
|
5
|
+
class Renderer
|
|
6
|
+
# Crystal-inspired bar characters
|
|
7
|
+
CRYSTAL_CHARS = {
|
|
8
|
+
filled: "█",
|
|
9
|
+
partial: ["░", "▒", "▓"],
|
|
10
|
+
empty: "░",
|
|
11
|
+
left_cap: "❮",
|
|
12
|
+
right_cap: "❯",
|
|
13
|
+
shimmer: ["✦", "✧", "⬥", "⬦", "◆", "◇"],
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Alternative styles
|
|
17
|
+
STYLES = {
|
|
18
|
+
crystal: {
|
|
19
|
+
filled: "█",
|
|
20
|
+
partial: ["░", "▒", "▓"],
|
|
21
|
+
empty: "░",
|
|
22
|
+
left: "⟨",
|
|
23
|
+
right: "⟩",
|
|
24
|
+
pulse: ["◈", "◇", "◆"],
|
|
25
|
+
},
|
|
26
|
+
blocks: {
|
|
27
|
+
filled: "█",
|
|
28
|
+
partial: ["▏", "▎", "▍", "▌", "▋", "▊", "▉"],
|
|
29
|
+
empty: "░",
|
|
30
|
+
left: "[",
|
|
31
|
+
right: "]",
|
|
32
|
+
pulse: ["█", "▓", "▒"],
|
|
33
|
+
},
|
|
34
|
+
dots: {
|
|
35
|
+
filled: "●",
|
|
36
|
+
partial: ["○", "◐", "◑", "●"],
|
|
37
|
+
empty: "○",
|
|
38
|
+
left: "(",
|
|
39
|
+
right: ")",
|
|
40
|
+
pulse: ["●", "◉", "○"],
|
|
41
|
+
},
|
|
42
|
+
arrows: {
|
|
43
|
+
filled: "▶",
|
|
44
|
+
partial: ["▷", "▸", "▹"],
|
|
45
|
+
empty: "▹",
|
|
46
|
+
left: "«",
|
|
47
|
+
right: "»",
|
|
48
|
+
pulse: ["▸", "▹", "▸"],
|
|
49
|
+
},
|
|
50
|
+
ascii: {
|
|
51
|
+
filled: "#",
|
|
52
|
+
partial: ["-", "=", "#"],
|
|
53
|
+
empty: "-",
|
|
54
|
+
left: "[",
|
|
55
|
+
right: "]",
|
|
56
|
+
pulse: ["#", "=", "-"],
|
|
57
|
+
},
|
|
58
|
+
# Animated styles
|
|
59
|
+
fire: {
|
|
60
|
+
filled: "█",
|
|
61
|
+
partial: ["░", "▒", "▓", "█"],
|
|
62
|
+
empty: "░",
|
|
63
|
+
left: "🔥",
|
|
64
|
+
right: "🔥",
|
|
65
|
+
pulse: ["🔥", "💥", "✨"],
|
|
66
|
+
animated: true,
|
|
67
|
+
},
|
|
68
|
+
nyan: {
|
|
69
|
+
filled: "█",
|
|
70
|
+
partial: ["░", "▒", "▓"],
|
|
71
|
+
empty: "░",
|
|
72
|
+
left: "🐱",
|
|
73
|
+
right: "🌈",
|
|
74
|
+
pulse: ["⭐", "✨", "💫"],
|
|
75
|
+
animated: true,
|
|
76
|
+
},
|
|
77
|
+
matrix: {
|
|
78
|
+
filled: "█",
|
|
79
|
+
partial: ["░", "▒", "▓"],
|
|
80
|
+
empty: "░",
|
|
81
|
+
left: "⟨",
|
|
82
|
+
right: "⟩",
|
|
83
|
+
pulse: ["0", "1", "█"],
|
|
84
|
+
animated: true,
|
|
85
|
+
},
|
|
86
|
+
glitch: {
|
|
87
|
+
filled: "█",
|
|
88
|
+
partial: ["▓", "▒", "░"],
|
|
89
|
+
empty: "░",
|
|
90
|
+
left: "⌈",
|
|
91
|
+
right: "⌉",
|
|
92
|
+
pulse: ["▓", "▒", "░", "█"],
|
|
93
|
+
animated: true,
|
|
94
|
+
},
|
|
95
|
+
plasma: {
|
|
96
|
+
filled: "◆",
|
|
97
|
+
partial: ["◇", "◈", "◆"],
|
|
98
|
+
empty: "·",
|
|
99
|
+
left: "《",
|
|
100
|
+
right: "》",
|
|
101
|
+
pulse: ["◆", "◈", "◇"],
|
|
102
|
+
animated: true,
|
|
103
|
+
},
|
|
104
|
+
wave: {
|
|
105
|
+
filled: "█",
|
|
106
|
+
partial: ["▁", "▂", "▃", "▄", "▅", "▆", "▇"],
|
|
107
|
+
empty: "▁",
|
|
108
|
+
left: "〈",
|
|
109
|
+
right: "〉",
|
|
110
|
+
pulse: ["~", "≈", "∿"],
|
|
111
|
+
animated: true,
|
|
112
|
+
},
|
|
113
|
+
electric: {
|
|
114
|
+
filled: "⚡",
|
|
115
|
+
partial: ["·", "∘", "○", "◉"],
|
|
116
|
+
empty: "·",
|
|
117
|
+
left: "⟪",
|
|
118
|
+
right: "⟫",
|
|
119
|
+
pulse: ["⚡", "✧", "★"],
|
|
120
|
+
animated: true,
|
|
121
|
+
},
|
|
122
|
+
skull: {
|
|
123
|
+
filled: "█",
|
|
124
|
+
partial: ["░", "▒", "▓"],
|
|
125
|
+
empty: "░",
|
|
126
|
+
left: "💀",
|
|
127
|
+
right: "☠️",
|
|
128
|
+
pulse: ["💀", "☠️", "🔥"],
|
|
129
|
+
animated: true,
|
|
130
|
+
},
|
|
131
|
+
retro: {
|
|
132
|
+
filled: "■",
|
|
133
|
+
partial: ["□", "▪", "■"],
|
|
134
|
+
empty: "□",
|
|
135
|
+
left: "[",
|
|
136
|
+
right: "]",
|
|
137
|
+
pulse: ["■", "□", "▪"],
|
|
138
|
+
},
|
|
139
|
+
hearts: {
|
|
140
|
+
filled: "♥",
|
|
141
|
+
partial: ["♡", "❤", "♥"],
|
|
142
|
+
empty: "♡",
|
|
143
|
+
left: "💕",
|
|
144
|
+
right: "💕",
|
|
145
|
+
pulse: ["💖", "💗", "💓"],
|
|
146
|
+
animated: true,
|
|
147
|
+
},
|
|
148
|
+
stars: {
|
|
149
|
+
filled: "★",
|
|
150
|
+
partial: ["☆", "✦", "★"],
|
|
151
|
+
empty: "☆",
|
|
152
|
+
left: "⭐",
|
|
153
|
+
right: "🌟",
|
|
154
|
+
pulse: ["✨", "💫", "⭐"],
|
|
155
|
+
animated: true,
|
|
156
|
+
},
|
|
157
|
+
cyberpunk: {
|
|
158
|
+
filled: "▰",
|
|
159
|
+
partial: ["▱", "▰"],
|
|
160
|
+
empty: "▱",
|
|
161
|
+
left: "⟦",
|
|
162
|
+
right: "⟧",
|
|
163
|
+
pulse: ["▰", "▱", "▰"],
|
|
164
|
+
animated: true,
|
|
165
|
+
},
|
|
166
|
+
pixel: {
|
|
167
|
+
filled: "▓",
|
|
168
|
+
partial: ["░", "▒", "▓"],
|
|
169
|
+
empty: "░",
|
|
170
|
+
left: "⌜",
|
|
171
|
+
right: "⌝",
|
|
172
|
+
pulse: ["▓", "▒", "░"],
|
|
173
|
+
},
|
|
174
|
+
snake: {
|
|
175
|
+
filled: "◉",
|
|
176
|
+
partial: ["○", "◎", "◉"],
|
|
177
|
+
empty: "·",
|
|
178
|
+
left: "🐍",
|
|
179
|
+
right: "🍎",
|
|
180
|
+
pulse: ["◉", "◎", "○"],
|
|
181
|
+
animated: true,
|
|
182
|
+
},
|
|
183
|
+
music: {
|
|
184
|
+
filled: "♫",
|
|
185
|
+
partial: ["♪", "♬", "♫"],
|
|
186
|
+
empty: "·",
|
|
187
|
+
left: "🎵",
|
|
188
|
+
right: "🎶",
|
|
189
|
+
pulse: ["🎵", "🎶", "🎼"],
|
|
190
|
+
animated: true,
|
|
191
|
+
},
|
|
192
|
+
rocket: {
|
|
193
|
+
filled: "█",
|
|
194
|
+
partial: ["░", "▒", "▓"],
|
|
195
|
+
empty: "·",
|
|
196
|
+
left: "🚀",
|
|
197
|
+
right: "🌙",
|
|
198
|
+
pulse: ["🚀", "💨", "✨"],
|
|
199
|
+
animated: true,
|
|
200
|
+
},
|
|
201
|
+
}.freeze
|
|
202
|
+
|
|
203
|
+
attr_reader :style, :width, :palette, :spinner_style, :rainbow_mode, :celebration_mode
|
|
204
|
+
|
|
205
|
+
def initialize(style: :crystal, width: 40, palette: :crystal, spinner: :braille,
|
|
206
|
+
rainbow_mode: false, celebration_mode: :confetti, glow: false)
|
|
207
|
+
@style = STYLES[style] || STYLES[:crystal]
|
|
208
|
+
@style_name = style
|
|
209
|
+
@width = width
|
|
210
|
+
@palette = palette
|
|
211
|
+
@spinner_style = spinner
|
|
212
|
+
@rainbow_mode = rainbow_mode
|
|
213
|
+
@celebration_mode = celebration_mode
|
|
214
|
+
@glow = glow
|
|
215
|
+
@animation_frame = 0
|
|
216
|
+
@last_render_time = Time.now
|
|
217
|
+
@start_time = Time.now
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Render the complete progress display
|
|
221
|
+
def render(state)
|
|
222
|
+
@animation_frame += 1
|
|
223
|
+
lines = []
|
|
224
|
+
|
|
225
|
+
# Main progress bar line
|
|
226
|
+
lines << render_progress_bar(state)
|
|
227
|
+
|
|
228
|
+
# Stats line
|
|
229
|
+
lines << render_stats(state)
|
|
230
|
+
|
|
231
|
+
# Metrics lines (if any)
|
|
232
|
+
if state[:metrics]&.any?
|
|
233
|
+
lines << "" # Spacer
|
|
234
|
+
lines.concat(render_metrics(state[:metrics]))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
lines
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Render just the progress bar
|
|
241
|
+
def render_progress_bar(state)
|
|
242
|
+
progress = state[:progress] || 0.0
|
|
243
|
+
progress = [[progress, 0.0].max, 1.0].min
|
|
244
|
+
|
|
245
|
+
filled_width = (progress * @width).floor
|
|
246
|
+
remaining = @width - filled_width
|
|
247
|
+
|
|
248
|
+
# Build the bar with gradient colors
|
|
249
|
+
bar = build_gradient_bar(filled_width, remaining, progress)
|
|
250
|
+
|
|
251
|
+
# Percentage with color
|
|
252
|
+
pct = (progress * 100).round(1)
|
|
253
|
+
pct_color = ANSI.palette_color(@palette, progress)
|
|
254
|
+
pct_str = "#{pct_color}#{format("%5.1f", pct)}%#{ANSI::RESET}"
|
|
255
|
+
|
|
256
|
+
# Caps
|
|
257
|
+
left = "#{ANSI::DIM}#{@style[:left]}#{ANSI::RESET}"
|
|
258
|
+
right = "#{ANSI::DIM}#{@style[:right]}#{ANSI::RESET}"
|
|
259
|
+
|
|
260
|
+
"#{pct_str} #{left}#{bar}#{right}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Render statistics line
|
|
264
|
+
def render_stats(state)
|
|
265
|
+
parts = []
|
|
266
|
+
|
|
267
|
+
# Items done / total
|
|
268
|
+
if state[:total]
|
|
269
|
+
done_color = ANSI.palette_color(@palette, 0.5)
|
|
270
|
+
total_color = ANSI::DIM
|
|
271
|
+
parts << "#{done_color}#{state[:current]}#{ANSI::RESET}#{total_color}/#{state[:total]}#{ANSI::RESET}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Rate
|
|
275
|
+
if state[:rate] && state[:rate] > 0
|
|
276
|
+
rate_color = ANSI.palette_color(@palette, 0.3)
|
|
277
|
+
rate_str = format_rate(state[:rate])
|
|
278
|
+
parts << "#{rate_color}#{rate_str}#{ANSI::RESET}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Elapsed time
|
|
282
|
+
if state[:elapsed]
|
|
283
|
+
elapsed_str = format_time(state[:elapsed])
|
|
284
|
+
parts << "#{ANSI::DIM}⏱#{ANSI::RESET} #{elapsed_str}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# ETA
|
|
288
|
+
if state[:eta] && state[:eta] > 0 && state[:eta] < Float::INFINITY
|
|
289
|
+
eta_color = ANSI.palette_color(@palette, 0.7)
|
|
290
|
+
eta_str = format_time(state[:eta])
|
|
291
|
+
parts << "#{ANSI::DIM}→#{ANSI::RESET} #{eta_color}#{eta_str}#{ANSI::RESET}"
|
|
292
|
+
elsif state[:progress] && state[:progress] >= 1.0
|
|
293
|
+
# CELEBRATION TIME!
|
|
294
|
+
celebration = render_celebration
|
|
295
|
+
done_color = ANSI.palette_color(@palette, 1.0)
|
|
296
|
+
parts << "#{celebration} #{done_color}✓ Done!#{ANSI::RESET} #{celebration}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Spinner for active work
|
|
300
|
+
if state[:progress] && state[:progress] < 1.0 && state[:progress] > 0
|
|
301
|
+
spinner = render_spinner
|
|
302
|
+
parts.unshift(spinner)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
parts.join(" ")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Render metrics section
|
|
309
|
+
def render_metrics(metrics)
|
|
310
|
+
metrics.format_all(palette: @palette)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
def build_gradient_bar(filled, remaining, progress)
|
|
316
|
+
bar = ""
|
|
317
|
+
time = Time.now - @start_time
|
|
318
|
+
|
|
319
|
+
# Filled portion with gradient (with optional rainbow cycling and shimmer)
|
|
320
|
+
filled.times do |i|
|
|
321
|
+
char_progress = i.to_f / [@width, 1].max
|
|
322
|
+
|
|
323
|
+
# Choose color based on mode
|
|
324
|
+
color = if @rainbow_mode
|
|
325
|
+
# Rainbow cycling animation
|
|
326
|
+
ANSI.rainbow_cycle(char_progress, time, 0.5)
|
|
327
|
+
elsif @style[:animated] && @style_name == :fire
|
|
328
|
+
# Fire flicker effect
|
|
329
|
+
base_colors = [[255, 80, 0], [255, 160, 0], [255, 200, 0]]
|
|
330
|
+
c = base_colors[(char_progress * (base_colors.length - 1)).round]
|
|
331
|
+
ANSI.fire_flicker(c[0], c[1], c[2], time + i * 0.1)
|
|
332
|
+
elsif @style[:animated] && @style_name == :glitch
|
|
333
|
+
# Glitch effect
|
|
334
|
+
palette = ANSI::CRYSTAL_PALETTE[@palette] || ANSI::CRYSTAL_PALETTE[:crystal]
|
|
335
|
+
c = palette[(char_progress * (palette.length - 1)).round]
|
|
336
|
+
ANSI.glitch(c[0], c[1], c[2], 0.05)
|
|
337
|
+
elsif @style[:animated] && @style_name == :matrix
|
|
338
|
+
# Matrix rain effect - occasionally show 0 or 1
|
|
339
|
+
if rand < 0.1
|
|
340
|
+
ANSI.rgb(0, 255, 0)
|
|
341
|
+
else
|
|
342
|
+
ANSI.palette_color(@palette, char_progress)
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
ANSI.palette_color(@palette, char_progress)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Add shimmer wave effect
|
|
349
|
+
shimmer_intensity = calculate_shimmer(i, filled, time)
|
|
350
|
+
if shimmer_intensity > 0
|
|
351
|
+
color = apply_shimmer_to_color(color, shimmer_intensity)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Apply glow effect if enabled
|
|
355
|
+
char = @style[:filled]
|
|
356
|
+
if @glow && i == filled - 1
|
|
357
|
+
# Leading edge gets glow
|
|
358
|
+
bar += "#{ANSI::BOLD}#{color}#{char}#{ANSI::RESET}"
|
|
359
|
+
else
|
|
360
|
+
bar += "#{color}#{char}#{ANSI::RESET}"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Partial block at the edge with pulsing
|
|
365
|
+
if remaining > 0 && progress > 0
|
|
366
|
+
partial_progress = (progress * @width) - filled
|
|
367
|
+
partial_index = (partial_progress * (@style[:partial].length - 1)).round
|
|
368
|
+
partial_char = @style[:partial][partial_index] || @style[:partial].last
|
|
369
|
+
|
|
370
|
+
edge_color = if @rainbow_mode
|
|
371
|
+
ANSI.rainbow_cycle(progress, time, 0.5)
|
|
372
|
+
else
|
|
373
|
+
ANSI.palette_color(@palette, progress)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
bar += "#{ANSI::BOLD}#{edge_color}#{partial_char}#{ANSI::RESET}"
|
|
377
|
+
remaining -= 1
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Empty portion with subtle animation
|
|
381
|
+
remaining.times do |i|
|
|
382
|
+
if @style[:animated] && rand < 0.02
|
|
383
|
+
# Occasional sparkle in empty space
|
|
384
|
+
sparkle = ["·", "∙", "•"][rand(3)]
|
|
385
|
+
bar += "#{ANSI::DIM}#{sparkle}#{ANSI::RESET}"
|
|
386
|
+
else
|
|
387
|
+
bar += "#{ANSI::DIM}#{@style[:empty]}#{ANSI::RESET}"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
bar
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def calculate_shimmer(position, total_filled, time)
|
|
395
|
+
return 0 if total_filled < 3
|
|
396
|
+
|
|
397
|
+
# Traveling wave shimmer
|
|
398
|
+
wave_speed = 3.0
|
|
399
|
+
wave_width = 5.0
|
|
400
|
+
wave_pos = (time * wave_speed * @width) % (@width * 2)
|
|
401
|
+
|
|
402
|
+
distance = (position - wave_pos).abs
|
|
403
|
+
if distance < wave_width
|
|
404
|
+
(1.0 - distance / wave_width) * 0.5
|
|
405
|
+
else
|
|
406
|
+
0
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def apply_shimmer_to_color(color_code, intensity)
|
|
411
|
+
# Extract RGB from color code if possible, boost brightness
|
|
412
|
+
# For simplicity, we'll just add BOLD for shimmer
|
|
413
|
+
"#{ANSI::BOLD}#{color_code}"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def render_spinner
|
|
417
|
+
spinners = ANSI::SPINNERS[@spinner_style] || ANSI::SPINNERS[:braille]
|
|
418
|
+
time = Time.now - @start_time
|
|
419
|
+
|
|
420
|
+
# Animate color
|
|
421
|
+
spinner_color = if @rainbow_mode
|
|
422
|
+
ANSI.rainbow_cycle(0, time, 2.0)
|
|
423
|
+
else
|
|
424
|
+
ANSI.palette_color(@palette, (time * 2) % 1.0)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
spinner_char = spinners[@animation_frame % spinners.length]
|
|
428
|
+
"#{spinner_color}#{spinner_char}#{ANSI::RESET}"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Render celebration effects when complete
|
|
432
|
+
def render_celebration
|
|
433
|
+
time = Time.now - @start_time
|
|
434
|
+
effects = ANSI::CELEBRATIONS[@celebration_mode] || ANSI::CELEBRATIONS[:confetti]
|
|
435
|
+
|
|
436
|
+
# Animated celebration
|
|
437
|
+
celebration = ""
|
|
438
|
+
5.times do |i|
|
|
439
|
+
char = effects[((@animation_frame + i * 3) % effects.length)]
|
|
440
|
+
color = ANSI.rainbow_cycle(i / 5.0, time, 3.0)
|
|
441
|
+
celebration += "#{color}#{char}#{ANSI::RESET}"
|
|
442
|
+
end
|
|
443
|
+
celebration
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def format_rate(rate)
|
|
447
|
+
if rate >= 1000
|
|
448
|
+
"#{(rate / 1000.0).round(1)}K/s"
|
|
449
|
+
elsif rate >= 1
|
|
450
|
+
"#{rate.round(1)}/s"
|
|
451
|
+
else
|
|
452
|
+
"#{(rate * 60).round(1)}/min"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def format_time(seconds)
|
|
457
|
+
return "∞" if seconds == Float::INFINITY
|
|
458
|
+
return "0s" if seconds <= 0
|
|
459
|
+
|
|
460
|
+
if seconds < 60
|
|
461
|
+
"#{seconds.round(1)}s"
|
|
462
|
+
elsif seconds < 3600
|
|
463
|
+
mins = (seconds / 60).floor
|
|
464
|
+
secs = (seconds % 60).round
|
|
465
|
+
"#{mins}m#{secs}s"
|
|
466
|
+
else
|
|
467
|
+
hours = (seconds / 3600).floor
|
|
468
|
+
mins = ((seconds % 3600) / 60).round
|
|
469
|
+
"#{hours}h#{mins}m"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ProgressBarNone
|
|
4
|
+
# Sparkline visualization for metrics
|
|
5
|
+
module Sparkline
|
|
6
|
+
# Unicode block characters for vertical bars (8 levels)
|
|
7
|
+
BLOCKS = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"].freeze
|
|
8
|
+
|
|
9
|
+
# Braille patterns for high-resolution sparklines
|
|
10
|
+
BRAILLE_BASE = 0x2800
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Generate a sparkline from an array of values
|
|
14
|
+
# @param values [Array<Numeric>] The values to visualize
|
|
15
|
+
# @param width [Integer] Maximum width of the sparkline
|
|
16
|
+
# @param min [Numeric, nil] Minimum value (auto-detect if nil)
|
|
17
|
+
# @param max [Numeric, nil] Maximum value (auto-detect if nil)
|
|
18
|
+
# @param style [Symbol] :blocks or :braille
|
|
19
|
+
# @return [String] The sparkline string
|
|
20
|
+
def generate(values, width: 20, min: nil, max: nil, style: :blocks)
|
|
21
|
+
return "" if values.empty?
|
|
22
|
+
|
|
23
|
+
# Sample values if we have more than width
|
|
24
|
+
sampled = sample_values(values, width)
|
|
25
|
+
return "" if sampled.empty?
|
|
26
|
+
|
|
27
|
+
min ||= sampled.min
|
|
28
|
+
max ||= sampled.max
|
|
29
|
+
range = max - min
|
|
30
|
+
|
|
31
|
+
case style
|
|
32
|
+
when :braille
|
|
33
|
+
generate_braille(sampled, min, range)
|
|
34
|
+
else
|
|
35
|
+
generate_blocks(sampled, min, range)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate with color gradient
|
|
40
|
+
def generate_colored(values, width: 20, palette: :crystal, min: nil, max: nil)
|
|
41
|
+
return "" if values.empty?
|
|
42
|
+
|
|
43
|
+
sampled = sample_values(values, width)
|
|
44
|
+
return "" if sampled.empty?
|
|
45
|
+
|
|
46
|
+
min ||= sampled.min
|
|
47
|
+
max ||= sampled.max
|
|
48
|
+
range = [max - min, 0.001].max
|
|
49
|
+
|
|
50
|
+
sampled.each_with_index.map do |val, i|
|
|
51
|
+
normalized = range.zero? ? 0.5 : (val - min) / range
|
|
52
|
+
block_index = (normalized * (BLOCKS.length - 1)).round
|
|
53
|
+
block = BLOCKS[block_index]
|
|
54
|
+
|
|
55
|
+
# Color based on position in the sparkline
|
|
56
|
+
progress = i.to_f / [sampled.length - 1, 1].max
|
|
57
|
+
color = ANSI.palette_color(palette, progress)
|
|
58
|
+
|
|
59
|
+
"#{color}#{block}#{ANSI::RESET}"
|
|
60
|
+
end.join
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Mini histogram
|
|
64
|
+
def histogram(values, width: 10, height: 3, palette: :crystal)
|
|
65
|
+
return "" if values.empty?
|
|
66
|
+
|
|
67
|
+
# Create buckets
|
|
68
|
+
min = values.min
|
|
69
|
+
max = values.max
|
|
70
|
+
range = [max - min, 0.001].max
|
|
71
|
+
|
|
72
|
+
buckets = Array.new(width, 0)
|
|
73
|
+
values.each do |v|
|
|
74
|
+
bucket = ((v - min) / range * (width - 1)).floor
|
|
75
|
+
bucket = [[bucket, 0].max, width - 1].min
|
|
76
|
+
buckets[bucket] += 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
max_count = buckets.max
|
|
80
|
+
return " " * width if max_count.zero?
|
|
81
|
+
|
|
82
|
+
# Generate rows from top to bottom
|
|
83
|
+
rows = (0...height).map do |row|
|
|
84
|
+
threshold = (height - row) / height.to_f * max_count
|
|
85
|
+
buckets.each_with_index.map do |count, i|
|
|
86
|
+
progress = i.to_f / [width - 1, 1].max
|
|
87
|
+
color = ANSI.palette_color(palette, progress)
|
|
88
|
+
char = count >= threshold ? "█" : " "
|
|
89
|
+
"#{color}#{char}#{ANSI::RESET}"
|
|
90
|
+
end.join
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
rows.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def sample_values(values, width)
|
|
99
|
+
return values if values.length <= width
|
|
100
|
+
|
|
101
|
+
# Downsample by taking averages
|
|
102
|
+
chunk_size = (values.length / width.to_f).ceil
|
|
103
|
+
values.each_slice(chunk_size).map do |chunk|
|
|
104
|
+
chunk.sum / chunk.length.to_f
|
|
105
|
+
end.first(width)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def generate_blocks(values, min, range)
|
|
109
|
+
values.map do |val|
|
|
110
|
+
normalized = range.zero? ? 0.5 : (val - min) / range
|
|
111
|
+
index = (normalized * (BLOCKS.length - 1)).round
|
|
112
|
+
BLOCKS[index]
|
|
113
|
+
end.join
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def generate_braille(values, min, range)
|
|
117
|
+
# Braille uses 2x4 dot patterns per character
|
|
118
|
+
# Group values in pairs
|
|
119
|
+
values.each_slice(2).map do |pair|
|
|
120
|
+
val1 = pair[0]
|
|
121
|
+
val2 = pair[1] || val1
|
|
122
|
+
|
|
123
|
+
n1 = range.zero? ? 0.5 : (val1 - min) / range
|
|
124
|
+
n2 = range.zero? ? 0.5 : (val2 - min) / range
|
|
125
|
+
|
|
126
|
+
# Map to 4 vertical dots
|
|
127
|
+
dots1 = (n1 * 4).round
|
|
128
|
+
dots2 = (n2 * 4).round
|
|
129
|
+
|
|
130
|
+
braille_char(dots1, dots2)
|
|
131
|
+
end.join
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def braille_char(left_dots, right_dots)
|
|
135
|
+
# Braille dot positions:
|
|
136
|
+
# 1 4
|
|
137
|
+
# 2 5
|
|
138
|
+
# 3 6
|
|
139
|
+
# 7 8
|
|
140
|
+
pattern = 0
|
|
141
|
+
|
|
142
|
+
# Left column (dots 1,2,3,7)
|
|
143
|
+
pattern |= 0x01 if left_dots >= 1
|
|
144
|
+
pattern |= 0x02 if left_dots >= 2
|
|
145
|
+
pattern |= 0x04 if left_dots >= 3
|
|
146
|
+
pattern |= 0x40 if left_dots >= 4
|
|
147
|
+
|
|
148
|
+
# Right column (dots 4,5,6,8)
|
|
149
|
+
pattern |= 0x08 if right_dots >= 1
|
|
150
|
+
pattern |= 0x10 if right_dots >= 2
|
|
151
|
+
pattern |= 0x20 if right_dots >= 3
|
|
152
|
+
pattern |= 0x80 if right_dots >= 4
|
|
153
|
+
|
|
154
|
+
(BRAILLE_BASE + pattern).chr(Encoding::UTF_8)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|