//! Pure text/history helpers for the client TUI. //! //! Nothing here touches the terminal, so it is unit-tested from `tests/` //! without a PTY. struct CommandHelp { name: &'static str, usage: &'static str, description: &'static str, } const COMMANDS: &[CommandHelp] = &[ CommandHelp { name: "connect", usage: "connect :", description: "connect to the server", }, CommandHelp { name: "send", usage: "send ", description: "send a message to the server", }, CommandHelp { name: "disconnect", usage: "disconnect", description: "disconnect from the server", }, CommandHelp { name: "ping", usage: "ping", description: "ping the server", }, CommandHelp { name: "login", usage: "login", description: "login to the server", }, CommandHelp { name: "register", usage: "register", description: "register a new user", }, CommandHelp { name: "setcred", usage: "setcred", description: "set credentials for a host", }, CommandHelp { name: "logout", usage: "logout", description: "logout from the server", }, CommandHelp { name: "info", usage: "info", description: "show account and connection info", }, CommandHelp { name: "play", usage: "play [bet]", description: "start a game (e.g. play slots 50)", }, CommandHelp { name: "games", usage: "games", description: "list available games", }, CommandHelp { name: "help", usage: "help", description: "show this message", }, CommandHelp { name: "forget-host", usage: "forget-host [ip:port]", description: "clear saved trust fingerprint and credentials", }, CommandHelp { name: "hosts", usage: "hosts", description: "list remembered hosts", }, CommandHelp { name: "quit", usage: "quit", description: "quit the program", }, ]; pub fn command_names() -> Vec<&'static str> { COMMANDS.iter().map(|command| command.name).collect() } pub fn help_lines() -> Vec { let mut lines = vec!["Available commands:".to_string()]; for command in COMMANDS { lines.push(format!( " {:<28} - {}", command.usage, command.description )); } lines } /// Wrap a completion list to `width` columns, one space between names. pub fn completion_lines(commands: &[&str], width: usize) -> Vec { let mut lines = vec!["Possible completions:".to_string()]; let mut current = String::new(); for command in commands { let needed = if current.is_empty() { command.len() } else { current.len() + 1 + command.len() }; if !current.is_empty() && needed > width { lines.push(current.clone()); current.clear(); } if !current.is_empty() { current.push(' '); } current.push_str(command); } if !current.is_empty() { lines.push(current); } lines } /// Add a command to the history, skipping blanks and consecutive duplicates. pub fn record_history(history: &mut Vec, line: &str) { let line = line.trim(); if line.is_empty() { return; } if history.last().is_some_and(|last| last == line) { return; } history.push(line.to_string()); } /// Parse the optional bet argument of `play [bet]`. pub fn parse_bet_arg(raw: &str) -> Result { let bet = raw .trim() .parse::() .map_err(|_| format!("Invalid bet: {raw}"))?; if bet < 10 { return Err("Bet must be at least 10 PLN".to_string()); } Ok(bet) } /// Lines to move the cursor up to reach the first line of a region that ends /// on the current line. `render()` has no trailing newline, so a 7-line board /// needs `MoveUp(6)`. pub fn lines_to_move_up(previous_lines: usize) -> u16 { previous_lines .saturating_sub(1) .min(u16::MAX as usize) as u16 } /// Column (0-based) to move to after the given prompt and input text. pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { prompt_len .saturating_add(input_len) .min(u16::MAX as usize) as u16 }