use std::sync::LazyLock;
use newtui::Key;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MetaAction {
Zoom,
Resize,
Redraw,
Help,
}
impl MetaAction {
fn parse(name: &str) -> Option<Self> {
match name {
"zoom" => Some(Self::Zoom),
"resize" => Some(Self::Resize),
"redraw" => Some(Self::Redraw),
"help" => Some(Self::Help),
_ => None,
}
}
pub(crate) fn name(self) -> &'static str {
match self {
Self::Zoom => "zoom",
Self::Resize => "resize",
Self::Redraw => "redraw",
Self::Help => "help",
}
}
}
pub(crate) const DEFAULT_PREFIX: &str = "ctrl+space";
pub(crate) fn parse_chord(text: &str) -> Option<Key> {
let rest = text.trim().to_ascii_lowercase();
let rest = rest
.strip_prefix("ctrl+")
.or_else(|| rest.strip_prefix("ctrl-"))?;
match rest {
"space" => Some(Key::Ctrl(' ')),
one if one.len() == 1 && one.as_bytes()[0].is_ascii_lowercase() => {
Some(Key::Ctrl(char::from(one.as_bytes()[0])))
}
_ => None,
}
}
pub(crate) fn chord_label(key: Key) -> String {
match key {
Key::Ctrl(' ') => "ctrl+space".to_string(),
Key::Ctrl(c) => format!("ctrl+{c}"),
other => format!("{other:?}"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Bindings(Vec<(char, MetaAction)>);
impl Bindings {
pub(crate) fn from_toml(text: &str) -> Result<Self, String> {
#[derive(serde::Deserialize)]
struct File {
bindings: std::collections::BTreeMap<String, String>,
}
let file: File = toml::from_str(text).map_err(|e| e.to_string())?;
file.bindings
.into_iter()
.map(|(key, action)| {
let mut chars = key.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return Err(format!("binding `{key}` is not one key"));
};
MetaAction::parse(&action)
.map(|action| (c, action))
.ok_or_else(|| format!("binding `{key}`: unknown operation `{action}`"))
})
.collect::<Result<_, _>>()
.map(Self)
}
fn action(&self, c: char) -> Option<MetaAction> {
self.0
.iter()
.find(|(k, _)| *k == c)
.map(|(_, action)| *action)
}
pub(crate) fn describe(&self) -> String {
self.0
.iter()
.map(|(k, action)| format!("{k} {}", action.name()))
.collect::<Vec<_>>()
.join(" · ")
}
}
pub(crate) static BINDINGS: LazyLock<Bindings> = LazyLock::new(|| {
Bindings::from_toml(include_str!("../assets/prefix_keys.toml"))
.expect("assets/prefix_keys.toml is compiled in and checked by this module's tests")
});
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Step {
Pass(Key),
Armed,
Act(MetaAction),
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Sequencer {
prefix: Key,
armed: bool,
}
impl Sequencer {
pub(crate) fn new(prefix: Key) -> Self {
Self {
prefix,
armed: false,
}
}
pub(crate) fn armed(self) -> bool {
self.armed
}
pub(crate) fn feed(&mut self, key: Key, bindings: &Bindings) -> Step {
if !self.armed {
if key == self.prefix {
self.armed = true;
return Step::Armed;
}
return Step::Pass(key);
}
self.armed = false;
if key == self.prefix {
return Step::Pass(key);
}
match key {
Key::Char(c) => bindings.action(c).map_or(Step::Cancelled, Step::Act),
_ => Step::Cancelled,
}
}
}
pub(crate) fn configured() -> Key {
parse_chord(&crate::settings_form::prefix_setting())
.unwrap_or_else(|| parse_chord(DEFAULT_PREFIX).expect("the default chord parses"))
}
static CURRENT: std::sync::RwLock<Option<Key>> = std::sync::RwLock::new(None);
pub(crate) fn current() -> Key {
if let Some(key) = CURRENT.read().ok().and_then(|current| *current) {
return key;
}
let key = configured();
if let Ok(mut current) = CURRENT.write() {
*current = Some(key);
}
key
}
pub(crate) fn invalidate() {
if let Ok(mut current) = CURRENT.write() {
*current = None;
}
}
pub(crate) fn active_label() -> String {
chord_label(current())
}
#[cfg(test)]
#[path = "prefix_tests.rs"]
mod tests;