← back rustamp | shortcuts Nov 15, 2025One thing I don't like is the different shortcuts for each UI engine. Ughh!
It should be relative simple to change that with the architecture we designed. Let us centralize that with
input.rs:use crate::engine::Command; use std::path::PathBuf; #[derive(Debug, Clone)] pub enum UiAction { CommandMode, Open, Add, Quit, } #[derive(Debug, Clone)] pub enum Action { Engine(Command), Ui(UiAction), } pub fn decode_key(ch: char) -> Option<Action> { Some(match ch { // meta ':' => Action::Ui(UiAction::CommandMode), 'q' | 'Q' => Action::Ui(UiAction::Quit), // dialogs / file actions 'o' | 'O' => Action::Ui(UiAction::Open), 'a' | 'A' => Action::Ui(UiAction::Add), // transport 'p' | 'P' => Action::Engine(Command::PlaySelected), 'i' | 'I' => Action::Engine(Command::Pause), 's' | 'S' => Action::Engine(Command::Stop), 't' | 'T' => Action::Engine(Command::TogglePlayPause), // playlist actions 'l' | 'L' => Action::Engine(Command::LoadSelected), 'd' | 'D' => Action::Engine(Command::RemoveSelected), // navigation 'j' => Action::Engine(Command::SelectDown), 'k' => Action::Engine(Command::SelectUp), 'g' => Action::Engine(Command::SelectTop), 'G' => Action::Engine(Command::SelectBottom), // reorder 'J' => Action::Engine(Command::MoveSelectedDown), 'K' => Action::Engine(Command::MoveSelectedUp), // track stepping 'n' | 'N' => Action::Engine(Command::NextTrack), 'b' | 'B' => Action::Engine(Command::PrevTrack), _ => return None, }) } pub fn parse_command_line(line: &str) -> Result<Vec<Command>, String> { let line = line.trim().strip_prefix(':').unwrap_or(line).trim(); if line.is_empty() { return Ok(vec![]); } let tokens = shell_split(line)?; if tokens.is_empty() { return Ok(vec![]); } let head = tokens[0].to_lowercase(); let cmd = match head.as_str() { "open" | "o" => { let p = tokens.get(1).ok_or_else(|| "open requires a path".to_string())?; Command::FilePicked(Some(PathBuf::from(p))) } "add" | "a" => { if tokens.len() < 2 { return Err("add requires one or more paths".into()); } let ps = tokens.iter().skip(1).map(|p| PathBuf::from(p)).collect(); return Ok(vec![Command::FilesPicked(ps)]); } "play" | "p" => Command::PlaySelected, "pause" | "i" => Command::Pause, "stop" | "s" => Command::Stop, "toggle" | "t" => Command::TogglePlayPause, "next" | "n" => Command::NextTrack, "prev" | "b" => Command::PrevTrack, "load" | "l" => Command::LoadSelected, "rm" | "del" | "d" => Command::RemoveSelected, _ => { let joined = tokens.join(" "); return Ok(vec![Command::FilePicked(Some(PathBuf::from(joined)))]); } }; Ok(vec![cmd]) } /// Minimal shell-like splitter: /// - whitespace separates tokens /// - supports single and double quotes /// - supports backslash escapes fn shell_split(s: &str) -> Result<Vec<String>, String> { let mut out: Vec<String> = vec![]; let mut cur = String::new(); let mut chars = s.chars().peekable(); let mut quote: Option<char> = None; while let Some(ch) = chars.next() { match (quote, ch) { (_, '\\') => { if let Some(next) = chars.next() { cur.push(next); } } (None, '\'' | '"') => quote = Some(ch), (Some(q), c) if c == q => quote = None, (None, c) if c.is_whitespace() => { if !cur.is_empty() { out.push(std::mem::take(&mut cur)); } while matches!(chars.peek(), Some(p) if p.is_whitespace()) { chars.next(); } } (_, c) => cur.push(c), } } if quote.is_some() { return Err("unterminated quote in command".into()); } if !cur.is_empty() { out.push(cur); } Ok(out) } pub fn help_line() -> &'static str { "keys: o open a add : cmd p play i pause s stop n next b prev j/k move d del q quit" }Notice the take one shortcuts? It's quite similar approach to VIM, ain't no coincidence broddha. I'm up for simplicity, always, and VIM approach, I, for one, believe is the best approach.
rustamp-tui
Now, at the RatatUI part, we need to update our
handle_norma_key:fn handle_normal_key(engine: &mut Engine, state: &mut UiState, code: KeyCode, mods: KeyModifiers) -> bool { if mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT) { return false; } let KeyCode::Char(ch) = code else { return false; }; let Some(action) = input::decode_key(ch) else { return false; }; match action { Action::Ui(UiAction::Quit) => return true, Action::Ui(UiAction::CommandMode) => { state.mode = Mode::Command; state.cmdline.clear(); } Action::Ui(UiAction::Open) => { state.mode = Mode::Command; state.cmdline = "open ".into(); } Action::Ui(UiAction::Add) => { state.mode = Mode::Command; state.cmdline = "add ".into(); } Action::Engine(cmd) => step(engine, cmd), } false }and update our parser:
match input::parse_command_line(&state.cmdline) { Ok(cmds) => for cmd in cmds { step(engine, cmd); }, Err(e) => engine.machine.error = Some(e), }and delete our now deprecated function
parse_cmdline.rustamp-iced
We only need to update our keyboard handler at
app:match input::decode_key(ch) { Some(Action::Ui(UiAction::Open)) => return Task::done(Message::OpenFile), Some(Action::Ui(UiAction::Add)) => return Task::done(Message::AddToPlaylist), Some(Action::Ui(UiAction::CommandMode)) => Task::none(), Some(Action::Ui(UiAction::Quit)) => Task::none(), Some(Action::Engine(cmd)) => { self.engine.apply(cmd); Task::none() } None => Task::none(), }