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,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
@@ -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