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}