From 86d5f832c8452aad2313489b82eb8fd514215b95 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:28:20 +0100 Subject: v0.2.3: add ClientCore and fix the game board rendering --- src/bin/client/repl.rs | 1074 +++++++++++++++--------------------------------- 1 file changed, 334 insertions(+), 740 deletions(-) (limited to 'src/bin/client/repl.rs') diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 24605e2..e9d32a0 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs @@ -1,34 +1,34 @@ use rusqlite::Connection; -use std::io::{stdout, BufRead, BufReader, Write}; +use std::io::{stdout, Write}; use std::sync::{Arc, Mutex}; -use std::time::Instant; - -use crate::connection::TlsStream; -use zagan_bet::client::database; -use zagan_bet::client::games::manager::GameManager; -use zagan_bet::client::games::traits::GamePhase; use crossterm::{ - cursor::{Hide, MoveToColumn, Show}, - event::{read, Event, KeyCode, KeyModifiers}, + cursor::{MoveToColumn, MoveUp, Show}, + event::{read, Event, KeyCode, KeyEventKind, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, }; use sha2::{Digest, Sha256}; +use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; +use zagan_bet::client::database; +use zagan_bet::client::ui::{input_cursor_column, lines_to_move_up}; + +use crate::connection::TlsStream; + 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, - pub game_manager: GameManager, + core: ClientCore, cleanup_done: Arc>, - in_game_mode: bool, - spinning: bool, + /// Number of terminal lines currently occupied by the game board. + game_lines: usize, + /// Text last drawn after the `> ` prompt, used to keep the command echo + /// in the transcript before printing command output. + prompt_text: String, } impl ClientState { @@ -49,128 +49,40 @@ impl ClientState { conn, addr, default_port, - history: vec![], - history_index: 0, - logged_in_user: None, - game_manager: GameManager::new(), + core: ClientCore::new(), cleanup_done: Arc::new(Mutex::new(false)), - in_game_mode: false, - spinning: false, - } - } - - pub fn enter_game_mode(&mut self) { - self.in_game_mode = true; - } - - pub fn exit_game_mode(&mut self) { - self.in_game_mode = false; - self.game_manager.end_game(); - } - - pub fn handle_game_key(&mut self, key_code: KeyCode) -> bool { - if !self.game_manager.is_in_game() { - return false; - } - - match key_code { - KeyCode::Esc | KeyCode::Char('q') => { - self.exit_game_mode(); - println!("\nGame exited."); - return true; - } - KeyCode::Char(' ') => { - if self.spinning { - return true; // Discard keypress while busy - } - let can_spin = self.game_manager.get_current_state() - .map(|s| s.phase == GamePhase::WaitingToStart || s.phase == GamePhase::Finished) - .unwrap_or(false); - if can_spin { - if let Some(mut stream) = self.stream.take() { - self.spinning = true; - match self.game_manager.start_game(&mut stream) { - Ok(()) => { - let response = self.read_server_response(&mut stream); - if response.trim().starts_with("ERROR") { - println!("\n{}", response.trim()); - } else { - self.play_spin_animation(&response); - } - self.game_manager.reset_phase_to_waiting(); - } - Err(e) => println!("\nError starting game: {:?}", e), - } - self.stream = Some(stream); - // Drain buffered key events BEFORE clearing spinning - // to prevent queued spins from triggering immediately - while crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { - let _ = crossterm::event::read(); - } - self.spinning = false; - } - } - return true; - } - KeyCode::Char('+') => { - // Increase bet - match self.game_manager.adjust_bet(10) { - Ok(_new_bet) => { - if let Some(state) = self.game_manager.get_current_state() { - if let Some(game) = self.game_manager.get_current_game() { - let lines = - self.game_manager.get_current_render_lines().unwrap_or(8); - redraw_game_lines(&game.render(state), lines); - } - } - } - Err(e) => println!("\nError: {:?}", e), - } - return true; - } - KeyCode::Char('-') => { - // Decrease bet - match self.game_manager.adjust_bet(-10) { - Ok(_new_bet) => { - if let Some(state) = self.game_manager.get_current_state() { - if let Some(game) = self.game_manager.get_current_game() { - let lines = - self.game_manager.get_current_render_lines().unwrap_or(8); - redraw_game_lines(&game.render(state), lines); - } - } - } - Err(e) => println!("\nError: {:?}", e), - } - return true; - } - _ => return true, // Lock into game mode: ignore unknown keys + game_lines: 0, + prompt_text: String::new(), } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); match crate::connection::connect(addr, &self.conn) { - Ok((s, authed_user)) => { - self.stream = Some(s); - self.logged_in_user = authed_user; + Ok((stream, authed_user)) => { + self.stream = Some(stream); + self.core.set_connected(true); + self.core.set_logged_in(authed_user); + let fingerprint = database::get_fingerprint(&self.conn, addr); + self.core + .set_host_info(Some(addr.to_string()), fingerprint); database::save_host(&self.conn, addr); } - Err(e) => println!("\nFailed to connect: {}", e), + Err(e) => { + self.core.set_connected(false); + println!("\nFailed to connect: {}", e); + } } } pub fn run(&mut self) { - // Set up cleanup handlers let cleanup_done = self.cleanup_done.clone(); - // Set up panic handler let panic_cleanup = cleanup_done.clone(); std::panic::set_hook(Box::new(move |_| { restore_terminal(&panic_cleanup); })); - // Set up Ctrl+C handler let ctrlc_cleanup = cleanup_done.clone(); if let Err(e) = ctrlc::set_handler(move || { restore_terminal(&ctrlc_cleanup); @@ -180,653 +92,331 @@ impl ClientState { } enable_raw_mode().unwrap(); - 'outer: loop { - let mut current_input = String::new(); - self.history_index = self.history.len(); - redraw(""); - loop { - let event = read().unwrap(); - match event { - Event::Key(key) => { - // Only handle key press events, not release events - if key.kind == crossterm::event::KeyEventKind::Press { - // Handle game mode keys first - if self.game_manager.is_in_game() && self.handle_game_key(key.code) { - // Game key was handled, continue loop - if !self.game_manager.is_in_game() { - // Game ended, redraw command prompt - redraw(""); - } - continue; - } + self.apply_prompt(); - // Handle normal command mode keys - 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()); - } - let line_trimmed = current_input.trim().to_string(); - if !line_trimmed.is_empty() { - self.history.push(line_trimmed.clone()); - } - - // Process normal commands - let parts: Vec<&str> = line_trimmed.splitn(3, ' ').collect(); - match parts[0] { - "play" => { - if self.stream.is_none() { - println!("\nNot connected. Use 'connect :' first."); - continue 'outer; - } - if self.logged_in_user.is_none() { - println!("\nNot logged in. Use 'login' first."); - continue 'outer; - } - if parts.len() < 2 { - let games = self.game_manager.get_available_games(); - println!("\nAvailable games:"); - for (gt, name) in games { - println!(" - {} ({})", gt, name); - } - println!("\nUsage: play "); - continue 'outer; - } - let game_type = parts[1]; - - if self.game_manager.is_in_game() { - println!("\nAlready in a game. Use 'quit' to exit current game."); - continue 'outer; - } - - match self.game_manager.enter_game(game_type) { - Ok(()) => { - self.enter_game_mode(); - if let Some(game) = - self.game_manager.get_current_game() - { - if let Some(state) = - self.game_manager.get_current_state() - { - redraw_game(&game.render(state)); - } - } - } - Err(e) => { - println!("\nFailed to enter game: {:?}", e); - } - } - } - "games" => { - let games = self.game_manager.get_available_games(); - println!("\nAvailable games:"); - for (gt, name) in games { - println!(" - {} ({})", gt, name); - } - } - "connect" => { - if self.stream.is_some() { - println!("\nAlready connected. Disconnect first."); - continue 'outer; - } - let addr_str = if parts.len() < 2 { - if let Some(h) = &self.addr { - h.clone() - } else { - println!("\nNo host specified. Usage: connect :"); - continue 'outer; - } - } else { - let mut a = parts[1].to_string(); - if a == "localhost" { - a = "127.0.0.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_password("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_password("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_password("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 let Some(addr) = &self.addr { - let fp = database::get_fingerprint(&self.conn, addr); - println!("\nConnected to: {}", addr); - match fp { - Some(fp) => println!("Fingerprint: {}", fp), - None => println!("Fingerprint: (not trusted)"), - } - } else { - println!("\nNot connected to any host."); - } - if let Some(s) = &self.stream { - let mut s = s.try_clone().unwrap(); - let _ = writeln!(s, "INFO"); - let mut reader = BufReader::new(s); - let mut response = String::new(); - let _ = reader.read_line(&mut response); - if !response.is_empty() { - println!("{}", response.trim()); - } - } - } - "help" => { - if self.game_manager.is_in_game() { - if let Some(game) = - self.game_manager.get_current_game() - { - println!("\n{}", game.get_help()); - } - } else { - help(); - } - } - "quit" => { - if self.game_manager.is_in_game() { - self.game_manager.end_game(); - println!("\nGame exited."); - } else { - restore_terminal(&self.cleanup_done); - break 'outer; - } - } - "forget-host" => { - let target = if parts.len() > 1 { - parts[1].to_string() - } else if let Some(addr) = &self.addr { - addr.clone() - } else { - println!( - "\nUsage: forget-host : (or connect first)" - ); - continue 'outer; - }; - match database::forget_host(&self.conn, &target) { - Ok(_) => println!( - "\nCleared trust and stored credentials for {}.", - target - ), - Err(e) => println!("\nFailed to forget host: {}", e), - } - } - "hosts" => { - let hosts = database::get_all_hosts(&self.conn); - if hosts.is_empty() { - println!("\nNo remembered hosts."); - } else { - println!("\nRemembered hosts:"); - for (host, fp) in &hosts { - match fp { - Some(fp) => println!(" {} - {}", host, fp), - None => println!(" {} - (no fingerprint)", host), - } - } - } - } - _ => help(), - } - break; - } - KeyCode::Backspace => { - if !current_input.is_empty() { - current_input.pop(); - execute!( - stdout(), - MoveToColumn(2), - Clear(ClearType::UntilNewLine) - ) - .unwrap(); - print!("{}", current_input); - stdout().flush().unwrap(); - } - } - KeyCode::Char(c) => { - if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { - restore_terminal(&self.cleanup_done); - std::process::exit(0); - } else if key.modifiers.contains(KeyModifiers::CONTROL) - && c == 'd' - { - restore_terminal(&self.cleanup_done); - std::process::exit(0); - } else if c == '\t' { - self.handle_tab_completion(&mut current_input); - } else { - current_input.push(c); - print!("{}", c); - stdout().flush().unwrap(); - } - } - KeyCode::Tab => { - self.handle_tab_completion(&mut current_input); - } - _ => {} - } - } - } - _ => {} + loop { + let event = read().unwrap(); + if let Event::Key(key) = event { + if key.kind != KeyEventKind::Press { + continue; + } + let Some(key) = map_key(key) else { + continue; + }; + let effects = self.core.handle_key(key); + if self.apply_effects(effects) { + break; } + self.apply_prompt(); } } + restore_terminal(&self.cleanup_done); } - 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 { - execute!(stdout(), Show).unwrap(); - println!("\nPossible completions:"); - for m in &matches { - print!("{} ", m); + fn apply_effects(&mut self, effects: Vec) -> bool { + for effect in effects { + match effect { + Effect::Print(lines) => { + self.commit_prompt_line(); + for line in lines { + println!("{}", line); + } + } + Effect::ReplaceRegion(lines) => self.draw_region(&lines), + Effect::ClearRegion => self.clear_region(), + Effect::SetPrompt { text, cursor_col } => self.draw_prompt(&text, cursor_col), + Effect::SendLine(line) => { + let reply = self.send_and_read(&line); + let more = self.core.handle_reply(&reply); + if self.stream.is_none() { + self.core.set_connected(false); + } + if self.apply_effects(more) { + return true; + } + } + Effect::Interactive(command) => { + self.commit_prompt_line(); + self.run_interactive(command); + } + Effect::Disconnect => { + self.stream = None; + self.clear_region(); + } + Effect::ListHosts => { + self.commit_prompt_line(); + self.list_hosts(); + } + Effect::ForgetHost(target) => { + self.commit_prompt_line(); + match database::forget_host(&self.conn, &target) { + Ok(_) => println!( + "Cleared trust and stored credentials for {}.", + target + ), + Err(error) => println!("Failed to forget host: {}", error), + } + } + Effect::Quit => { + let had_board = self.game_lines > 0; + self.clear_region(); + self.commit_prompt_line(); + if had_board { + println!(); + } + return true; + } } - println!(); - execute!(stdout(), Hide).unwrap(); - redraw(current_input); } - // else do nothing + false } - /// Read the server response after PLAY_START. - fn read_server_response(&self, stream: &mut TlsStream) -> String { - let mut response = String::new(); - let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); - let _ = reader.read_line(&mut response); - response + fn apply_prompt(&mut self) { + if let Some(prompt) = self.core.prompt_effect() { + let _ = self.apply_effects(vec![prompt]); + } } - /// Run the spinning animation and display the final result. - fn play_spin_animation(&mut self, response: &str) { - use std::io::Write; - use std::thread; - use std::time::Duration; - - let symbols = ['C', 'L', 'O', 'G', 'D', 'S']; - let frames = 20; - let delay = Duration::from_millis(50); - - for _ in 0..frames { - let reels: String = (0..3) - .map(|_| symbols[rand::random::() % symbols.len()]) - .collect(); - - execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); - print!( - "[ {} ] [ {} ] [ {} ]", - reels.chars().nth(0).unwrap(), - reels.chars().nth(1).unwrap(), - reels.chars().nth(2).unwrap() - ); - stdout().flush().unwrap(); - - thread::sleep(delay); + /// Leave the command currently on the prompt line visible in the + /// transcript before printing output below it. + fn commit_prompt_line(&mut self) { + if self.prompt_text.is_empty() { + return; } + print!("\r\n"); + stdout().flush().unwrap(); + self.prompt_text.clear(); + } - let response = response.trim(); - if response.starts_with("GAME_RESULT") { - let parts: Vec<&str> = response.split_whitespace().collect(); - if parts.len() >= 7 { - let reels = parts[5]; - let win_type = parts[6]; - - execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); - print!( - "[ {} ] [ {} ] [ {} ]", - reels.chars().nth(0).unwrap_or('?'), - reels.chars().nth(1).unwrap_or('?'), - reels.chars().nth(2).unwrap_or('?') - ); - println!(); - - if win_type == "jackpot" { - println!("\n🎉 JACKPOT! 🎉"); - } else if win_type == "two" { - println!("\n✨ Two of a kind! ✨"); - } else { - println!("\nNo win."); + fn draw_region(&mut self, lines: &[String]) { + if self.game_lines == 0 { + self.commit_prompt_line(); + } else { + execute!(stdout(), MoveUp(lines_to_move_up(self.game_lines))).unwrap(); + } + execute!(stdout(), MoveToColumn(0), Clear(ClearType::FromCursorDown)).unwrap(); + print!("{}", lines.join("\r\n")); + stdout().flush().unwrap(); + self.game_lines = lines.len().max(1); + } + + fn clear_region(&mut self) { + if self.game_lines == 0 { + return; + } + execute!( + stdout(), + MoveToColumn(0), + MoveUp(lines_to_move_up(self.game_lines)), + Clear(ClearType::FromCursorDown) + ) + .unwrap(); + self.game_lines = 0; + } + + fn draw_prompt(&mut self, text: &str, cursor_col: u16) { + execute!(stdout(), MoveToColumn(0), Clear(ClearType::CurrentLine)).unwrap(); + print!("> {}", text); + stdout().flush().unwrap(); + execute!(stdout(), MoveToColumn(cursor_col)).unwrap(); + self.prompt_text = text.to_string(); + } + + fn send_and_read(&mut self, line: &str) -> String { + let Some(mut stream) = self.stream.take() else { + return "ERROR Not connected".to_string(); + }; + + let result = (|| -> std::io::Result { + stream.write_line(line)?; + let mut response = String::new(); + let read = stream.read_line(&mut response)?; + if read == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "connection closed", + )); + } + Ok(response) + })(); + + match result { + Ok(response) => { + self.stream = Some(stream); + response + } + Err(_) => { + self.stream = None; + "ERROR Connection lost".to_string() + } + } + } + + fn run_interactive(&mut self, command: InteractiveCommand) { + match command { + InteractiveCommand::Login => self.do_login(), + InteractiveCommand::Register => self.do_register(), + InteractiveCommand::SetCred => self.do_setcred(), + InteractiveCommand::Connect(target) => { + if self.stream.is_some() { + println!("Already connected. Disconnect first."); + return; + } + let addr = match target { + Some(target) => self.normalize_addr(target), + None => match &self.addr { + Some(addr) => addr.clone(), + None => { + println!("No host specified. Usage: connect :"); + return; + } + }, + }; + self.connect_to(&addr); + } + } + } + + fn normalize_addr(&self, mut addr: String) -> String { + if addr == "localhost" { + addr = "127.0.0.1".to_string(); + } + if !addr.contains(':') { + addr = format!("{}:{}", addr, self.default_port); + } + addr + } + + fn do_login(&mut self) { + loop { + let username = read_line("Username: "); + if username.is_empty() { + break; + } + let password = read_password("Password: "); + if password.is_empty() { + break; + } + + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); + let message = format!("AUTH {} {}", username, hashed); + let response = self.send_and_read(&message); + let response = response.trim(); + + if response.starts_with("OK") { + if let Some(addr) = &self.addr { + if let Err(error) = + database::save_credentials(&self.conn, addr, &username, &hashed) + { + println!("Failed to save credentials: {}", error); + } } + self.core.set_logged_in(Some(username.clone())); + println!("Logged in as {}", username); + break; + } else if response.starts_with("ERROR ") { + println!("Authentication failed: {}", &response[6..]); + println!("Try again or press Enter to cancel."); + } else { + println!("{}", response); + } - println!("Winnings: {} PLN", parts[2]); - println!("New Balance: {} PLN", parts[4]); - stdout().flush().unwrap(); + if self.stream.is_none() { + break; } - } else if response.starts_with("ERROR") { - println!("\n{}", response); } } -} -fn restore_terminal(cleanup_done: &Arc>) { - let mut done = cleanup_done.lock().unwrap(); - if !*done { - let _ = disable_raw_mode(); - let _ = execute!(stdout(), Show); - *done = true; + fn do_register(&mut self) { + let username = read_line("Username: "); + if username.is_empty() { + return; + } + let password = read_password("Password: "); + if password.is_empty() { + return; + } + + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); + let message = format!("REGISTER {} {}", username, hashed); + let response = self.send_and_read(&message); + let response = response.trim(); + + if response == format!("REGISTERED {}", username) { + println!("Successfully registered as {}", username); + } else if response.starts_with("ERROR ") { + println!("Registration failed: {}", &response[6..]); + } else { + println!("{}", response); + } } -} -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(), - "play".to_string(), - "help".to_string(), - "quit".to_string(), - "forget-host".to_string(), - "hosts".to_string(), - ] -} + fn do_setcred(&mut self) { + if self.addr.is_none() { + println!("No host connected. Use 'connect :' first."); + return; + } + let username = read_line("Username: "); + if username.is_empty() { + return; + } + let password = read_password("Password: "); + if password.is_empty() { + return; + } + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); + if let Some(addr) = &self.addr { + match database::save_credentials(&self.conn, addr, &username, &hashed) { + Ok(_) => println!("Credentials saved for {}", addr), + Err(error) => println!("Failed to save credentials: {}", error), + } + } + } -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 - play [bet] - start a game (e.g., play slots 50)\r - help - show this message\r - forget-host [ip:port] - clear saved trust fingerprint and credentials\r - quit - quit the program" - ); + fn list_hosts(&mut self) { + let hosts = database::get_all_hosts(&self.conn); + if hosts.is_empty() { + println!("No remembered hosts."); + return; + } + println!("Remembered hosts:"); + for (host, fingerprint) in hosts { + match fingerprint { + Some(fingerprint) => println!(" {} - {}", host, fingerprint), + None => println!(" {} - (no fingerprint)", host), + } + } + } } -fn redraw_game(game_output: &str) { - // After Enter, \r moves cursor to col 0 of the "> command" line. - // Clear that line and everything below, then draw game UI. - execute!( - stdout(), - Clear(ClearType::CurrentLine), - Clear(ClearType::FromCursorDown), - MoveToColumn(0) - ) - .unwrap(); - print!("{}", game_output); - stdout().flush().unwrap(); -} +fn map_key(key: crossterm::event::KeyEvent) -> Option { + if key.modifiers.contains(KeyModifiers::CONTROL) { + match key.code { + KeyCode::Char('c') | KeyCode::Char('d') => return Some(Key::CtrlC), + _ => {} + } + } -fn redraw_game_lines(game_output: &str, lines: usize) { - use crossterm::cursor::MoveUp; - if lines > 0 { - execute!(stdout(), MoveUp(lines as u16)).unwrap(); + match key.code { + KeyCode::Char(c) => Some(Key::Char(c)), + KeyCode::Enter => Some(Key::Enter), + KeyCode::Backspace => Some(Key::Backspace), + KeyCode::Up => Some(Key::Up), + KeyCode::Down => Some(Key::Down), + KeyCode::Tab => Some(Key::Tab), + KeyCode::Esc => Some(Key::Esc), + _ => None, } - execute!( - stdout(), - Clear(ClearType::CurrentLine), - Clear(ClearType::FromCursorDown), - MoveToColumn(0) - ) - .unwrap(); - print!("{}", game_output); - stdout().flush().unwrap(); } -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 restore_terminal(cleanup_done: &Arc>) { + let mut done = cleanup_done.lock().unwrap(); + if !*done { + let _ = disable_raw_mode(); + let _ = execute!(stdout(), Show); + *done = true; + } } fn read_line(prompt: &str) -> String { @@ -840,7 +430,7 @@ fn read_line(prompt: &str) -> String { loop { match read().unwrap() { Event::Key(key) => { - if key.kind == crossterm::event::KeyEventKind::Press { + if key.kind == KeyEventKind::Press { match key.code { KeyCode::Enter => { println!(); @@ -849,10 +439,12 @@ fn read_line(prompt: &str) -> String { KeyCode::Backspace => { if !input.is_empty() { input.pop(); - // Move cursor back and clear from current position to end of line execute!( stdout(), - MoveToColumn((prompt_len + input.len() + 1) as u16), + MoveToColumn(input_cursor_column( + prompt_len, + input.len() + )), Clear(ClearType::UntilNewLine) ) .unwrap(); @@ -885,7 +477,7 @@ fn read_password(prompt: &str) -> String { loop { match read().unwrap() { Event::Key(key) => { - if key.kind == crossterm::event::KeyEventKind::Press { + if key.kind == KeyEventKind::Press { match key.code { KeyCode::Enter => { println!(); @@ -894,10 +486,12 @@ fn read_password(prompt: &str) -> String { KeyCode::Backspace => { if !input.is_empty() { input.pop(); - // Move cursor back and clear from current position to end of line execute!( stdout(), - MoveToColumn((prompt_len + input.len() + 1) as u16), + MoveToColumn(input_cursor_column( + prompt_len, + input.len() + )), Clear(ClearType::UntilNewLine) ) .unwrap(); -- cgit v1.2.3