Skip to main content

codesynapse_cli/
ui.rs

1use console::{style, Style, Term};
2use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
3use std::time::Duration;
4
5pub fn is_tty() -> bool {
6    Term::stdout().is_term()
7}
8
9fn is_stderr_tty() -> bool {
10    Term::stderr().is_term()
11}
12
13pub fn ok(msg: &str) {
14    println!("  {}  {}", style("✓").green().bold(), msg);
15}
16
17pub fn warn(msg: &str) {
18    println!("  {}  {}", style("⚠").yellow(), msg);
19}
20
21pub fn fail(msg: &str) {
22    println!("  {}  {}", style("✗").red(), msg);
23}
24
25pub fn info(msg: &str) {
26    println!("  {}  {}", style("·").dim(), style(msg).dim());
27}
28
29pub fn spinner(label: impl Into<String>) -> ProgressBar {
30    let label = label.into();
31    if !is_stderr_tty() {
32        info(&label);
33        return ProgressBar::hidden();
34    }
35    let pb = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr());
36    pb.set_style(
37        ProgressStyle::with_template("  {spinner:.cyan}  {msg}")
38            .unwrap()
39            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ "),
40    );
41    pb.set_message(label);
42    pb.enable_steady_tick(Duration::from_millis(80));
43    pb
44}
45
46pub fn spin_ok(pb: &ProgressBar, msg: &str) {
47    pb.finish_and_clear();
48    ok(msg);
49}
50
51pub fn spin_warn(pb: &ProgressBar, msg: &str) {
52    pb.finish_and_clear();
53    warn(msg);
54}
55
56pub fn spin_fail(pb: &ProgressBar, msg: &str) {
57    pb.finish_and_clear();
58    fail(msg);
59}
60
61pub fn fmt_duration(d: std::time::Duration) -> String {
62    let ms = d.as_millis();
63    if ms < 1000 {
64        format!("{}ms", ms)
65    } else {
66        format!("{:.1}s", d.as_secs_f64())
67    }
68}
69
70pub fn fmt_count(n: usize) -> String {
71    let s = n.to_string();
72    let mut result = String::new();
73    for (i, c) in s.chars().rev().enumerate() {
74        if i > 0 && i % 3 == 0 {
75            result.push(',');
76        }
77        result.push(c);
78    }
79    result.chars().rev().collect()
80}
81
82pub fn print_banner() {
83    if !is_tty() {
84        return;
85    }
86    let version = env!("CARGO_PKG_VERSION");
87    let b = Style::new().cyan().bold();
88    let d = Style::new().dim();
89    println!();
90    println!(
91        "  {}",
92        b.apply_to("░█▀▀░░░░█▀█░░░░█▀▄░░░░█▀▀░░░░█▀▀░░░░█░█░░░░█▀█░░░░█▀█░░░░█▀█░░░░█▀▀░░░░█▀▀")
93    );
94    println!(
95        "  {}",
96        b.apply_to("░█░░░░░░█░█░░░░█░█░░░░█▀▀░░░░▀▀█░░░░░█░░░░░█░█░░░░█▀█░░░░█▀▀░░░░▀▀█░░░░█▀▀")
97    );
98    println!(
99        "  {}",
100        b.apply_to("░▀▀▀░▀░░▀▀▀░▀░░▀▀░░▀░░▀▀▀░▀░░▀▀▀░▀░░░▀░░▀░░▀░▀░▀░░▀░▀░▀░░▀░░░▀░░▀▀▀░▀░░▀▀▀")
101    );
102    println!();
103    println!("  {}  {}", d.apply_to("code intelligence MCP server — gives AI coding assistants architecture-level knowledge of your codebase"), d.apply_to(format!("·  v{}", version)));
104    println!();
105}
106
107pub fn print_setup_summary(registered: &[String], failed: &[String], hybrid: bool) {
108    fn display_name(id: &str) -> &str {
109        match id {
110            "vscode" => "VS Code",
111            "cursor" => "Cursor",
112            "windsurf" => "Windsurf",
113            "zed" => "Zed",
114            "claude" => "Claude",
115            other => other,
116        }
117    }
118
119    let restart_hint = if registered.is_empty() {
120        "Re-run with --client to register an editor.".to_string()
121    } else {
122        let names: Vec<&str> = registered.iter().map(|n| display_name(n)).collect();
123        match names.as_slice() {
124            [single] => format!("Restart {} to apply changes.", single),
125            [a, b] => format!("Restart {} and {} to apply changes.", a, b),
126            _ => {
127                let (last, rest) = names.split_last().unwrap();
128                format!("Restart {} and {} to apply changes.", rest.join(", "), last)
129            }
130        }
131    };
132
133    println!();
134    println!(
135        "  {}  Ready  ·  {}",
136        style("✓").green().bold(),
137        style(&restart_hint).dim()
138    );
139    if !failed.is_empty() {
140        let failed_names: Vec<&str> = failed.iter().map(|n| display_name(n)).collect();
141        println!(
142            "  {}  {} failed — check permissions",
143            style("⚠").yellow(),
144            failed_names.join(", ")
145        );
146    }
147    if !hybrid {
148        println!(
149            "  {}  {}",
150            style("·").dim(),
151            style("Hybrid search disabled — model not found.").dim()
152        );
153    }
154    println!();
155}