From 24e3bb54db579fb9aad8df2a591527dc77fe4cb4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 19:19:49 +0100 Subject: 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). --- src/client/shell.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/client/shell.rs (limited to 'src/client/shell.rs') 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 @@ +//! Parser for the terminal's command line. +//! +//! The core speaks `Command`; this module turns one typed line into either a +//! core command or an action only an interactive frontend can perform +//! (connecting, prompting for a password, listing hosts). + +use crate::client::ui::parse_bet_arg; +use crate::client::view::Command; +use crate::history::{DEFAULT_HISTORY, MAX_HISTORY}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Shell { + /// A command the core can run on its own. + Run(Command), + Login, + Register, + SetCred, + Connect(Option), + Disconnect, + Hosts, + ForgetHost(Option), + ForgetLogin(Option), + Help, + Quit, + Empty, + Usage(String), +} + +/// Split off the first whitespace-delimited word. +/// +/// The remainder keeps its internal spacing (so `send` can echo a message +/// verbatim) but loses the separator run before it. +fn split_word(line: &str) -> (&str, &str) { + match line.find(char::is_whitespace) { + Some(index) => (&line[..index], line[index..].trim_start()), + None => (line, ""), + } +} + +pub fn parse(line: &str) -> Shell { + let line = line.trim(); + if line.is_empty() { + return Shell::Empty; + } + + let (word, rest) = split_word(line); + match word { + "play" => { + let (game, bet) = split_word(rest); + if game.is_empty() { + return Shell::Usage("Usage: play [bet]".to_string()); + } + let bet = if bet.is_empty() { + None + } else { + match parse_bet_arg(bet) { + Ok(bet) => Some(bet), + Err(message) => return Shell::Usage(message), + } + }; + Shell::Run(Command::Play { + game: game.to_string(), + bet, + }) + } + "history" => { + let (raw, _) = split_word(rest); + let limit = if raw.is_empty() { + DEFAULT_HISTORY + } else { + match raw.parse::() { + Ok(count) if (1..=MAX_HISTORY).contains(&count) => count, + _ => return Shell::Usage(format!("Usage: history [1-{MAX_HISTORY}]")), + } + }; + Shell::Run(Command::History { limit }) + } + "games" => Shell::Run(Command::Games), + "logout" => Shell::Run(Command::Logout), + "ping" => Shell::Run(Command::Ping), + "send" => { + if rest.is_empty() { + Shell::Usage("Usage: send ".to_string()) + } else { + Shell::Run(Command::Echo(rest.to_string())) + } + } + "info" => Shell::Run(Command::Info), + "help" => Shell::Help, + "connect" => Shell::Connect(optional_word(rest)), + "login" => Shell::Login, + "register" => Shell::Register, + "setcred" => Shell::SetCred, + "disconnect" => Shell::Disconnect, + "hosts" => Shell::Hosts, + "forget-host" => Shell::ForgetHost(optional_word(rest)), + "forget-login" => Shell::ForgetLogin(optional_word(rest)), + "quit" => Shell::Quit, + _ => Shell::Help, + } +} + +fn optional_word(rest: &str) -> Option { + let (word, _) = split_word(rest); + if word.is_empty() { + None + } else { + Some(word.to_string()) + } +} -- cgit v1.2.3