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
data/lib/progress/360/237/246/253bar/360/237/246/253none/download_thread_state/360/237/246/253.rb
ADDED
|
@@ -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
|
data/lib/progress/360/237/246/253bar/360/237/246/253none/enumerable_extension/360/237/246/253.rb
ADDED
|
@@ -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
|