1use crate::compression::engine::Tool;
4use crate::compression::{CompressionEngine, CompressionLevel};
5
6const TITLE: &str = "\
7\x1b[32m█▀▄▀█ █▀▀ █▀█ █▀▀ █▀█ █▀▄▀█ █▀█ █▀█ █▀▀ █▀▀ █▀▀ █▀█ █▀█\x1b[0m
8\x1b[32m█ ▀ █ █▄▄ █▀▀ █▄▄ █▄█ █ ▀ █ █▀▀ █▀▄ ██▄ ▄▄█ ▄▄█ █▄█ █▀▄\x1b[0m";
9
10pub fn compression_stats(tools: &[Tool]) -> CompressionStats {
12 let original: usize = tools
13 .iter()
14 .map(|t| {
15 let name_len = t.name.len();
16 let desc_len = t.description.as_deref().unwrap_or("").len();
17 let schema_len = t
18 .input_schema
19 .get("properties")
20 .and_then(|p| serde_json::to_string(p).ok())
21 .map(|s| s.len())
22 .unwrap_or(0);
23 name_len + desc_len + schema_len
24 })
25 .sum();
26
27 let levels = [
28 CompressionLevel::Low,
29 CompressionLevel::Medium,
30 CompressionLevel::High,
31 CompressionLevel::Max,
32 ];
33
34 let compressed: Vec<(CompressionLevel, usize)> = levels
35 .iter()
36 .map(|level| (level.clone(), compressed_frontend_size(tools, level)))
37 .collect();
38
39 CompressionStats {
40 original_size: original,
41 compressed,
42 }
43}
44
45pub struct CompressionStats {
46 pub original_size: usize,
47 pub compressed: Vec<(CompressionLevel, usize)>,
48}
49
50fn compressed_frontend_size(tools: &[Tool], level: &CompressionLevel) -> usize {
51 let engine = CompressionEngine::new(level.clone());
52 let listing = engine.format_listing(tools);
53
54 let get_tool_schema_description = format!(
55 "Get the complete schema and description for one backend tool. Available tools:\n{listing}"
56 );
57 let invoke_tool_description = "Invoke one backend tool by name with JSON input.";
58 let list_tools_description = "List backend tools available through this compressed MCP server.";
59
60 let schema_wrapper = serde_json::json!({
61 "type": "object",
62 "properties": {
63 "tool_name": {"type": "string", "description": "Name of the backend tool"}
64 },
65 "required": ["tool_name"]
66 });
67 let invoke_wrapper = serde_json::json!({
68 "type": "object",
69 "properties": {
70 "tool_name": {"type": "string", "description": "Name of the backend tool"},
71 "tool_input": {"type": "object", "description": "JSON input for the backend tool"}
72 },
73 "required": ["tool_name", "tool_input"]
74 });
75 let list_wrapper = serde_json::json!({
76 "type": "object",
77 "properties": {}
78 });
79
80 let mut size = get_tool_schema_description.len()
81 + invoke_tool_description.len()
82 + schema_wrapper.to_string().len()
83 + invoke_wrapper.to_string().len();
84
85 if *level == CompressionLevel::Max {
86 size += list_tools_description.len() + list_wrapper.to_string().len();
87 }
88
89 size
90}
91
92pub fn print_banner(
94 server_name: Option<&str>,
95 transport_type: &str,
96 active_level: &CompressionLevel,
97 tools: &[Tool],
98 cli_info: Option<CliInfo<'_>>,
99) {
100 let columns = terminal_width().min(80);
101 if columns < 63 {
102 return;
103 }
104
105 let content_width = columns - 6;
106 let header = format!("╭{}╮", "─".repeat(columns - 2));
107 let footer = format!("╰{}╯", "─".repeat(columns - 2));
108 let separator = format!("├{}┤", "─".repeat(columns - 2));
109 let blank = format!("│{}│", " ".repeat(columns - 2));
110
111 let stats = compression_stats(tools);
112
113 let mut lines = vec![header.clone(), blank.clone()];
114 for title_line in TITLE.lines() {
115 lines.push(pad_line(title_line, content_width, true));
116 }
117 lines.push(blank.clone());
118 lines.push(pad_line(
119 "https://atlassian-labs.github.io/mcp-compressor/",
120 content_width,
121 true,
122 ));
123 if let Some(name) = server_name {
124 lines.push(blank.clone());
125 lines.push(pad_line(
126 &format!("\x1b[32m●\x1b[0m Backend server name: {name}"),
127 content_width,
128 false,
129 ));
130 }
131 lines.push(pad_line(
132 &format!(
133 "\x1b[32m●\x1b[0m Backend server transport: {}",
134 transport_type.to_uppercase()
135 ),
136 content_width,
137 false,
138 ));
139 lines.push(blank.clone());
140 lines.push(separator.clone());
141 lines.push(blank.clone());
142
143 lines.push(pad_line(
144 &format!(
145 "📊 Compression Statistics (current = {}):",
146 capitalize(active_level)
147 ),
148 content_width - 1,
149 false,
150 ));
151 lines.push(blank.clone());
152 lines.extend(format_chart(&stats, content_width, active_level));
153
154 if let Some(info) = cli_info {
155 lines.push(blank.clone());
156 lines.push(separator.clone());
157 lines.push(blank.clone());
158 if let Some(script) = info.script_path {
159 lines.push(pad_line(
160 &format!("Script: {script}"),
161 content_width,
162 false,
163 ));
164 }
165 if let Some(bridge) = info.bridge_url {
166 lines.push(pad_line(
167 &format!("Bridge: {bridge}"),
168 content_width,
169 false,
170 ));
171 }
172 if let Some(invoke) = info.invoke_prefix {
173 lines.push(pad_line(
174 &format!("Run: {invoke} --help"),
175 content_width,
176 false,
177 ));
178 }
179 }
180
181 lines.push(blank.clone());
182 lines.push(footer);
183
184 eprintln!("{}", lines.join("\n"));
185}
186
187pub struct CliInfo<'a> {
188 pub script_path: Option<&'a str>,
189 pub bridge_url: Option<&'a str>,
190 pub invoke_prefix: Option<&'a str>,
191}
192
193fn format_chart(
194 stats: &CompressionStats,
195 width: usize,
196 active_level: &CompressionLevel,
197) -> Vec<String> {
198 let chart_width = width.saturating_sub(16);
199 let original = stats.original_size;
200 let mut lines = Vec::new();
201
202 let bar = "█".repeat(chart_width);
204 lines.push(pad_line(&format!("Original {bar} 100.0%"), width, false));
205
206 let levels = [
208 CompressionLevel::Low,
209 CompressionLevel::Medium,
210 CompressionLevel::High,
211 CompressionLevel::Max,
212 ];
213 for level in &levels {
214 let size = stats
215 .compressed
216 .iter()
217 .find(|(l, _)| l == level)
218 .map(|(_, s)| *s)
219 .unwrap_or(0);
220 let ratio = if original > 0 {
221 size as f64 / original as f64
222 } else {
223 0.0
224 };
225 let filled = (ratio * chart_width as f64).round() as usize;
226 let filled = filled.min(chart_width);
227 let bar = format!("{}{}", "█".repeat(filled), "░".repeat(chart_width - filled));
228 let pct = ratio * 100.0;
229 let label = format!("{:<8}", capitalize(level));
230 let mut line = pad_line(&format!("{label} {bar} {pct:5.1}%"), width, false);
231
232 if level == active_level {
233 line = highlight_bar(&line);
234 }
235 lines.push(line);
236 }
237 lines
238}
239
240fn highlight_bar(line: &str) -> String {
242 if let Some(fade_byte) = line.char_indices().find(|(_, c)| *c == '░').map(|(i, _)| i) {
244 let prefix_end = line
247 .char_indices()
248 .take_while(|(_, c)| *c != '█' && *c != '░')
249 .last()
250 .map(|(i, c)| i + c.len_utf8())
251 .unwrap_or(0);
252 format!(
253 "{}\x1b[1;32m{}\x1b[0m{}",
254 &line[..prefix_end],
255 &line[prefix_end..fade_byte],
256 &line[fade_byte..]
257 )
258 } else {
259 format!("\x1b[1;32m{line}\x1b[0m")
261 }
262}
263
264fn capitalize(level: &CompressionLevel) -> String {
265 let s = level.to_string();
266 let mut chars = s.chars();
267 match chars.next() {
268 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
269 None => String::new(),
270 }
271}
272
273fn pad_line(line: &str, total_width: usize, center: bool) -> String {
274 let clean: String = strip_ansi(line);
276 let clean_width = clean.chars().count();
277
278 if center {
279 let padding_total = total_width.saturating_sub(clean_width);
280 let padding_left = padding_total / 2;
281 let padding_right = padding_total - padding_left;
282 format!(
283 "│ {}{}{} │",
284 " ".repeat(padding_left),
285 line,
286 " ".repeat(padding_right)
287 )
288 } else {
289 let padding_right = total_width.saturating_sub(clean_width);
290 format!("│ {}{} │", line, " ".repeat(padding_right))
291 }
292}
293
294fn strip_ansi(s: &str) -> String {
295 let mut result = String::with_capacity(s.len());
296 let mut in_escape = false;
297 for c in s.chars() {
298 if in_escape {
299 if c.is_ascii_alphabetic() {
300 in_escape = false;
301 }
302 } else if c == '\x1b' {
303 in_escape = true;
304 } else {
305 result.push(c);
306 }
307 }
308 result
309}
310
311fn terminal_width() -> usize {
312 #[cfg(unix)]
314 {
315 use std::mem::MaybeUninit;
316 unsafe {
317 let mut ws = MaybeUninit::<libc::winsize>::zeroed();
318 if libc::ioctl(2, libc::TIOCGWINSZ, ws.as_mut_ptr()) == 0 {
319 let ws = ws.assume_init();
320 if ws.ws_col > 0 {
321 return ws.ws_col as usize;
322 }
323 }
324 }
325 }
326 80
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn highlight_bar_handles_unicode_box_border() {
335 let line =
336 "│ Medium █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15.5% │";
337 let highlighted = highlight_bar(line);
338 assert!(highlighted.contains("\x1b[1;32m"));
339 assert!(highlighted.contains("Medium"));
340 assert_eq!(strip_ansi(&highlighted), line);
341 }
342
343 #[test]
344 fn format_chart_only_shows_compression_levels() {
345 let stats = CompressionStats {
346 original_size: 129,
347 compressed: vec![
348 (CompressionLevel::Low, 80),
349 (CompressionLevel::Medium, 20),
350 (CompressionLevel::High, 10),
351 (CompressionLevel::Max, 5),
352 ],
353 };
354
355 let lines = format_chart(&stats, 80, &CompressionLevel::Medium);
356 assert_eq!(lines.len(), 5);
357 assert!(lines.iter().any(|line| line.contains("Original")));
358 assert!(lines.iter().any(|line| line.contains("Low")));
359 assert!(lines.iter().any(|line| line.contains("Medium")));
360 assert!(lines.iter().any(|line| line.contains("High")));
361 assert!(lines.iter().any(|line| line.contains("Max")));
362 assert!(!lines.iter().any(|line| line.contains("CLI mode")));
363 let medium = lines.iter().find(|line| line.contains("Medium")).unwrap();
364 assert!(medium.contains("\x1b[1;32m"));
365 }
366
367 #[test]
368 fn max_compression_stat_includes_wrapper_schema_surface() {
369 let tools = vec![Tool::new(
370 "echo",
371 Some("Echo a message".to_string()),
372 serde_json::json!({
373 "type": "object",
374 "properties": {"message": {"type": "string"}},
375 "required": ["message"]
376 }),
377 )];
378 let stats = compression_stats(&tools);
379 let max = stats
380 .compressed
381 .iter()
382 .find(|(level, _)| *level == CompressionLevel::Max)
383 .map(|(_, size)| *size)
384 .unwrap();
385 assert!(max > 0);
386 }
387}