summaryrefslogtreecommitdiffstats
path: root/src/client/input.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 19:00:04 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 19:00:04 +0100
commitd251b54fe40f6f8f95261b4df65bced2fb728c63 (patch)
tree11a4ddffd68f1f497498d36096435a56d9f13c1b /src/client/input.rs
parentv4.0.5: login and registration as core commands (diff)
downloadzagan-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.rs183
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
6use crate::client::games::roulette::Direction;
7use crate::client::ui::{
8 command_names, completion_lines, display_width, input_cursor_column, record_history,
9};
10use crate::client::view::Command;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub 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)]
28pub 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)]
42pub enum GameInput {
43 None,
44 Quit,
45 Command(Command),
46}
47
48/// The prompt buffer, its history and its cursor.
49pub struct Editor {
50 input: String,
51 history: Vec<String>,
52 history_index: usize,
53}
54
55impl Default for Editor {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl 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.
147pub 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}