use std::collections::HashMap;
use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyCombo {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyCombo {
pub fn simple(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::NONE,
}
}
pub fn ctrl(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::CONTROL,
}
}
pub fn shift(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::SHIFT,
}
}
pub fn ctrl_shift(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
}
}
pub fn parse(s: &str) -> Option<Self> {
if s.is_empty() {
return None;
}
let (modifiers_str, key_part) = if s == "+" {
("", "+")
} else if let Some(stripped) = s.strip_suffix("++") {
(stripped, "+")
} else {
match s.rfind('+') {
Some(idx) => (&s[..idx], &s[idx + 1..]),
None => ("", s),
}
};
let mut modifiers = KeyModifiers::NONE;
if !modifiers_str.is_empty() {
let parts: Vec<&str> = modifiers_str.split('+').collect();
for part in parts {
let part_lower = part.to_lowercase();
match part_lower.as_str() {
"ctrl" | "control" => modifiers |= KeyModifiers::CONTROL,
"shift" => modifiers |= KeyModifiers::SHIFT,
"alt" => modifiers |= KeyModifiers::ALT,
"super" | "meta" | "cmd" => modifiers |= KeyModifiers::SUPER,
_ => return None,
}
}
}
let code = parse_key_code(key_part)?;
Some(Self { code, modifiers })
}
pub fn to_display_string(&self) -> String {
let key = key_code_to_string(&self.code);
let mut result = String::with_capacity(24);
let mut need_sep = false;
if self.modifiers.contains(KeyModifiers::CONTROL) {
result.push_str("Ctrl");
need_sep = true;
}
if self.modifiers.contains(KeyModifiers::SHIFT) {
if need_sep {
result.push('+');
}
result.push_str("Shift");
need_sep = true;
}
if self.modifiers.contains(KeyModifiers::ALT) {
if need_sep {
result.push('+');
}
result.push_str("Alt");
need_sep = true;
}
if self.modifiers.contains(KeyModifiers::SUPER) {
if need_sep {
result.push('+');
}
result.push_str("Super");
need_sep = true;
}
if need_sep {
result.push('+');
}
result.push_str(&key);
result
}
pub fn matches(&self, event: &KeyEvent) -> bool {
if self.code != event.code {
return false;
}
if self.code == KeyCode::BackTab {
let self_mods = self.modifiers & !KeyModifiers::SHIFT;
let event_mods = event.modifiers & !KeyModifiers::SHIFT;
self_mods == event_mods
} else {
self.modifiers == event.modifiers
}
}
}
fn parse_key_code(s: &str) -> Option<KeyCode> {
let s_lower = s.to_lowercase();
match s_lower.as_str() {
"enter" | "return" => Some(KeyCode::Enter),
"esc" | "escape" => Some(KeyCode::Esc),
"backspace" | "bs" => Some(KeyCode::Backspace),
"tab" => Some(KeyCode::Tab),
"backtab" => Some(KeyCode::BackTab),
"space" | " " => Some(KeyCode::Char(' ')),
"delete" | "del" => Some(KeyCode::Delete),
"insert" | "ins" => Some(KeyCode::Insert),
"home" => Some(KeyCode::Home),
"end" => Some(KeyCode::End),
"pageup" | "pgup" => Some(KeyCode::PageUp),
"pagedown" | "pgdn" => Some(KeyCode::PageDown),
"up" => Some(KeyCode::Up),
"down" => Some(KeyCode::Down),
"left" => Some(KeyCode::Left),
"right" => Some(KeyCode::Right),
"f1" => Some(KeyCode::F(1)),
"f2" => Some(KeyCode::F(2)),
"f3" => Some(KeyCode::F(3)),
"f4" => Some(KeyCode::F(4)),
"f5" => Some(KeyCode::F(5)),
"f6" => Some(KeyCode::F(6)),
"f7" => Some(KeyCode::F(7)),
"f8" => Some(KeyCode::F(8)),
"f9" => Some(KeyCode::F(9)),
"f10" => Some(KeyCode::F(10)),
"f11" => Some(KeyCode::F(11)),
"f12" => Some(KeyCode::F(12)),
_ if s.len() == 1 => {
let c = s.chars().next()?;
Some(KeyCode::Char(c.to_ascii_lowercase()))
}
_ => None,
}
}
use std::borrow::Cow;
fn key_code_to_string(code: &KeyCode) -> Cow<'static, str> {
match code {
KeyCode::Enter => Cow::Borrowed("Enter"),
KeyCode::Esc => Cow::Borrowed("Esc"),
KeyCode::Backspace => Cow::Borrowed("Backspace"),
KeyCode::Tab => Cow::Borrowed("Tab"),
KeyCode::BackTab => Cow::Borrowed("BackTab"),
KeyCode::Delete => Cow::Borrowed("Delete"),
KeyCode::Insert => Cow::Borrowed("Insert"),
KeyCode::Home => Cow::Borrowed("Home"),
KeyCode::End => Cow::Borrowed("End"),
KeyCode::PageUp => Cow::Borrowed("PageUp"),
KeyCode::PageDown => Cow::Borrowed("PageDown"),
KeyCode::Up => Cow::Borrowed("Up"),
KeyCode::Down => Cow::Borrowed("Down"),
KeyCode::Left => Cow::Borrowed("Left"),
KeyCode::Right => Cow::Borrowed("Right"),
KeyCode::F(n) => Cow::Owned(format!("F{n}")),
KeyCode::Char(' ') => Cow::Borrowed("Space"),
KeyCode::Char(c) => Cow::Owned(c.to_string()),
_ => Cow::Borrowed("?"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ListAction {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
Open,
Delete,
Quit,
Help,
OpenLocation,
CycleFocus,
Confirm,
Cancel,
ToggleExternalEditor,
NewFromTemplate,
CreateFolder,
CreateNote,
RenameFolder,
MoveNote,
ManageTags,
CollapseFolder,
ExpandFolder,
OpenCommandPalette,
Rename,
Duplicate,
TogglePin,
CycleSort,
Search,
JumpToTop,
JumpToBottom,
PageUp,
PageDown,
OpenTrash,
TogglePreview,
TogglePreviewFullscreen,
TogglePreviewWrap,
OpenGraph,
OpenCanvas,
CreatePinstar,
ToggleSelectMode,
ToggleSelectItem,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EditAction {
Quit,
Back,
CycleFocus,
SelectAll,
Copy,
Cut,
Paste,
Undo,
Redo,
DeleteWord,
DeleteNextWord,
MoveToTop,
MoveToBottom,
ToggleMarkdownPreview,
TogglePreviewFullscreen,
TogglePreviewWrap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HelpAction {
Close,
ScrollUp,
ScrollDown,
NextTab,
PrevTab,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GraphAction {
Quit,
PanUp,
PanDown,
PanLeft,
PanRight,
ZoomIn,
ZoomOut,
OpenNote,
AutoFit,
Help,
ToggleSearch,
ToggleMinimap,
ToggleLegend,
ToggleGrid,
ToggleStatus,
Refresh,
ReloadConfig,
TogglePreview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DrawAction {
Quit,
SelectDrawTool,
ToggleShapeSelector,
SelectTextTool,
SelectEraseTool,
ShapeSelectorUp,
ShapeSelectorDown,
ShapeSelectorConfirm,
ShapeSelectorCancel,
TextEditorConfirm,
TextEditorCancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CanvasAction {
Quit,
Save,
ZoomFineIn,
ZoomFineOut,
ZoomIn,
ZoomOut,
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
EditOrConnect,
OpenContextMenu,
ToggleGrid,
ToggleEditorPane,
CycleFocus,
Help,
RenameConfirm,
RenameCancel,
MenuClose,
MenuUp,
MenuDown,
MenuSelect,
CloseEditor,
CloseEditorAlt,
ConfirmResize,
CancelResize,
EditorUnfocus,
EditorSyncRaw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackupAction {
Back,
MoveDown,
MoveUp,
ScrollDiffDown,
ScrollDiffUp,
Refresh,
EnterCommit,
Push,
OpenSettings,
CycleSection,
CancelCommit,
ConfirmCommit,
CloseSettings,
ToggleFileSelect,
NextField,
PrevField,
ActivateField,
CancelEditField,
ConfirmEditField,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentTreeAction {
MoveUp,
MoveDown,
ToggleCollapse,
ExpandAll,
CollapseAll,
Open,
Back,
Help,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct KeybindsToml {
#[serde(default)]
pub list: HashMap<ListAction, Vec<String>>,
#[serde(default)]
pub edit: HashMap<EditAction, Vec<String>>,
#[serde(default)]
pub help: HashMap<HelpAction, Vec<String>>,
#[serde(default)]
pub graph: HashMap<GraphAction, Vec<String>>,
#[serde(default)]
pub draw: HashMap<DrawAction, Vec<String>>,
#[serde(default)]
pub canvas: HashMap<CanvasAction, Vec<String>>,
#[serde(default)]
pub backup: HashMap<BackupAction, Vec<String>>,
#[serde(default)]
pub content_tree: HashMap<ContentTreeAction, Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct Keybinds {
pub list: HashMap<ListAction, Vec<KeyCombo>>,
pub edit: HashMap<EditAction, Vec<KeyCombo>>,
pub help: HashMap<HelpAction, Vec<KeyCombo>>,
pub graph: HashMap<GraphAction, Vec<KeyCombo>>,
pub draw: HashMap<DrawAction, Vec<KeyCombo>>,
pub canvas: HashMap<CanvasAction, Vec<KeyCombo>>,
pub backup: HashMap<BackupAction, Vec<KeyCombo>>,
pub content_tree: HashMap<ContentTreeAction, Vec<KeyCombo>>,
}
impl Default for Keybinds {
fn default() -> Self {
let mut list = HashMap::new();
list.insert(
ListAction::MoveUp,
vec![
KeyCombo::simple(KeyCode::Up),
KeyCombo::simple(KeyCode::Char('k')),
],
);
list.insert(
ListAction::MoveDown,
vec![
KeyCombo::simple(KeyCode::Down),
KeyCombo::simple(KeyCode::Char('j')),
],
);
list.insert(
ListAction::MoveLeft,
vec![
KeyCombo::simple(KeyCode::Left),
KeyCombo::simple(KeyCode::Char('h')),
],
);
list.insert(
ListAction::MoveRight,
vec![
KeyCombo::simple(KeyCode::Right),
KeyCombo::simple(KeyCode::Char('l')),
],
);
list.insert(ListAction::Open, vec![KeyCombo::simple(KeyCode::Enter)]);
list.insert(
ListAction::Delete,
vec![
KeyCombo::simple(KeyCode::Char('d')),
KeyCombo::simple(KeyCode::Delete),
],
);
list.insert(ListAction::Quit, vec![KeyCombo::simple(KeyCode::Char('q'))]);
list.insert(
ListAction::Help,
vec![
KeyCombo::simple(KeyCode::Char('?')),
KeyCombo::simple(KeyCode::F(1)),
],
);
list.insert(
ListAction::OpenLocation,
vec![KeyCombo::ctrl(KeyCode::Char('f'))],
);
list.insert(
ListAction::CycleFocus,
vec![
KeyCombo::simple(KeyCode::Tab),
KeyCombo::simple(KeyCode::BackTab),
],
);
list.insert(
ListAction::Confirm,
vec![
KeyCombo::simple(KeyCode::Char('y')),
KeyCombo::simple(KeyCode::Enter),
],
);
list.insert(
ListAction::Cancel,
vec![
KeyCombo::simple(KeyCode::Char('n')),
KeyCombo::simple(KeyCode::Esc),
],
);
list.insert(
ListAction::ToggleExternalEditor,
vec![KeyCombo::simple(KeyCode::Char('e'))],
);
list.insert(
ListAction::NewFromTemplate,
vec![KeyCombo::simple(KeyCode::Char('t'))],
);
list.insert(
ListAction::CreateFolder,
vec![KeyCombo::simple(KeyCode::Char('n'))],
);
list.insert(
ListAction::CreateNote,
vec![KeyCombo::simple(KeyCode::Char('a'))],
);
list.insert(
ListAction::RenameFolder,
vec![KeyCombo::simple(KeyCode::Char('r'))],
);
list.insert(
ListAction::MoveNote,
vec![KeyCombo::simple(KeyCode::Char('m'))],
);
list.insert(
ListAction::ManageTags,
vec![KeyCombo::simple(KeyCode::Char('.'))],
);
list.insert(
ListAction::OpenCommandPalette,
vec![
KeyCombo::ctrl(KeyCode::Char('p')),
KeyCombo::shift(KeyCode::Enter),
],
);
list.insert(
ListAction::Rename,
vec![KeyCombo::simple(KeyCode::Char('r'))],
);
list.insert(
ListAction::Duplicate,
vec![KeyCombo::simple(KeyCode::Char('y'))],
);
list.insert(
ListAction::TogglePin,
vec![KeyCombo::simple(KeyCode::Char('p'))],
);
list.insert(
ListAction::CycleSort,
vec![KeyCombo::simple(KeyCode::Char('s'))],
);
list.insert(
ListAction::Search,
vec![KeyCombo::simple(KeyCode::Char('f'))],
);
list.insert(
ListAction::JumpToTop,
vec![KeyCombo::shift(KeyCode::Char('U'))],
);
list.insert(
ListAction::JumpToBottom,
vec![KeyCombo::shift(KeyCode::Char('D'))],
);
list.insert(ListAction::PageUp, vec![KeyCombo::ctrl(KeyCode::Char('u'))]);
list.insert(
ListAction::PageDown,
vec![KeyCombo::ctrl(KeyCode::Char('d'))],
);
list.insert(
ListAction::OpenTrash,
vec![KeyCombo::shift(KeyCode::Char('T'))],
);
list.insert(
ListAction::TogglePreview,
vec![KeyCombo::shift(KeyCode::Char('P'))],
);
list.insert(
ListAction::TogglePreviewFullscreen,
vec![KeyCombo::ctrl(KeyCode::Char('e'))],
);
list.insert(
ListAction::TogglePreviewWrap,
vec![KeyCombo::ctrl(KeyCode::Char('w'))],
);
list.insert(
ListAction::OpenGraph,
vec![KeyCombo::ctrl(KeyCode::Char('g'))],
);
list.insert(
ListAction::ToggleSelectMode,
vec![KeyCombo::simple(KeyCode::Char('v'))],
);
list.insert(
ListAction::ToggleSelectItem,
vec![KeyCombo::simple(KeyCode::Char(' '))],
);
let mut edit = HashMap::new();
edit.insert(EditAction::Back, vec![KeyCombo::simple(KeyCode::Esc)]);
edit.insert(
EditAction::CycleFocus,
vec![
KeyCombo::simple(KeyCode::Tab),
KeyCombo::simple(KeyCode::BackTab),
],
);
edit.insert(
EditAction::SelectAll,
vec![KeyCombo::ctrl(KeyCode::Char('a'))],
);
edit.insert(
EditAction::Copy,
vec![
KeyCombo::ctrl_shift(KeyCode::Char('c')),
KeyCombo::ctrl(KeyCode::Insert),
],
);
edit.insert(
EditAction::Cut,
vec![
KeyCombo::ctrl_shift(KeyCode::Char('x')),
KeyCombo::shift(KeyCode::Delete),
],
);
edit.insert(
EditAction::Paste,
vec![
KeyCombo::ctrl_shift(KeyCode::Char('v')),
KeyCombo::shift(KeyCode::Insert),
],
);
edit.insert(EditAction::Undo, vec![KeyCombo::ctrl(KeyCode::Char('z'))]);
edit.insert(
EditAction::Redo,
vec![
KeyCombo::ctrl(KeyCode::Char('y')),
KeyCombo::ctrl_shift(KeyCode::Char('z')),
],
);
edit.insert(
EditAction::DeleteWord,
vec![KeyCombo::ctrl(KeyCode::Backspace)],
);
edit.insert(
EditAction::DeleteNextWord,
vec![KeyCombo::ctrl(KeyCode::Delete)],
);
edit.insert(EditAction::MoveToTop, vec![KeyCombo::ctrl(KeyCode::Home)]);
edit.insert(EditAction::MoveToBottom, vec![KeyCombo::ctrl(KeyCode::End)]);
edit.insert(
EditAction::ToggleMarkdownPreview,
vec![KeyCombo::ctrl(KeyCode::Char('p'))],
);
edit.insert(
EditAction::TogglePreviewFullscreen,
vec![KeyCombo::ctrl(KeyCode::Char('e'))],
);
edit.insert(
EditAction::TogglePreviewWrap,
vec![KeyCombo::ctrl(KeyCode::Char('w'))],
);
let mut help = HashMap::new();
help.insert(
HelpAction::Close,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
KeyCombo::simple(KeyCode::Char('?')),
KeyCombo::simple(KeyCode::F(1)),
],
);
help.insert(
HelpAction::NextTab,
vec![
KeyCombo::simple(KeyCode::Right),
KeyCombo::simple(KeyCode::Char('l')),
KeyCombo::simple(KeyCode::Tab),
],
);
help.insert(
HelpAction::PrevTab,
vec![
KeyCombo::simple(KeyCode::Left),
KeyCombo::simple(KeyCode::Char('h')),
KeyCombo::simple(KeyCode::BackTab),
],
);
help.insert(
HelpAction::ScrollUp,
vec![
KeyCombo::simple(KeyCode::Up),
KeyCombo::simple(KeyCode::Char('k')),
],
);
help.insert(
HelpAction::ScrollDown,
vec![
KeyCombo::simple(KeyCode::Down),
KeyCombo::simple(KeyCode::Char('j')),
],
);
let mut graph = HashMap::new();
graph.insert(
GraphAction::Quit,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
graph.insert(
GraphAction::PanUp,
vec![
KeyCombo::simple(KeyCode::Up),
KeyCombo::simple(KeyCode::Char('k')),
],
);
graph.insert(
GraphAction::PanDown,
vec![
KeyCombo::simple(KeyCode::Down),
KeyCombo::simple(KeyCode::Char('j')),
],
);
graph.insert(
GraphAction::PanLeft,
vec![
KeyCombo::simple(KeyCode::Left),
KeyCombo::simple(KeyCode::Char('h')),
],
);
graph.insert(
GraphAction::PanRight,
vec![
KeyCombo::simple(KeyCode::Right),
KeyCombo::simple(KeyCode::Char('l')),
],
);
graph.insert(
GraphAction::ZoomIn,
vec![
KeyCombo::simple(KeyCode::Char('+')),
KeyCombo::ctrl(KeyCode::Char('j')),
],
);
graph.insert(
GraphAction::ZoomOut,
vec![
KeyCombo::simple(KeyCode::Char('-')),
KeyCombo::ctrl(KeyCode::Char('k')),
],
);
graph.insert(
GraphAction::OpenNote,
vec![KeyCombo::simple(KeyCode::Enter)],
);
graph.insert(
GraphAction::AutoFit,
vec![KeyCombo::simple(KeyCode::Char('a'))],
);
graph.insert(
GraphAction::Help,
vec![
KeyCombo::simple(KeyCode::Char('?')),
KeyCombo::simple(KeyCode::F(1)),
],
);
graph.insert(
GraphAction::ToggleSearch,
vec![KeyCombo::simple(KeyCode::Char('f'))],
);
graph.insert(
GraphAction::ToggleMinimap,
vec![KeyCombo::shift(KeyCode::Char('M'))],
);
graph.insert(
GraphAction::ToggleLegend,
vec![KeyCombo::shift(KeyCode::Char('L'))],
);
graph.insert(
GraphAction::ToggleGrid,
vec![KeyCombo::shift(KeyCode::Char('G'))],
);
graph.insert(
GraphAction::ToggleStatus,
vec![KeyCombo::shift(KeyCode::Char('S'))],
);
graph.insert(
GraphAction::Refresh,
vec![KeyCombo::simple(KeyCode::Char('r'))],
);
graph.insert(
GraphAction::ReloadConfig,
vec![KeyCombo::ctrl(KeyCode::Char('r'))],
);
graph.insert(
GraphAction::TogglePreview,
vec![KeyCombo::shift(KeyCode::Char('P'))],
);
let mut draw = HashMap::new();
draw.insert(
DrawAction::Quit,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
draw.insert(
DrawAction::SelectDrawTool,
vec![KeyCombo::simple(KeyCode::Char('d'))],
);
draw.insert(
DrawAction::ToggleShapeSelector,
vec![KeyCombo::simple(KeyCode::Char('s'))],
);
draw.insert(
DrawAction::SelectTextTool,
vec![KeyCombo::simple(KeyCode::Char('t'))],
);
draw.insert(
DrawAction::SelectEraseTool,
vec![KeyCombo::simple(KeyCode::Char('e'))],
);
draw.insert(
DrawAction::ShapeSelectorUp,
vec![KeyCombo::simple(KeyCode::Up)],
);
draw.insert(
DrawAction::ShapeSelectorDown,
vec![KeyCombo::simple(KeyCode::Down)],
);
draw.insert(
DrawAction::ShapeSelectorConfirm,
vec![KeyCombo::simple(KeyCode::Enter)],
);
draw.insert(
DrawAction::ShapeSelectorCancel,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
draw.insert(
DrawAction::TextEditorConfirm,
vec![KeyCombo::simple(KeyCode::Enter)],
);
draw.insert(
DrawAction::TextEditorCancel,
vec![KeyCombo::simple(KeyCode::Esc)],
);
let mut canvas = HashMap::new();
canvas.insert(
CanvasAction::Quit,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
canvas.insert(CanvasAction::Save, vec![KeyCombo::ctrl(KeyCode::Char('s'))]);
canvas.insert(
CanvasAction::ZoomFineIn,
vec![KeyCombo::ctrl(KeyCode::Char('j'))],
);
canvas.insert(
CanvasAction::ZoomFineOut,
vec![KeyCombo::ctrl(KeyCode::Char('k'))],
);
canvas.insert(
CanvasAction::ZoomIn,
vec![
KeyCombo::simple(KeyCode::Char('+')),
KeyCombo::simple(KeyCode::Char('=')),
],
);
canvas.insert(
CanvasAction::ZoomOut,
vec![
KeyCombo::simple(KeyCode::Char('-')),
KeyCombo::simple(KeyCode::Char('_')),
],
);
canvas.insert(
CanvasAction::MoveLeft,
vec![
KeyCombo::simple(KeyCode::Left),
KeyCombo::simple(KeyCode::Char('h')),
],
);
canvas.insert(
CanvasAction::MoveRight,
vec![
KeyCombo::simple(KeyCode::Right),
KeyCombo::simple(KeyCode::Char('l')),
],
);
canvas.insert(
CanvasAction::MoveUp,
vec![
KeyCombo::simple(KeyCode::Up),
KeyCombo::simple(KeyCode::Char('k')),
],
);
canvas.insert(
CanvasAction::MoveDown,
vec![
KeyCombo::simple(KeyCode::Down),
KeyCombo::simple(KeyCode::Char('j')),
],
);
canvas.insert(
CanvasAction::EditOrConnect,
vec![
KeyCombo::simple(KeyCode::Char('i')),
KeyCombo::simple(KeyCode::Enter),
],
);
canvas.insert(
CanvasAction::OpenContextMenu,
vec![KeyCombo::simple(KeyCode::Char('a'))],
);
canvas.insert(
CanvasAction::ToggleGrid,
vec![KeyCombo::ctrl(KeyCode::Char('g'))],
);
canvas.insert(
CanvasAction::ToggleEditorPane,
vec![KeyCombo::ctrl(KeyCode::Char('e'))],
);
canvas.insert(
CanvasAction::CycleFocus,
vec![
KeyCombo::simple(KeyCode::Tab),
KeyCombo::simple(KeyCode::BackTab),
],
);
canvas.insert(
CanvasAction::Help,
vec![KeyCombo::simple(KeyCode::Char('?'))],
);
canvas.insert(
CanvasAction::RenameConfirm,
vec![KeyCombo::simple(KeyCode::Enter)],
);
canvas.insert(
CanvasAction::RenameCancel,
vec![KeyCombo::simple(KeyCode::Esc)],
);
canvas.insert(
CanvasAction::MenuClose,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
canvas.insert(
CanvasAction::MenuUp,
vec![
KeyCombo::simple(KeyCode::Up),
KeyCombo::simple(KeyCode::Char('k')),
],
);
canvas.insert(
CanvasAction::MenuDown,
vec![
KeyCombo::simple(KeyCode::Down),
KeyCombo::simple(KeyCode::Char('j')),
],
);
canvas.insert(
CanvasAction::MenuSelect,
vec![KeyCombo::simple(KeyCode::Enter)],
);
canvas.insert(
CanvasAction::CloseEditor,
vec![KeyCombo::simple(KeyCode::Esc)],
);
canvas.insert(
CanvasAction::CloseEditorAlt,
vec![KeyCombo::ctrl(KeyCode::Enter)],
);
canvas.insert(
CanvasAction::ConfirmResize,
vec![KeyCombo::simple(KeyCode::Enter)],
);
canvas.insert(
CanvasAction::CancelResize,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
canvas.insert(
CanvasAction::EditorUnfocus,
vec![KeyCombo::simple(KeyCode::Esc)],
);
canvas.insert(
CanvasAction::EditorSyncRaw,
vec![KeyCombo::ctrl(KeyCode::Char('s'))],
);
let mut backup = HashMap::new();
backup.insert(
BackupAction::Back,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
backup.insert(
BackupAction::MoveDown,
vec![
KeyCombo::simple(KeyCode::Char('j')),
KeyCombo::simple(KeyCode::Down),
],
);
backup.insert(
BackupAction::MoveUp,
vec![
KeyCombo::simple(KeyCode::Char('k')),
KeyCombo::simple(KeyCode::Up),
],
);
backup.insert(
BackupAction::ScrollDiffDown,
vec![KeyCombo::simple(KeyCode::PageDown)],
);
backup.insert(
BackupAction::ScrollDiffUp,
vec![KeyCombo::simple(KeyCode::PageUp)],
);
backup.insert(
BackupAction::Refresh,
vec![KeyCombo::simple(KeyCode::Char('r'))],
);
backup.insert(
BackupAction::EnterCommit,
vec![KeyCombo::simple(KeyCode::Char('s'))],
);
backup.insert(
BackupAction::Push,
vec![KeyCombo::simple(KeyCode::Char('p'))],
);
backup.insert(
BackupAction::OpenSettings,
vec![KeyCombo::ctrl(KeyCode::Char('p'))],
);
backup.insert(
BackupAction::ToggleFileSelect,
vec![KeyCombo::simple(KeyCode::Char(' '))],
);
backup.insert(
BackupAction::CycleSection,
vec![
KeyCombo::simple(KeyCode::Tab),
KeyCombo::simple(KeyCode::BackTab),
],
);
backup.insert(
BackupAction::CancelCommit,
vec![KeyCombo::simple(KeyCode::Esc)],
);
backup.insert(
BackupAction::ConfirmCommit,
vec![KeyCombo::simple(KeyCode::Enter)],
);
backup.insert(
BackupAction::CloseSettings,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
backup.insert(
BackupAction::NextField,
vec![
KeyCombo::simple(KeyCode::Char('j')),
KeyCombo::simple(KeyCode::Down),
],
);
backup.insert(
BackupAction::PrevField,
vec![
KeyCombo::simple(KeyCode::Char('k')),
KeyCombo::simple(KeyCode::Up),
],
);
backup.insert(
BackupAction::ActivateField,
vec![KeyCombo::simple(KeyCode::Enter)],
);
backup.insert(
BackupAction::CancelEditField,
vec![KeyCombo::simple(KeyCode::Esc)],
);
backup.insert(
BackupAction::ConfirmEditField,
vec![KeyCombo::simple(KeyCode::Enter)],
);
let mut content_tree = HashMap::new();
content_tree.insert(
ContentTreeAction::MoveUp,
vec![
KeyCombo::simple(KeyCode::Char('k')),
KeyCombo::simple(KeyCode::Up),
],
);
content_tree.insert(
ContentTreeAction::MoveDown,
vec![
KeyCombo::simple(KeyCode::Char('j')),
KeyCombo::simple(KeyCode::Down),
],
);
content_tree.insert(
ContentTreeAction::ToggleCollapse,
vec![
KeyCombo::simple(KeyCode::Tab),
KeyCombo::simple(KeyCode::Left),
KeyCombo::simple(KeyCode::Right),
],
);
content_tree.insert(
ContentTreeAction::ExpandAll,
vec![KeyCombo::simple(KeyCode::Char('e'))],
);
content_tree.insert(
ContentTreeAction::CollapseAll,
vec![KeyCombo::simple(KeyCode::Char('c'))],
);
content_tree.insert(
ContentTreeAction::Open,
vec![KeyCombo::simple(KeyCode::Enter)],
);
content_tree.insert(
ContentTreeAction::Back,
vec![
KeyCombo::simple(KeyCode::Esc),
KeyCombo::simple(KeyCode::Char('q')),
],
);
content_tree.insert(
ContentTreeAction::Help,
vec![
KeyCombo::simple(KeyCode::Char('?')),
KeyCombo::simple(KeyCode::F(1)),
],
);
Self {
list,
edit,
help,
graph,
draw,
canvas,
backup,
content_tree,
}
}
}
impl Keybinds {
pub fn load(path: &Path) -> Result<Self> {
let mut keybinds = Self::default();
if !path.exists() {
return Ok(keybinds);
}
let content = fs::read_to_string(path).context("failed to read keybinds file")?;
let toml: KeybindsToml =
toml::from_str(&content).context("failed to parse keybinds file")?;
for (action, combos_str) in &toml.list {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.list.insert(*action, combos);
}
}
for (action, combos_str) in &toml.edit {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.edit.insert(*action, combos);
}
}
for (action, combos_str) in &toml.help {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.help.insert(*action, combos);
}
}
for (action, combos_str) in &toml.graph {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.graph.insert(*action, combos);
}
}
for (action, combos_str) in &toml.draw {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.draw.insert(*action, combos);
}
}
for (action, combos_str) in &toml.canvas {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.canvas.insert(*action, combos);
}
}
for (action, combos_str) in &toml.backup {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.backup.insert(*action, combos);
}
}
for (action, combos_str) in &toml.content_tree {
let combos: Vec<KeyCombo> = combos_str
.iter()
.filter_map(|s| KeyCombo::parse(s))
.collect();
if !combos.is_empty() {
keybinds.content_tree.insert(*action, combos);
}
}
Ok(keybinds)
}
pub fn save(&self, path: &Path) -> Result<()> {
let toml = self.to_toml();
let content = toml::to_string_pretty(&toml).context("failed to serialize keybinds")?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("failed to create keybinds directory")?;
}
crate::fsutil::atomic_write(path, content.as_bytes())?;
Ok(())
}
pub fn to_toml(&self) -> KeybindsToml {
let mut toml = KeybindsToml::default();
for (action, combos) in &self.list {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.list.insert(*action, values);
}
for (action, combos) in &self.edit {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.edit.insert(*action, values);
}
for (action, combos) in &self.help {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.help.insert(*action, values);
}
for (action, combos) in &self.graph {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.graph.insert(*action, values);
}
for (action, combos) in &self.draw {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.draw.insert(*action, values);
}
for (action, combos) in &self.canvas {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.canvas.insert(*action, values);
}
for (action, combos) in &self.backup {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.backup.insert(*action, values);
}
for (action, combos) in &self.content_tree {
let values: Vec<String> = combos.iter().map(KeyCombo::to_display_string).collect();
toml.content_tree.insert(*action, values);
}
toml
}
pub fn matches_list(&self, action: ListAction, event: &KeyEvent) -> bool {
self.list
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_edit(&self, action: EditAction, event: &KeyEvent) -> bool {
self.edit
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_help(&self, action: HelpAction, event: &KeyEvent) -> bool {
self.help
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_graph(&self, action: GraphAction, event: &KeyEvent) -> bool {
self.graph
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_draw(&self, action: DrawAction, event: &KeyEvent) -> bool {
self.draw
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_canvas(&self, action: CanvasAction, event: &KeyEvent) -> bool {
self.canvas
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_backup(&self, action: BackupAction, event: &KeyEvent) -> bool {
self.backup
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn matches_content_tree(&self, action: ContentTreeAction, event: &KeyEvent) -> bool {
self.content_tree
.get(&action)
.is_some_and(|combos| combos.iter().any(|c| c.matches(event)))
}
pub fn list_keys_display(&self, action: ListAction) -> String {
self.list
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn edit_keys_display(&self, action: EditAction) -> String {
self.edit
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn help_keys_display(&self, action: HelpAction) -> String {
self.help
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn graph_keys_display(&self, action: GraphAction) -> String {
self.graph
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn draw_keys_display(&self, action: DrawAction) -> String {
self.draw
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn canvas_keys_display(&self, action: CanvasAction) -> String {
self.canvas
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn backup_keys_display(&self, action: BackupAction) -> String {
self.backup
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
pub fn content_tree_keys_display(&self, action: ContentTreeAction) -> String {
self.content_tree
.get(&action)
.map(|combos| {
combos
.iter()
.map(KeyCombo::to_display_string)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key_combo_simple() {
let combo = KeyCombo::parse("q").unwrap();
assert_eq!(combo.code, KeyCode::Char('q'));
assert_eq!(combo.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_parse_key_combo_ctrl() {
let combo = KeyCombo::parse("Ctrl+q").unwrap();
assert_eq!(combo.code, KeyCode::Char('q'));
assert_eq!(combo.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn test_parse_key_combo_ctrl_shift() {
let combo = KeyCombo::parse("Ctrl+Shift+z").unwrap();
assert_eq!(combo.code, KeyCode::Char('z'));
assert_eq!(combo.modifiers, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
}
#[test]
fn test_parse_special_keys() {
assert_eq!(KeyCombo::parse("Enter").unwrap().code, KeyCode::Enter);
assert_eq!(KeyCombo::parse("Esc").unwrap().code, KeyCode::Esc);
assert_eq!(KeyCombo::parse("F1").unwrap().code, KeyCode::F(1));
assert_eq!(KeyCombo::parse("Delete").unwrap().code, KeyCode::Delete);
}
#[test]
fn test_key_combo_matches() {
let combo = KeyCombo::ctrl(KeyCode::Char('q'));
let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
assert!(combo.matches(&event));
let wrong_event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert!(!combo.matches(&wrong_event));
let bt_combo1 = KeyCombo::simple(KeyCode::BackTab);
let bt_combo2 = KeyCombo::shift(KeyCode::BackTab);
let bt_event1 = KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE);
let bt_event2 = KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT);
assert!(bt_combo1.matches(&bt_event1));
assert!(bt_combo1.matches(&bt_event2));
assert!(bt_combo2.matches(&bt_event1));
assert!(bt_combo2.matches(&bt_event2));
let bt_ctrl_event = KeyEvent::new(KeyCode::BackTab, KeyModifiers::CONTROL);
assert!(!bt_combo1.matches(&bt_ctrl_event));
}
#[test]
fn test_default_keybinds() {
let keybinds = Keybinds::default();
assert!(!keybinds.list.is_empty());
assert!(!keybinds.edit.is_empty());
assert!(!keybinds.help.is_empty());
assert!(!keybinds.draw.is_empty());
assert!(!keybinds.canvas.is_empty());
assert!(!keybinds.backup.is_empty());
assert!(!keybinds.content_tree.is_empty());
let toml = keybinds.to_toml();
assert!(!toml.draw.is_empty());
assert!(!toml.canvas.is_empty());
assert!(!toml.content_tree.is_empty());
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("keybinds.toml");
keybinds.save(&path).unwrap();
let loaded_keybinds = Keybinds::load(&path).unwrap();
assert_eq!(loaded_keybinds.draw, keybinds.draw);
assert_eq!(loaded_keybinds.canvas, keybinds.canvas);
assert_eq!(loaded_keybinds.backup, keybinds.backup);
assert_eq!(loaded_keybinds.content_tree, keybinds.content_tree);
}
#[test]
fn test_matches_list_action() {
let keybinds = Keybinds::default();
let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert!(keybinds.matches_list(ListAction::Quit, &event));
}
#[test]
fn test_new_action_displays() {
let keybinds = Keybinds::default();
assert_eq!(keybinds.draw_keys_display(DrawAction::SelectDrawTool), "d");
assert_eq!(keybinds.canvas_keys_display(CanvasAction::Quit), "Esc/q");
assert_eq!(keybinds.canvas_keys_display(CanvasAction::Save), "Ctrl+s");
}
}