use serde::{Deserialize, Serialize};
use crate::msg::{By, Dir, Msg};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyCode {
Char(char),
Enter,
Backspace,
Delete,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Tab,
BackTab,
Esc,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Mods {
#[serde(default)]
pub shift: bool,
#[serde(default)]
pub ctrl: bool,
#[serde(default)]
pub alt: bool,
#[serde(default)]
pub cmd: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Key {
pub code: KeyCode,
#[serde(default)]
pub mods: Mods,
}
impl Key {
pub fn plain(code: KeyCode) -> Key {
Key {
code,
mods: Mods::default(),
}
}
}
fn mv(dir: Dir, by: By, extend: bool) -> Option<Msg> {
Some(Msg::Move { dir, by, extend })
}
pub fn keymap(key: &Key) -> Option<Msg> {
let m = key.mods;
let shift = m.shift;
use Dir::{Backward as B, Forward as F};
match key.code {
KeyCode::Char(c) => {
let lower = c.to_ascii_lowercase();
let shift = shift || c.is_ascii_uppercase();
if m.cmd || m.ctrl {
match (lower, m.cmd) {
('a', true) => return Some(Msg::SelectAll),
('a', false) => return mv(B, By::LineStart, shift),
('e', false) => return mv(F, By::LineEnd, shift),
('w', false) => return Some(Msg::DeleteWordBackward),
('u', false) => return Some(Msg::DeleteToLineStart),
('k', false) => return Some(Msg::KillLine),
('h', false) => return Some(Msg::DeleteBackward),
_ => {}
}
return match lower {
'z' if shift => Some(Msg::Redo),
'z' => Some(Msg::Undo),
'y' | 'r' => Some(Msg::Redo),
'c' => Some(Msg::Copy),
'x' => Some(Msg::Cut),
'v' => Some(Msg::Paste { text: None }),
's' => Some(Msg::Save),
'q' => Some(Msg::Quit),
_ => None,
};
}
if m.alt {
return match lower {
'b' => mv(B, By::Word, shift),
'f' => mv(F, By::Word, shift),
'd' => Some(Msg::DeleteWordForward),
'a' => Some(Msg::SelectAll),
_ => None,
};
}
let c = if m.shift { c.to_uppercase().next().unwrap_or(c) } else { c };
Some(Msg::InsertText {
text: c.to_string(),
})
}
KeyCode::Enter => Some(Msg::InsertNewline),
KeyCode::Tab => Some(Msg::InsertText { text: "\t".into() }),
KeyCode::BackTab => None,
KeyCode::Esc => Some(Msg::Collapse),
KeyCode::Backspace => Some(if m.cmd {
Msg::DeleteToLineStart
} else if m.alt || m.ctrl {
Msg::DeleteWordBackward
} else {
Msg::DeleteBackward
}),
KeyCode::Delete => Some(if m.cmd {
Msg::DeleteToLineEnd
} else if m.alt || m.ctrl {
Msg::DeleteWordForward
} else {
Msg::DeleteForward
}),
KeyCode::Left if m.cmd => mv(B, By::LineStart, shift),
KeyCode::Right if m.cmd => mv(F, By::LineEnd, shift),
KeyCode::Left if m.alt || m.ctrl => mv(B, By::Word, shift),
KeyCode::Right if m.alt || m.ctrl => mv(F, By::Word, shift),
KeyCode::Left => mv(B, By::Grapheme, shift),
KeyCode::Right => mv(F, By::Grapheme, shift),
KeyCode::Up if m.cmd => mv(B, By::DocStart, shift),
KeyCode::Down if m.cmd => mv(F, By::DocEnd, shift),
KeyCode::Up if m.alt || m.ctrl => None,
KeyCode::Down if m.alt || m.ctrl => None,
KeyCode::Up => mv(B, By::VisualLine, shift),
KeyCode::Down => mv(F, By::VisualLine, shift),
KeyCode::Home if m.ctrl || m.cmd => mv(B, By::DocStart, shift),
KeyCode::End if m.ctrl || m.cmd => mv(F, By::DocEnd, shift),
KeyCode::Home => mv(B, By::LineStart, shift),
KeyCode::End => mv(F, By::LineEnd, shift),
KeyCode::PageUp => mv(B, By::Page, shift),
KeyCode::PageDown => mv(F, By::Page, shift),
}
}
pub fn outline_keymap(key: &Key) -> Option<Msg> {
let m = key.mods;
use Dir::{Backward as B, Forward as F};
match key.code {
KeyCode::Tab if !(m.ctrl || m.alt || m.cmd) => {
return Some(if m.shift { Msg::Outdent } else { Msg::Indent });
}
KeyCode::BackTab => return Some(Msg::Outdent),
KeyCode::Enter if m.shift && !(m.ctrl || m.alt || m.cmd) => return Some(Msg::SoftBreak),
KeyCode::Char(c) if m.ctrl && !m.cmd && !m.alt => match c.to_ascii_lowercase() {
't' => return Some(Msg::TaskCycle),
'j' => return Some(Msg::SoftBreak),
_ => {}
},
KeyCode::Char(c) if m.alt && !m.ctrl && !m.cmd && c.eq_ignore_ascii_case(&'v') => {
return Some(Msg::PastePlain { text: None });
}
KeyCode::Up if m.alt && !m.cmd => return Some(Msg::MoveBlock { dir: B }),
KeyCode::Down if m.alt && !m.cmd => return Some(Msg::MoveBlock { dir: F }),
KeyCode::Up if m.ctrl && !m.cmd => return mv(B, By::Block, m.shift),
KeyCode::Down if m.ctrl && !m.cmd => return mv(F, By::Block, m.shift),
_ => {}
}
keymap(key)
}
pub fn keymap_for(outline: bool, key: &Key) -> Option<Msg> {
if outline {
outline_keymap(key)
} else {
keymap(key)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScriptItem {
Key(Key),
Wait(u64),
}
pub fn parse_keys(script: &str) -> Result<Vec<ScriptItem>, String> {
let chars: Vec<char> = script.chars().collect();
let mut items = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '<' {
if let Some(len) = chars[i + 1..].iter().position(|&c| c == '>') {
let token: String = chars[i + 1..i + 1 + len].iter().collect();
if !token.is_empty() {
items.push(parse_token(&token)?);
i += len + 2;
continue;
}
}
}
let code = match c {
'\n' | '\r' => KeyCode::Enter,
'\t' => KeyCode::Tab,
c => KeyCode::Char(c),
};
items.push(ScriptItem::Key(Key::plain(code)));
i += 1;
}
Ok(items)
}
fn parse_token(token: &str) -> Result<ScriptItem, String> {
if let Some(ms) = token.strip_prefix("wait:") {
return ms
.parse()
.map(ScriptItem::Wait)
.map_err(|_| format!("bad wait in <{token}>"));
}
let mut mods = Mods::default();
let mut rest = token;
loop {
let lower = rest.to_ascii_lowercase();
if rest.len() > 2 && rest.as_bytes()[1] == b'-' {
match &lower[..1] {
"s" => mods.shift = true,
"c" => mods.ctrl = true,
"a" | "m" => mods.alt = true,
"d" => mods.cmd = true,
_ => return Err(format!("unknown modifier in <{token}>")),
}
rest = &rest[2..];
continue;
}
break;
}
let name = rest.to_ascii_lowercase();
let code = match name.as_str() {
"cr" | "enter" | "ret" | "return" => KeyCode::Enter,
"bs" | "backspace" => KeyCode::Backspace,
"del" | "delete" => KeyCode::Delete,
"tab" if mods.shift => {
mods.shift = false;
KeyCode::BackTab
}
"tab" => KeyCode::Tab,
"esc" => KeyCode::Esc,
"space" => KeyCode::Char(' '),
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pgup" | "pageup" => KeyCode::PageUp,
"pgdn" | "pagedown" => KeyCode::PageDown,
"lt" => KeyCode::Char('<'),
"gt" => KeyCode::Char('>'),
_ => {
let mut cs = rest.chars();
match (cs.next(), cs.next()) {
(Some(c), None) => KeyCode::Char(c),
_ => return Err(format!("unknown key <{token}>")),
}
}
};
Ok(ScriptItem::Key(Key { code, mods }))
}
pub fn script_to_msgs(script: &str, now_ms: u64) -> Result<Vec<Msg>, String> {
script_to_msgs_for(script, now_ms, false)
}
pub fn script_to_msgs_for(script: &str, now_ms: u64, outline: bool) -> Result<Vec<Msg>, String> {
let mut now = now_ms;
let mut msgs = Vec::new();
for item in parse_keys(script)? {
match item {
ScriptItem::Key(key) => msgs.extend(keymap_for(outline, &key)),
ScriptItem::Wait(ms) => {
now += ms;
msgs.push(Msg::Tick { now_ms: now });
}
}
}
Ok(msgs)
}