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,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ module DownloadThreadState
5
+ ThreadState = Struct.new(
6
+ :site,
7
+ :thread,
8
+ :total,
9
+ :done,
10
+ :status,
11
+ :reason,
12
+ :resume_at,
13
+ :slug,
14
+ :percent,
15
+ :size_str,
16
+ :speed_str,
17
+ :attempt,
18
+ :message,
19
+ keyword_init: true
20
+ )
21
+
22
+ class Tracker
23
+ attr_reader :cycle_id
24
+
25
+ def initialize(sites:, threads_per_site:)
26
+ @mutex = Mutex.new
27
+ @cycle_id = "%d-%04d" % [Process.pid, rand(1000..9999)]
28
+ @states = {}
29
+
30
+ sites.each do |site|
31
+ @states[site.to_sym] = {}
32
+ (1..threads_per_site.to_i).each do |thread_number|
33
+ @states[site.to_sym][thread_number] = ThreadState.new(
34
+ site: site.to_sym,
35
+ thread: thread_number,
36
+ total: 0,
37
+ done: 0,
38
+ status: :idle,
39
+ reason: :waiting,
40
+ resume_at: nil,
41
+ slug: nil,
42
+ percent: nil,
43
+ size_str: nil,
44
+ speed_str: nil,
45
+ attempt: nil,
46
+ message: "waiting"
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ def set_plan(site:, thread:, total:)
53
+ mutate(site, thread) do |state|
54
+ state.total = [total.to_i, 0].max
55
+ state.done = 0
56
+ state.status = :idle
57
+ state.reason = state.total.zero? ? :no_work : :waiting
58
+ state.resume_at = nil
59
+ state.slug = nil
60
+ state.percent = nil
61
+ state.size_str = nil
62
+ state.speed_str = nil
63
+ state.attempt = nil
64
+ state.message = state.total.zero? ? "blocked: no work" : "queued"
65
+ end
66
+ end
67
+
68
+ def mark_active(site:, thread:, slug:)
69
+ mutate(site, thread) do |state|
70
+ state.status = :active
71
+ state.reason = nil
72
+ state.resume_at = nil
73
+ state.slug = slug
74
+ state.message = "active"
75
+ end
76
+ end
77
+
78
+ def mark_progress(site:, thread:, slug:, percent:, size_str:, speed_str:)
79
+ mutate(site, thread) do |state|
80
+ state.status = :active
81
+ state.reason = nil
82
+ state.resume_at = nil
83
+ state.slug = slug
84
+ state.percent = percent.to_f
85
+ state.size_str = size_str.to_s
86
+ state.speed_str = speed_str.to_s
87
+ state.message = "downloading"
88
+ end
89
+ end
90
+
91
+ def mark_blocked(site:, thread:, reason:, resume_at: nil, attempt: nil, message: nil)
92
+ mutate(site, thread) do |state|
93
+ state.status = :blocked
94
+ state.reason = reason.to_sym
95
+ state.resume_at = resume_at
96
+ state.attempt = attempt.to_i if attempt
97
+ state.message = message.to_s if message
98
+ end
99
+ end
100
+
101
+ def mark_resumed(site:, thread:)
102
+ mutate(site, thread) do |state|
103
+ state.status = :active
104
+ state.reason = nil
105
+ state.resume_at = nil
106
+ state.message = "resumed"
107
+ end
108
+ end
109
+
110
+ def increment_done(site:, thread:, ok:, cause: nil)
111
+ mutate(site, thread) do |state|
112
+ state.done += 1
113
+ state.status = :idle
114
+ state.reason = ok ? :waiting : (cause || :failed)
115
+ state.resume_at = nil
116
+ state.percent = nil
117
+ state.size_str = nil
118
+ state.speed_str = nil
119
+ state.message = ok ? "ok" : "failed: #{cause || :unknown}"
120
+ end
121
+ end
122
+
123
+ def finish_thread(site:, thread:)
124
+ mutate(site, thread) do |state|
125
+ if state.total.zero?
126
+ state.status = :blocked
127
+ state.reason = :no_work
128
+ state.message = "blocked: no work"
129
+ else
130
+ state.status = :done
131
+ state.reason = nil
132
+ state.message = "done"
133
+ end
134
+ state.resume_at = nil
135
+ state.percent = nil
136
+ state.size_str = nil
137
+ state.speed_str = nil
138
+ end
139
+ end
140
+
141
+ def snapshot
142
+ @mutex.synchronize do
143
+ now = Time.now
144
+ {
145
+ cycle_id: @cycle_id,
146
+ sites: @states.each_with_object({}) do |(site, threads), out|
147
+ out[site] = threads.each_with_object({}) do |(thread, state), th_out|
148
+ resume_in = if state.resume_at
149
+ [state.resume_at - now, 0.0].max
150
+ end
151
+
152
+ th_out[thread] = {
153
+ total: state.total,
154
+ done: state.done,
155
+ status: state.status,
156
+ reason: state.reason,
157
+ slug: state.slug,
158
+ percent: state.percent,
159
+ size_str: state.size_str,
160
+ speed_str: state.speed_str,
161
+ attempt: state.attempt,
162
+ resume_at: state.resume_at,
163
+ resume_in: resume_in,
164
+ message: state.message
165
+ }
166
+ end
167
+ end
168
+ }
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def mutate(site, thread)
175
+ @mutex.synchronize do
176
+ state = @states.dig(site.to_sym, thread.to_i)
177
+ return unless state
178
+
179
+ yield(state)
180
+ end
181
+ end
182
+ end
183
+
184
+ MUTEX = Mutex.new
185
+
186
+ class << self
187
+ def start_cycle!(sites:, threads_per_site: 2)
188
+ tracker = Tracker.new(sites: sites, threads_per_site: threads_per_site)
189
+ MUTEX.synchronize { @tracker = tracker }
190
+ tracker
191
+ end
192
+
193
+ def tracker
194
+ MUTEX.synchronize { @tracker }
195
+ end
196
+
197
+ def clear_cycle!
198
+ MUTEX.synchronize { @tracker = nil }
199
+ end
200
+
201
+ def snapshot
202
+ tracker&.snapshot || { cycle_id: nil, sites: {} }
203
+ end
204
+
205
+ def format_line(site:, thread:, state:)
206
+ prefix = "%s t%d" % [site.to_s, thread.to_i]
207
+ total = state[:total].to_i
208
+ done = state[:done].to_i
209
+
210
+ case state[:status]
211
+ when :blocked
212
+ reason = state[:reason] || :blocked
213
+ if state[:resume_in]
214
+ "%s | %d/%d | blocked(%s), resumes in %ds" % [prefix, done, total, reason, state[:resume_in].ceil]
215
+ else
216
+ "%s | %d/%d | blocked(%s)" % [prefix, done, total, reason]
217
+ end
218
+ when :active
219
+ if state[:percent]
220
+ "%s | %d/%d | %.0f%% of %s @ %s | %s" % [
221
+ prefix,
222
+ done,
223
+ total,
224
+ state[:percent].to_f,
225
+ state[:size_str],
226
+ state[:speed_str],
227
+ state[:slug].to_s
228
+ ]
229
+ else
230
+ "%s | %d/%d | active | %s" % [prefix, done, total, state[:slug].to_s]
231
+ end
232
+ when :done
233
+ "%s | %d/%d | done" % [prefix, done, total]
234
+ else
235
+ "%s | %d/%d | %s" % [prefix, done, total, state[:message] || "idle"]
236
+ end
237
+ end
238
+
239
+ def backoff_wait(seconds, tick: 1)
240
+ remain = [seconds.to_i, 0].max
241
+ while remain.positive?
242
+ yield(remain) if block_given?
243
+ sleep([tick.to_i, remain].min)
244
+ remain -= tick.to_i
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ # A wrapper that provides progress tracking for any enumerable
5
+ class ProgressEnumerator
6
+ include Enumerable
7
+
8
+ def initialize(enumerable, bar_options = {})
9
+ @enumerable = enumerable
10
+ @bar_options = bar_options
11
+ @items = nil
12
+ end
13
+
14
+ def each(&block)
15
+ return to_enum(:each) unless block_given?
16
+
17
+ # Materialize the enumerable to get count
18
+ @items ||= @enumerable.to_a
19
+ total = @items.length
20
+
21
+ return if total.zero?
22
+
23
+ bar = Bar.new(total: total, **@bar_options)
24
+ bar.start
25
+
26
+ begin
27
+ @items.each do |item|
28
+ # Yield to the block
29
+ result = yield(item)
30
+
31
+ # Check if result includes metrics
32
+ if result.is_a?(Hash)
33
+ # If it has a :metrics key, use that
34
+ if result.key?(:metrics)
35
+ bar.increment(metrics: result[:metrics])
36
+ else
37
+ # Treat the whole hash as metrics
38
+ bar.increment(metrics: result)
39
+ end
40
+ else
41
+ bar.increment
42
+ end
43
+ end
44
+ ensure
45
+ bar.finish
46
+ end
47
+ end
48
+
49
+ # Forward common Enumerable methods
50
+ def size
51
+ @items ||= @enumerable.to_a
52
+ @items.size
53
+ end
54
+
55
+ def length
56
+ size
57
+ end
58
+ end
59
+ end
60
+
61
+ # Extend Enumerable with progress bar support
62
+ module Enumerable
63
+ # Iterate with a spectacular progress bar
64
+ #
65
+ # @param title [String, nil] Optional title for the progress bar
66
+ # @param style [Symbol] Visual style (:crystal, :blocks, :dots, :arrows, :ascii, :fire, :nyan, :matrix, etc.)
67
+ # @param palette [Symbol] Color palette (:crystal, :fire, :ocean, :neon, :synthwave, :vaporwave, :acid, etc.)
68
+ # @param width [Integer] Width of the progress bar (default: 40)
69
+ # @param output [IO] Output stream (default: $stderr)
70
+ # @param spinner [Symbol] Spinner style (:braille, :moon, :clock, :earth, :fire, :nyan, etc.)
71
+ # @param rainbow_mode [Boolean] Enable rainbow color cycling animation
72
+ # @param celebration [Symbol] Celebration effect on completion (:confetti, :firework, :party, :success)
73
+ # @param glow [Boolean] Enable neon glow effect
74
+ # @param fps [Integer] Frames per second for animation (default: 15)
75
+ # @return [ProgressEnumerator] A progress-tracking enumerator
76
+ #
77
+ # @example Basic usage
78
+ # (1..100).with_progress.each { |i| sleep(0.01) }
79
+ #
80
+ # @example MAXIMUM PIZZAZZ mode
81
+ # items.with_progress(
82
+ # title: "🔥 TURBO MODE 🔥",
83
+ # style: :fire,
84
+ # palette: :neon,
85
+ # rainbow_mode: true,
86
+ # spinner: :rocket,
87
+ # celebration: :firework,
88
+ # glow: true
89
+ # ).each { |item| process(item) }
90
+ #
91
+ # @example Reporting metrics
92
+ # files.with_progress(title: "Analyzing").each do |file|
93
+ # size = File.size(file)
94
+ # lines = File.readlines(file).count
95
+ # { bytes: size, lines: lines } # Return metrics hash
96
+ # end
97
+ #
98
+ def with_progress(title: nil, style: :crystal, palette: :crystal, width: 40, output: $stderr,
99
+ spinner: :braille, rainbow_mode: false, celebration: :confetti, glow: false, fps: 15)
100
+ ProgressBarNone::ProgressEnumerator.new(
101
+ self,
102
+ title: title,
103
+ style: style,
104
+ palette: palette,
105
+ width: width,
106
+ output: output,
107
+ spinner: spinner,
108
+ rainbow_mode: rainbow_mode,
109
+ celebration: celebration,
110
+ glow: glow,
111
+ fps: fps
112
+ )
113
+ end
114
+ end
115
+
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ # Decorative frames and borders for the progress cockpit
5
+ module Frames
6
+ class << self
7
+ # Draw a decorative frame around content
8
+ # @param content [Array<String>] Lines of content
9
+ # @param style [Symbol] Frame style (:single, :double, :rounded, :bold, :ascii, :cyber, :neon)
10
+ # @param palette [Symbol] Color palette for the frame
11
+ # @param title [String, nil] Optional title for the frame
12
+ # @param animated [Boolean] Enable animated frame
13
+ # @param frame_num [Integer] Animation frame number
14
+ # @return [Array<String>] Framed content lines
15
+ def wrap(content, style: :rounded, palette: :crystal, title: nil, animated: false, frame_num: 0)
16
+ lines = content.is_a?(Array) ? content : content.split("\n")
17
+ return lines if lines.empty?
18
+
19
+ # Calculate max width
20
+ max_width = lines.map { |l| ANSI.visible_length(l) }.max
21
+
22
+ # Get frame characters
23
+ frame = frame_chars(style)
24
+
25
+ # Build framed output
26
+ result = []
27
+
28
+ # Top border
29
+ top_border = build_top_border(frame, max_width, title, palette, animated, frame_num)
30
+ result << top_border
31
+
32
+ # Content lines
33
+ lines.each_with_index do |line, i|
34
+ padding = max_width - ANSI.visible_length(line)
35
+ left = frame_color(frame[:v], palette, i, animated, frame_num)
36
+ right = frame_color(frame[:v], palette, i, animated, frame_num)
37
+ result << "#{left} #{line}#{" " * padding} #{right}"
38
+ end
39
+
40
+ # Bottom border
41
+ bottom_border = build_bottom_border(frame, max_width, palette, animated, frame_num)
42
+ result << bottom_border
43
+
44
+ result
45
+ end
46
+
47
+ # Create a simple horizontal separator
48
+ def separator(width: 40, style: :single, palette: :crystal, animated: false, frame_num: 0)
49
+ frame = frame_chars(style)
50
+ char = frame[:h]
51
+
52
+ if animated
53
+ # Animated separator with traveling highlight
54
+ result = ""
55
+ width.times do |i|
56
+ highlight_pos = (frame_num * 2) % width
57
+ if (i - highlight_pos).abs < 3
58
+ intensity = 1.0 - (i - highlight_pos).abs / 3.0
59
+ result += "#{ANSI::BOLD}#{ANSI.palette_color(palette, intensity)}#{char}#{ANSI::RESET}"
60
+ else
61
+ result += "#{ANSI::DIM}#{char}#{ANSI::RESET}"
62
+ end
63
+ end
64
+ result
65
+ else
66
+ color = ANSI.palette_color(palette, 0.5)
67
+ "#{color}#{char * width}#{ANSI::RESET}"
68
+ end
69
+ end
70
+
71
+ # Create a decorative header banner
72
+ def banner(text, style: :double, palette: :neon, animated: false, frame_num: 0)
73
+ width = text.length + 4
74
+ frame = frame_chars(style)
75
+
76
+ lines = []
77
+
78
+ # Top
79
+ top_color = animated ? ANSI.rainbow_cycle(0, frame_num * 0.1, 1.0) : ANSI.palette_color(palette, 0.0)
80
+ lines << "#{top_color}#{frame[:tl]}#{frame[:h] * width}#{frame[:tr]}#{ANSI::RESET}"
81
+
82
+ # Middle with text
83
+ mid_color = animated ? ANSI.rainbow_cycle(0.5, frame_num * 0.1, 1.0) : ANSI.palette_color(palette, 0.5)
84
+ text_color = animated ? rainbow_text(text, frame_num) : "#{ANSI::BOLD}#{mid_color}#{text}#{ANSI::RESET}"
85
+ lines << "#{mid_color}#{frame[:v]}#{ANSI::RESET} #{text_color} #{mid_color}#{frame[:v]}#{ANSI::RESET}"
86
+
87
+ # Bottom
88
+ bot_color = animated ? ANSI.rainbow_cycle(1.0, frame_num * 0.1, 1.0) : ANSI.palette_color(palette, 1.0)
89
+ lines << "#{bot_color}#{frame[:bl]}#{frame[:h] * width}#{frame[:br]}#{ANSI::RESET}"
90
+
91
+ lines
92
+ end
93
+
94
+ # ASCII art title banners
95
+ def ascii_title(text, style: :block, palette: :neon)
96
+ case style
97
+ when :block
98
+ block_title(text, palette)
99
+ when :shadow
100
+ shadow_title(text, palette)
101
+ when :outline
102
+ outline_title(text, palette)
103
+ else
104
+ simple_title(text, palette)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def frame_chars(style)
111
+ case style
112
+ when :single
113
+ { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" }
114
+ when :double
115
+ { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" }
116
+ when :rounded
117
+ { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" }
118
+ when :bold
119
+ { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" }
120
+ when :ascii
121
+ { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" }
122
+ when :cyber
123
+ { tl: "⟦", tr: "⟧", bl: "⟦", br: "⟧", h: "═", v: "▐" }
124
+ when :neon
125
+ { tl: "◤", tr: "◥", bl: "◣", br: "◢", h: "━", v: "┃" }
126
+ when :stars
127
+ { tl: "✦", tr: "✦", bl: "✦", br: "✦", h: "✧", v: "✧" }
128
+ else
129
+ { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" }
130
+ end
131
+ end
132
+
133
+ def build_top_border(frame, width, title, palette, animated, frame_num)
134
+ h_char = frame[:h]
135
+ total_width = width + 2 # +2 for padding
136
+
137
+ if title && !title.empty?
138
+ title_display = " #{title} "
139
+ title_len = ANSI.visible_length(title_display)
140
+ left_width = 2
141
+ right_width = total_width - left_width - title_len
142
+
143
+ left = frame_color(frame[:tl], palette, 0, animated, frame_num)
144
+ right = frame_color(frame[:tr], palette, 0, animated, frame_num)
145
+ left_h = frame_color(h_char * left_width, palette, 0, animated, frame_num)
146
+ right_h = frame_color(h_char * [right_width, 0].max, palette, 0, animated, frame_num)
147
+
148
+ title_color = animated ? rainbow_text(title_display, frame_num) :
149
+ "#{ANSI::BOLD}#{ANSI.palette_color(palette, 0.5)}#{title_display}#{ANSI::RESET}"
150
+
151
+ "#{left}#{left_h}#{title_color}#{right_h}#{right}"
152
+ else
153
+ left = frame_color(frame[:tl], palette, 0, animated, frame_num)
154
+ right = frame_color(frame[:tr], palette, 0, animated, frame_num)
155
+ middle = frame_color(h_char * total_width, palette, 0, animated, frame_num)
156
+
157
+ "#{left}#{middle}#{right}"
158
+ end
159
+ end
160
+
161
+ def build_bottom_border(frame, width, palette, animated, frame_num)
162
+ h_char = frame[:h]
163
+ total_width = width + 2
164
+
165
+ left = frame_color(frame[:bl], palette, 1, animated, frame_num)
166
+ right = frame_color(frame[:br], palette, 1, animated, frame_num)
167
+ middle = frame_color(h_char * total_width, palette, 1, animated, frame_num)
168
+
169
+ "#{left}#{middle}#{right}"
170
+ end
171
+
172
+ def frame_color(char, palette, position, animated, frame_num)
173
+ if animated
174
+ color = ANSI.rainbow_cycle(position * 0.5, frame_num * 0.1, 1.0)
175
+ "#{color}#{char}#{ANSI::RESET}"
176
+ else
177
+ color = ANSI.palette_color(palette, position)
178
+ "#{color}#{char}#{ANSI::RESET}"
179
+ end
180
+ end
181
+
182
+ def rainbow_text(text, frame_num)
183
+ result = ""
184
+ text.each_char.with_index do |char, i|
185
+ color = ANSI.rainbow_cycle(i * 0.1, frame_num * 0.1, 1.0)
186
+ result += "#{color}#{char}"
187
+ end
188
+ result + ANSI::RESET
189
+ end
190
+
191
+ def block_title(text, palette)
192
+ # Simple block-style title
193
+ color = ANSI.palette_color(palette, 0.5)
194
+ [
195
+ "#{color}#{ANSI::BOLD}█▀▀ #{text.upcase} ▀▀█#{ANSI::RESET}",
196
+ ]
197
+ end
198
+
199
+ def shadow_title(text, palette)
200
+ color = ANSI.palette_color(palette, 0.5)
201
+ shadow = ANSI::DIM
202
+ [
203
+ "#{color}#{ANSI::BOLD}#{text}#{ANSI::RESET}",
204
+ "#{shadow}#{text.gsub(/[^ ]/, "░")}#{ANSI::RESET}",
205
+ ]
206
+ end
207
+
208
+ def outline_title(text, palette)
209
+ color = ANSI.palette_color(palette, 0.5)
210
+ border = "═" * (text.length + 2)
211
+ [
212
+ "#{color}╔#{border}╗#{ANSI::RESET}",
213
+ "#{color}║ #{ANSI::BOLD}#{text}#{ANSI::RESET}#{color} ║#{ANSI::RESET}",
214
+ "#{color}╚#{border}╝#{ANSI::RESET}",
215
+ ]
216
+ end
217
+
218
+ def simple_title(text, palette)
219
+ color = ANSI.palette_color(palette, 0.5)
220
+ ["#{color}#{ANSI::BOLD}▸ #{text}#{ANSI::RESET}"]
221
+ end
222
+ end
223
+ end
224
+ end