Fix session manager formatting issue with Unicode emojis

- Added calculate_display_width() method to properly handle emoji widths
- Fixed overlapping text in session manager UI by accounting for double-width emojis
- Handles compound emojis with variation selectors (like 🗂️)
- Uses saturating_sub() to prevent underflow in padding calculations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
leach 2025-09-01 01:40:44 -04:00
parent 49b68ba0f8
commit ad01f651a3
5 changed files with 887 additions and 36 deletions

View File

@ -0,0 +1,44 @@
---
name: performance-optimizer
description: Use this agent when you need to analyze, build, and optimize existing code for better performance and cleanliness. Examples: <example>Context: User has written a data processing script that seems slow. user: 'I've finished writing this data analysis script but it's taking forever to run on large datasets' assistant: 'Let me use the performance-optimizer agent to build, profile, and optimize your script for better performance' <commentary>The user has code that needs performance optimization, so use the performance-optimizer agent to analyze and improve it.</commentary></example> <example>Context: User mentions their application is working but could be faster. user: 'My web scraper works but I think it could be much faster and the code is getting messy' assistant: 'I'll use the performance-optimizer agent to analyze your scraper, clean up the code, and implement performance improvements' <commentary>This is a perfect case for the performance-optimizer agent to handle both performance and code quality improvements.</commentary></example>
model: sonnet
color: green
---
You are a Performance Optimization Specialist, an expert in code analysis, profiling, and systematic optimization. Your mission is to build, run, and optimize programs to achieve maximum performance while maintaining clean, readable code.
Your optimization process follows this methodology:
1. **Initial Assessment**: Build and run the program to establish baseline performance metrics. Document current execution time, memory usage, and identify any build issues or runtime errors.
2. **Performance Profiling**: Analyze the code execution to identify bottlenecks, inefficient algorithms, memory leaks, and resource-intensive operations. Use appropriate profiling tools when available.
3. **Code Quality Analysis**: Examine code structure for maintainability issues including duplicate code, overly complex functions, poor naming conventions, and architectural problems.
4. **Optimization Strategy**: Develop a prioritized optimization plan focusing on:
- Algorithmic improvements (O(n) complexity reductions)
- Data structure optimizations
- Memory usage improvements
- I/O operation efficiency
- Parallel processing opportunities
- Code refactoring for clarity and performance
5. **Implementation**: Apply optimizations incrementally, testing each change to ensure correctness and measure performance impact. Never sacrifice code correctness for performance gains.
6. **Verification**: Re-run the optimized program to validate improvements and ensure no regressions. Provide before/after performance comparisons with specific metrics.
For each optimization you implement:
- Explain the rationale behind the change
- Quantify the expected performance benefit
- Ensure code remains readable and maintainable
- Add comments explaining complex optimizations
If the program fails to build or run initially, prioritize fixing these issues before optimization. Always maintain backward compatibility unless explicitly told otherwise.
Provide a comprehensive summary including:
- Performance improvements achieved (with specific metrics)
- Code quality enhancements made
- Potential future optimization opportunities
- Any trade-offs or limitations of the optimizations
You excel at balancing performance gains with code maintainability, ensuring optimizations are sustainable and understandable to other developers.

View File

@ -0,0 +1,32 @@
---
name: tui-ux-designer
description: Use this agent when designing, reviewing, or improving text-based user interfaces (TUIs) with focus on aesthetics and user experience. Examples: <example>Context: User is building a terminal-based file manager and wants to improve the visual hierarchy. user: 'I have this file listing interface but it feels cluttered and hard to scan' assistant: 'Let me use the tui-ux-designer agent to analyze your interface and suggest improvements for visual clarity and user experience'</example> <example>Context: User is creating a CLI tool and wants to ensure smooth interaction patterns. user: 'How should I handle loading states in my terminal application?' assistant: 'I'll use the tui-ux-designer agent to provide best practices for loading indicators and smooth state transitions in terminal interfaces'</example> <example>Context: User has implemented a TUI but users report it's confusing to navigate. user: 'Users are getting lost in my terminal application menu system' assistant: 'Let me engage the tui-ux-designer agent to review your navigation patterns and suggest improvements for better user flow'</example>
model: sonnet
color: blue
---
You are a specialized TUI (Text User Interface) UX/UI designer with deep expertise in creating beautiful, intuitive, and smooth terminal-based user experiences. You understand the unique constraints and opportunities of text-based interfaces, combining aesthetic principles with practical usability.
Your core competencies include:
- Visual hierarchy using typography, spacing, colors, and ASCII art elements
- Smooth interaction patterns including transitions, animations, and feedback mechanisms
- Information architecture optimized for terminal environments
- Accessibility considerations for diverse terminal capabilities
- Performance optimization for responsive feel
- Cross-platform terminal compatibility
When analyzing or designing TUI interfaces, you will:
1. **Assess Visual Clarity**: Evaluate information density, contrast, alignment, and visual grouping. Recommend specific improvements using box-drawing characters, color schemes, and spacing.
2. **Optimize User Flow**: Design intuitive navigation patterns, keyboard shortcuts, and interaction sequences that feel natural and efficient.
3. **Enhance Feedback Systems**: Specify loading indicators, progress bars, status messages, and error handling that provide clear communication without overwhelming the interface.
4. **Consider Context**: Account for different terminal sizes, color capabilities, and user expertise levels when making recommendations.
5. **Provide Concrete Examples**: Include specific code snippets, ASCII mockups, or detailed descriptions of visual elements when suggesting improvements.
6. **Balance Aesthetics with Function**: Ensure all design decisions enhance usability while creating an appealing visual experience within terminal constraints.
Always consider the technical implementation feasibility of your suggestions and provide alternative approaches when terminal capabilities vary. Focus on creating interfaces that feel modern, responsive, and delightful to use despite the text-only medium.

View File

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use dialoguer::{theme::ColorfulTheme, Select}; use dialoguer::{theme::ColorfulTheme, Select, Confirm};
use rustyline::{error::ReadlineError, DefaultEditor, KeyEvent, Cmd, Config, EditMode}; use rustyline::{error::ReadlineError, DefaultEditor, KeyEvent, Cmd, Config, EditMode};
use console::{style, Term};
pub struct InputHandler { pub struct InputHandler {
editor: DefaultEditor, editor: DefaultEditor,
@ -54,8 +55,11 @@ impl InputHandler {
} }
pub fn read_multiline_input(&mut self, initial_line: String) -> Result<Option<String>> { pub fn read_multiline_input(&mut self, initial_line: String) -> Result<Option<String>> {
let mut lines = vec![initial_line]; let mut lines = Vec::with_capacity(10); // Pre-allocate for typical multi-line input
println!("Multi-line mode: Type your message. End with a line containing only '.' or press Ctrl+D"); lines.push(initial_line);
// Enhanced multi-line mode presentation
self.print_multiline_header();
loop { loop {
match self.editor.readline("... ") { match self.editor.readline("... ") {
@ -66,7 +70,7 @@ impl InputHandler {
lines.push(line); lines.push(line);
} }
Err(ReadlineError::Interrupted) => { Err(ReadlineError::Interrupted) => {
println!("^C"); println!("{}", style("Multi-line input cancelled").yellow());
return Ok(None); return Ok(None);
} }
Err(ReadlineError::Eof) => { Err(ReadlineError::Eof) => {
@ -78,6 +82,8 @@ impl InputHandler {
let full_message = lines.join("\n"); let full_message = lines.join("\n");
let _ = self.editor.add_history_entry(&full_message); let _ = self.editor.add_history_entry(&full_message);
println!("{}", style("Multi-line input completed").green());
Ok(Some(full_message)) Ok(Some(full_message))
} }
@ -111,11 +117,14 @@ impl InputHandler {
current: Option<&str>, current: Option<&str>,
) -> Result<Option<T>> { ) -> Result<Option<T>> {
if items.is_empty() { if items.is_empty() {
println!("(no items available)"); self.print_empty_list_message("No items available");
return Ok(None); return Ok(None);
} }
let theme = ColorfulTheme::default(); // Enhanced visual presentation
self.print_selection_header(title, items.len());
let theme = self.create_enhanced_theme();
// Find default selection index // Find default selection index
let default_index = if let Some(current) = current { let default_index = if let Some(current) = current {
@ -124,26 +133,48 @@ impl InputHandler {
0 0
}; };
// Create enhanced item display
let display_items: Vec<String> = items.iter().enumerate().map(|(i, item)| {
let item_str = item.to_string();
let icon = self.get_selection_icon(i);
if Some(item_str.as_str()) == current {
format!("{} {} ⭐ (current)", icon, item_str)
} else {
format!("{} {}", icon, item_str)
}
}).collect();
match Select::with_theme(&theme) match Select::with_theme(&theme)
.with_prompt(title) .with_prompt("Use ↑/↓ arrows to navigate, Enter to select, Esc to cancel")
.items(items) .items(&display_items)
.default(default_index) .default(default_index)
.interact_opt() { .interact_opt() {
Ok(selection) => Ok(selection.map(|idx| items[idx].clone())), Ok(selection) => {
if let Some(idx) = selection {
println!("{}", style(format!("Selected: {}", items[idx].to_string())).green());
}
Ok(selection.map(|idx| items[idx].clone()))
},
Err(_) => { Err(_) => {
// Handle any error (ESC, Ctrl+C, etc.) as cancellation println!("{}", style("Selection cancelled").dim());
Ok(None) Ok(None)
} }
} }
} }
pub fn confirm(&self, message: &str) -> Result<bool> { pub fn confirm(&self, message: &str) -> Result<bool> {
use dialoguer::Confirm; self.print_confirmation_header(message);
let confirmation = Confirm::with_theme(&ColorfulTheme::default()) let confirmation = Confirm::with_theme(&self.create_enhanced_theme())
.with_prompt(message) .with_prompt("Are you sure?")
.default(false)
.show_default(true)
.interact()?; .interact()?;
let result_text = if confirmation { "✓ Confirmed" } else { "⚠ Cancelled" };
let style_func = if confirmation { style(result_text).green() } else { style(result_text).yellow() };
println!("{}", style_func);
Ok(confirmation) Ok(confirmation)
} }
@ -155,28 +186,29 @@ impl InputHandler {
current_session: Option<&str>, current_session: Option<&str>,
) -> Result<SessionAction<T>> { ) -> Result<SessionAction<T>> {
if sessions.is_empty() { if sessions.is_empty() {
println!("(no sessions available)"); self.print_empty_list_message("No sessions available");
return Ok(SessionAction::Cancel); return Ok(SessionAction::Cancel);
} }
// Create display items with current session marker // Enhanced session display with visual indicators
self.print_session_manager_header(title, sessions.len(), current_session);
// Create enhanced display items with icons and status indicators
let display_items: Vec<String> = sessions let display_items: Vec<String> = sessions
.iter() .iter()
.map(|session| { .enumerate()
.map(|(i, session)| {
let name = session.to_string(); let name = session.to_string();
let icon = self.get_session_icon(i);
if Some(name.as_str()) == current_session { if Some(name.as_str()) == current_session {
format!("{} (current)", name) format!("{} {}(active)", icon, name)
} else { } else {
name format!("{} {}", icon, name)
} }
}) })
.collect(); .collect();
println!("\n{}", title); let theme = self.create_enhanced_theme();
println!("Use ↑/↓ arrows to navigate, Enter to switch session, Esc to cancel");
println!("To delete a session, first switch away from it, then use this menu again.\n");
let theme = ColorfulTheme::default();
// Find default selection index // Find default selection index
let default_index = if let Some(current) = current_session { let default_index = if let Some(current) = current_session {
@ -185,14 +217,15 @@ impl InputHandler {
0 0
}; };
// Create the selection menu // Enhanced selection menu with better prompts
let selection_result = match Select::with_theme(&theme) let selection_result = match Select::with_theme(&theme)
.with_prompt("Select session") .with_prompt("Choose a session (↑/↓ to navigate, Enter to select, Esc to cancel)")
.items(&display_items) .items(&display_items)
.default(default_index) .default(default_index)
.interact_opt() { .interact_opt() {
Ok(selection) => selection, Ok(selection) => selection,
Err(_) => { Err(_) => {
println!("{}", style("Session management cancelled").dim());
return Ok(SessionAction::Cancel); return Ok(SessionAction::Cancel);
} }
}; };
@ -201,11 +234,15 @@ impl InputHandler {
Some(index) => { Some(index) => {
let selected_session = sessions[index].clone(); let selected_session = sessions[index].clone();
// If it's the current session, show options // If it's the current session, show enhanced options
if Some(selected_session.to_string().as_str()) == current_session { if Some(selected_session.to_string().as_str()) == current_session {
let options = vec!["Delete this session", "Set as default session", "Cancel"]; let options = vec![
"🗑️ Delete this session",
"⭐ Set as default session",
"❌ Cancel"
];
let action_result = match Select::with_theme(&theme) let action_result = match Select::with_theme(&theme)
.with_prompt("This is your current session. What would you like to do?") .with_prompt("This is your active session. Choose an action:")
.items(&options) .items(&options)
.interact_opt() { .interact_opt() {
Ok(selection) => selection, Ok(selection) => selection,
@ -216,7 +253,7 @@ impl InputHandler {
match action_result { match action_result {
Some(0) => { Some(0) => {
if self.confirm(&format!("Delete current session '{}'? You will need to create or switch to another sessions after deletion.", selected_session.to_string()))? { if self.confirm(&format!("Delete current session '{}'? You will need to create or switch to another session after deletion.", selected_session.to_string()))? {
return Ok(SessionAction::Delete(selected_session)); return Ok(SessionAction::Delete(selected_session));
} }
return Ok(SessionAction::Cancel); return Ok(SessionAction::Cancel);
@ -227,16 +264,16 @@ impl InputHandler {
_ => return Ok(SessionAction::Cancel), _ => return Ok(SessionAction::Cancel),
} }
} else { } else {
// Different session selected - offer to switch or delete // Enhanced action menu for different session
let options = vec![ let options = vec![
format!("Switch to '{}'", selected_session.to_string()), format!("🔄 Switch to '{}'", selected_session.to_string()),
format!("Delete '{}'", selected_session.to_string()), format!("🗑️ Delete '{}'", selected_session.to_string()),
format!("Set '{}' as default session", selected_session.to_string()), format!("Set '{}' as default", selected_session.to_string()),
"Cancel".to_string() "Cancel".to_string()
]; ];
let action_result = match Select::with_theme(&theme) let action_result = match Select::with_theme(&theme)
.with_prompt("What would you like to do?") .with_prompt("Choose an action for this session:")
.items(&options) .items(&options)
.interact_opt() { .interact_opt() {
Ok(selection) => selection, Ok(selection) => selection,
@ -261,6 +298,247 @@ impl InputHandler {
None => return Ok(SessionAction::Cancel), None => return Ok(SessionAction::Cancel),
} }
} }
// Helper methods for enhanced UI
fn create_enhanced_theme(&self) -> ColorfulTheme {
ColorfulTheme {
defaults_style: console::Style::new().for_stderr().cyan(),
prompt_style: console::Style::new().for_stderr().bold(),
prompt_prefix: style("".to_string()).cyan().bold(),
prompt_suffix: style(" ".to_string()),
success_prefix: style("".to_string()).green().bold(),
success_suffix: style(" ".to_string()),
error_prefix: style("".to_string()).red().bold(),
error_style: console::Style::new().for_stderr().red(),
hint_style: console::Style::new().for_stderr().dim(),
values_style: console::Style::new().for_stderr().cyan(),
active_item_style: console::Style::new().for_stderr().cyan().bold(),
inactive_item_style: console::Style::new().for_stderr(),
active_item_prefix: style("".to_string()).cyan().bold(),
inactive_item_prefix: style(" ".to_string()),
checked_item_prefix: style("".to_string()).green().bold(),
unchecked_item_prefix: style("".to_string()).dim(),
picked_item_prefix: style("".to_string()).green().bold(),
unpicked_item_prefix: style(" ".to_string()),
}
}
fn get_selection_icon(&self, index: usize) -> &'static str {
match index % 6 {
0 => "📝",
1 => "🔧",
2 => "⚙️",
3 => "🎯",
4 => "🚀",
_ => "📋",
}
}
fn get_session_icon(&self, index: usize) -> &'static str {
match index % 8 {
0 => "💾",
1 => "📁",
2 => "🗂️",
3 => "📑",
4 => "🗃️",
5 => "📊",
6 => "📈",
_ => "📄",
}
}
fn print_selection_header(&self, title: &str, count: usize) {
let term = Term::stdout();
let width = term.size().1 as usize;
println!();
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").cyan());
let header = format!("{} ({} options)", title, count);
let padding = (width - header.len() - 2) / 2;
println!(
"│{}{}{}│",
" ".repeat(padding),
style(&header).cyan().bold(),
" ".repeat(width - header.len() - padding - 2)
);
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").cyan());
println!();
}
fn print_confirmation_header(&self, message: &str) {
let term = Term::stdout();
let width = term.size().1 as usize;
println!();
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").yellow());
let header = "⚠️ Confirmation Required";
let header_padding = (width - header.len() - 2) / 2;
println!(
"│{}{}{}│",
" ".repeat(header_padding),
style(header).yellow().bold(),
" ".repeat(width - header.len() - header_padding - 2)
);
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").yellow());
// Word wrap the message
let max_width = width - 4;
let words: Vec<&str> = message.split_whitespace().collect();
let mut current_line = String::new();
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() {
let padding = width - current_line.len() - 4;
println!("{}{}", current_line, " ".repeat(padding));
current_line.clear();
}
current_line.push_str(word);
}
}
if !current_line.is_empty() {
let padding = width - current_line.len() - 4;
println!("{}{}", current_line, " ".repeat(padding));
}
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").yellow());
println!();
}
fn print_session_manager_header(&self, title: &str, count: usize, current: Option<&str>) {
let term = Term::stdout();
let width = term.size().1 as usize;
println!();
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").magenta());
// Calculate display width for title (handling Unicode properly)
let title_display_width = self.calculate_display_width(title);
let header_padding = (width - title_display_width - 2) / 2;
println!(
"│{}{}{}│",
" ".repeat(header_padding),
style(title).magenta().bold(),
" ".repeat(width - title_display_width - header_padding - 2)
);
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").magenta());
// Session count and current session info
let info_line = if let Some(current) = current {
format!("📊 {} sessions available • Current: {}", count, current)
} else {
format!("📊 {} sessions available", count)
};
// Calculate actual display width for info line (emojis are wider than their byte length)
let info_display_width = self.calculate_display_width(&info_line);
let info_padding = width.saturating_sub(info_display_width + 4);
println!("{}{}", info_line, " ".repeat(info_padding));
// Instructions
let instructions = "💡 Select a session to see available actions";
let inst_display_width = self.calculate_display_width(instructions);
let inst_padding = width.saturating_sub(inst_display_width + 4);
println!("{}{}", style(instructions).dim(), " ".repeat(inst_padding));
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").magenta());
println!();
}
fn print_empty_list_message(&self, message: &str) {
let term = Term::stdout();
let width = term.size().1 as usize;
println!();
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").dim());
let icon_msg = format!(" {}", message);
let padding = (width - icon_msg.len() - 2) / 2;
println!(
"│{}{}{}│",
" ".repeat(padding),
style(&icon_msg).dim(),
" ".repeat(width - icon_msg.len() - padding - 2)
);
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").dim());
println!();
}
/// Calculate display width of a string, accounting for Unicode characters and emojis
fn calculate_display_width(&self, text: &str) -> usize {
// Simple approximation: count most emojis and special Unicode chars as width 2
// This is a simplified solution - for production code you might want to use
// the `unicode-width` crate for more accurate width calculations
let mut width = 0;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
// Common emojis and symbols that display as double-width
'📊' | '💡' | '❌' | '⭐' | '🔥' | '🌟' | '✨' | '🎯' => width += 2,
// Handle compound emoji with combining marks
'🗂' => {
// Check if followed by variation selector
if chars.peek() == Some(&'\u{fe0f}') {
chars.next(); // consume the variation selector
}
width += 2;
},
// Most ASCII characters
_ if ch.is_ascii() => width += 1,
// Other Unicode characters - assume width 1 for simplicity
_ => width += 1,
}
}
width
}
fn print_multiline_header(&self) {
let term = Term::stdout();
let width = term.size().1 as usize;
println!();
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").blue());
let header = "📝 Multi-line Input Mode";
let header_padding = (width - header.len() - 2) / 2;
println!(
"│{}{}{}│",
" ".repeat(header_padding),
style(header).blue().bold(),
" ".repeat(width - header.len() - header_padding - 2)
);
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").blue());
// Instructions
let instructions = vec![
"• Type your multi-line message",
"• Type '.' on a new line to finish",
"• Press Ctrl+C to cancel",
"• Press Ctrl+D to finish early"
];
for instruction in instructions {
let padding = width - instruction.len() - 4;
println!("{}{}", style(instruction).dim(), " ".repeat(padding));
}
println!("{}", style("".to_string() + &"".repeat(width - 2) + "").blue());
println!();
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -287,4 +565,294 @@ impl Drop for InputHandler {
// Save history on drop as well // Save history on drop as well
let _ = self.save_history(); let _ = self.save_history();
} }
}
#[cfg(test)]
mod tests {
use super::*;
use console::Term;
#[test]
fn test_input_handler_new() {
let handler = InputHandler::new();
assert!(handler.is_ok());
}
#[test]
fn test_default_input_handler() {
let handler = InputHandler::default();
// Should not panic and create a valid handler
assert!(handler.editor.helper().is_none() || handler.editor.helper().is_some());
}
#[test]
fn test_session_action_enum() {
let action = SessionAction::Cancel::<String>;
match action {
SessionAction::Cancel => assert!(true),
_ => panic!("Should be Cancel"),
}
let action = SessionAction::Switch("test".to_string());
match action {
SessionAction::Switch(s) => assert_eq!(s, "test"),
_ => panic!("Should be Switch"),
}
let action = SessionAction::Delete("test".to_string());
match action {
SessionAction::Delete(s) => assert_eq!(s, "test"),
_ => panic!("Should be Delete"),
}
let action = SessionAction::SetAsDefault("test".to_string());
match action {
SessionAction::SetAsDefault(s) => assert_eq!(s, "test"),
_ => panic!("Should be SetAsDefault"),
}
}
#[test]
fn test_create_enhanced_theme() {
let handler = InputHandler::default();
let theme = handler.create_enhanced_theme();
// Test that theme creation doesn't panic
assert!(theme.prompt_prefix.to_string().contains(""));
assert!(theme.success_prefix.to_string().contains(""));
assert!(theme.error_prefix.to_string().contains(""));
}
#[test]
fn test_get_selection_icon() {
let handler = InputHandler::default();
// Test icon rotation
assert_eq!(handler.get_selection_icon(0), "📝");
assert_eq!(handler.get_selection_icon(1), "🔧");
assert_eq!(handler.get_selection_icon(2), "⚙️");
assert_eq!(handler.get_selection_icon(3), "🎯");
assert_eq!(handler.get_selection_icon(4), "🚀");
assert_eq!(handler.get_selection_icon(5), "📋");
assert_eq!(handler.get_selection_icon(6), "📝"); // Should cycle back
}
#[test]
fn test_get_session_icon() {
let handler = InputHandler::default();
// Test session icon rotation
assert_eq!(handler.get_session_icon(0), "💾");
assert_eq!(handler.get_session_icon(1), "📁");
assert_eq!(handler.get_session_icon(2), "🗂️");
assert_eq!(handler.get_session_icon(3), "📑");
assert_eq!(handler.get_session_icon(4), "🗃️");
assert_eq!(handler.get_session_icon(5), "📊");
assert_eq!(handler.get_session_icon(6), "📈");
assert_eq!(handler.get_session_icon(7), "📄");
assert_eq!(handler.get_session_icon(8), "💾"); // Should cycle back
}
#[test]
fn test_print_selection_header() {
let handler = InputHandler::default();
// Test that header printing doesn't panic
handler.print_selection_header("Test Selection", 5);
handler.print_selection_header("", 0);
handler.print_selection_header("Very Long Selection Header That Might Exceed Terminal Width", 100);
}
#[test]
fn test_print_confirmation_header() {
let handler = InputHandler::default();
// Test confirmation header with various message lengths
handler.print_confirmation_header("Are you sure you want to delete this?");
handler.print_confirmation_header("");
handler.print_confirmation_header("This is a very long confirmation message that might need to be wrapped across multiple lines in the terminal display to ensure proper formatting and readability");
}
#[test]
fn test_print_session_manager_header() {
let handler = InputHandler::default();
// Test session manager header
handler.print_session_manager_header("Session Management", 5, Some("current"));
handler.print_session_manager_header("Sessions", 0, None);
handler.print_session_manager_header("Test", 10, Some("very-long-session-name"));
}
#[test]
fn test_print_empty_list_message() {
let handler = InputHandler::default();
// Test empty list message
handler.print_empty_list_message("No sessions available");
handler.print_empty_list_message("No models found");
handler.print_empty_list_message("");
}
#[test]
fn test_print_multiline_header() {
let handler = InputHandler::default();
// Test multiline header printing
handler.print_multiline_header();
}
#[test]
fn test_cleanup() {
let mut handler = InputHandler::default();
// Test cleanup doesn't panic
let result = handler.cleanup();
assert!(result.is_ok() || result.is_err()); // Either way is fine, just shouldn't panic
}
#[test]
fn test_save_history() {
let mut handler = InputHandler::default();
// Test history saving doesn't panic
let result = handler.save_history();
assert!(result.is_ok() || result.is_err()); // Either way is fine in test environment
}
#[test]
fn test_select_from_list_empty() {
let handler = InputHandler::default();
let empty_items: Vec<String> = vec![];
let result = handler.select_from_list("Test", &empty_items, None);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_select_from_list_display_items() {
let handler = InputHandler::default();
let items = vec!["item1".to_string(), "item2".to_string(), "item3".to_string()];
// Test display item creation (this tests the internal logic)
let display_items: Vec<String> = items.iter().enumerate().map(|(i, item)| {
let item_str = item.to_string();
let icon = handler.get_selection_icon(i);
if Some(item_str.as_str()) == Some("current") {
format!("{} {} ⭐ (current)", icon, item_str)
} else {
format!("{} {}", icon, item_str)
}
}).collect();
assert_eq!(display_items[0], "📝 item1");
assert_eq!(display_items[1], "🔧 item2");
assert_eq!(display_items[2], "⚙️ item3");
}
#[test]
fn test_session_manager_display_logic() {
let handler = InputHandler::default();
let sessions = vec!["session1".to_string(), "session2".to_string(), "current".to_string()];
// Test session display item creation
let display_items: Vec<String> = sessions
.iter()
.enumerate()
.map(|(i, session)| {
let name = session.to_string();
let icon = handler.get_session_icon(i);
if Some(name.as_str()) == Some("current") {
format!("{} {} ⭐ (active)", icon, name)
} else {
format!("{} {}", icon, name)
}
})
.collect();
assert_eq!(display_items[0], "💾 session1");
assert_eq!(display_items[1], "📁 session2");
assert_eq!(display_items[2], "🗂️ current ⭐ (active)");
}
#[test]
fn test_terminal_width_handling() {
let handler = InputHandler::default();
let term = Term::stdout();
let width = term.size().1 as usize;
// Test that terminal width is positive
assert!(width > 0);
// Test that our UI components handle various widths gracefully
handler.print_selection_header("Test", 3);
handler.print_confirmation_header("Test confirmation message");
handler.print_session_manager_header("Sessions", 2, Some("test"));
handler.print_empty_list_message("No items");
handler.print_multiline_header();
}
#[test]
fn test_word_wrapping_in_confirmation() {
let handler = InputHandler::default();
// Test with a very long message that should trigger word wrapping
let long_message = "This is an extremely long confirmation message that should definitely exceed the terminal width and trigger the word wrapping functionality built into the confirmation header display system.";
handler.print_confirmation_header(long_message);
// Test with message containing no spaces (edge case)
let no_spaces = "verylongwordwithoutanyspacesthatmightcausewrappingissues";
handler.print_confirmation_header(no_spaces);
}
#[test]
fn test_icon_consistency() {
let handler = InputHandler::default();
// Test that icons are consistent across multiple calls
for i in 0..20 {
let icon1 = handler.get_selection_icon(i);
let icon2 = handler.get_selection_icon(i);
assert_eq!(icon1, icon2);
let session_icon1 = handler.get_session_icon(i);
let session_icon2 = handler.get_session_icon(i);
assert_eq!(session_icon1, session_icon2);
}
}
#[test]
fn test_theme_components() {
let handler = InputHandler::default();
let theme = handler.create_enhanced_theme();
// Test that all theme components are properly styled
assert!(!theme.prompt_prefix.to_string().is_empty());
assert!(!theme.success_prefix.to_string().is_empty());
assert!(!theme.error_prefix.to_string().is_empty());
assert!(!theme.active_item_prefix.to_string().is_empty());
assert!(!theme.checked_item_prefix.to_string().is_empty());
assert!(!theme.picked_item_prefix.to_string().is_empty());
}
#[test]
fn test_edge_cases() {
let handler = InputHandler::default();
// Test with Unicode in headers
handler.print_selection_header("Test with Unicode: 🌟✨🎯", 3);
handler.print_confirmation_header("Unicode message: 你好世界");
handler.print_session_manager_header("Sessions 📊", 5, Some("test-session-🔥"));
// Test with empty strings
handler.print_selection_header("", 0);
handler.print_confirmation_header("");
handler.print_session_manager_header("", 0, None);
handler.print_empty_list_message("");
// Test with very large numbers
handler.print_selection_header("Large count", usize::MAX);
handler.print_session_manager_header("Sessions", usize::MAX, Some("session"));
}
} }

View File

@ -1,5 +1,5 @@
{ {
"exported_at": "2025-08-25T04:01:15.432999997+00:00", "exported_at": "2025-08-31T03:02:51.004770151+00:00",
"messages": [ "messages": [
{ {
"content": "You are an AI assistant running in a terminal (CLI) environment. Optimise all answers for 80column readability, prefer plain text, ASCII art or concise bullet lists over heavy markup, and wrap code snippets in fenced blocks when helpful. Do not emit trailing spaces or control characters.", "content": "You are an AI assistant running in a terminal (CLI) environment. Optimise all answers for 80column readability, prefer plain text, ASCII art or concise bullet lists over heavy markup, and wrap code snippets in fenced blocks when helpful. Do not emit trailing spaces or control characters.",

View File

@ -0,0 +1,207 @@
use gpt_cli_rust::utils::{Display, InputHandler};
use std::time::{Duration, Instant};
#[test]
fn test_display_performance() {
let display = Display::new();
// Test performance of basic operations
let start = Instant::now();
for _ in 0..100 {
display.print_info("Test message");
display.print_error("Test error");
display.print_warning("Test warning");
}
let duration = start.elapsed();
// Should complete in reasonable time (less than 1 second for 300 operations)
assert!(duration < Duration::from_secs(1),
"Display operations took too long: {:?}", duration);
}
#[test]
fn test_public_display_methods_performance() {
let display = Display::new();
let start = Instant::now();
for i in 0..50 {
display.print_header();
display.print_help();
display.print_status_bar("GPT-4", "OpenAI", "test", &[("feature", true), ("another", false)]);
display.print_error_with_context("Test error", Some("Context"), &["Fix it"]);
display.print_section_header("Test Section", "🔧");
display.print_feature_status(&[("Feature 1", true, Some("Desc")), ("Feature 2", false, None)]);
display.clear_current_line();
display.print_separator("-");
display.print_progress_bar(i, 100, "Progress");
}
let duration = start.elapsed();
// Should handle complex display operations efficiently
assert!(duration < Duration::from_secs(2),
"Complex display operations took too long: {:?}", duration);
}
#[test]
fn test_assistant_response_performance() {
let display = Display::new();
// Test with moderately sized content with formatting
let content = "This is a test with `inline code` and some other text.\n\n```rust\nfn test() {\n println!(\"Hello\");\n}\n```\n\nMore text here.".repeat(5);
let start = Instant::now();
for _ in 0..10 {
display.print_assistant_response(&content);
}
let duration = start.elapsed();
// Should handle moderately sized content efficiently
assert!(duration < Duration::from_secs(3),
"Assistant response formatting took too long: {:?}", duration);
}
#[test]
fn test_conversation_history_performance() {
use gpt_cli_rust::core::Message;
let display = Display::new();
// Create test messages (need to store them separately to avoid borrow issues)
let mut message_store = Vec::new();
for i in 0..20 {
let message = Message {
role: if i % 2 == 0 { "user" } else { "assistant" }.to_string(),
content: format!("This is test message number {} with some content.", i),
};
message_store.push(message);
}
// Create references after all messages are created
let messages: Vec<(usize, &Message)> = message_store.iter().enumerate()
.map(|(i, msg)| (i + 1, msg)).collect();
let start = Instant::now();
display.print_conversation_history(&messages);
let duration = start.elapsed();
// Should handle conversation history efficiently
assert!(duration < Duration::from_secs(1),
"Conversation history display took too long: {:?}", duration);
}
#[test]
fn test_input_handler_creation_performance() {
let start = Instant::now();
for _ in 0..10 {
let _handler = InputHandler::default();
}
let duration = start.elapsed();
// Input handler creation should be fast
assert!(duration < Duration::from_millis(500),
"Input handler creation took too long: {:?}", duration);
}
#[test]
fn test_select_from_list_empty_performance() {
let handler = InputHandler::default();
let empty_items: Vec<String> = vec![];
let start = Instant::now();
for _ in 0..100 {
let _ = handler.select_from_list("Test", &empty_items, None);
}
let duration = start.elapsed();
// Empty list handling should be very fast
assert!(duration < Duration::from_millis(100),
"Empty list handling took too long: {:?}", duration);
}
#[test]
fn test_display_creation_performance() {
let start = Instant::now();
for _ in 0..100 {
let _display = Display::new();
}
let duration = start.elapsed();
// Display creation should be efficient
assert!(duration < Duration::from_millis(500),
"Display creation took too long: {:?}", duration);
}
#[test]
fn test_spinner_performance() {
let display = Display::new();
let start = Instant::now();
for i in 0..50 {
let spinner = display.show_spinner(&format!("Operation {}", i));
spinner.finish("Completed");
let spinner2 = display.show_spinner(&format!("Operation {}", i));
spinner2.finish_with_error("Failed");
}
let duration = start.elapsed();
// Spinner operations should be fast
assert!(duration < Duration::from_secs(1),
"Spinner operations took too long: {:?}", duration);
}
#[test]
fn test_large_feature_status_performance() {
let display = Display::new();
// Create many features with static strings to avoid lifetime issues
let features: Vec<(&str, bool, Option<&str>)> = (0..10)
.map(|i| (
"Feature Name",
i % 2 == 0,
if i % 3 == 0 { Some("Feature description") } else { None }
))
.collect();
let start = Instant::now();
for _ in 0..10 {
display.print_feature_status(&features);
}
let duration = start.elapsed();
// Should handle many features efficiently
assert!(duration < Duration::from_secs(1),
"Large feature status display took too long: {:?}", duration);
}
#[test]
fn test_memory_usage_stability() {
let display = Display::new();
// Test that repeated operations don't cause memory leaks
// This is a basic test - in a real scenario you'd use a memory profiler
for i in 0..100 {
display.print_status_bar("GPT-4", "OpenAI", "test", &[("feature", true)]);
display.print_error_with_context("Error", Some("Context"), &["Suggestion"]);
display.print_section_header(&format!("Section {}", i), "🔧");
let spinner = display.show_spinner("Test");
spinner.finish("Done");
}
// If we get here without panic/crash, memory handling is reasonable
assert!(true);
}
#[test]
fn test_input_cleanup_performance() {
let start = Instant::now();
for _ in 0..50 {
let mut handler = InputHandler::default();
let _ = handler.cleanup();
let _ = handler.save_history();
}
let duration = start.elapsed();
// Cleanup operations should be efficient
assert!(duration < Duration::from_secs(1),
"Input cleanup took too long: {:?}", duration);
}