use std::io::{BufRead, BufReader, Write, stdout}; use std::net::TcpStream; use rusqlite::Connection; use std::time::Instant; use crate::connection; use crate::database; use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; use sha2::{Sha256, Digest}; pub struct ClientState { pub stream: Option, pub conn: Connection, pub addr: Option, pub default_port: u16, pub history: Vec, pub history_index: usize, pub logged_in_user: Option, } impl ClientState { pub fn new() -> Self { let conn = database::init_db(); let default_ip = "127.0.0.1".to_string(); let default_port = 11525; let last_host = database::get_last_host(&conn); let default_host = format!("{}:{}", default_ip, default_port); let addr = last_host.or(Some(default_host)); Self { stream: None, conn, addr, default_port, history: vec![], history_index: 0, logged_in_user: None, } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); match connection::connect(addr, &self.conn) { Ok((s, authed_user)) => { self.stream = Some(s); self.logged_in_user = authed_user; database::save_host(&self.conn, addr); } Err(e) => println!("Failed to connect: {}", e), } } pub fn run(&mut self) { enable_raw_mode().unwrap(); 'outer: loop { let mut current_input = String::new(); self.history_index = self.history.len(); redraw(""); loop { match read().unwrap() { Event::Key(key) => match key.code { KeyCode::Up => { if self.history_index > 0 { self.history_index -= 1; current_input = self.history[self.history_index].clone(); redraw(¤t_input); } } KeyCode::Down => { if self.history_index < self.history.len() { self.history_index += 1; current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; redraw(¤t_input); } } KeyCode::Enter => { let line = current_input.trim().to_string(); if !line.is_empty() { self.history.push(line.clone()); } // Process command let parts: Vec<&str> = line.splitn(2, ' ').collect(); match parts[0] { "connect" => { let addr_str = if parts.len() < 2 { if let Some(h) = &self.addr { h.clone() } else { println!("\nNo host specified and no previous connection. Usage: connect :"); continue 'outer; } } else { let mut a = parts[1].to_string(); if !a.contains(':') { a = format!("{}:{}", a, self.default_port); } a }; self.connect_to(&addr_str); } "send" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if parts.len() < 2 { println!("\nUsage: send "); continue 'outer; } let msg = parts[1]; let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { println!("\nEcho: {}", response.trim()); } } "disconnect" => { self.stream = None; self.logged_in_user = None; println!("\nDisconnected"); } "ping" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let start = Instant::now(); let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "PING") { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { if response.trim() == "PONG" { let elapsed = start.elapsed(); println!("\nPong ({:?})", elapsed); } else { self.stream = None; println!("\nConnection lost"); } } } "register" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_line("Password: "); if password.is_empty() { continue 'outer; } let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); let msg = format!("REGISTER {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { let resp = response.trim(); if resp == format!("REGISTERED {}", username) { println!("\nSuccessfully registered as {}", username); } else if resp.starts_with("ERROR ") { println!("\nRegistration failed: {}", &resp[6..]); } else { println!("\n{}", resp); } } } "login" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if self.logged_in_user.is_some() { println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap()); continue 'outer; } loop { let username = read_line("\nUsername: "); if username.is_empty() { break; } let password = read_line("Password: "); if password.is_empty() { break; } let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); let msg = format!("AUTH {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); break; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); break; } else { let resp = response.trim(); if resp.starts_with("OK") { // Save credentials if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { println!("Failed to save credentials: {}", e); } } self.logged_in_user = Some(username.clone()); println!("\nLogged in as {}", username); break; } else if resp.starts_with("ERROR ") { println!("\nAuthentication failed: {}", &resp[6..]); println!("Try again or press Enter to cancel."); } else { println!("\n{}", resp); } } } } "setcred" => { if self.addr.is_none() { println!("\nNo host connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_line("Password: "); if password.is_empty() { continue 'outer; } let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { println!("Failed to save credentials: {}", e); } else { println!("Credentials saved for {}", addr); } } } "logout" => { self.logged_in_user = None; println!("\nLogged out"); } "info" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "INFO") { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { let resp = response.trim(); if resp.starts_with("INFO ") { let parts: Vec<&str> = resp.split_whitespace().collect(); if parts.len() == 4 { println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); } else { println!("\n{}", resp); } } else if resp.starts_with("ERROR ") { println!("\nError: {}", &resp[6..]); } else { println!("\n{}", resp); } } } "help" => { help(); } "quit" => break 'outer, _ => help(), } break; } KeyCode::Backspace => { if !current_input.is_empty() { current_input.pop(); redraw(¤t_input); } } KeyCode::Char(c) => { if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); return; } else if c == '\t' { self.handle_tab_completion(&mut current_input); } else { current_input.push(c); redraw(¤t_input); } } KeyCode::Tab => { self.handle_tab_completion(&mut current_input); } _ => {} } _ => {} } } } disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); } fn handle_tab_completion(&mut self, current_input: &mut String) { let prefix = current_input.trim(); let commands = get_commands(); let matches: Vec<_> = commands.iter().filter(|c| c.starts_with(prefix)).collect(); if matches.len() == 1 { *current_input = matches[0].clone(); redraw(current_input); } else if matches.len() > 1 { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); println!("\nPossible completions:"); for m in &matches { print!("{} ", m); } println!(); enable_raw_mode().unwrap(); redraw(current_input); } // else do nothing } } fn get_commands() -> Vec { vec![ "connect".to_string(), "send".to_string(), "disconnect".to_string(), "ping".to_string(), "login".to_string(), "register".to_string(), "setcred".to_string(), "logout".to_string(), "info".to_string(), "help".to_string(), "quit".to_string(), ] } fn help() { println!("\nAvailable commands:\r connect : - connect to the server\r send - send a message to the server\r disconnect - disconnect from the server\r ping - ping the server\r login - login to the server\r register - register a new user\r setcred - set credentials for a host\r logout - logout from the server\r info - show login info\r help - show this message\r quit - quit the program\n"); } fn redraw(current_input: &str) { execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); print!("> {}", current_input); stdout().flush().unwrap(); execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); } fn read_line(prompt: &str) -> String { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); print!("{}", prompt); stdout().flush().unwrap(); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); enable_raw_mode().unwrap(); input.trim().to_string() }