//! Terminal input for the adapter: the command-line editor and key mapping. //! //! `ClientCore` knows nothing about keys or the prompt; this module turns //! keystrokes into `Command`s or a submitted line. use crate::client::games::roulette::Direction; use crate::client::ui::{ command_names, completion_lines, display_width, input_cursor_column, record_history, }; use crate::client::view::Command; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Key { Char(char), Enter, Backspace, Up, Down, Left, Right, Tab, Esc, CtrlC, } /// What the prompt wants the adapter to do after a key. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Input { /// Nothing changed. None, /// Redraw the prompt. Prompt, /// Run this command line. Submit(String), /// Print lines above the prompt. Print(Vec), Quit, } /// What a key means inside a running game. #[derive(Debug, Clone, PartialEq, Eq)] pub enum GameInput { None, Quit, Command(Command), } /// The prompt buffer, its history and its cursor. pub struct Editor { input: String, history: Vec, history_index: usize, } impl Default for Editor { fn default() -> Self { Self::new() } } impl Editor { pub fn new() -> Self { Self { input: String::new(), history: Vec::new(), history_index: 0, } } pub fn input(&self) -> &str { &self.input } pub fn clear(&mut self) { self.input.clear(); } /// Column of the cursor after the `> ` prompt. pub fn cursor_column(&self) -> u16 { input_cursor_column(2, display_width(&self.input)) } pub fn handle(&mut self, key: Key) -> Input { match key { Key::CtrlC => Input::Quit, Key::Enter => { let line = self.input.trim().to_string(); if line.is_empty() { return Input::None; } record_history(&mut self.history, &line); self.history_index = self.history.len(); self.input.clear(); Input::Submit(line) } Key::Backspace => { self.input.pop(); Input::Prompt } Key::Char(c) => { self.input.push(c); Input::Prompt } Key::Up => { if self.history_index > 0 { self.history_index -= 1; self.input = self.history[self.history_index].clone(); } Input::Prompt } Key::Down => { if self.history_index < self.history.len() { self.history_index += 1; self.input = if self.history_index == self.history.len() { String::new() } else { self.history[self.history_index].clone() }; } Input::Prompt } Key::Tab => { let prefix = self.input.trim(); let matches: Vec<&'static str> = command_names() .into_iter() .filter(|command| command.starts_with(prefix)) .collect(); match matches.len() { 1 => { self.input = matches[0].to_string(); Input::Prompt } 0 => Input::None, _ => Input::Print(completion_lines(&matches, 76)), } } // Esc, Left and Right do nothing on the prompt line. Key::Esc | Key::Left | Key::Right => Input::None, } } } /// Map a key to a command inside `game`. /// /// Invalid combinations simply produce a command the core ignores (for /// example `h` when it is not the player's turn), so this stays stateless. pub fn game_input(game: &str, key: Key) -> GameInput { match key { Key::CtrlC => GameInput::Quit, Key::Char('q') => GameInput::Command(Command::Leave), Key::Char('+') | Key::Char('=') => GameInput::Command(Command::AdjustBet(10)), Key::Char('-') => GameInput::Command(Command::AdjustBet(-10)), _ => match game { "roulette" => match key { Key::Up => GameInput::Command(Command::MoveCursor(Direction::Up)), Key::Down => GameInput::Command(Command::MoveCursor(Direction::Down)), Key::Left => GameInput::Command(Command::MoveCursor(Direction::Left)), Key::Right => GameInput::Command(Command::MoveCursor(Direction::Right)), Key::Enter => GameInput::Command(Command::PlaceBet), Key::Char(' ') => GameInput::Command(Command::Spin), Key::Char('c') => GameInput::Command(Command::ClearBets), _ => GameInput::None, }, "blackjack" => match key { Key::Char(' ') => GameInput::Command(Command::Deal), Key::Char('h') => GameInput::Command(Command::Hit), Key::Char('s') => GameInput::Command(Command::Stand), Key::Char('d') => GameInput::Command(Command::Double), _ => GameInput::None, }, "crash" => match key { Key::Char(' ') => GameInput::Command(Command::Start), Key::Char('c') => GameInput::Command(Command::CashOut), _ => GameInput::None, }, _ => match key { Key::Char(' ') => GameInput::Command(Command::Spin), Key::Esc => GameInput::Command(Command::Leave), _ => GameInput::None, }, }, } }