accel.rs raw

//! 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<Self> {
        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<AccelIr, Diagnostic> {
    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;