#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Prefix {
K,
Q,
O,
P,
}
impl Prefix {
pub fn label(self) -> &'static str {
match self {
Prefix::K => "^K Block & File",
Prefix::Q => "^Q Quick",
Prefix::O => "^O Onscreen",
Prefix::P => "^P Project",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cmd {
Up,
Down,
Left,
Right,
WordLeft,
WordRight,
ScrollUp,
ScrollDown,
PageUp,
PageDown,
LineStart,
LineEnd,
ScreenTop,
ScreenBottom,
DocStart,
DocEnd,
SentenceBack,
SentenceFwd,
ParaBack,
ParaFwd,
PrevPosition,
Outline,
NextMisspelling,
DeleteRight,
DeleteLeft,
DeleteWordRight,
DeleteLine,
DeleteToLineEnd,
InsertBlankLine,
TransposeWords,
TransposeChars,
Undo,
FindIncremental,
FindReplace,
FindNext,
MacroRecord,
MacroPlay,
Palette,
BlockBegin,
BlockEnd,
BlockCopy,
BlockMove,
BlockDelete,
BlockWrite,
BlockRead,
BlockHide,
BlockPrev,
Put,
CopyFromOther,
JumpBlockBegin,
JumpBlockEnd,
JumpBlockSource,
Save,
SaveExit,
Quit,
ExportClean,
ExportManuscript,
ExportDocx,
ExportEpub,
ExportHtml,
ExportProjectDocx,
ExportProjectEpub,
ExportProjectHtml,
CycleTheme,
RevealCodes,
ToggleWrap,
SetWrapMargin,
ToggleInsert,
CycleHelpLevel,
ToggleSpellcheck,
AddToDictionary,
ToggleTypewriter,
OtherWindow,
ProjectNew,
ProjectOpen,
BinderToggle,
BinderMoveUp,
BinderMoveDown,
ProjectAddDoc,
ProjectRemoveDoc,
WordCount,
SetGoal,
StatsOverlay,
ProjectFind,
ProjectReplace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chord {
Bare(char),
Pref(Prefix, char),
}
pub struct Binding {
pub cmd: Cmd,
pub chord: Chord,
pub name: &'static str,
}
use Chord::{Bare, Pref};
use Prefix::{K, O, P, Q};
pub const BINDINGS: &[Binding] = &[
Binding {
cmd: Cmd::Up,
chord: Bare('e'),
name: "cursor up",
},
Binding {
cmd: Cmd::Down,
chord: Bare('x'),
name: "cursor down",
},
Binding {
cmd: Cmd::Left,
chord: Bare('s'),
name: "cursor left",
},
Binding {
cmd: Cmd::Right,
chord: Bare('d'),
name: "cursor right",
},
Binding {
cmd: Cmd::WordLeft,
chord: Bare('a'),
name: "word left",
},
Binding {
cmd: Cmd::WordRight,
chord: Bare('f'),
name: "word right",
},
Binding {
cmd: Cmd::ScrollUp,
chord: Bare('w'),
name: "scroll up",
},
Binding {
cmd: Cmd::ScrollDown,
chord: Bare('z'),
name: "scroll down",
},
Binding {
cmd: Cmd::PageUp,
chord: Bare('r'),
name: "page up",
},
Binding {
cmd: Cmd::PageDown,
chord: Bare('c'),
name: "page down",
},
Binding {
cmd: Cmd::DeleteRight,
chord: Bare('g'),
name: "delete char",
},
Binding {
cmd: Cmd::DeleteLeft,
chord: Bare('h'),
name: "delete left",
},
Binding {
cmd: Cmd::DeleteWordRight,
chord: Bare('t'),
name: "delete word",
},
Binding {
cmd: Cmd::DeleteLine,
chord: Bare('y'),
name: "delete line",
},
Binding {
cmd: Cmd::InsertBlankLine,
chord: Bare('n'),
name: "insert line",
},
Binding {
cmd: Cmd::Undo,
chord: Bare('u'),
name: "undo",
},
Binding {
cmd: Cmd::FindNext,
chord: Bare('l'),
name: "find next",
},
Binding {
cmd: Cmd::ToggleInsert,
chord: Bare('v'),
name: "insert/overtype",
},
Binding {
cmd: Cmd::LineStart,
chord: Pref(Q, 's'),
name: "line start",
},
Binding {
cmd: Cmd::LineEnd,
chord: Pref(Q, 'd'),
name: "line end",
},
Binding {
cmd: Cmd::ScreenTop,
chord: Pref(Q, 'e'),
name: "screen top",
},
Binding {
cmd: Cmd::ScreenBottom,
chord: Pref(Q, 'x'),
name: "screen bottom",
},
Binding {
cmd: Cmd::DocStart,
chord: Pref(Q, 'r'),
name: "document top",
},
Binding {
cmd: Cmd::DocEnd,
chord: Pref(Q, 'c'),
name: "document end",
},
Binding {
cmd: Cmd::SentenceBack,
chord: Pref(Q, ','),
name: "sentence back",
},
Binding {
cmd: Cmd::SentenceFwd,
chord: Pref(Q, '.'),
name: "sentence forward",
},
Binding {
cmd: Cmd::ParaBack,
chord: Pref(Q, '['),
name: "paragraph back",
},
Binding {
cmd: Cmd::ParaFwd,
chord: Pref(Q, ']'),
name: "paragraph forward",
},
Binding {
cmd: Cmd::PrevPosition,
chord: Pref(Q, 'p'),
name: "previous position",
},
Binding {
cmd: Cmd::Outline,
chord: Pref(Q, 'o'),
name: "outline / headings",
},
Binding {
cmd: Cmd::NextMisspelling,
chord: Pref(Q, 'n'),
name: "next misspelling",
},
Binding {
cmd: Cmd::FindIncremental,
chord: Pref(Q, 'f'),
name: "find",
},
Binding {
cmd: Cmd::FindReplace,
chord: Pref(Q, 'a'),
name: "find & replace",
},
Binding {
cmd: Cmd::DeleteToLineEnd,
chord: Pref(Q, 'y'),
name: "delete to line end",
},
Binding {
cmd: Cmd::TransposeWords,
chord: Pref(Q, 't'),
name: "transpose words",
},
Binding {
cmd: Cmd::TransposeChars,
chord: Pref(Q, 'g'),
name: "transpose chars",
},
Binding {
cmd: Cmd::MacroRecord,
chord: Pref(Q, 'm'),
name: "record macro",
},
Binding {
cmd: Cmd::MacroPlay,
chord: Pref(Q, 'j'),
name: "play macro",
},
Binding {
cmd: Cmd::JumpBlockBegin,
chord: Pref(Q, 'b'),
name: "to block begin",
},
Binding {
cmd: Cmd::JumpBlockEnd,
chord: Pref(Q, 'k'),
name: "to block end",
},
Binding {
cmd: Cmd::JumpBlockSource,
chord: Pref(Q, 'v'),
name: "to block source",
},
Binding {
cmd: Cmd::BlockBegin,
chord: Pref(K, 'b'),
name: "mark block begin",
},
Binding {
cmd: Cmd::BlockEnd,
chord: Pref(K, 'k'),
name: "mark block end",
},
Binding {
cmd: Cmd::BlockCopy,
chord: Pref(K, 'c'),
name: "copy block here",
},
Binding {
cmd: Cmd::BlockMove,
chord: Pref(K, 'v'),
name: "move block here",
},
Binding {
cmd: Cmd::BlockDelete,
chord: Pref(K, 'y'),
name: "delete block",
},
Binding {
cmd: Cmd::BlockWrite,
chord: Pref(K, 'w'),
name: "write block to file",
},
Binding {
cmd: Cmd::BlockRead,
chord: Pref(K, 'r'),
name: "read file here",
},
Binding {
cmd: Cmd::BlockHide,
chord: Pref(K, 'h'),
name: "hide/show block",
},
Binding {
cmd: Cmd::BlockPrev,
chord: Pref(K, 'u'),
name: "previous block",
},
Binding {
cmd: Cmd::Put,
chord: Pref(K, 'p'),
name: "put (cycle clippings)",
},
Binding {
cmd: Cmd::CopyFromOther,
chord: Pref(K, 'a'),
name: "copy block from other window",
},
Binding {
cmd: Cmd::Save,
chord: Pref(K, 'd'),
name: "save",
},
Binding {
cmd: Cmd::Save,
chord: Pref(K, 's'),
name: "save",
},
Binding {
cmd: Cmd::SaveExit,
chord: Pref(K, 'x'),
name: "save & exit",
},
Binding {
cmd: Cmd::Quit,
chord: Pref(K, 'q'),
name: "quit",
},
Binding {
cmd: Cmd::ExportClean,
chord: Pref(K, 'e'),
name: "export (strip notes)",
},
Binding {
cmd: Cmd::ExportManuscript,
chord: Pref(K, 'm'),
name: "export manuscript RTF",
},
Binding {
cmd: Cmd::ExportDocx,
chord: Pref(K, 'j'),
name: "export DOCX",
},
Binding {
cmd: Cmd::ExportEpub,
chord: Pref(K, 'g'),
name: "export EPUB",
},
Binding {
cmd: Cmd::ExportHtml,
chord: Pref(K, 'l'),
name: "export HTML",
},
Binding {
cmd: Cmd::CycleTheme,
chord: Pref(O, 'b'),
name: "cycle theme",
},
Binding {
cmd: Cmd::RevealCodes,
chord: Pref(O, 'd'),
name: "reveal codes",
},
Binding {
cmd: Cmd::ToggleWrap,
chord: Pref(O, 'w'),
name: "word wrap on/off",
},
Binding {
cmd: Cmd::SetWrapMargin,
chord: Pref(O, 'r'),
name: "set wrap margin",
},
Binding {
cmd: Cmd::CycleHelpLevel,
chord: Pref(O, 'h'),
name: "cycle help level",
},
Binding {
cmd: Cmd::ToggleSpellcheck,
chord: Pref(O, 's'),
name: "spellcheck on/off",
},
Binding {
cmd: Cmd::AddToDictionary,
chord: Pref(O, 'a'),
name: "add word to dictionary",
},
Binding {
cmd: Cmd::ToggleTypewriter,
chord: Pref(O, 't'),
name: "typewriter scrolling on/off",
},
Binding {
cmd: Cmd::OtherWindow,
chord: Pref(O, 'k'),
name: "other window (open/switch)",
},
Binding {
cmd: Cmd::WordCount,
chord: Pref(O, 'c'),
name: "toggle word count",
},
Binding {
cmd: Cmd::SetGoal,
chord: Pref(O, 'g'),
name: "set session goal",
},
Binding {
cmd: Cmd::StatsOverlay,
chord: Pref(O, 'i'),
name: "writing stats",
},
Binding {
cmd: Cmd::ProjectNew,
chord: Pref(P, 'n'),
name: "new project",
},
Binding {
cmd: Cmd::ProjectOpen,
chord: Pref(P, 'p'),
name: "open project",
},
Binding {
cmd: Cmd::BinderToggle,
chord: Pref(P, 'b'),
name: "toggle binder",
},
Binding {
cmd: Cmd::BinderMoveUp,
chord: Pref(P, 'e'),
name: "binder: move doc up",
},
Binding {
cmd: Cmd::BinderMoveDown,
chord: Pref(P, 'x'),
name: "binder: move doc down",
},
Binding {
cmd: Cmd::ProjectAddDoc,
chord: Pref(P, 'a'),
name: "add document to project",
},
Binding {
cmd: Cmd::ProjectRemoveDoc,
chord: Pref(P, 'r'),
name: "remove document from project",
},
Binding {
cmd: Cmd::ExportProjectDocx,
chord: Pref(P, 'd'),
name: "export project DOCX",
},
Binding {
cmd: Cmd::ExportProjectEpub,
chord: Pref(P, 'f'),
name: "export project EPUB",
},
Binding {
cmd: Cmd::ExportProjectHtml,
chord: Pref(P, 'h'),
name: "export project HTML",
},
Binding {
cmd: Cmd::ProjectFind,
chord: Pref(P, 's'),
name: "project search",
},
Binding {
cmd: Cmd::ProjectReplace,
chord: Pref(P, 'w'),
name: "project replace",
},
];
pub fn chord_label(chord: Chord) -> String {
match chord {
Bare(c) => format!("^{}", c.to_ascii_uppercase()),
Pref(p, c) => {
let p = match p {
K => 'K',
Q => 'Q',
O => 'O',
P => 'P',
};
format!("^{p}{}", c.to_ascii_uppercase())
}
}
}
pub fn filtered_entries(query: &str) -> Vec<(Cmd, &'static str, String)> {
let q = query.to_lowercase();
palette_entries()
.into_iter()
.filter(|(_, name, chord)| {
q.is_empty() || name.to_lowercase().contains(&q) || chord.to_lowercase().contains(&q)
})
.collect()
}
pub fn palette_entries() -> Vec<(Cmd, &'static str, String)> {
let mut seen = Vec::new();
let mut out = Vec::new();
for b in BINDINGS {
if !seen.contains(&b.cmd) {
seen.push(b.cmd);
out.push((b.cmd, b.name, chord_label(b.chord)));
}
}
out
}
pub fn lookup_bare(c: char) -> Option<Cmd> {
BINDINGS.iter().find_map(|b| match b.chord {
Bare(k) if k == c => Some(b.cmd),
_ => None,
})
}
pub fn lookup_prefixed(prefix: Prefix, c: char) -> Option<Cmd> {
BINDINGS.iter().find_map(|b| match b.chord {
Pref(p, k) if p == prefix && k == c => Some(b.cmd),
_ => None,
})
}
pub fn menu_entries(prefix: Prefix) -> Vec<(char, &'static str)> {
let mut seen = Vec::new();
let mut out = Vec::new();
for b in BINDINGS {
if let Pref(p, k) = b.chord {
if p == prefix && !seen.contains(&b.cmd) {
seen.push(b.cmd);
out.push((k, b.name));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_prefix_has_a_label() {
assert_eq!(Prefix::P.label(), "^P Project");
}
#[test]
fn project_prefix_has_commands() {
assert!(!menu_entries(Prefix::P).is_empty());
assert_eq!(lookup_prefixed(Prefix::P, 'p'), Some(Cmd::ProjectOpen));
}
#[test]
fn existing_prefixes_still_populated() {
assert!(!menu_entries(Prefix::K).is_empty());
assert!(!menu_entries(Prefix::Q).is_empty());
assert!(!menu_entries(Prefix::O).is_empty());
}
#[test]
fn chord_label_renders_p_prefix() {
assert_eq!(chord_label(Pref(P, 'b')), "^PB");
}
#[test]
fn palette_entries_are_deduplicated() {
let saves = palette_entries()
.into_iter()
.filter(|(cmd, _, _)| *cmd == Cmd::Save)
.count();
assert_eq!(saves, 1);
}
}