Skip to main content

lean_ctx/
terminal_ui.rs

1use std::io::{self, IsTerminal, Write};
2
3const LOGO: [&str; 6] = [
4    r"  ██╗     ███████╗ █████╗ ███╗   ██╗     ██████╗████████╗██╗  ██╗",
5    r"  ██║     ██╔════╝██╔══██╗████╗  ██║    ██╔════╝╚══██╔══╝╚██╗██╔╝",
6    r"  ██║     █████╗  ███████║██╔██╗ ██║    ██║        ██║    ╚███╔╝ ",
7    r"  ██║     ██╔══╝  ██╔══██║██║╚██╗██║    ██║        ██║    ██╔██╗ ",
8    r"  ███████╗███████╗██║  ██║██║ ╚████║    ╚██████╗   ██║   ██╔╝ ██╗",
9    r"  ╚══════╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝     ╚═════╝   ╚═╝   ╚═╝  ╚═╝",
10];
11
12const TAGLINE: &str = "The Intelligence Layer for AI Coding";
13
14fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
15    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
16    let h2 = h / 60.0;
17    let x = c * (1.0 - (h2 % 2.0 - 1.0).abs());
18    let (r1, g1, b1) = match h2 as u32 {
19        0 => (c, x, 0.0),
20        1 => (x, c, 0.0),
21        2 => (0.0, c, x),
22        3 => (0.0, x, c),
23        4 => (x, 0.0, c),
24        _ => (c, 0.0, x),
25    };
26    let m = l - c / 2.0;
27    (
28        ((r1 + m) * 255.0) as u8,
29        ((g1 + m) * 255.0) as u8,
30        ((b1 + m) * 255.0) as u8,
31    )
32}
33
34fn rgb_fg(r: u8, g: u8, b: u8) -> String {
35    format!("\x1b[38;2;{r};{g};{b}m")
36}
37
38pub fn print_logo_animated() {
39    if !io::stdout().is_terminal() {
40        print_logo_static();
41        return;
42    }
43
44    let mut stdout = io::stdout();
45    let frames = 32;
46    let frame_ms = 50;
47
48    for frame in 0..frames {
49        if frame > 0 {
50            print!("\x1b[{}A", LOGO.len() + 2);
51        }
52
53        let base_hue = (frame as f64 / frames as f64) * 360.0;
54
55        for (i, line) in LOGO.iter().enumerate() {
56            let chars: Vec<char> = line.chars().collect();
57            let mut buf = String::with_capacity(chars.len() * 20);
58
59            for (j, ch) in chars.iter().enumerate() {
60                if *ch == ' ' {
61                    buf.push(' ');
62                    continue;
63                }
64                let hue = (base_hue + (j as f64 * 2.5) + (i as f64 * 15.0)) % 360.0;
65                let (r, g, b) = hsl_to_rgb(hue, 0.85, 0.65);
66                buf.push_str(&rgb_fg(r, g, b));
67                buf.push(*ch);
68            }
69            buf.push_str("\x1b[0m");
70            let _ = writeln!(stdout, "{buf}");
71        }
72
73        let tag_hue = (base_hue + 120.0) % 360.0;
74        let (tr, tg, tb) = hsl_to_rgb(tag_hue, 0.5, 0.55);
75        let _ = writeln!(
76            stdout,
77            "{}             {TAGLINE}\x1b[0m",
78            rgb_fg(tr, tg, tb)
79        );
80        let _ = writeln!(stdout);
81
82        let _ = stdout.flush();
83        std::thread::sleep(std::time::Duration::from_millis(frame_ms));
84    }
85
86    print!("\x1b[{}A", LOGO.len() + 2);
87    print_logo_final_gradient();
88}
89
90pub fn print_logo_static() {
91    print_logo_final_gradient();
92}
93
94fn print_logo_final_gradient() {
95    let mut stdout = io::stdout();
96
97    for (i, line) in LOGO.iter().enumerate() {
98        let chars: Vec<char> = line.chars().collect();
99        let mut buf = String::with_capacity(chars.len() * 20);
100
101        for (j, ch) in chars.iter().enumerate() {
102            if *ch == ' ' {
103                buf.push(' ');
104                continue;
105            }
106            let t = if chars.len() > 1 {
107                j as f64 / (chars.len() - 1) as f64
108            } else {
109                0.5
110            };
111            let row_shift = i as f64 * 8.0;
112            let hue = 160.0 + t * 80.0 + row_shift;
113            let (r, g, b) = hsl_to_rgb(hue % 360.0, 0.75, 0.6);
114            buf.push_str(&rgb_fg(r, g, b));
115            buf.push(*ch);
116        }
117        buf.push_str("\x1b[0m");
118        let _ = writeln!(stdout, "{buf}");
119    }
120
121    let (tr, tg, tb) = hsl_to_rgb(200.0, 0.4, 0.55);
122    let _ = writeln!(
123        stdout,
124        "{}             {TAGLINE}\x1b[0m",
125        rgb_fg(tr, tg, tb)
126    );
127    let _ = writeln!(stdout);
128    let _ = stdout.flush();
129}
130
131pub fn print_command_box() {
132    let dim = "\x1b[2m";
133    let rst = "\x1b[0m";
134    let bold = "\x1b[1m";
135    let cyan = "\x1b[36m";
136    let green = "\x1b[32m";
137
138    println!("  {dim}┌─────────────────────────────────────────────────────────┐{rst}");
139    println!(
140        "  {dim}│{rst}  {cyan}{bold}lean-ctx gain{rst}        {dim}Token savings dashboard{rst}         {dim}│{rst}"
141    );
142    println!(
143        "  {dim}│{rst}  {cyan}{bold}lean-ctx dashboard{rst}   {dim}Web analytics (browser){rst}        {dim}│{rst}"
144    );
145    println!(
146        "  {dim}│{rst}  {cyan}{bold}lean-ctx benchmark{rst}   {dim}Test compression quality{rst}        {dim}│{rst}"
147    );
148    println!(
149        "  {dim}│{rst}  {cyan}{bold}lean-ctx config{rst}      {dim}Edit settings{rst}                   {dim}│{rst}"
150    );
151    println!(
152        "  {dim}│{rst}  {cyan}{bold}lean-ctx doctor{rst}      {dim}Verify installation{rst}             {dim}│{rst}"
153    );
154    println!(
155        "  {dim}│{rst}  {cyan}{bold}lean-ctx update{rst}      {dim}Self-update to latest{rst}           {dim}│{rst}"
156    );
157    println!(
158        "  {dim}│{rst}  {cyan}{bold}lean-ctx off{rst} / {cyan}{bold}on{rst}    {dim}Toggle compression{rst}              {dim}│{rst}"
159    );
160    println!(
161        "  {dim}│{rst}  {cyan}{bold}lean-ctx uninstall{rst}   {dim}Clean removal{rst}                   {dim}│{rst}"
162    );
163    println!("  {dim}└─────────────────────────────────────────────────────────┘{rst}");
164    println!("  {green}Ready!{rst} Your next AI command will be automatically optimized.");
165    println!("  {dim}Docs: https://leanctx.com/docs{rst}");
166    println!();
167}
168
169pub fn print_step_header(step: u8, total: u8, title: &str) {
170    let dim = "\x1b[2m";
171    let bold = "\x1b[1m";
172    let cyan = "\x1b[36m";
173    let rst = "\x1b[0m";
174    println!();
175    println!("  {cyan}{bold}[{step}/{total}]{rst} {bold}{title}{rst}");
176    println!("  {dim}─────────────────────────────────────────────────────{rst}");
177}
178
179pub fn print_status_ok(msg: &str) {
180    println!("  \x1b[32m✓\x1b[0m {msg}");
181}
182
183pub fn print_status_skip(msg: &str) {
184    println!("  \x1b[2m○\x1b[0m \x1b[2m{msg}\x1b[0m");
185}
186
187pub fn print_status_new(msg: &str) {
188    println!("  \x1b[1;32m✓\x1b[0m \x1b[1m{msg}\x1b[0m");
189}
190
191pub fn print_status_warn(msg: &str) {
192    println!("  \x1b[33m⚠\x1b[0m {msg}");
193}
194
195pub fn spinner_tick(msg: &str, frame: usize) {
196    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
197    let ch = frames[frame % frames.len()];
198    print!("\r  \x1b[36m{ch}\x1b[0m {msg}");
199    let _ = io::stdout().flush();
200}
201
202pub fn spinner_done(msg: &str) {
203    print!("\r  \x1b[32m✓\x1b[0m {msg}\x1b[K\n");
204    let _ = io::stdout().flush();
205}
206
207pub fn print_setup_header() {
208    let dim = "\x1b[2m";
209    let bold = "\x1b[1m";
210    let green = "\x1b[32m";
211    let rst = "\x1b[0m";
212    println!();
213    println!("  {dim}╭──────────────────────────────────────────╮{rst}");
214    println!(
215        "  {dim}│{rst}  {green}{bold}◆ lean-ctx setup{rst}                         {dim}│{rst}"
216    );
217    println!("  {dim}│{rst}  {dim}Configuring your development environment{rst} {dim}│{rst}");
218    println!("  {dim}╰──────────────────────────────────────────╯{rst}");
219    println!();
220}