1use ratatui::prelude::*;
11use ratatui::widgets::Paragraph;
12
13pub const FRAMES: usize = 30;
15const FILL_FRAMES: usize = 20;
17
18const GOLD: Color = Color::Rgb(212, 175, 55);
20const SEAM: Color = Color::Rgb(90, 90, 90);
21
22const TAGLINE: &str = "a local-first safety layer for AI coding agents";
23
24const GLYPHS: &[[&str; 5]] = &[
26 ["█ █", "█ █ ", "██ ", "█ █ ", "█ █"], ["███", " █ ", " █ ", " █ ", "███"], ["█ █", "██ █", "█ ██", "█ █", "█ █"], ["█████", " █ ", " █ ", " █ ", " █ "], [" ███", "█ ", " ██ ", " █", "███ "], ["█ █", "█ █", "█ █", "█ █", " ██ "], [" ███", "█ ", "█ ██", "█ █", " ███"], ["███", " █ ", " █ ", " █ ", "███"], ];
35
36fn wordmark_rows() -> [String; 5] {
38 let mut rows: [String; 5] = Default::default();
39 for (gi, g) in GLYPHS.iter().enumerate() {
40 for (r, row) in rows.iter_mut().enumerate() {
41 if gi > 0 {
42 row.push(' ');
43 }
44 row.push_str(g[r]);
45 }
46 }
47 rows
48}
49
50pub fn render(f: &mut Frame, area: Rect, frame: usize, color: bool) {
52 let rows = wordmark_rows();
53 let width = rows[0].chars().count();
54
55 if (area.width as usize) < width + 2 {
57 let lines = vec![
58 Line::from(""),
59 Line::from(Span::styled(
60 "KINTSUGI",
61 style_if(color, Style::default().fg(GOLD)).add_modifier(Modifier::BOLD),
62 )),
63 Line::from(Span::styled(TAGLINE, dim_if(color))),
64 ];
65 f.render_widget(
66 Paragraph::new(lines).alignment(Alignment::Center),
67 centered(area, 3),
68 );
69 return;
70 }
71
72 let fill = (frame.min(FILL_FRAMES) as f32 / FILL_FRAMES as f32 * width as f32).round() as usize;
74 let done = frame >= FILL_FRAMES;
76
77 let mut lines: Vec<Line> = Vec::new();
78 if area.height >= 16 {
81 for ml in mark_lines(color) {
82 lines.push(ml);
83 }
84 lines.push(Line::from(""));
85 }
86 for row in &rows {
87 let mut spans = Vec::new();
88 for (col, ch) in row.chars().enumerate() {
89 if ch == '█' {
90 let filled = col < fill;
91 let glyph = if filled || done { "█" } else { "░" };
92 let style = if !color {
93 Style::default()
94 } else if filled || done {
95 Style::default().fg(GOLD).add_modifier(Modifier::BOLD)
96 } else {
97 Style::default().fg(SEAM)
98 };
99 spans.push(Span::styled(glyph.to_string(), style));
100 } else {
101 spans.push(Span::raw(" "));
102 }
103 }
104 lines.push(Line::from(spans));
105 }
106
107 lines.push(Line::from(""));
109 if fill * 2 >= width {
110 lines.push(Line::from(Span::styled(TAGLINE, dim_if(color))));
111 } else {
112 lines.push(Line::from(""));
113 }
114 lines.push(Line::from(""));
115 if done {
116 lines.push(Line::from(Span::styled("press any key", dim_if(color))));
117 } else {
118 lines.push(Line::from(""));
119 }
120
121 let content_height = lines.len() as u16;
122 f.render_widget(
123 Paragraph::new(lines).alignment(Alignment::Center),
124 centered(area, content_height),
125 );
126}
127
128fn mark_lines(color: bool) -> Vec<Line<'static>> {
131 let frame_style = if color {
132 Style::default().fg(SEAM)
133 } else {
134 Style::default()
135 };
136 let gold = if color {
137 Style::default().fg(GOLD).add_modifier(Modifier::BOLD)
138 } else {
139 Style::default()
140 };
141 let row = |left: &'static str, seam: &'static str, right: &'static str| {
143 Line::from(vec![
144 Span::styled(left, frame_style),
145 Span::styled(seam, gold),
146 Span::styled(right, frame_style),
147 ])
148 };
149 vec![
152 Line::from(Span::styled("╭───────╮", frame_style)),
153 row("│ ", "╲", " │"),
154 row("│ ", "╲ ╱", " │"),
155 row("│ ", "╳", " │"),
156 row("│ ", "╱ ╲", " │"),
157 row("│ ", "╱", " │"),
158 Line::from(Span::styled("╰───────╯", frame_style)),
159 ]
160}
161
162fn centered(area: Rect, height: u16) -> Rect {
164 let h = height.min(area.height);
165 let y = area.y + (area.height.saturating_sub(h)) / 2;
166 Rect {
167 x: area.x,
168 y,
169 width: area.width,
170 height: h,
171 }
172}
173
174fn dim_if(color: bool) -> Style {
175 style_if(color, Style::default().add_modifier(Modifier::DIM))
176}
177
178fn style_if(color: bool, s: Style) -> Style {
179 if color {
180 s
181 } else {
182 Style::default()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use ratatui::backend::TestBackend;
190 use ratatui::Terminal;
191
192 fn frame_text(frame: usize, color: bool, w: u16, h: u16) -> String {
193 let mut term = Terminal::new(TestBackend::new(w, h)).unwrap();
194 term.draw(|f| render(f, f.area(), frame, color)).unwrap();
195 let buf = term.backend().buffer().clone();
196 buf.content().iter().map(|c| c.symbol()).collect()
197 }
198
199 #[test]
200 fn wordmark_rows_are_aligned() {
201 let rows = wordmark_rows();
202 let w = rows[0].chars().count();
203 assert!(rows.iter().all(|r| r.chars().count() == w), "rows ragged");
204 assert!(w >= 30, "banner should be a real width");
205 }
206
207 #[test]
208 fn early_frame_shows_unfilled_seam_glyph() {
209 let text = frame_text(0, true, 80, 24);
211 assert!(text.contains('░'), "expected unfilled seam at frame 0");
212 }
213
214 #[test]
215 fn final_frame_is_fully_filled_and_prompts() {
216 let text = frame_text(FRAMES, true, 80, 24);
217 assert!(text.contains('█'), "expected filled blocks at the end");
218 assert!(!text.contains('░'), "nothing should remain unfilled");
219 assert!(text.contains("press any key"));
220 assert!(text.contains("safety layer"));
221 }
222
223 #[test]
224 fn mono_animation_still_sweeps_without_color() {
225 let early = frame_text(0, false, 80, 24);
227 let late = frame_text(FRAMES, false, 80, 24);
228 assert!(early.contains('░'));
229 assert!(!late.contains('░'));
230 }
231
232 #[test]
233 fn narrow_terminal_degrades_without_panic() {
234 let text = frame_text(10, true, 24, 10);
235 assert!(text.contains("KINTSUGI"));
236 }
237
238 #[test]
239 fn brand_mark_is_a_perfect_rectangle() {
240 let lines = mark_lines(false);
242 let widths: Vec<usize> = lines
243 .iter()
244 .map(|l| l.spans.iter().map(|s| s.content.chars().count()).sum())
245 .collect();
246 assert!(
247 widths.windows(2).all(|w| w[0] == w[1]),
248 "tile rows ragged: {widths:?}"
249 );
250 }
251}