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;