use std::io; use std::sync::mpsc; use std::thread; use std::time::Duration; use std::collections::VecDeque; use application::UseCase; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use domain::{CreateProduct, CreateUser, Product, User}; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Style}, widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, Frame, Terminal, }; use textwrap; const MAX_HISTORY: usize = 100; pub struct App { input: String, messages: Vec, should_quit: bool, command_history: VecDeque, history_index: Option, cursor_position: usize, } impl App { pub fn new() -> Self { Self { input: String::new(), messages: vec!["Welcome to Sharenet CLI!".to_string()], should_quit: false, command_history: VecDeque::with_capacity(MAX_HISTORY), history_index: None, cursor_position: 0, } } pub fn add_message(&mut self, message: String) { self.messages.push(message); } pub fn clear_input(&mut self) { self.input.clear(); self.cursor_position = 0; } pub fn add_to_history(&mut self, command: String) { if !command.trim().is_empty() { self.command_history.push_front(command); if self.command_history.len() > MAX_HISTORY { self.command_history.pop_back(); } } } pub fn move_cursor(&mut self, delta: isize) { let new_pos = self.cursor_position as isize + delta; if new_pos >= 0 && new_pos <= self.input.len() as isize { self.cursor_position = new_pos as usize; } } pub fn insert_char(&mut self, c: char) { self.input.insert(self.cursor_position, c); self.cursor_position += 1; } pub fn delete_char(&mut self) { if self.cursor_position > 0 { self.input.remove(self.cursor_position - 1); self.cursor_position -= 1; } } } pub async fn run_tui(user_service: U, product_service: P) -> anyhow::Result<()> where U: UseCase + Clone + Send + 'static, P: UseCase + Clone + Send + 'static, { // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; // Create app and run it let app = App::new(); let res = run_app(&mut terminal, app, user_service, product_service).await; // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{:?}", err); } Ok(()) } async fn run_app( terminal: &mut Terminal, mut app: App, user_service: U, product_service: P, ) -> anyhow::Result<()> where U: UseCase + Clone + Send + 'static, P: UseCase + Clone + Send + 'static, { let (tx, rx) = mpsc::channel(); let user_service_clone = user_service.clone(); let product_service_clone = product_service.clone(); // Spawn a thread to handle user input thread::spawn(move || { loop { if event::poll(Duration::from_millis(50)).unwrap() { if let Event::Key(key) = event::read().unwrap() { tx.send(key).unwrap(); } } } }); loop { terminal.draw(|f| ui(f, &app))?; if let Ok(key) = rx.try_recv() { match key.code { KeyCode::Char(c) => { app.insert_char(c); } KeyCode::Backspace => { app.delete_char(); } KeyCode::Left => { app.move_cursor(-1); } KeyCode::Right => { app.move_cursor(1); } KeyCode::Up => { if let Some(index) = app.history_index { if index + 1 < app.command_history.len() { app.history_index = Some(index + 1); app.input = app.command_history[index + 1].clone(); app.cursor_position = app.input.len(); } } else if !app.command_history.is_empty() { app.history_index = Some(0); app.input = app.command_history[0].clone(); app.cursor_position = app.input.len(); } } KeyCode::Down => { if let Some(index) = app.history_index { if index > 0 { app.history_index = Some(index - 1); app.input = app.command_history[index - 1].clone(); app.cursor_position = app.input.len(); } else { app.history_index = None; app.clear_input(); } } } KeyCode::Enter => { let input = app.input.clone(); app.add_to_history(input.clone()); app.clear_input(); app.history_index = None; // Display the command in a distinct color app.add_message(format!("> {}", input)); // Handle commands match input.trim() { "exit" => { app.add_message("Goodbye!".to_string()); app.should_quit = true; } "help" => { print_help(&mut app); } cmd if cmd.starts_with("user create") => { match parse_user_create(cmd) { Ok((username, email)) => { match user_service_clone .create(CreateUser { username, email }) .await { Ok(user) => app.add_message(format!("Created user: {:?}", user)), Err(e) => app.add_message(format!("Error: {}", e)), } } Err(e) => app.add_message(format!("Error: {}", e)), } } "user list" => { match user_service_clone.list().await { Ok(users) => app.add_message(format!("Users: {:?}", users)), Err(e) => app.add_message(format!("Error: {}", e)), } } cmd if cmd.starts_with("product create") => { match parse_product_create(cmd) { Ok((name, description)) => { match product_service_clone .create(CreateProduct { name, description }) .await { Ok(product) => app.add_message(format!("Created product: {:?}", product)), Err(e) => app.add_message(format!("Error: {}", e)), } } Err(e) => app.add_message(format!("Error: {}", e)), } } "product list" => { match product_service_clone.list().await { Ok(products) => app.add_message(format!("Products: {:?}", products)), Err(e) => app.add_message(format!("Error: {}", e)), } } "" => {} _ => { app.add_message("Unknown command. Type 'help' for available commands.".to_string()); } } } KeyCode::Esc => { app.should_quit = true; } _ => {} } } if app.should_quit { return Ok(()); } } } fn print_help(app: &mut App) { app.add_message("\nAvailable commands:".to_string()); app.add_message(" user create -u -e ".to_string()); app.add_message(" Example: user create -u \"john doe\" -e \"john@example.com\"".to_string()); app.add_message(" user list".to_string()); app.add_message(" product create -n -d ".to_string()); app.add_message(" Example: product create -n \"My Product\" -d \"A great product description\"".to_string()); app.add_message(" product list".to_string()); app.add_message("\nTips:".to_string()); app.add_message(" - Use quotes for values with spaces".to_string()); app.add_message(" - Use Up/Down arrows to navigate command history".to_string()); app.add_message(" - Press Esc to exit".to_string()); app.add_message(" - Type 'help' to show this message".to_string()); } fn ui(f: &mut Frame, app: &App) { // Create the layout let chunks = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints([ Constraint::Min(1), Constraint::Length(3), ]) .split(f.size()); // Create the messages list with styling and wrapping let messages: Vec = app .messages .iter() .flat_map(|msg| { let style = if msg.starts_with("Error:") { Style::default().fg(Color::Red) } else if msg.starts_with("Created") { Style::default().fg(Color::Green) } else if msg.starts_with(">") { Style::default().fg(Color::Cyan) } else { Style::default() }; // Calculate available width for text (accounting for borders and margins) let available_width = chunks[0].width.saturating_sub(2) as usize; // Split message into wrapped lines let wrapped_lines = textwrap::wrap(msg, available_width); // Convert each wrapped line into a ListItem wrapped_lines.into_iter().map(move |line| { ListItem::new(line.to_string()).style(style) }) }) .collect(); let messages = List::new(messages) .block(Block::default().title("Messages").borders(Borders::ALL)); f.render_widget(messages, chunks[0]); // Create the input box with cursor and wrapping let input = Paragraph::new(app.input.as_str()) .style(Style::default().fg(Color::Yellow)) .block(Block::default().title("Input (Press 'Esc' to exit)").borders(Borders::ALL)) .wrap(Wrap { trim: true }); f.render_widget(input, chunks[1]); // Show cursor f.set_cursor( chunks[1].x + app.cursor_position as u16 + 1, chunks[1].y + 1, ); } fn parse_user_create(cmd: &str) -> anyhow::Result<(String, String)> { let parts: Vec<&str> = cmd.split_whitespace().collect(); if parts.len() < 6 { return Err(anyhow::anyhow!( "Invalid command format. Use: user create -u -e \nExample: user create -u \"john doe\" -e \"john@example.com\"" )); } let mut username = None; let mut email = None; let mut current_arg = None; let mut current_value = Vec::new(); // Skip "user create" command let mut i = 2; while i < parts.len() { match parts[i] { "-u" => { if let Some(arg_type) = current_arg { match arg_type { "username" => username = Some(current_value.join(" ")), "email" => email = Some(current_value.join(" ")), _ => {} } } current_arg = Some("username"); current_value.clear(); i += 1; } "-e" => { if let Some(arg_type) = current_arg { match arg_type { "username" => username = Some(current_value.join(" ")), "email" => email = Some(current_value.join(" ")), _ => {} } } current_arg = Some("email"); current_value.clear(); i += 1; } _ => { if current_arg.is_some() { current_value.push(parts[i].trim_matches('"')); } i += 1; } } } // Handle the last argument if let Some(arg_type) = current_arg { match arg_type { "username" => username = Some(current_value.join(" ")), "email" => email = Some(current_value.join(" ")), _ => {} } } match (username, email) { (Some(u), Some(e)) if !u.is_empty() && !e.is_empty() => Ok((u, e)), _ => Err(anyhow::anyhow!( "Invalid command format. Use: user create -u -e \nExample: user create -u \"john doe\" -e \"john@example.com\"" )), } } fn parse_product_create(cmd: &str) -> anyhow::Result<(String, String)> { let parts: Vec<&str> = cmd.split_whitespace().collect(); if parts.len() < 6 { return Err(anyhow::anyhow!( "Invalid command format. Use: product create -n -d \nExample: product create -n \"My Product\" -d \"A great product description\"" )); } let mut name = None; let mut description = None; let mut current_arg = None; let mut current_value = Vec::new(); // Skip "product create" command let mut i = 2; while i < parts.len() { match parts[i] { "-n" => { if let Some(arg_type) = current_arg { match arg_type { "name" => name = Some(current_value.join(" ")), "description" => description = Some(current_value.join(" ")), _ => {} } } current_arg = Some("name"); current_value.clear(); i += 1; } "-d" => { if let Some(arg_type) = current_arg { match arg_type { "name" => name = Some(current_value.join(" ")), "description" => description = Some(current_value.join(" ")), _ => {} } } current_arg = Some("description"); current_value.clear(); i += 1; } _ => { if current_arg.is_some() { current_value.push(parts[i].trim_matches('"')); } i += 1; } } } // Handle the last argument if let Some(arg_type) = current_arg { match arg_type { "name" => name = Some(current_value.join(" ")), "description" => description = Some(current_value.join(" ")), _ => {} } } match (name, description) { (Some(n), Some(d)) if !n.is_empty() && !d.is_empty() => Ok((n, d)), _ => Err(anyhow::anyhow!( "Invalid command format. Use: product create -n -d \nExample: product create -n \"My Product\" -d \"A great product description\"" )), } }