diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 19:19:49 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 19:19:49 +0100 |
| commit | 24e3bb54db579fb9aad8df2a591527dc77fe4cb4 (patch) | |
| tree | af9a0c290f2675068e8396823f641f24bbe4d308 /src/client/shell.rs | |
| parent | v4.0.6: move the prompt editor and key mapping out of the core (diff) | |
| download | zagan-bet-24e3bb54db579fb9aad8df2a591527dc77fe4cb4.tar.gz zagan-bet-24e3bb54db579fb9aad8df2a591527dc77fe4cb4.zip | |
v4.0.7: finish the Phase 1 boundary with a command shell
The terminal command line moves to client::shell: a typed line becomes a core Command or an adapter action (connect, login, register, set-credential, host maintenance). ClientCore loses submit_line and the last TUI-shaped effects (Interactive, SetPrompt, ListHosts, ForgetHost, AnimateRoulette); the adapter animates roulette from the Result view.
Adds forget-login, which drops the stored credential for the pinned key and keeps trust, distinct from logout (session only) and forget-host (trust too).
Diffstat (limited to '')
| -rw-r--r-- | src/client/shell.rs | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/client/shell.rs b/src/client/shell.rs new file mode 100644 index 0000000..73843c8 --- /dev/null +++ b/src/client/shell.rs | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | //! Parser for the terminal's command line. | ||
| 2 | //! | ||
| 3 | //! The core speaks `Command`; this module turns one typed line into either a | ||
| 4 | //! core command or an action only an interactive frontend can perform | ||
| 5 | //! (connecting, prompting for a password, listing hosts). | ||
| 6 | |||
| 7 | use crate::client::ui::parse_bet_arg; | ||
| 8 | use crate::client::view::Command; | ||
| 9 | use crate::history::{DEFAULT_HISTORY, MAX_HISTORY}; | ||
| 10 | |||
| 11 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 12 | pub enum Shell { | ||
| 13 | /// A command the core can run on its own. | ||
| 14 | Run(Command), | ||
| 15 | Login, | ||
| 16 | Register, | ||
| 17 | SetCred, | ||
| 18 | Connect(Option<String>), | ||
| 19 | Disconnect, | ||
| 20 | Hosts, | ||
| 21 | ForgetHost(Option<String>), | ||
| 22 | ForgetLogin(Option<String>), | ||
| 23 | Help, | ||
| 24 | Quit, | ||
| 25 | Empty, | ||
| 26 | Usage(String), | ||
| 27 | } | ||
| 28 | |||
| 29 | /// Split off the first whitespace-delimited word. | ||
| 30 | /// | ||
| 31 | /// The remainder keeps its internal spacing (so `send` can echo a message | ||
| 32 | /// verbatim) but loses the separator run before it. | ||
| 33 | fn split_word(line: &str) -> (&str, &str) { | ||
| 34 | match line.find(char::is_whitespace) { | ||
| 35 | Some(index) => (&line[..index], line[index..].trim_start()), | ||
| 36 | None => (line, ""), | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | pub fn parse(line: &str) -> Shell { | ||
| 41 | let line = line.trim(); | ||
| 42 | if line.is_empty() { | ||
| 43 | return Shell::Empty; | ||
| 44 | } | ||
| 45 | |||
| 46 | let (word, rest) = split_word(line); | ||
| 47 | match word { | ||
| 48 | "play" => { | ||
| 49 | let (game, bet) = split_word(rest); | ||
| 50 | if game.is_empty() { | ||
| 51 | return Shell::Usage("Usage: play <game> [bet]".to_string()); | ||
| 52 | } | ||
| 53 | let bet = if bet.is_empty() { | ||
| 54 | None | ||
| 55 | } else { | ||
| 56 | match parse_bet_arg(bet) { | ||
| 57 | Ok(bet) => Some(bet), | ||
| 58 | Err(message) => return Shell::Usage(message), | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | Shell::Run(Command::Play { | ||
| 62 | game: game.to_string(), | ||
| 63 | bet, | ||
| 64 | }) | ||
| 65 | } | ||
| 66 | "history" => { | ||
| 67 | let (raw, _) = split_word(rest); | ||
| 68 | let limit = if raw.is_empty() { | ||
| 69 | DEFAULT_HISTORY | ||
| 70 | } else { | ||
| 71 | match raw.parse::<u32>() { | ||
| 72 | Ok(count) if (1..=MAX_HISTORY).contains(&count) => count, | ||
| 73 | _ => return Shell::Usage(format!("Usage: history [1-{MAX_HISTORY}]")), | ||
| 74 | } | ||
| 75 | }; | ||
| 76 | Shell::Run(Command::History { limit }) | ||
| 77 | } | ||
| 78 | "games" => Shell::Run(Command::Games), | ||
| 79 | "logout" => Shell::Run(Command::Logout), | ||
| 80 | "ping" => Shell::Run(Command::Ping), | ||
| 81 | "send" => { | ||
| 82 | if rest.is_empty() { | ||
| 83 | Shell::Usage("Usage: send <message>".to_string()) | ||
| 84 | } else { | ||
| 85 | Shell::Run(Command::Echo(rest.to_string())) | ||
| 86 | } | ||
| 87 | } | ||
| 88 | "info" => Shell::Run(Command::Info), | ||
| 89 | "help" => Shell::Help, | ||
| 90 | "connect" => Shell::Connect(optional_word(rest)), | ||
| 91 | "login" => Shell::Login, | ||
| 92 | "register" => Shell::Register, | ||
| 93 | "setcred" => Shell::SetCred, | ||
| 94 | "disconnect" => Shell::Disconnect, | ||
| 95 | "hosts" => Shell::Hosts, | ||
| 96 | "forget-host" => Shell::ForgetHost(optional_word(rest)), | ||
| 97 | "forget-login" => Shell::ForgetLogin(optional_word(rest)), | ||
| 98 | "quit" => Shell::Quit, | ||
| 99 | _ => Shell::Help, | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | fn optional_word(rest: &str) -> Option<String> { | ||
| 104 | let (word, _) = split_word(rest); | ||
| 105 | if word.is_empty() { | ||
| 106 | None | ||
| 107 | } else { | ||
| 108 | Some(word.to_string()) | ||
| 109 | } | ||
| 110 | } | ||
