1use console::style;
4
5const LINES: [&str; 4] = [
7 " ██████╗ ██╗████████╗ ███████╗ █████╗ ███╗ ███╗███████╗",
8 "██╔════╝ ██║╚══██╔══╝ ██╔════╝██╔══██╗████╗ ████║██╔════╝",
9 "██║ ███╗██║ ██║█████╗███████╗███████║██╔████╔██║█████╗ ",
10 "██║ ██║██║ ██║╚════╝╚════██║██╔══██║██║╚██╔╝██║██╔══╝ ",
11];
12
13const LINE5_PREFIX: &str = "╚██████╔╝██║ ██║ ███████║██║ ██║██║ ╚═╝ ██║█";
15
16const LINE5_SUFFIX: &str = "╗";
18
19const LAST_LINE: &str = " ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝";
21
22const ART_WIDTH: usize = 62;
24
25const GRADIENT_STOPS: [(u8, u8, u8); 3] = [
27 (59, 130, 246), (6, 182, 212), (34, 197, 94), ];
31
32const ANIMATED_GRADIENT_STOPS: [(u8, u8, u8); 5] = [
34 (59, 130, 246), (6, 182, 212), (34, 197, 94), (99, 102, 241), (147, 51, 234), ];
40
41pub fn subheadline() -> &'static str {
43 env!("CARGO_PKG_DESCRIPTION")
44}
45
46pub fn print_banner() {
48 let version = env!("CARGO_PKG_VERSION");
49 let version_display = format!("{:^6}", version);
50
51 println!();
52 for text in &LINES {
53 println!("{}", cli_gradient_line(text, &GRADIENT_STOPS));
54 }
55 println!("{}", cli_line5(&version_display, &GRADIENT_STOPS));
56 println!("{}", cli_gradient_line(LAST_LINE, &GRADIENT_STOPS));
57
58 let subtitle = subheadline();
59 let visible_len = subtitle.chars().count();
60 let pad = if visible_len < ART_WIDTH {
61 (ART_WIDTH - visible_len) / 2
62 } else {
63 0
64 };
65 println!(
66 "{}{}\n",
67 " ".repeat(pad.saturating_sub(1)),
68 style(subtitle).dim()
69 );
70}
71
72fn styled_gradient_chunk(text: &str, r: u8, g: u8, b: u8, force_styling: bool) -> String {
73 let styled = style(text).true_color(r, g, b).bold();
74 if force_styling {
75 format!("{}", styled.force_styling(true))
76 } else {
77 format!("{}", styled)
78 }
79}
80
81fn styled_version_badge(text: &str, r: u8, g: u8, b: u8, force_styling: bool) -> String {
82 let styled = style(text).black().on_true_color(r, g, b).bold();
83 if force_styling {
84 format!("{}", styled.force_styling(true))
85 } else {
86 format!("{}", styled)
87 }
88}
89
90fn cli_gradient_line(text: &str, stops: &[(u8, u8, u8)]) -> String {
91 cli_gradient_line_with_force(text, stops, false)
92}
93
94fn cli_gradient_line_with_force(text: &str, stops: &[(u8, u8, u8)], force_styling: bool) -> String {
95 let chars: Vec<char> = text.chars().collect();
96 let len = chars.len().max(1);
97
98 chars
99 .into_iter()
100 .enumerate()
101 .map(|(i, ch)| {
102 let t = i as f64 / (len - 1).max(1) as f64;
103 let (r, g, b) = interpolate_stops(stops, t);
104 styled_gradient_chunk(&ch.to_string(), r, g, b, force_styling)
105 })
106 .collect()
107}
108
109fn cli_line5(version_display: &str, stops: &[(u8, u8, u8)]) -> String {
110 cli_line5_with_force(version_display, stops, false)
111}
112
113fn cli_line5_with_force(
114 version_display: &str,
115 stops: &[(u8, u8, u8)],
116 force_styling: bool,
117) -> String {
118 let prefix_len = LINE5_PREFIX.chars().count();
119 let version_len = version_display.chars().count();
120 let full_len = prefix_len + version_len + LINE5_SUFFIX.chars().count();
121 let denom = (full_len - 1).max(1) as f64;
122
123 let mut out = String::new();
124 for (i, ch) in LINE5_PREFIX.chars().enumerate() {
125 let t = i as f64 / denom;
126 let (r, g, b) = interpolate_stops(stops, t);
127 out.push_str(&styled_gradient_chunk(
128 &ch.to_string(),
129 r,
130 g,
131 b,
132 force_styling,
133 ));
134 }
135
136 let ver_t = prefix_len as f64 / denom;
137 let (vr, vg, vb) = interpolate_stops(stops, ver_t);
138 out.push_str(&styled_version_badge(
139 version_display,
140 vr,
141 vg,
142 vb,
143 force_styling,
144 ));
145
146 let suffix_pos = prefix_len + version_len;
147 let suffix_t = suffix_pos as f64 / denom;
148 let (r, g, b) = interpolate_stops(stops, suffix_t);
149 out.push_str(&styled_gradient_chunk(LINE5_SUFFIX, r, g, b, force_styling));
150
151 out
152}
153
154#[cfg(feature = "tui")]
159use ratatui::{
160 layout::Rect,
161 style::{Color, Modifier, Style},
162 text::{Line, Span},
163 widgets::Paragraph,
164 Frame,
165};
166
167pub(crate) fn interpolate_stops(stops: &[(u8, u8, u8)], t: f64) -> (u8, u8, u8) {
169 let t = t.clamp(0.0, 1.0);
170 let segments = stops.len() - 1;
171 let scaled = t * segments as f64;
172 let idx = (scaled as usize).min(segments - 1);
173 let local_t = scaled - idx as f64;
174 let (r1, g1, b1) = stops[idx];
175 let (r2, g2, b2) = stops[idx + 1];
176 let lerp = |a: u8, b: u8, t: f64| -> u8 { (a as f64 + (b as f64 - a as f64) * t) as u8 };
177 (
178 lerp(r1, r2, local_t),
179 lerp(g1, g2, local_t),
180 lerp(b1, b2, local_t),
181 )
182}
183
184#[cfg(feature = "tui")]
186fn gradient_line<'a>(text: &'a str, stops: &[(u8, u8, u8)]) -> Line<'a> {
187 let chars: Vec<&str> = text.split_inclusive(|_: char| true).collect();
188 let len = chars.len().max(1);
189 let spans: Vec<Span<'a>> = chars
190 .into_iter()
191 .enumerate()
192 .map(|(i, ch)| {
193 let t = i as f64 / (len - 1).max(1) as f64;
194 let (r, g, b) = interpolate_stops(stops, t);
195 Span::styled(
196 ch.to_string(),
197 Style::default()
198 .fg(Color::Rgb(r, g, b))
199 .add_modifier(Modifier::BOLD),
200 )
201 })
202 .collect();
203 Line::from(spans)
204}
205
206#[cfg(feature = "tui")]
210fn sweep_color(stops: &[(u8, u8, u8)], base_t: f64, phase: f64) -> (u8, u8, u8) {
211 let wave_start = 2.0 * phase - 1.0;
212 let wave_t = base_t - wave_start;
213 if !(0.0..1.0).contains(&wave_t) {
214 stops[0]
215 } else {
216 interpolate_stops(stops, wave_t)
217 }
218}
219
220#[cfg(feature = "tui")]
224fn animated_gradient_line<'a>(text: &'a str, stops: &[(u8, u8, u8)], phase: f64) -> Line<'a> {
225 let chars: Vec<&str> = text.split_inclusive(|_: char| true).collect();
226 let len = chars.len().max(1);
227 let spans: Vec<Span<'a>> = chars
228 .into_iter()
229 .enumerate()
230 .map(|(i, ch)| {
231 let base_t = i as f64 / (len - 1).max(1) as f64;
232 let (r, g, b) = sweep_color(stops, base_t, phase);
233 Span::styled(
234 ch.to_string(),
235 Style::default()
236 .fg(Color::Rgb(r, g, b))
237 .add_modifier(Modifier::BOLD),
238 )
239 })
240 .collect();
241 Line::from(spans)
242}
243
244#[cfg(feature = "tui")]
246pub fn render_banner(frame: &mut Frame, area: Rect) {
247 let version = env!("CARGO_PKG_VERSION");
248 let version_display = format!("{:^6}", version);
249 let stops = &GRADIENT_STOPS;
250
251 let mut banner_lines: Vec<Line> = Vec::new();
252 for text in &LINES {
253 banner_lines.push(gradient_line(text, stops));
254 }
255
256 let full_len =
258 LINE5_PREFIX.chars().count() + version_display.len() + LINE5_SUFFIX.chars().count();
259 let mut line5_spans: Vec<Span> = Vec::new();
260 for (i, ch) in LINE5_PREFIX.split_inclusive(|_: char| true).enumerate() {
261 let t = i as f64 / (full_len - 1).max(1) as f64;
262 let (r, g, b) = interpolate_stops(stops, t);
263 line5_spans.push(Span::styled(
264 ch.to_string(),
265 Style::default()
266 .fg(Color::Rgb(r, g, b))
267 .add_modifier(Modifier::BOLD),
268 ));
269 }
270 let ver_pos = LINE5_PREFIX.chars().count();
271 let ver_t = ver_pos as f64 / (full_len - 1).max(1) as f64;
272 let (vr, vg, vb) = interpolate_stops(stops, ver_t);
273 line5_spans.push(Span::styled(
274 version_display,
275 Style::default()
276 .fg(Color::Black)
277 .bg(Color::Rgb(vr, vg, vb))
278 .add_modifier(Modifier::BOLD),
279 ));
280 let suffix_pos = ver_pos + 6;
281 let t = suffix_pos as f64 / (full_len - 1).max(1) as f64;
282 let (r, g, b) = interpolate_stops(stops, t);
283 line5_spans.push(Span::styled(
284 LINE5_SUFFIX.to_string(),
285 Style::default()
286 .fg(Color::Rgb(r, g, b))
287 .add_modifier(Modifier::BOLD),
288 ));
289 banner_lines.push(Line::from(line5_spans));
290
291 banner_lines.push(gradient_line(LAST_LINE, stops));
292
293 let banner = Paragraph::new(banner_lines).centered();
294 frame.render_widget(banner, area);
295}
296
297#[cfg(feature = "tui")]
301pub fn render_animated_banner(frame: &mut Frame, area: Rect, phase: f64) {
302 let version = env!("CARGO_PKG_VERSION");
303 let version_display = format!("{:^6}", version);
304 let stops: &[(u8, u8, u8)] = &ANIMATED_GRADIENT_STOPS;
305
306 let mut banner_lines: Vec<Line> = Vec::new();
307 for text in &LINES {
308 banner_lines.push(animated_gradient_line(text, stops, phase));
309 }
310
311 let full_len =
313 LINE5_PREFIX.chars().count() + version_display.len() + LINE5_SUFFIX.chars().count();
314 let mut line5_spans: Vec<Span> = Vec::new();
315 for (i, ch) in LINE5_PREFIX.split_inclusive(|_: char| true).enumerate() {
316 let base_t = i as f64 / (full_len - 1).max(1) as f64;
317 let (r, g, b) = sweep_color(stops, base_t, phase);
318 line5_spans.push(Span::styled(
319 ch.to_string(),
320 Style::default()
321 .fg(Color::Rgb(r, g, b))
322 .add_modifier(Modifier::BOLD),
323 ));
324 }
325 let ver_pos = LINE5_PREFIX.chars().count();
326 let ver_base_t = ver_pos as f64 / (full_len - 1).max(1) as f64;
327 let (vr, vg, vb) = sweep_color(stops, ver_base_t, phase);
328 line5_spans.push(Span::styled(
329 version_display,
330 Style::default()
331 .fg(Color::Black)
332 .bg(Color::Rgb(vr, vg, vb))
333 .add_modifier(Modifier::BOLD),
334 ));
335 let suffix_pos = ver_pos + 6;
336 let suffix_base_t = suffix_pos as f64 / (full_len - 1).max(1) as f64;
337 let (r, g, b) = sweep_color(stops, suffix_base_t, phase);
338 line5_spans.push(Span::styled(
339 LINE5_SUFFIX.to_string(),
340 Style::default()
341 .fg(Color::Rgb(r, g, b))
342 .add_modifier(Modifier::BOLD),
343 ));
344 banner_lines.push(Line::from(line5_spans));
345
346 banner_lines.push(animated_gradient_line(LAST_LINE, stops, phase));
347
348 let banner = Paragraph::new(banner_lines).centered();
349 frame.render_widget(banner, area);
350}
351
352#[cfg(test)]
353#[path = "banner_tests.rs"]
354mod tests;