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.
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ # ANSI escape codes for colors, cursor control, and styling
5
+ module ANSI
6
+ # Escape sequence prefix
7
+ ESC = "\e["
8
+
9
+ # Cursor control
10
+ HIDE_CURSOR = "#{ESC}?25l"
11
+ SHOW_CURSOR = "#{ESC}?25h"
12
+ SAVE_CURSOR = "\e7"
13
+ RESTORE_CURSOR = "\e8"
14
+ CLEAR_LINE = "#{ESC}2K"
15
+ CLEAR_TO_END = "#{ESC}0K"
16
+
17
+ # Text styles
18
+ RESET = "#{ESC}0m"
19
+ BOLD = "#{ESC}1m"
20
+ DIM = "#{ESC}2m"
21
+ ITALIC = "#{ESC}3m"
22
+ UNDERLINE = "#{ESC}4m"
23
+ BLINK = "#{ESC}5m"
24
+ REVERSE = "#{ESC}7m"
25
+
26
+ # Standard colors (foreground)
27
+ BLACK = "#{ESC}30m"
28
+ RED = "#{ESC}31m"
29
+ GREEN = "#{ESC}32m"
30
+ YELLOW = "#{ESC}33m"
31
+ BLUE = "#{ESC}34m"
32
+ MAGENTA = "#{ESC}35m"
33
+ CYAN = "#{ESC}36m"
34
+ WHITE = "#{ESC}37m"
35
+
36
+ # Bright colors
37
+ BRIGHT_BLACK = "#{ESC}90m"
38
+ BRIGHT_RED = "#{ESC}91m"
39
+ BRIGHT_GREEN = "#{ESC}92m"
40
+ BRIGHT_YELLOW = "#{ESC}93m"
41
+ BRIGHT_BLUE = "#{ESC}94m"
42
+ BRIGHT_MAGENTA = "#{ESC}95m"
43
+ BRIGHT_CYAN = "#{ESC}96m"
44
+ BRIGHT_WHITE = "#{ESC}97m"
45
+
46
+ # Background colors
47
+ BG_BLACK = "#{ESC}40m"
48
+ BG_RED = "#{ESC}41m"
49
+ BG_GREEN = "#{ESC}42m"
50
+ BG_YELLOW = "#{ESC}43m"
51
+ BG_BLUE = "#{ESC}44m"
52
+ BG_MAGENTA = "#{ESC}45m"
53
+ BG_CYAN = "#{ESC}46m"
54
+ BG_WHITE = "#{ESC}47m"
55
+
56
+ # Crystal color palette - beautiful gradients (moved to module level)
57
+ CRYSTAL_PALETTE = {
58
+ # Cyan to purple crystal gradient
59
+ crystal: [
60
+ [80, 220, 255], # Bright cyan
61
+ [100, 200, 255], # Light blue
62
+ [130, 180, 255], # Sky blue
63
+ [160, 160, 255], # Periwinkle
64
+ [190, 140, 255], # Light purple
65
+ [220, 120, 255], # Bright purple
66
+ [255, 100, 220], # Pink
67
+ ],
68
+ # Fire gradient
69
+ fire: [
70
+ [255, 80, 0], # Orange
71
+ [255, 120, 0], # Light orange
72
+ [255, 160, 0], # Yellow-orange
73
+ [255, 200, 0], # Yellow
74
+ [255, 220, 100], # Light yellow
75
+ ],
76
+ # Ocean gradient
77
+ ocean: [
78
+ [0, 80, 120], # Deep blue
79
+ [0, 120, 160], # Ocean blue
80
+ [0, 160, 200], # Bright blue
81
+ [0, 200, 220], # Turquoise
82
+ [100, 220, 220], # Aqua
83
+ ],
84
+ # Forest gradient
85
+ forest: [
86
+ [0, 80, 40], # Deep green
87
+ [0, 120, 60], # Forest
88
+ [40, 160, 80], # Green
89
+ [80, 200, 100], # Bright green
90
+ [160, 220, 120], # Light green
91
+ ],
92
+ # Sunset gradient
93
+ sunset: [
94
+ [100, 0, 120], # Deep purple
95
+ [160, 0, 100], # Purple
96
+ [200, 40, 80], # Magenta
97
+ [255, 80, 60], # Red-orange
98
+ [255, 140, 40], # Orange
99
+ [255, 200, 80], # Yellow
100
+ ],
101
+ # Rainbow
102
+ rainbow: [
103
+ [255, 0, 0], # Red
104
+ [255, 127, 0], # Orange
105
+ [255, 255, 0], # Yellow
106
+ [0, 255, 0], # Green
107
+ [0, 0, 255], # Blue
108
+ [75, 0, 130], # Indigo
109
+ [148, 0, 211], # Violet
110
+ ],
111
+ # Monochrome
112
+ mono: [
113
+ [60, 60, 60],
114
+ [100, 100, 100],
115
+ [140, 140, 140],
116
+ [180, 180, 180],
117
+ [220, 220, 220],
118
+ ],
119
+ # Extended palettes
120
+ # Neon cyberpunk
121
+ neon: [
122
+ [255, 0, 102], # Hot pink
123
+ [255, 0, 255], # Magenta
124
+ [0, 255, 255], # Cyan
125
+ [0, 255, 128], # Neon green
126
+ [255, 255, 0], # Electric yellow
127
+ [255, 0, 102], # Back to hot pink (loop)
128
+ ],
129
+ # Synthwave/Outrun
130
+ synthwave: [
131
+ [15, 5, 40], # Deep purple night
132
+ [139, 0, 139], # Dark magenta
133
+ [255, 0, 100], # Hot pink
134
+ [255, 110, 199], # Pink
135
+ [0, 255, 255], # Cyan glow
136
+ [255, 255, 100], # Pale yellow sun
137
+ ],
138
+ # Vaporwave aesthetic
139
+ vaporwave: [
140
+ [255, 113, 206], # Pink
141
+ [185, 103, 255], # Purple
142
+ [1, 205, 254], # Cyan
143
+ [5, 255, 161], # Teal
144
+ [255, 251, 150], # Yellow
145
+ [255, 113, 206], # Back to pink
146
+ ],
147
+ # Acid/Psychedelic
148
+ acid: [
149
+ [0, 255, 0], # Nuclear green
150
+ [255, 255, 0], # Bright yellow
151
+ [255, 0, 255], # Magenta
152
+ [0, 255, 255], # Cyan
153
+ [255, 128, 0], # Orange
154
+ [128, 255, 0], # Lime
155
+ ],
156
+ # Plasma/Electric
157
+ plasma: [
158
+ [128, 0, 255], # Electric purple
159
+ [255, 0, 128], # Electric pink
160
+ [255, 0, 255], # Magenta
161
+ [0, 128, 255], # Electric blue
162
+ [0, 255, 255], # Cyan
163
+ [128, 0, 255], # Back to purple
164
+ ],
165
+ # Matrix green
166
+ matrix: [
167
+ [0, 40, 0], # Dark green
168
+ [0, 80, 0], # Forest green
169
+ [0, 140, 0], # Green
170
+ [0, 200, 0], # Bright green
171
+ [0, 255, 0], # Neon green
172
+ [180, 255, 180], # White-green glow
173
+ ],
174
+ # Lava/Magma
175
+ lava: [
176
+ [80, 0, 0], # Dark red
177
+ [180, 0, 0], # Red
178
+ [255, 60, 0], # Red-orange
179
+ [255, 120, 0], # Orange
180
+ [255, 200, 0], # Yellow-orange
181
+ [255, 255, 100], # Hot yellow
182
+ ],
183
+ # Ice/Frozen
184
+ ice: [
185
+ [200, 240, 255], # Pale blue
186
+ [150, 220, 255], # Light blue
187
+ [100, 200, 255], # Sky blue
188
+ [50, 180, 255], # Blue
189
+ [0, 160, 255], # Deep blue
190
+ [255, 255, 255], # White sparkle
191
+ ],
192
+ # Galaxy/Cosmic
193
+ galaxy: [
194
+ [10, 0, 30], # Deep space
195
+ [60, 0, 100], # Purple nebula
196
+ [150, 50, 200], # Violet
197
+ [200, 100, 255], # Light purple
198
+ [255, 200, 255], # Pink star
199
+ [255, 255, 255], # White star
200
+ ],
201
+ # Toxic/Radioactive
202
+ toxic: [
203
+ [0, 0, 0], # Black
204
+ [0, 80, 0], # Dark green
205
+ [0, 180, 0], # Green
206
+ [180, 255, 0], # Yellow-green
207
+ [255, 255, 0], # Warning yellow
208
+ [0, 255, 0], # Neon green glow
209
+ ],
210
+ # Hacker/Terminal
211
+ hacker: [
212
+ [0, 20, 0], # Almost black
213
+ [0, 60, 0], # Very dark green
214
+ [0, 100, 0], # Dark green
215
+ [0, 150, 10], # Green
216
+ [20, 200, 20], # Bright green
217
+ [100, 255, 100], # Glow green
218
+ ],
219
+ }.freeze
220
+
221
+ class << self
222
+ # Move cursor up n lines
223
+ def up(n = 1)
224
+ "#{ESC}#{n}A"
225
+ end
226
+
227
+ # Move cursor down n lines
228
+ def down(n = 1)
229
+ "#{ESC}#{n}B"
230
+ end
231
+
232
+ # Move cursor forward n columns
233
+ def forward(n = 1)
234
+ "#{ESC}#{n}C"
235
+ end
236
+
237
+ # Move cursor backward n columns
238
+ def backward(n = 1)
239
+ "#{ESC}#{n}D"
240
+ end
241
+
242
+ # Move cursor to column n
243
+ def column(n)
244
+ "#{ESC}#{n}G"
245
+ end
246
+
247
+ # Move cursor to specific position
248
+ def position(row, col)
249
+ "#{ESC}#{row};#{col}H"
250
+ end
251
+
252
+ # 256-color foreground
253
+ def fg256(color)
254
+ "#{ESC}38;5;#{color}m"
255
+ end
256
+
257
+ # 256-color background
258
+ def bg256(color)
259
+ "#{ESC}48;5;#{color}m"
260
+ end
261
+
262
+ # True color (24-bit) foreground
263
+ def rgb(r, g, b)
264
+ "#{ESC}38;2;#{r};#{g};#{b}m"
265
+ end
266
+
267
+ # True color (24-bit) background
268
+ def bg_rgb(r, g, b)
269
+ "#{ESC}48;2;#{r};#{g};#{b}m"
270
+ end
271
+
272
+ # Get color from palette based on progress (0.0 to 1.0)
273
+ def palette_color(palette_name, progress)
274
+ palette = CRYSTAL_PALETTE[palette_name] || CRYSTAL_PALETTE[:crystal]
275
+ return rgb(*palette.first) if progress <= 0
276
+ return rgb(*palette.last) if progress >= 1
277
+
278
+ # Interpolate between colors
279
+ scaled = progress * (palette.length - 1)
280
+ index = scaled.floor
281
+ fraction = scaled - index
282
+
283
+ c1 = palette[index]
284
+ c2 = palette[[index + 1, palette.length - 1].min]
285
+
286
+ r = (c1[0] + (c2[0] - c1[0]) * fraction).round
287
+ g = (c1[1] + (c2[1] - c1[1]) * fraction).round
288
+ b = (c1[2] + (c2[2] - c1[2]) * fraction).round
289
+
290
+ rgb(r, g, b)
291
+ end
292
+
293
+ # Create a shimmer effect (slight brightness variation)
294
+ def shimmer(r, g, b, phase)
295
+ shimmer_amount = (Math.sin(phase) * 30).round
296
+ r = [[r + shimmer_amount, 0].max, 255].min
297
+ g = [[g + shimmer_amount, 0].max, 255].min
298
+ b = [[b + shimmer_amount, 0].max, 255].min
299
+ rgb(r, g, b)
300
+ end
301
+
302
+ # Rainbow color from hue (0.0 to 1.0)
303
+ def hue_to_rgb(hue, saturation = 1.0, lightness = 0.5)
304
+ hue = hue % 1.0
305
+ c = (1 - (2 * lightness - 1).abs) * saturation
306
+ x = c * (1 - ((hue * 6) % 2 - 1).abs)
307
+ m = lightness - c / 2
308
+
309
+ r, g, b = case (hue * 6).floor
310
+ when 0 then [c, x, 0]
311
+ when 1 then [x, c, 0]
312
+ when 2 then [0, c, x]
313
+ when 3 then [0, x, c]
314
+ when 4 then [x, 0, c]
315
+ else [c, 0, x]
316
+ end
317
+
318
+ rgb(((r + m) * 255).round, ((g + m) * 255).round, ((b + m) * 255).round)
319
+ end
320
+
321
+ # Animated rainbow color based on position and time
322
+ def rainbow_cycle(position, time, speed = 1.0)
323
+ hue = (position + time * speed) % 1.0
324
+ hue_to_rgb(hue, 1.0, 0.5)
325
+ end
326
+
327
+ # Neon glow effect - returns [bg_color, fg_color] for glow effect
328
+ def neon_glow(r, g, b, intensity = 1.0)
329
+ # Dim background glow
330
+ glow_r = (r * 0.3 * intensity).round
331
+ glow_g = (g * 0.3 * intensity).round
332
+ glow_b = (b * 0.3 * intensity).round
333
+ # Bright foreground
334
+ fg = rgb([[r + 50, 255].min, 0].max, [[g + 50, 255].min, 0].max, [[b + 50, 255].min, 0].max)
335
+ bg = bg_rgb(glow_r, glow_g, glow_b)
336
+ [bg, fg]
337
+ end
338
+
339
+ # Pulsing brightness effect
340
+ def pulse(r, g, b, time, min_brightness = 0.5, max_brightness = 1.0)
341
+ brightness = min_brightness + (max_brightness - min_brightness) * (0.5 + 0.5 * Math.sin(time * Math::PI * 2))
342
+ rgb((r * brightness).round.clamp(0, 255),
343
+ (g * brightness).round.clamp(0, 255),
344
+ (b * brightness).round.clamp(0, 255))
345
+ end
346
+
347
+ # Fire flicker effect
348
+ def fire_flicker(base_r, base_g, base_b, time)
349
+ flicker = rand(-20..20) + (Math.sin(time * 10) * 15).round
350
+ rgb((base_r + flicker).clamp(0, 255),
351
+ (base_g + flicker / 2).clamp(0, 255),
352
+ (base_b).clamp(0, 255))
353
+ end
354
+
355
+ # Glitch effect - occasionally scramble color
356
+ def glitch(r, g, b, probability = 0.1)
357
+ if rand < probability
358
+ # Random color shift
359
+ shift = rand(-100..100)
360
+ rgb((r + shift).clamp(0, 255), (g + shift).clamp(0, 255), (b + shift).clamp(0, 255))
361
+ else
362
+ rgb(r, g, b)
363
+ end
364
+ end
365
+
366
+ # Strip ANSI codes from string
367
+ def strip(str)
368
+ str.gsub(/\e\[[0-9;]*[a-zA-Z]/, "")
369
+ end
370
+
371
+ # Visible length of string (excluding ANSI codes)
372
+ def visible_length(str)
373
+ strip(str).length
374
+ end
375
+ end
376
+
377
+ # Extended spinner styles for maximum pizzazz
378
+ SPINNERS = {
379
+ braille: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
380
+ dots: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
381
+ moon: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
382
+ clock: ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"],
383
+ earth: ["🌍", "🌎", "🌏"],
384
+ bounce: ["⠁", "⠂", "⠄", "⠂"],
385
+ arc: ["◜", "◠", "◝", "◞", "◡", "◟"],
386
+ square: ["◰", "◳", "◲", "◱"],
387
+ arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
388
+ box: ["▖", "▘", "▝", "▗"],
389
+ triangle: ["◢", "◣", "◤", "◥"],
390
+ binary: ["0", "1"],
391
+ hearts: ["💖", "💗", "💓", "💗"],
392
+ fire: ["🔥", "🔥", "🔥", "🔥", "🔥", "🔥", "🔥", "🔥", "✨", "✨"],
393
+ sparkle: ["✨", "💫", "⭐", "🌟", "💫", "✨"],
394
+ nyan: ["🐱", "🐱", "🐱", "🌈"],
395
+ snake: ["⠁", "⠉", "⠋", "⠛", "⠟", "⠿", "⡿", "⣿", "⣶", "⣤", "⣀"],
396
+ grow: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂"],
397
+ wave: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"],
398
+ toggle: ["⊶", "⊷"],
399
+ balloon: [".", "o", "O", "@", "*", " "],
400
+ noise: ["▓", "▒", "░", "▒"],
401
+ dna: ["╔", "╗", "╝", "╚"],
402
+ weather: ["🌤️", "⛅", "🌥️", "☁️", "🌧️", "⛈️", "🌩️", "🌨️"],
403
+ rocket: ["🚀", "🚀", "💨", "✨"],
404
+ skull: ["💀", "☠️"],
405
+ eyes: ["👁️", "👀", "👁️", "👀"],
406
+ explosion: ["💥", "✨", "🔥", "💫"],
407
+ }.freeze
408
+
409
+ # Celebration effects
410
+ CELEBRATIONS = {
411
+ firework: [".", "*", "✦", "✸", "✹", "✺", "✹", "✸", "✦", "*", "."],
412
+ confetti: ["🎊", "🎉", "✨", "🌟", "💫", "⭐"],
413
+ party: ["🎈", "🎁", "🎂", "🍰", "🎊", "🎉"],
414
+ success: ["✓", "✔", "✔️", "☑️", "✅"],
415
+ }.freeze
416
+
417
+ # Box drawing characters for frames
418
+ FRAMES = {
419
+ single: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
420
+ double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" },
421
+ rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
422
+ bold: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
423
+ ascii: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" },
424
+ }.freeze
425
+ end
426
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ # The main progress bar class
5
+ class Bar
6
+ attr_reader :total, :current, :metrics, :title
7
+
8
+ # Initialize a new progress bar
9
+ # @param total [Integer] Total number of items
10
+ # @param title [String, nil] Optional title for the progress bar
11
+ # @param style [Symbol] Visual style (:crystal, :blocks, :dots, :arrows, :ascii, :fire, :nyan, :matrix, etc.)
12
+ # @param palette [Symbol] Color palette (:crystal, :fire, :ocean, :neon, :synthwave, :vaporwave, :acid, etc.)
13
+ # @param width [Integer] Width of the progress bar
14
+ # @param output [IO] Output stream (default: $stderr)
15
+ # @param spinner [Symbol] Spinner style (:braille, :moon, :clock, :earth, :fire, :nyan, etc.)
16
+ # @param rainbow_mode [Boolean] Enable rainbow color cycling animation
17
+ # @param celebration [Symbol] Celebration effect on completion (:confetti, :firework, :party, :success)
18
+ # @param glow [Boolean] Enable neon glow effect on the progress bar edge
19
+ # @param fps [Integer] Frames per second for animation (default: 15)
20
+ def initialize(total:, title: nil, style: :crystal, palette: :crystal, width: 40, output: $stderr,
21
+ spinner: :braille, rainbow_mode: false, celebration: :confetti, glow: false, fps: 15)
22
+ @total = total
23
+ @current = 0
24
+ @title = title
25
+ @output = output
26
+ @started_at = nil
27
+ @last_render_at = nil
28
+ @render_interval = 1.0 / fps
29
+ @metrics = Metrics.new
30
+ @renderer = Renderer.new(
31
+ style: style,
32
+ width: width,
33
+ palette: palette,
34
+ spinner: spinner,
35
+ rainbow_mode: rainbow_mode,
36
+ celebration_mode: celebration,
37
+ glow: glow
38
+ )
39
+ @lines_rendered = 0
40
+ @mutex = Mutex.new
41
+ @finished = false
42
+ @rate_samples = []
43
+ @last_increment_at = nil
44
+ end
45
+
46
+ # Start the progress bar
47
+ def start
48
+ @started_at = Time.now
49
+ @last_render_at = Time.now
50
+ @output.print ANSI::HIDE_CURSOR
51
+ render
52
+ self
53
+ end
54
+
55
+ # Increment progress by a given amount
56
+ # @param amount [Integer] Amount to increment by
57
+ # @param metrics [Hash, nil] Optional metrics hash to record
58
+ def increment(amount = 1, metrics: nil)
59
+ @mutex.synchronize do
60
+ @current += amount
61
+ @current = [@current, @total].min
62
+
63
+ # Track rate
64
+ now = Time.now
65
+ if @last_increment_at
66
+ interval = now - @last_increment_at
67
+ if interval > 0
68
+ @rate_samples << amount / interval
69
+ @rate_samples.shift if @rate_samples.length > 10
70
+ end
71
+ end
72
+ @last_increment_at = now
73
+
74
+ # Record metrics if provided
75
+ @metrics.record(metrics) if metrics
76
+ end
77
+
78
+ maybe_render
79
+ self
80
+ end
81
+
82
+ # Set progress to a specific value
83
+ # @param value [Integer] The value to set
84
+ # @param metrics [Hash, nil] Optional metrics hash
85
+ def set(value, metrics: nil)
86
+ @mutex.synchronize do
87
+ @current = [[value, 0].max, @total].min
88
+ @metrics.record(metrics) if metrics
89
+ end
90
+ maybe_render
91
+ self
92
+ end
93
+
94
+ # Update progress and report metrics
95
+ # @param metrics [Hash] Metrics hash with numeric values
96
+ def report(metrics)
97
+ @metrics.record(metrics)
98
+ maybe_render
99
+ self
100
+ end
101
+
102
+ # Mark the progress as finished
103
+ def finish
104
+ @mutex.synchronize do
105
+ @current = @total
106
+ @finished = true
107
+ end
108
+ render(force: true)
109
+ @output.puts
110
+ @output.print ANSI::SHOW_CURSOR
111
+ self
112
+ end
113
+
114
+ # Clear the progress bar display
115
+ def clear
116
+ @mutex.synchronize do
117
+ clear_rendered_lines
118
+ end
119
+ @output.print ANSI::SHOW_CURSOR
120
+ self
121
+ end
122
+
123
+ # Get current progress as a float (0.0 to 1.0)
124
+ def progress
125
+ return 0.0 if @total.zero?
126
+ @current.to_f / @total
127
+ end
128
+
129
+ # Get elapsed time in seconds
130
+ def elapsed
131
+ return 0 unless @started_at
132
+ Time.now - @started_at
133
+ end
134
+
135
+ # Get estimated time remaining
136
+ def eta
137
+ return Float::INFINITY if @current.zero?
138
+ return 0 if @current >= @total
139
+
140
+ elapsed_time = elapsed
141
+ return Float::INFINITY if elapsed_time.zero?
142
+
143
+ rate = @current / elapsed_time
144
+ remaining = @total - @current
145
+ remaining / rate
146
+ end
147
+
148
+ # Get current rate (items per second)
149
+ def rate
150
+ return 0.0 if @rate_samples.empty?
151
+ @rate_samples.sum / @rate_samples.length
152
+ end
153
+
154
+ # Iterate with progress tracking
155
+ # @param enumerable [Enumerable] The collection to iterate
156
+ # @yield [item] Block to execute for each item
157
+ def self.each(enumerable, **options, &block)
158
+ items = enumerable.to_a
159
+ bar = new(total: items.length, **options)
160
+ bar.start
161
+
162
+ begin
163
+ items.each do |item|
164
+ result = yield(item)
165
+ # If block returns a hash with :metrics key, record it
166
+ if result.is_a?(Hash) && result.key?(:metrics)
167
+ bar.increment(metrics: result[:metrics])
168
+ else
169
+ bar.increment
170
+ end
171
+ end
172
+ ensure
173
+ bar.finish
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def maybe_render
180
+ render
181
+ end
182
+
183
+ def render(force: false)
184
+ @mutex.synchronize do
185
+ now = Time.now
186
+ return if !force && @last_render_at && (now - @last_render_at) < @render_interval
187
+
188
+ clear_rendered_lines
189
+
190
+ state = build_state
191
+ lines = @renderer.render(state)
192
+
193
+ # Add title if present
194
+ if @title && !@title.empty?
195
+ title_line = "#{ANSI::BOLD}#{ANSI.palette_color(@renderer.palette, 0.5)}#{@title}#{ANSI::RESET}"
196
+ lines.unshift(title_line)
197
+ end
198
+
199
+ # Output lines
200
+ lines.each_with_index do |line, i|
201
+ @output.print "#{ANSI::CLEAR_LINE}#{line}"
202
+ @output.print "\n" if i < lines.length - 1
203
+ end
204
+
205
+ @lines_rendered = lines.length
206
+ @last_render_at = now
207
+ end
208
+ end
209
+
210
+ def clear_rendered_lines
211
+ return if @lines_rendered.zero?
212
+
213
+ # Move up and clear each line
214
+ (@lines_rendered - 1).times do
215
+ @output.print ANSI.up(1)
216
+ @output.print ANSI::CLEAR_LINE
217
+ end
218
+ @output.print "\r#{ANSI::CLEAR_LINE}"
219
+ end
220
+
221
+ def build_state
222
+ {
223
+ progress: progress,
224
+ current: @current,
225
+ total: @total,
226
+ elapsed: elapsed,
227
+ eta: eta,
228
+ rate: rate,
229
+ metrics: @metrics,
230
+ finished: @finished,
231
+ }
232
+ end
233
+ end
234
+ end