Skip to main content

git_same/
banner.rs

1//! ASCII banner for gisa — shared across CLI and TUI.
2
3use console::style;
4
5/// Banner lines 1-4 (shared between CLI and TUI).
6const LINES: [&str; 4] = [
7    " ██████╗ ██╗████████╗   ███████╗ █████╗ ███╗   ███╗███████╗",
8    "██╔════╝ ██║╚══██╔══╝   ██╔════╝██╔══██╗████╗ ████║██╔════╝",
9    "██║  ███╗██║   ██║█████╗███████╗███████║██╔████╔██║█████╗  ",
10    "██║   ██║██║   ██║╚════╝╚════██║██╔══██║██║╚██╔╝██║██╔══╝  ",
11];
12
13/// Line 5 prefix (before version badge).
14const LINE5_PREFIX: &str = "╚██████╔╝██║   ██║      ███████║██║  ██║██║ ╚═╝ ██║█";
15
16/// Line 5 suffix (after version badge).
17const LINE5_SUFFIX: &str = "╗";
18
19/// Line 6.
20const LAST_LINE: &str = " ╚═════╝ ╚═╝   ╚═╝      ╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝";
21
22/// Visual width of the ASCII logo (in monospace columns).
23const ART_WIDTH: usize = 62;
24
25/// Static gradient color stops: Blue → Cyan → Green.
26const GRADIENT_STOPS: [(u8, u8, u8); 3] = [
27    (59, 130, 246), // Blue
28    (6, 182, 212),  // Cyan
29    (34, 197, 94),  // Green
30];
31
32/// Animated gradient color stops: Blue → Cyan → Green → Indigo → Purple.
33const ANIMATED_GRADIENT_STOPS: [(u8, u8, u8); 5] = [
34    (59, 130, 246), // Blue
35    (6, 182, 212),  // Cyan
36    (34, 197, 94),  // Green
37    (99, 102, 241), // Indigo
38    (147, 51, 234), // Purple
39];
40
41/// Canonical subheadline used by CLI + Dashboard.
42pub fn subheadline() -> &'static str {
43    env!("CARGO_PKG_DESCRIPTION")
44}
45
46/// Prints the gisa ASCII art banner to stdout (CLI mode).
47pub 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// ---------------------------------------------------------------------------
155// TUI rendering (feature-gated)
156// ---------------------------------------------------------------------------
157
158#[cfg(feature = "tui")]
159use ratatui::{
160    layout::Rect,
161    style::{Color, Modifier, Style},
162    text::{Line, Span},
163    widgets::Paragraph,
164    Frame,
165};
166
167/// Linearly interpolate between RGB color stops.
168pub(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/// Apply a static gradient to a line of text.
185#[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/// Compute the color for a character at normalized position `base_t`
207/// during a left-to-right sweep animation at the given `phase`.
208/// Returns the first stop color when the character is outside the wave.
209#[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/// Apply an animated gradient sweep to a line of text (left-to-right wave).
221/// `phase` in [0.0, 1.0] drives the sweep: 0.0 and 1.0 = all first-stop color,
222/// 0.5 = full gradient visible.
223#[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/// Render the GIT-SAME banner with a static Blue → Cyan → Green gradient.
245#[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    // Line 5: gradient prefix + inverted version + gradient suffix
257    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/// Render the GIT-SAME banner with animated gradient sweep (left-to-right wave).
298/// `phase` in [0.0, 1.0] drives the sweep: 0.0 and 1.0 = all first-stop color,
299/// 0.5 = full gradient visible.
300#[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    // Line 5: sweep prefix + inverted version badge + sweep suffix
312    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;