diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 19:00:04 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 19:00:04 +0100 |
| commit | d251b54fe40f6f8f95261b4df65bced2fb728c63 (patch) | |
| tree | 11a4ddffd68f1f497498d36096435a56d9f13c1b /src/client/input.rs | |
| parent | v4.0.5: login and registration as core commands (diff) | |
| download | zagan-bet-d251b54fe40f6f8f95261b4df65bced2fb728c63.tar.gz zagan-bet-d251b54fe40f6f8f95261b4df65bced2fb728c63.zip | |
v4.0.6: move the prompt editor and key mapping out of the core
Diffstat (limited to '')
| -rw-r--r-- | src/client/input.rs | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/src/client/input.rs b/src/client/input.rs new file mode 100644 index 0000000..0c9d99d --- /dev/null +++ b/src/client/input.rs | |||
| @@ -0,0 +1,183 @@ | |||
| 1 | //! Terminal input for the adapter: the command-line editor and key mapping. | ||
| 2 | //! | ||
| 3 | //! `ClientCore` knows nothing about keys or the prompt; this module turns | ||
| 4 | //! keystrokes into `Command`s or a submitted line. | ||
| 5 | |||
| 6 | use crate::client::games::roulette::Direction; | ||
| 7 | use crate::client::ui::{ | ||
| 8 | command_names, completion_lines, display_width, input_cursor_column, record_history, | ||
| 9 | }; | ||
| 10 | use crate::client::view::Command; | ||
| 11 | |||
| 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 13 | pub enum Key { | ||
| 14 | Char(char), | ||
| 15 | Enter, | ||
| 16 | Backspace, | ||
| 17 | Up, | ||
| 18 | Down, | ||
| 19 | Left, | ||
| 20 | Right, | ||
| 21 | Tab, | ||
| 22 | Esc, | ||
| 23 | CtrlC, | ||
| 24 | } | ||
| 25 | |||
| 26 | /// What the prompt wants the adapter to do after a key. | ||
| 27 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 28 | pub enum Input { | ||
| 29 | /// Nothing changed. | ||
| 30 | None, | ||
| 31 | /// Redraw the prompt. | ||
| 32 | Prompt, | ||
| 33 | /// Run this command line. | ||
| 34 | Submit(String), | ||
| 35 | /// Print lines above the prompt. | ||
| 36 | Print(Vec<String>), | ||
| 37 | Quit, | ||
| 38 | } | ||
| 39 | |||
| 40 | /// What a key means inside a running game. | ||
| 41 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 42 | pub enum GameInput { | ||
| 43 | None, | ||
| 44 | Quit, | ||
| 45 | Command(Command), | ||
| 46 | } | ||
| 47 | |||
| 48 | /// The prompt buffer, its history and its cursor. | ||
| 49 | pub struct Editor { | ||
| 50 | input: String, | ||
| 51 | history: Vec<String>, | ||
| 52 | history_index: usize, | ||
| 53 | } | ||
| 54 | |||
| 55 | impl Default for Editor { | ||
| 56 | fn default() -> Self { | ||
| 57 | Self::new() | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | impl Editor { | ||
| 62 | pub fn new() -> Self { | ||
| 63 | Self { | ||
| 64 | input: String::new(), | ||
| 65 | history: Vec::new(), | ||
| 66 | history_index: 0, | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn input(&self) -> &str { | ||
| 71 | &self.input | ||
| 72 | } | ||
| 73 | |||
| 74 | pub fn clear(&mut self) { | ||
| 75 | self.input.clear(); | ||
| 76 | } | ||
| 77 | |||
| 78 | /// Column of the cursor after the `> ` prompt. | ||
| 79 | pub fn cursor_column(&self) -> u16 { | ||
| 80 | input_cursor_column(2, display_width(&self.input)) | ||
| 81 | } | ||
| 82 | |||
| 83 | pub fn handle(&mut self, key: Key) -> Input { | ||
| 84 | match key { | ||
| 85 | Key::CtrlC => Input::Quit, | ||
| 86 | Key::Enter => { | ||
| 87 | let line = self.input.trim().to_string(); | ||
| 88 | if line.is_empty() { | ||
| 89 | return Input::None; | ||
| 90 | } | ||
| 91 | record_history(&mut self.history, &line); | ||
| 92 | self.history_index = self.history.len(); | ||
| 93 | self.input.clear(); | ||
| 94 | Input::Submit(line) | ||
| 95 | } | ||
| 96 | Key::Backspace => { | ||
| 97 | self.input.pop(); | ||
| 98 | Input::Prompt | ||
| 99 | } | ||
| 100 | Key::Char(c) => { | ||
| 101 | self.input.push(c); | ||
| 102 | Input::Prompt | ||
| 103 | } | ||
| 104 | Key::Up => { | ||
| 105 | if self.history_index > 0 { | ||
| 106 | self.history_index -= 1; | ||
| 107 | self.input = self.history[self.history_index].clone(); | ||
| 108 | } | ||
| 109 | Input::Prompt | ||
| 110 | } | ||
| 111 | Key::Down => { | ||
| 112 | if self.history_index < self.history.len() { | ||
| 113 | self.history_index += 1; | ||
| 114 | self.input = if self.history_index == self.history.len() { | ||
| 115 | String::new() | ||
| 116 | } else { | ||
| 117 | self.history[self.history_index].clone() | ||
| 118 | }; | ||
| 119 | } | ||
| 120 | Input::Prompt | ||
| 121 | } | ||
| 122 | Key::Tab => { | ||
| 123 | let prefix = self.input.trim(); | ||
| 124 | let matches: Vec<&'static str> = command_names() | ||
| 125 | .into_iter() | ||
| 126 | .filter(|command| command.starts_with(prefix)) | ||
| 127 | .collect(); | ||
| 128 | match matches.len() { | ||
| 129 | 1 => { | ||
| 130 | self.input = matches[0].to_string(); | ||
| 131 | Input::Prompt | ||
| 132 | } | ||
| 133 | 0 => Input::None, | ||
| 134 | _ => Input::Print(completion_lines(&matches, 76)), | ||
| 135 | } | ||
| 136 | } | ||
| 137 | // Esc, Left and Right do nothing on the prompt line. | ||
| 138 | Key::Esc | Key::Left | Key::Right => Input::None, | ||
| 139 | } | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | /// Map a key to a command inside `game`. | ||
| 144 | /// | ||
| 145 | /// Invalid combinations simply produce a command the core ignores (for | ||
| 146 | /// example `h` when it is not the player's turn), so this stays stateless. | ||
| 147 | pub fn game_input(game: &str, key: Key) -> GameInput { | ||
| 148 | match key { | ||
| 149 | Key::CtrlC => GameInput::Quit, | ||
| 150 | Key::Char('q') => GameInput::Command(Command::Leave), | ||
| 151 | Key::Char('+') | Key::Char('=') => GameInput::Command(Command::AdjustBet(10)), | ||
| 152 | Key::Char('-') => GameInput::Command(Command::AdjustBet(-10)), | ||
| 153 | _ => match game { | ||
| 154 | "roulette" => match key { | ||
| 155 | Key::Up => GameInput::Command(Command::MoveCursor(Direction::Up)), | ||
| 156 | Key::Down => GameInput::Command(Command::MoveCursor(Direction::Down)), | ||
| 157 | Key::Left => GameInput::Command(Command::MoveCursor(Direction::Left)), | ||
| 158 | Key::Right => GameInput::Command(Command::MoveCursor(Direction::Right)), | ||
| 159 | Key::Enter => GameInput::Command(Command::PlaceBet), | ||
| 160 | Key::Char(' ') => GameInput::Command(Command::Spin), | ||
| 161 | Key::Char('c') => GameInput::Command(Command::ClearBets), | ||
| 162 | _ => GameInput::None, | ||
| 163 | }, | ||
| 164 | "blackjack" => match key { | ||
| 165 | Key::Char(' ') => GameInput::Command(Command::Deal), | ||
| 166 | Key::Char('h') => GameInput::Command(Command::Hit), | ||
| 167 | Key::Char('s') => GameInput::Command(Command::Stand), | ||
| 168 | Key::Char('d') => GameInput::Command(Command::Double), | ||
| 169 | _ => GameInput::None, | ||
| 170 | }, | ||
| 171 | "crash" => match key { | ||
| 172 | Key::Char(' ') => GameInput::Command(Command::Start), | ||
| 173 | Key::Char('c') => GameInput::Command(Command::CashOut), | ||
| 174 | _ => GameInput::None, | ||
| 175 | }, | ||
| 176 | _ => match key { | ||
| 177 | Key::Char(' ') => GameInput::Command(Command::Spin), | ||
| 178 | Key::Esc => GameInput::Command(Command::Leave), | ||
| 179 | _ => GameInput::None, | ||
| 180 | }, | ||
| 181 | }, | ||
| 182 | } | ||
| 183 | } | ||
