diff --git a/src/cli.rs b/src/cli.rs index 409e332..c6a6b6b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -42,16 +42,19 @@ impl ChatCLI { pub async fn run(&mut self) -> Result<()> { self.display.print_header(); - self.display - .print_info("Type your message and press Enter. Commands start with '/'."); - self.display.print_info("Type /help for help."); - + + // Enhanced status bar with comprehensive information let provider = get_provider_for_model(&self.session.model); let display_name = get_display_name_for_model(&self.session.model); - self.display - .print_model_info(&display_name, provider.as_str()); - self.display.print_session_info(&self.session.name); - + let features = vec![ + ("Web Search", self.session.enable_web_search), + ("Reasoning", self.session.enable_reasoning_summary), + ("Extended Thinking", self.session.enable_extended_thinking), + ]; + + self.display.print_status_bar(&display_name, provider.as_str(), &self.session.name, &features); + + self.display.print_info("Type your message and press Enter. Commands start with '/' (try /help)."); println!(); loop { @@ -68,7 +71,16 @@ impl ChatCLI { } } else { if let Err(e) = self.handle_user_message(line).await { - self.display.print_error(&format!("Error: {}", e)); + // Enhanced error display with context and suggestions + let error_msg = format!("Error: {}", e); + let context = Some("This error occurred while processing your message."); + let suggestions = vec![ + "Check your API key configuration", + "Verify your internet connection", + "Try switching to a different model with /model", + "Use /help to see available commands" + ]; + self.display.print_error_with_context(&error_msg, context, &suggestions); } println!(); // Add padding before next prompt } @@ -425,33 +437,18 @@ impl ChatCLI { async fn tools_manager(&mut self) -> Result<()> { loop { - // Show current tool status - self.display.print_info("Tool Management:"); - - let web_status = if self.session.enable_web_search { - "✓ enabled" - } else { - "✗ disabled" - }; - let reasoning_status = if self.session.enable_reasoning_summary { - "✓ enabled" - } else { - "✗ disabled" - }; - let extended_thinking_status = if self.session.enable_extended_thinking { - "✓ enabled" - } else { - "✗ disabled" - }; - - println!(" Web Search: {}", web_status); - println!(" Reasoning Summaries: {}", reasoning_status); - println!(" Reasoning Effort: {}", self.session.reasoning_effort); - println!(" Extended Thinking: {}", extended_thinking_status); - println!( - " Thinking Budget: {} tokens", - self.session.thinking_budget_tokens - ); + // Show current tool status using enhanced display + let features = vec![ + ("Web Search", self.session.enable_web_search, Some("Search the web for up-to-date information")), + ("Reasoning Summaries", self.session.enable_reasoning_summary, Some("Show reasoning process summaries")), + ("Extended Thinking", self.session.enable_extended_thinking, Some("Enable deeper reasoning capabilities")), + ]; + + self.display.print_feature_status(&features); + + // Additional status information + self.display.print_info(&format!("Reasoning Effort: {}", self.session.reasoning_effort)); + self.display.print_info(&format!("Thinking Budget: {} tokens", self.session.thinking_budget_tokens)); // Check model compatibility let model = self.session.model.clone(); diff --git a/src/utils/display.rs b/src/utils/display.rs index af85d36..9a879e1 100644 --- a/src/utils/display.rs +++ b/src/utils/display.rs @@ -21,45 +21,235 @@ impl Display { } } + fn get_terminal_width(&self) -> usize { + self.term.size().1 as usize + } + + fn strip_ansi(&self, text: &str) -> String { + // Simple ANSI escape sequence removal for length calculation + match regex::Regex::new(r"\x1b\[[0-9;]*m") { + Ok(re) => re.replace_all(text, "").to_string(), + Err(_) => text.to_string(), // Fallback: return original text if regex compilation fails + } + } + pub fn print_header(&self) { self.term.clear_screen().ok(); - println!("{}", style("🤖 GPT CLI (Rust)").bold().magenta()); - println!("{}", style("─".repeat(50)).dim()); + + // Simple, clean header without complex width calculations + println!("{}", style("━".repeat(60)).cyan()); + println!("🤖 {} {}", + style("GPT CLI (Rust)").cyan().bold(), + style("v0.1.0").dim() + ); + println!("{}", style("━".repeat(60)).cyan()); + println!(); } pub fn print_info(&self, message: &str) { - println!("{} {}", style("ℹ").blue(), style(message).dim()); + self.print_message_with_icon("ℹ", message, "blue", false); } #[allow(dead_code)] pub fn print_success(&self, message: &str) { - println!("{} {}", style("✓").green(), style(message).green()); + self.print_message_with_icon("✓", message, "green", false); } pub fn print_warning(&self, message: &str) { - println!("{} {}", style("⚠").yellow(), style(message).yellow()); + self.print_message_with_icon("⚠", message, "yellow", false); } pub fn print_error(&self, message: &str) { - eprintln!("{} {}", style("✗").red(), style(message).red()); + self.print_message_with_icon("✗", message, "red", true); + } + + fn print_message_with_icon(&self, icon: &str, message: &str, color: &str, is_error: bool) { + let formatted_message = match color { + "red" => format!("{} {}", style(icon).red().bold(), style(message).red()), + "green" => format!("{} {}", style(icon).green().bold(), style(message).green()), + "yellow" => format!("{} {}", style(icon).yellow().bold(), style(message).yellow()), + "blue" => format!("{} {}", style(icon).blue().bold(), style(message).blue()), + "cyan" => format!("{} {}", style(icon).cyan().bold(), style(message).cyan()), + "magenta" => format!("{} {}", style(icon).magenta().bold(), style(message).magenta()), + _ => format!("{} {}", style(icon).white().bold(), style(message).white()), + }; + + if is_error { + eprintln!("{}", formatted_message); + } else { + println!("{}", formatted_message); + } + } + + pub fn print_model_info(&self, model: &str, provider: &str) { + self.print_status_item("🔧", "Model", &format!("{} ({})", model, provider)); + } + + pub fn print_session_info(&self, session_name: &str) { + self.print_status_item("💾", "Session", session_name); + } + + pub fn print_status_bar(&self, model: &str, provider: &str, session: &str, features: &[(&str, bool)]) { + // Clean, simple status display + println!("🤖 {} {} • 💾 {}", + style(model).cyan().bold(), + style(provider).dim(), + style(session).magenta() + ); + + // Features on one line, if any + if !features.is_empty() { + let feature_status: Vec = features.iter() + .map(|(name, enabled)| { + let icon = if *enabled { "✓" } else { "✗" }; + let styled_icon = if *enabled { + style(icon).green() + } else { + style(icon).red() + }; + format!("{} {}", styled_icon, name) + }) + .collect(); + println!("🔧 Features: {}", feature_status.join(" • ")); + } + + println!(); + } + + fn print_status_item(&self, icon: &str, label: &str, value: &str) { + println!( + "{} {}: {}", + style(icon).cyan(), + style(label).cyan().bold(), + style(value).magenta() + ); + } + + fn print_status_line(&self, left: &str, right: &str) { + let width = self.get_terminal_width(); + let content_width = width - 4; // Account for borders and padding + + if right.is_empty() { + let padding = content_width.saturating_sub(self.strip_ansi(left).len()); + println!( + "│ {}{}│", + left, + " ".repeat(padding) + ); + } else { + let left_len = self.strip_ansi(left).len(); + let right_len = self.strip_ansi(right).len(); + let padding = content_width.saturating_sub(left_len + right_len); + + println!( + "│ {}{}{}│", + left, + " ".repeat(padding), + right + ); + } + } + + pub fn print_command_result(&self, message: &str) { + self.print_message_with_icon("✓", message, "green", false); } #[allow(dead_code)] pub fn print_user_input(&self, content: &str) { - println!("{}> {}", style("👤").cyan(), content); - println!(); // Add single line of padding after user input + self.print_message_bubble("👤", "You", content, "cyan", false); } pub fn print_assistant_response(&self, content: &str) { println!(); // Add padding before AI response - print!("{}> ", style("🤖").magenta()); + self.print_message_bubble("🤖", "Assistant", content, "magenta", true); + } + + fn print_message_bubble(&self, icon: &str, role: &str, content: &str, color: &str, format_content: bool) { + let width = self.get_terminal_width(); + let header = format!("{} {}", icon, role); + + // Message header + let border_style = match color { + "cyan" => style("┌".to_string() + &"─".repeat(width - 2) + "┐").cyan(), + "magenta" => style("┌".to_string() + &"─".repeat(width - 2) + "┐").magenta(), + _ => style("┌".to_string() + &"─".repeat(width - 2) + "┐").white(), + }; + println!("{}", border_style); + + let padding = width - header.len() - 4; + let header_styled = match color { + "cyan" => style(&header).cyan().bold(), + "magenta" => style(&header).magenta().bold(), + _ => style(&header).white().bold(), + }; + println!("│ {} {}│", header_styled, " ".repeat(padding)); + + let separator_style = match color { + "cyan" => style("├".to_string() + &"─".repeat(width - 2) + "┤").cyan(), + "magenta" => style("├".to_string() + &"─".repeat(width - 2) + "┤").magenta(), + _ => style("├".to_string() + &"─".repeat(width - 2) + "┤").white(), + }; + println!("{}", separator_style); + + // Message content + if format_content { + self.print_formatted_content_in_box(content, color); + } else { + self.print_content_in_box(content, color); + } + + // Message footer + let footer_style = match color { + "cyan" => style("└".to_string() + &"─".repeat(width - 2) + "┘").cyan(), + "magenta" => style("└".to_string() + &"─".repeat(width - 2) + "┘").magenta(), + _ => style("└".to_string() + &"─".repeat(width - 2) + "┘").white(), + }; + println!("{}", footer_style); + println!(); + } + + fn print_content_in_box(&self, content: &str, border_color: &str) { + let width = self.get_terminal_width(); + let content_width = width - 4; // Account for borders and padding + + let border_char = match border_color { + "cyan" => style("│").cyan(), + "magenta" => style("│").magenta(), + _ => style("│").white(), + }; + + for line in content.lines() { + let line_len = line.len(); + if line_len <= content_width { + let padding = content_width - line_len; + println!("{} {}{}", border_char, line, " ".repeat(padding)); + } else { + // Wrap long lines + for chunk in line.as_bytes().chunks(content_width) { + let chunk_str = String::from_utf8_lossy(chunk); + let padding = content_width - chunk_str.len(); + println!("{} {}{}", border_char, chunk_str, " ".repeat(padding)); + } + } + } + } + + fn print_formatted_content_in_box(&self, content: &str, border_color: &str) { + // For now, use the existing pagination method + // TODO: Integrate box formatting with formatted content + let border_char = match border_color { + "cyan" => style("│").cyan(), + "magenta" => style("│").magenta(), + _ => style("│").white(), + }; + print!("{} ", border_char); self.print_formatted_content_with_pagination(content); } fn print_formatted_content_with_pagination(&self, content: &str) { let lines: Vec<&str> = content.lines().collect(); let terminal_height = self.term.size().0 as usize; - let lines_per_page = terminal_height.saturating_sub(5); // Leave space for prompts + let lines_per_page = terminal_height.saturating_sub(8); // Leave more space for UI elements if lines.len() <= lines_per_page { // Short content, no pagination needed @@ -68,22 +258,45 @@ impl Display { } let mut current_line = 0; + let total_pages = (lines.len() + lines_per_page - 1) / lines_per_page; while current_line < lines.len() { let end_line = (current_line + lines_per_page).min(lines.len()); let page_content = lines[current_line..end_line].join("\n"); + let current_page = (current_line / lines_per_page) + 1; self.print_formatted_content(&page_content); if end_line < lines.len() { - print!("\n{} ", style("Press Enter to continue, 'q' to finish...").dim()); + // Enhanced pagination prompt + let progress = format!("Page {} of {}", current_page, total_pages); + let prompt = format!( + "\n{} {} {} {} {}", + style("─".repeat(10)).dim(), + style(&progress).cyan().bold(), + style("─".repeat(5)).dim(), + style("[Enter: next, q: quit, h: help]").dim(), + style("─".repeat(10)).dim() + ); + print!("{} ", prompt); io::stdout().flush().ok(); let mut input = String::new(); if io::stdin().read_line(&mut input).is_ok() { - if input.trim().to_lowercase() == "q" { - println!("{}", style("(response truncated)").dim()); - break; + match input.trim().to_lowercase().as_str() { + "q" | "quit" => { + println!("{}", style("\n(response truncated)").dim()); + break; + } + "h" | "help" => { + println!( + "{}\n{}", + style("\nPagination help:").bold(), + style(" Enter or Space: Next page\n q or quit: Quit reading\n h or help: This help").dim() + ); + continue; // Don't advance to next page + } + _ => {} // Continue to next page } } } @@ -132,8 +345,8 @@ impl Display { // Print text before inline code print!("{}", &text[last_end..full_match.start()]); - // Print inline code with background highlighting - print!("{}", style(code).on_black().white()); + // Print inline code with enhanced styling + print!("{}", style(format!(" {} ", code)).on_black().white().bold()); last_end = full_match.end(); } @@ -148,6 +361,8 @@ impl Display { } fn print_code_block(&self, code: &str, language: &str) { + let width = self.get_terminal_width(); + // Find the appropriate syntax definition let syntax = self.syntax_set.find_syntax_by_extension(language) .or_else(|| self.syntax_set.find_syntax_by_name(language)) @@ -156,86 +371,175 @@ impl Display { // Use a dark theme for code highlighting let theme = &self.theme_set.themes["base16-ocean.dark"]; - println!("{}", style("```").dim()); + // Code block header with language info + let header = if !language.is_empty() { + format!("📝 {}", language) + } else { + "📝 Code".to_string() + }; + println!("{}", style("┌".to_string() + &"─".repeat(width - 2) + "┐").blue()); + let padding = width - self.strip_ansi(&header).len() - 4; + println!("│ {} {}│", style(&header).blue().bold(), " ".repeat(padding)); + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").blue()); + + // Code content with syntax highlighting let mut highlighter = HighlightLines::new(syntax, theme); for line in LinesWithEndings::from(code) { let ranges: Vec<(Style, &str)> = highlighter.highlight_line(line, &self.syntax_set).unwrap(); let escaped = as_24_bit_terminal_escaped(&ranges[..], false); - print!("{}", escaped); + + // Ensure proper line formatting within the box + print!("{} ", style("│").blue()); + print!("{}", escaped.trim_end()); + + // Calculate remaining space and fill with padding + let line_content = line.trim_end(); + let content_width = width - 4; // Account for borders + if line_content.len() < content_width { + let padding = content_width - line_content.len(); + print!("{}", " ".repeat(padding)); + } + println!(" {}", style("│").blue()); } - println!("{}", style("```").dim()); - } - - pub fn print_command_result(&self, message: &str) { - println!("{} {}", style("📝").blue(), style(message).dim()); - } - - pub fn print_model_info(&self, model: &str, provider: &str) { - println!( - "{} Model: {} ({})", - style("🔧").yellow(), - style(model).bold(), - style(provider).dim() - ); - } - - pub fn print_session_info(&self, session_name: &str) { - println!( - "{} Session: {}", - style("💾").blue(), - style(session_name).bold() - ); + // Code block footer + println!("{}", style("└".to_string() + &"─".repeat(width - 2) + "┘").blue()); + println!(); } pub fn print_help(&self) { - let help_text = r#" -Available Commands: - /help - Show this help message - /exit - Exit the CLI - /model - Interactive model switcher - /new - Create a new session - /switch - Interactive session manager (switch/delete) - /clear - Clear current conversation - /history [user|assistant] [number] - View conversation history - /export [format] - Export conversation to exports/ (markdown, json, txt) - /save - Save current session with a new name - /tools - Interactive tool and feature manager - -Input Features: - Multi-line input - End your message with '\' to enter multi-line mode - Keyboard shortcuts - Ctrl+C: Cancel, Ctrl+D: Exit, Arrow keys: History - Syntax highlighting - Code blocks are automatically highlighted - Pagination - Long responses are paginated (press Enter or 'q') - -Environment Variables: - OPENAI_API_KEY - Required for OpenAI models - ANTHROPIC_API_KEY - Required for Anthropic models - OPENAI_BASE_URL - Optional custom base URL for OpenAI - DEFAULT_MODEL - Default model if not specified - -Supported Models: - OpenAI: GPT-4.1, GPT-4.1 Mini, GPT-4o, GPT-5, GPT-5 Chat Latest, o1, o3, o4 Mini, o3 Mini - Anthropic: Claude Opus 4.1, Claude Sonnet 4.0, Claude 3.7 Sonnet, - Claude 3.5 Haiku, Claude 3.0 Haiku -"#; - println!("{}", style(help_text).dim()); + let width = self.get_terminal_width(); + + // Help header + println!("{}", style("┌".to_string() + &"─".repeat(width - 2) + "┐").white()); + self.print_centered_text("GPT CLI Help", "cyan"); + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").white()); + + // Commands section + self.print_help_section("Commands", &[ + ("/help", "Show this help message"), + ("/exit", "Exit the CLI"), + ("/model", "Interactive model switcher"), + ("/new ", "Create a new session"), + ("/switch", "Interactive session manager (switch/delete)"), + ("/clear", "Clear current conversation"), + ("/history [user|assistant] [number]", "View conversation history"), + ("/export [format]", "Export conversation (markdown, json, txt)"), + ("/save ", "Save current session with a new name"), + ("/tools", "Interactive tool and feature manager"), + ]); + + // Input features section + self.print_help_section("Input Features", &[ + ("Multi-line input", "End your message with '\\' to enter multi-line mode"), + ("Keyboard shortcuts", "Ctrl+C: Cancel, Ctrl+D: Exit, Arrow keys: History"), + ("Syntax highlighting", "Code blocks are automatically highlighted"), + ("Pagination", "Long responses are paginated (press Enter or 'q')"), + ]); + + // Environment variables section + self.print_help_section("Environment Variables", &[ + ("OPENAI_API_KEY", "Required for OpenAI models"), + ("ANTHROPIC_API_KEY", "Required for Anthropic models"), + ("OPENAI_BASE_URL", "Optional custom base URL for OpenAI"), + ("DEFAULT_MODEL", "Default model if not specified"), + ]); + + // Footer + println!("{}", style("└".to_string() + &"─".repeat(width - 2) + "┘").white()); + println!(); + } + + fn print_help_section(&self, title: &str, items: &[(&str, &str)]) { + // Section title + self.print_help_line(&format!("📚 {}", title), ""); + + // Section items + for (command, description) in items { + self.print_help_line( + &format!(" {}", style(command).blue().bold()), + description + ); + } + + // Separator + let width = self.get_terminal_width(); + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").white()); + } + + fn print_help_line(&self, left: &str, right: &str) { + let width = self.get_terminal_width(); + let left_clean = self.strip_ansi(left); + + if right.is_empty() { + let padding = width - left_clean.len() - 4; + println!("│ {}{} │", left, " ".repeat(padding)); + } else { + let max_desc_width = width - left_clean.len() - 8; // Account for spacing and borders + if right.len() <= max_desc_width { + let padding = width - left_clean.len() - right.len() - 4; + println!("│ {}{}{} │", left, " ".repeat(padding.saturating_sub(2)), style(right).dim()); + } else { + // Split long descriptions across multiple lines + let words: Vec<&str> = right.split_whitespace().collect(); + let mut current_line = String::new(); + + for word in words { + if current_line.len() + word.len() + 1 <= max_desc_width { + if !current_line.is_empty() { + current_line.push(' '); + } + current_line.push_str(word); + } else { + if !current_line.is_empty() { + let padding = width - left_clean.len() - current_line.len() - 4; + println!("│ {}{}{} │", left, " ".repeat(padding.saturating_sub(2)), style(¤t_line).dim()); + current_line.clear(); + } + current_line.push_str(word); + } + } + + if !current_line.is_empty() { + let padding = width - left_clean.len() - current_line.len() - 4; + println!("│ {}{}{} │", left, " ".repeat(padding.saturating_sub(2)), style(¤t_line).dim()); + } + } + } + } + + fn print_centered_text(&self, text: &str, color: &str) { + let width = self.get_terminal_width(); + let padding = (width - text.len() - 2) / 2; + let styled_text = match color { + "cyan" => style(text).cyan().bold(), + "magenta" => style(text).magenta().bold(), + "blue" => style(text).blue().bold(), + _ => style(text).white().bold(), + }; + println!( + "│{}{}{}│", + " ".repeat(padding), + styled_text, + " ".repeat(width - text.len() - padding - 2) + ); } pub fn show_spinner(&self, message: &str) -> SpinnerHandle { - print!("{} {}... ", style("⏳").yellow(), message); - io::stdout().flush().ok(); - SpinnerHandle::new() + SpinnerHandle::new(message.to_string()) } pub fn print_conversation_history(&self, messages: &[(usize, &crate::core::Message)]) { - println!("{}", style("📜 Conversation History").bold().cyan()); - println!("{}", style("─".repeat(50)).dim()); + let width = self.get_terminal_width(); - let mut content = String::new(); + // History header + println!("{}", style("┌".to_string() + &"─".repeat(width - 2) + "┐").white()); + self.print_centered_text(&format!("📜 Conversation History ({} messages)", messages.len()), "cyan"); + println!("{}", style("└".to_string() + &"─".repeat(width - 2) + "┘").white()); + println!(); - for (original_index, message) in messages { + for (index, (original_index, message)) in messages.iter().enumerate() { let role_icon = match message.role.as_str() { "user" => "👤", "assistant" => "🤖", @@ -248,20 +552,52 @@ Supported Models: _ => &message.role, }; - content.push_str(&format!( - "\n{} {} {} [Message #{}]\n{}\n", - style("•").dim(), - role_icon, - style(role_name).bold(), - original_index, - style("─".repeat(40)).dim() - )); + let color = match message.role.as_str() { + "user" => "cyan", + "assistant" => "magenta", + _ => "white", + }; - content.push_str(&message.content); - content.push_str("\n\n"); + // Message header + let border_style = match color { + "cyan" => style("┌".to_string() + &"─".repeat(width - 2) + "┐").cyan(), + "magenta" => style("┌".to_string() + &"─".repeat(width - 2) + "┐").magenta(), + _ => style("┌".to_string() + &"─".repeat(width - 2) + "┐").white(), + }; + println!("{}", border_style); + + let header = format!("{} {} [Message #{}]", role_icon, role_name, original_index); + let padding = width - self.strip_ansi(&header).len() - 4; + let header_styled = match color { + "cyan" => style(&header).cyan().bold(), + "magenta" => style(&header).magenta().bold(), + _ => style(&header).white().bold(), + }; + println!("│ {} {}│", header_styled, " ".repeat(padding)); + + let separator_style = match color { + "cyan" => style("├".to_string() + &"─".repeat(width - 2) + "┤").cyan(), + "magenta" => style("├".to_string() + &"─".repeat(width - 2) + "┤").magenta(), + _ => style("├".to_string() + &"─".repeat(width - 2) + "┤").white(), + }; + println!("{}", separator_style); + + // Message content + self.print_content_in_box(&message.content, color); + + // Message footer + let footer_style = match color { + "cyan" => style("└".to_string() + &"─".repeat(width - 2) + "┘").cyan(), + "magenta" => style("└".to_string() + &"─".repeat(width - 2) + "┘").magenta(), + _ => style("└".to_string() + &"─".repeat(width - 2) + "┘").white(), + }; + println!("{}", footer_style); + + // Add spacing between messages except for the last one + if index < messages.len() - 1 { + println!(); + } } - - self.print_formatted_content_with_pagination(&content); } } @@ -273,22 +609,540 @@ impl Default for Display { pub struct SpinnerHandle { start_time: std::time::Instant, + message: String, } impl SpinnerHandle { - fn new() -> Self { + fn new(message: String) -> Self { + // Show initial spinner state + print!("⏳ {}...", message); + io::stdout().flush().ok(); + Self { start_time: std::time::Instant::now(), + message, } } pub fn finish(self, message: &str) { let elapsed = self.start_time.elapsed(); - println!("{} ({:.2}s)", style(message).green(), elapsed.as_secs_f32()); + // Clear the spinner line and print result + print!("\r\x1b[K"); + println!( + "{} {} {}", + style("✓").green().bold(), + style(message).green(), + style(format!("({:.2}s)", elapsed.as_secs_f32())).dim() + ); } pub fn finish_with_error(self, message: &str) { let elapsed = self.start_time.elapsed(); - println!("{} ({:.2}s)", style(message).red(), elapsed.as_secs_f32()); + // Clear the spinner line and print error + print!("\r\x1b[K"); + println!( + "{} {} {}", + style("✗").red().bold(), + style(message).red(), + style(format!("({:.2}s)", elapsed.as_secs_f32())).dim() + ); + } +} + +// Enhanced error display and utility methods +impl Display { + pub fn print_error_with_context(&self, error: &str, context: Option<&str>, suggestions: &[&str]) { + let width = self.get_terminal_width(); + + // Error header + println!("{}", style("┌".to_string() + &"─".repeat(width - 2) + "┐").red()); + self.print_error_line("✗ Error", ""); + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").red()); + + // Error message + self.print_error_line("", error); + + // Context if provided + if let Some(ctx) = context { + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").red()); + self.print_error_line("📄 Context:", ctx); + } + + // Suggestions if provided + if !suggestions.is_empty() { + println!("{}", style("├".to_string() + &"─".repeat(width - 2) + "┤").red()); + self.print_error_line("💡 Suggestions:", ""); + for (i, suggestion) in suggestions.iter().enumerate() { + self.print_error_line(&format!(" {}.", i + 1), suggestion); + } + } + + // Error footer + println!("{}", style("└".to_string() + &"─".repeat(width - 2) + "┘").red()); + println!(); + } + + fn print_error_line(&self, left: &str, right: &str) { + let width = self.get_terminal_width(); + + if right.is_empty() { + let padding = width - left.len() - 4; + println!("│ {}{} │", style(left).red().bold(), " ".repeat(padding)); + } else { + let left_len = left.len(); + let right_len = right.len(); + let available_width = width - 4; + + if left_len + right_len + 1 <= available_width { + let padding = available_width - left_len - right_len; + println!("│ {}{}{} │", style(left).red().bold(), " ".repeat(padding), right); + } else { + // Handle long text by wrapping + self.print_error_line(left, ""); + let words: Vec<&str> = right.split_whitespace().collect(); + let mut current_line = String::new(); + let max_width = available_width - 2; // Account for indentation + + for word in words { + if current_line.len() + word.len() + 1 <= max_width { + if !current_line.is_empty() { + current_line.push(' '); + } + current_line.push_str(word); + } else { + if !current_line.is_empty() { + self.print_error_line(" ", ¤t_line); + current_line.clear(); + } + current_line.push_str(word); + } + } + + if !current_line.is_empty() { + self.print_error_line(" ", ¤t_line); + } + } + } + } + + pub fn print_section_header(&self, title: &str, icon: &str) { + let width = self.get_terminal_width(); + let header = format!("{} {}", icon, title); + + println!(); + println!("{}", style("┌".to_string() + &"─".repeat(width - 2) + "┐").white()); + + let padding = (width - self.strip_ansi(&header).len() - 2) / 2; + println!( + "│{}{}{}│", + " ".repeat(padding), + style(&header).cyan().bold(), + " ".repeat(width - self.strip_ansi(&header).len() - padding - 2) + ); + + println!("{}", style("└".to_string() + &"─".repeat(width - 2) + "┘").white()); + println!(); + } + + pub fn print_progress_bar(&self, current: usize, total: usize, label: &str) { + let width = self.get_terminal_width(); + let bar_width = width.saturating_sub(20); // Reserve space for text and borders + let progress = if total > 0 { current as f32 / total as f32 } else { 0.0 }; + let filled = (bar_width as f32 * progress) as usize; + let empty = bar_width - filled; + + let bar = format!( + "[{}{}] {}/{} {}", + "█".repeat(filled), + "░".repeat(empty), + current, + total, + label + ); + + print!("\r{}", bar); + io::stdout().flush().ok(); + + if current >= total { + println!(); // New line when complete + } + } + + pub fn clear_current_line(&self) { + print!("\r\x1b[K"); + io::stdout().flush().ok(); + } + + pub fn print_separator(&self, style_char: &str) { + let width = self.get_terminal_width(); + println!("{}", style(style_char.repeat(width)).dim()); + } + + pub fn print_feature_status(&self, features: &[(&str, bool, Option<&str>)]) { + self.print_section_header("Feature Status", "⚙️"); + + for (name, enabled, description) in features { + let icon = if *enabled { "✓" } else { "✗" }; + let color = if *enabled { "green" } else { "red" }; + let status = if *enabled { "enabled" } else { "disabled" }; + + let icon_styled = match color { + "green" => style(icon).green().bold(), + "red" => style(icon).red().bold(), + _ => style(icon).white().bold(), + }; + + let status_styled = match color { + "green" => style(status).green(), + "red" => style(status).red(), + _ => style(status).white(), + }; + + if let Some(desc) = description { + println!( + "{} {} {} - {}", + icon_styled, + style(name).cyan().bold(), + status_styled, + style(desc).dim() + ); + } else { + println!( + "{} {} {}", + icon_styled, + style(name).cyan().bold(), + status_styled + ); + } + } + + println!(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to create a display instance for testing + fn create_test_display() -> Display { + Display::new() + } + + #[test] + fn test_display_new() { + let display = Display::new(); + assert!(display.term.size().1 > 0); // Should have some width + } + + #[test] + fn test_get_terminal_width() { + let display = create_test_display(); + let width = display.get_terminal_width(); + assert!(width > 0); + assert!(width <= 400); // Reasonable upper bound + } + + #[test] + fn test_strip_ansi() { + let display = create_test_display(); + + // Test with ANSI codes + let text_with_ansi = "\x1b[31mRed text\x1b[0m"; + let stripped = display.strip_ansi(text_with_ansi); + assert_eq!(stripped, "Red text"); + + // Test with no ANSI codes + let plain_text = "Plain text"; + let stripped_plain = display.strip_ansi(plain_text); + assert_eq!(stripped_plain, "Plain text"); + + // Test with multiple ANSI sequences + let complex_ansi = "\x1b[1m\x1b[31mBold Red\x1b[0m\x1b[32m Green\x1b[0m"; + let stripped_complex = display.strip_ansi(complex_ansi); + assert_eq!(stripped_complex, "Bold Red Green"); + } + + #[test] + fn test_print_message_with_icon() { + let display = create_test_display(); + + // Test doesn't panic and completes + display.print_message_with_icon("✓", "Test message", "green", false); + display.print_message_with_icon("✗", "Error message", "red", true); + display.print_message_with_icon("ℹ", "Info message", "blue", false); + display.print_message_with_icon("⚠", "Warning message", "yellow", false); + display.print_message_with_icon("🔧", "Custom message", "cyan", false); + display.print_message_with_icon("📝", "Unknown color", "unknown", false); + } + + #[test] + fn test_spinner_handle() { + let message = "Testing spinner"; + let spinner = SpinnerHandle::new(message.to_string()); + + // Test finish + spinner.finish("Operation completed"); + + // Test finish with error + let spinner2 = SpinnerHandle::new("Another test".to_string()); + spinner2.finish_with_error("Operation failed"); + } + + #[test] + fn test_print_status_bar() { + let display = create_test_display(); + let features = vec![ + ("Web Search", true), + ("Reasoning", false), + ("Extended Thinking", true), + ]; + + // Should not panic + display.print_status_bar("GPT-4", "OpenAI", "default", &features); + display.print_status_bar("Claude-3", "Anthropic", "test-session", &[]); + } + + #[test] + fn test_print_formatted_content_basic() { + let display = create_test_display(); + + // Test plain text + let plain_text = "This is plain text without any formatting."; + display.print_formatted_content(plain_text); + + // Test text with inline code + let text_with_code = "This has `inline code` in it."; + display.print_formatted_content(text_with_code); + + // Test text with code block + let text_with_block = "Here's a code block:\n```rust\nfn main() {\n println!(\"Hello\");\n}\n```\nEnd of block."; + display.print_formatted_content(text_with_block); + } + + #[test] + fn test_print_code_block() { + let display = create_test_display(); + + let rust_code = "fn main() {\n println!(\"Hello, world!\");\n}"; + display.print_code_block(rust_code, "rust"); + + let python_code = "def hello():\n print(\"Hello, world!\")"; + display.print_code_block(python_code, "python"); + + let plain_code = "echo \"Hello\""; + display.print_code_block(plain_code, ""); + } + + #[test] + fn test_print_text_with_inline_code() { + let display = create_test_display(); + + let text = "Use `cargo build` to build the project and `cargo test` to run tests."; + display.print_text_with_inline_code(text); + + let text_no_code = "This text has no inline code."; + display.print_text_with_inline_code(text_no_code); + + let text_multiple = "Commands: `ls`, `cd`, `mkdir`, and `rm`."; + display.print_text_with_inline_code(text_multiple); + } + + #[test] + fn test_print_content_in_box() { + let display = create_test_display(); + + let short_content = "Short content"; + display.print_content_in_box(short_content, "cyan"); + + let multi_line_content = "Line 1\nLine 2\nLine 3"; + display.print_content_in_box(multi_line_content, "magenta"); + + // Test with very long line + let long_content = "A".repeat(200); + display.print_content_in_box(&long_content, "white"); + } + + #[test] + fn test_print_error_with_context() { + let display = create_test_display(); + + let error = "Configuration file not found"; + let context = Some("The application was looking for config.toml in the current directory"); + let suggestions = vec![ + "Create a config.toml file", + "Check the current directory", + "Run with --help for more options" + ]; + + display.print_error_with_context(error, context, &suggestions); + display.print_error_with_context("Simple error", None, &[]); + } + + #[test] + fn test_print_section_header() { + let display = create_test_display(); + + display.print_section_header("Configuration", "⚙️"); + display.print_section_header("Test Results", "📊"); + display.print_section_header("Long Section Name That Might Need Wrapping", "🔧"); + } + + #[test] + fn test_print_progress_bar() { + let display = create_test_display(); + + display.print_progress_bar(0, 10, "Starting"); + display.print_progress_bar(5, 10, "Halfway"); + display.print_progress_bar(10, 10, "Complete"); + display.print_progress_bar(3, 0, "Edge case"); // Division by zero protection + } + + #[test] + fn test_print_feature_status() { + let display = create_test_display(); + + let features = vec![ + ("API Connection", true, Some("Connected to OpenAI")), + ("Web Search", false, Some("Disabled in config")), + ("Syntax Highlighting", true, None), + ("Auto-save", false, None), + ]; + + display.print_feature_status(&features); + display.print_feature_status(&[]); // Empty features + } + + #[test] + fn test_print_conversation_history() { + let display = create_test_display(); + + // Create test messages + let message1 = crate::core::Message { + role: "user".to_string(), + content: "Hello, how are you?".to_string(), + }; + let message2 = crate::core::Message { + role: "assistant".to_string(), + content: "I'm doing well, thank you! How can I help you today?".to_string(), + }; + let message3 = crate::core::Message { + role: "user".to_string(), + content: "Can you help me with some code?".to_string(), + }; + + let messages = vec![ + (1, &message1), + (2, &message2), + (3, &message3), + ]; + + display.print_conversation_history(&messages); + display.print_conversation_history(&[]); // Empty history + } + + #[test] + fn test_display_methods_dont_panic() { + let display = create_test_display(); + + // Test all print methods don't panic with various inputs + display.print_info("Info message"); + display.print_warning("Warning message"); + display.print_error("Error message"); + display.print_command_result("Command completed successfully"); + display.print_header(); + display.print_help(); + display.clear_current_line(); + display.print_separator("─"); + + // Test with empty/edge case inputs + display.print_info(""); + display.print_error(""); + display.print_formatted_content(""); + display.print_content_in_box("", "blue"); + } + + #[test] + fn test_print_status_line() { + let display = create_test_display(); + + // Test with both left and right content + display.print_status_line("Left content", "Right content"); + + // Test with only left content + display.print_status_line("Only left", ""); + + // Test with very long content + let long_left = "Very ".repeat(50); + let long_right = "Long ".repeat(50); + display.print_status_line(&long_left, &long_right); + } + + #[test] + fn test_print_centered_text() { + let display = create_test_display(); + + display.print_centered_text("Centered Text", "cyan"); + display.print_centered_text("Short", "blue"); + display.print_centered_text("A very long text that might exceed normal width", "magenta"); + display.print_centered_text("", "white"); + } + + #[test] + fn test_print_help_methods() { + let display = create_test_display(); + + let items = vec![ + ("/help", "Show help message"), + ("/exit", "Exit the application"), + ("/very-long-command-name", "This is a very long description that should test the word wrapping functionality of the help system"), + ]; + + display.print_help_section("Test Commands", &items); + display.print_help_section("Empty Section", &[]); + + display.print_help_line("Command", "Description"); + display.print_help_line("Very long command name", ""); + display.print_help_line("", "Description only"); + } + + #[test] + fn test_regex_compilation_safety() { + let display = create_test_display(); + + // Test that ANSI stripping handles regex compilation failures gracefully + // This is tested indirectly through strip_ansi method + let test_text = "Normal text without ANSI codes"; + let result = display.strip_ansi(test_text); + assert_eq!(result, test_text); + } + + #[test] + fn test_box_drawing_characters() { + let display = create_test_display(); + + // Test that box drawing doesn't cause issues with different terminal widths + // This tests the Unicode box-drawing implementation + display.print_section_header("Unicode Test ♦♣♠♥", "🎯"); + + let unicode_content = "Content with Unicode: ♦♣♠♥ αβγδ ñáéíóú"; + display.print_content_in_box(unicode_content, "green"); + } + + #[test] + fn test_edge_cases() { + let display = create_test_display(); + + // Test with null bytes and control characters + let problematic_content = "Content\0with\tnull\nand\rcontrol\x1b[31mchars"; + display.print_formatted_content(problematic_content); + + // Test with extremely long single word (no spaces) + let long_word = "supercalifragilisticexpialidocious".repeat(10); + display.print_content_in_box(&long_word, "yellow"); + + // Test with mixed line endings + let mixed_endings = "Line 1\nLine 2\r\nLine 3\rLine 4"; + display.print_content_in_box(mixed_endings, "blue"); } } \ No newline at end of file