1use escriba_ui::chrome::ChromePalette;
10use escriba_ui::splash::{Splash, SplashRole};
11
12const SGR_RESET: &str = "\x1b[0m";
15
16#[must_use]
22pub fn render_splash_ansi(
23 splash: &Splash,
24 chrome: &ChromePalette,
25 width: u16,
26 height: u16,
27) -> String {
28 let chunks = splash.screen_chunks(width, height);
29 if chunks.is_empty() {
30 return String::new();
31 }
32 let mut out = String::with_capacity(chunks.len() * 24);
33 let mut current: Option<SplashRole> = None;
36 for chunk in &chunks {
37 if chunk.text.trim().is_empty() {
40 out.push_str(&chunk.text);
41 continue;
42 }
43 if current != Some(chunk.role) {
44 push_fg(&mut out, chunk.role.color(chrome));
45 current = Some(chunk.role);
46 }
47 out.push_str(&chunk.text);
48 }
49 out.push_str(SGR_RESET);
50 out
51}
52
53fn push_fg(out: &mut String, c: ishou_tokens::Rgb) {
56 out.push_str("\x1b[38;2;");
57 push_u8(out, c.r);
58 out.push(';');
59 push_u8(out, c.g);
60 out.push(';');
61 push_u8(out, c.b);
62 out.push('m');
63}
64
65fn push_u8(out: &mut String, mut n: u8) {
66 if n == 0 {
67 out.push('0');
68 return;
69 }
70 let mut buf = [0u8; 3];
71 let mut i = buf.len();
72 while n > 0 {
73 i -= 1;
74 buf[i] = b'0' + (n % 10);
75 n /= 10;
76 }
77 out.push_str(core::str::from_utf8(&buf[i..]).unwrap_or("0"));
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use escriba_core::Action;
84 use escriba_ui::splash::SplashEntry;
85
86 fn sample() -> Splash {
87 Splash {
88 art: vec!["ESCRIBA".into()],
89 tagline: "a modal editor".into(),
90 entries: vec![SplashEntry {
91 key: 'q',
92 label: "quit".into(),
93 action: Action::Quit,
94 }],
95 facts: vec!["v0.1.0".into()],
96 }
97 }
98
99 #[test]
100 fn the_screen_survives_the_trip_through_ansi() {
101 let out = render_splash_ansi(&sample(), &ChromePalette::prescribed(), 80, 24);
102 assert!(out.contains("ESCRIBA"), "{out:?}");
103 assert!(out.contains("a modal editor"));
104 assert!(out.contains("quit"));
105 assert!(out.contains("v0.1.0"));
106 }
107
108 #[test]
109 fn color_is_emitted_and_always_reset() {
110 let out = render_splash_ansi(&sample(), &ChromePalette::prescribed(), 80, 24);
111 assert!(out.contains("\x1b[38;2;"), "no 24-bit color emitted");
112 assert!(
113 out.ends_with(SGR_RESET),
114 "a face must hand the terminal back clean",
115 );
116 }
117
118 #[test]
119 fn a_canvas_with_no_room_renders_nothing_at_all() {
120 assert!(render_splash_ansi(&sample(), &ChromePalette::prescribed(), 80, 1).is_empty());
122 }
123
124 #[test]
125 fn push_u8_covers_the_whole_byte_range() {
126 for n in [0u8, 7, 42, 100, 255] {
127 let mut s = String::new();
128 push_u8(&mut s, n);
129 assert_eq!(s, n.to_string());
130 }
131 }
132}