//! Accelerator declarations: `:accel "Ctrl+S"` on a menu item. //! //! The grammar is here, in the compiler, so a mistyped accelerator is a //! pointed diagnostic rather than a shortcut that silently never fires. The //! runtime turns [`AccelIr`] into its own `Keystroke`. //! //! [`AccelKey`] is deliberately *not* the runtime's `Key`. That enum says //! what the platform sent — it has `Backspace`, `Tab`, `Other`. This one says //! what an accelerator may name, which is a different (overlapping) question: //! `Other` is not something you can write down, and `Tab` is not something an //! accelerator may steal. Two vocabularies, one conversion, no duplication. use crate::diagnostics::Diagnostic; use crate::sexpr::Span; /// A parsed accelerator: modifiers plus the key they qualify. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AccelIr { pub key: AccelKey, pub ctrl: bool, pub shift: bool, pub alt: bool, pub logo: bool, } /// A key an accelerator may name. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AccelKey { /// A single character, stored uppercase; matching is case-insensitive, so /// `Ctrl+S` and `Ctrl+s` are one accelerator. Character(String), Enter, Delete, Home, End, Left, Right, Up, Down, } impl AccelKey { /// The canonical spelling, for rendering the accelerator in the item. pub fn name(&self) -> String { match self { Self::Character(c) => c.clone(), Self::Enter => "Enter".to_owned(), Self::Delete => "Delete".to_owned(), Self::Home => "Home".to_owned(), Self::End => "End".to_owned(), Self::Left => "Left".to_owned(), Self::Right => "Right".to_owned(), Self::Up => "Up".to_owned(), Self::Down => "Down".to_owned(), } } fn parse(name: &str) -> Option { Some(match name.to_ascii_lowercase().as_str() { "enter" | "return" => Self::Enter, "delete" | "del" => Self::Delete, "home" => Self::Home, "end" => Self::End, "left" => Self::Left, "right" => Self::Right, "up" => Self::Up, "down" => Self::Down, _ => { let mut chars = name.chars(); let (Some(c), None) = (chars.next(), chars.next()) else { return None; }; Self::Character(c.to_ascii_uppercase().to_string()) } }) } } impl AccelIr { /// The canonical spelling — modifiers in a fixed order, then the key — so /// an item renders `Ctrl+Shift+S` however the author wrote it. pub fn display(&self) -> String { let mut out = String::new(); for (held, name) in [ (self.ctrl, "Ctrl"), (self.alt, "Alt"), (self.shift, "Shift"), (self.logo, "Super"), ] { if held { out.push_str(name); out.push('+'); } } out.push_str(&self.key.name()); out } } /// Parse `"Ctrl+Shift+S"`. pub fn parse_accel(text: &str, span: Span) -> Result { let bad = |what: &str| { Diagnostic::new( format!( "`{text}` is not an accelerator: {what}. Write modifiers and a \ key joined by `+`, like `Ctrl+S`, `Ctrl+Shift+P`, or `Delete` \ (modifiers: Ctrl, Alt, Shift, Super)" ), span, ) }; let mut accel = AccelIr { key: AccelKey::Enter, ctrl: false, shift: false, alt: false, logo: false, }; let mut key = None; for part in text.split('+') { let part = part.trim(); if part.is_empty() { return Err(bad("it has an empty part")); } let slot = match part.to_ascii_lowercase().as_str() { "ctrl" | "control" => &mut accel.ctrl, "shift" => &mut accel.shift, "alt" => &mut accel.alt, "super" | "logo" | "cmd" | "meta" => &mut accel.logo, _ => { if key.is_some() { return Err(bad("it names more than one key")); } key = Some(AccelKey::parse(part).ok_or_else(|| { bad(&format!("`{part}` is neither a modifier nor a key name")) })?); continue; } }; if *slot { return Err(bad(&format!("`{part}` appears twice"))); } *slot = true; } let Some(key) = key else { return Err(bad("it names only modifiers")); }; accel.key = key; Ok(accel) } #[cfg(test)] mod tests;