From 0c11c2db304b71123aab649689e550cda8596961 Mon Sep 17 00:00:00 2001 From: Builditluc Date: Thu, 21 Nov 2024 13:32:18 +0100 Subject: [PATCH] Add global keybindings This commit will add and document configuration options for global keybindings. It'll also add a system for extending the keybinding options in future commits. Signed-off-by: Builditluc --- Cargo.lock | 1 + Cargo.toml | 2 +- docs/docs/changelog/index.md | 2 + docs/docs/configuration/keybindings.md | 168 +++++++++----- src/app.rs | 50 ++-- src/components/page_language_popup.rs | 43 ++-- src/components/page_viewer.rs | 2 +- src/components/search.rs | 4 +- src/components/search_bar.rs | 24 +- src/components/search_language_popup.rs | 43 ++-- src/config.rs | 292 ++++++++++++++++++++++++ 11 files changed, 508 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 601c4d57..06b96af0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,7 @@ dependencies = [ "libc", "mio", "parking_lot", + "serde", "signal-hook", "signal-hook-mio", "winapi", diff --git a/Cargo.toml b/Cargo.toml index 683bdc70..4e22603f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ anyhow = "1.0.75" better-panic = "0.3.0" clap = { version = "4.4.11", features = ["cargo", "derive"] } color-eyre = "0.6.2" -crossterm = { version = "0.27.0", default-features = false, features = ["event-stream"] } +crossterm = { version = "0.27.0", default-features = false, features = ["event-stream", "serde"] } directories = "5.0.1" futures = "0.3.28" human-panic = "1.2.2" diff --git a/docs/docs/changelog/index.md b/docs/docs/changelog/index.md index b6cfcc4b..f6ee77c4 100644 --- a/docs/docs/changelog/index.md +++ b/docs/docs/changelog/index.md @@ -11,6 +11,7 @@ - Add options for changing the scrollbar colors - Add options for changing the statusbar colors - Add zen-mode to hide ui elements +- Add multiple new configurable keybindings ## Changes @@ -23,6 +24,7 @@ - Change the application pattern to a component-based architecture - Change the logging library used to `tracing` - Change the cli library from `structopt` to `clap` +- Change the configuration layout of the keybindings - Improve debug panic messages using `better-panic` - Improve release panic messages using `human-panic` - Improve the cli interface diff --git a/docs/docs/configuration/keybindings.md b/docs/docs/configuration/keybindings.md index 142fd8bd..4e1a795a 100644 --- a/docs/docs/configuration/keybindings.md +++ b/docs/docs/configuration/keybindings.md @@ -2,68 +2,130 @@ There are certain actions that you can change the Keybindings for. The configuration for each action is the same. -## Action Configuration +## Binding Configuration -### Key +!!! tip "Multiple Bindings per Action" + You can also define multiple bindings for one action by putting them into an array (`action = [bidning1, binding2, etc.]`). -The key setting can be a simple character or a non-character key +A bindig can be either a keycode or a keycode combined with one or more modifiers for that key. All +of the following are a valid way of configuring a binding (where `action` is one of the configurable +actions) + +```toml +action = "esc" +``` +> A keycode without any modifiers + +```toml +action = { code = "l", modifiers = "CONTROL | SHIFT" } # or with just a single modifier +action = { code = "l", modifiers = "CONTROL" } +``` +> A keycode with modifiers + +These can be mixed together when defining multiple bindings for one action + +```toml +action = [ + { code = "l", modifiers = "CONTROL" }, + "esc", +] +``` + +### Keycodes + +A keycode can be a simple character or a non-character key These are the supported non-character keys (lower-/uppercase doesn't matter); -| Key | Config Name | -|------------------|----------------| -| ++insert++ | `insert` | -| ++delete++ | `delete` | -| ++home++ | `home` | -| ++end++ | `end` | -| ++page-up++ | `pageup` | -| ++page-down++ | `pagedown` | -| ++break++ | `pausebreak` | -| ++num-enter++ | `numpadcenter` | -| ++f1++ - ++f12++ | `f1` - `f12` | +| Key | Config Name | +|----------------|-------------| +| ++backspace++ | backspace | +| ++enter++ | enter | +| ++left++ | left | +| ++right++ | right | +| ++up++ | up | +| ++down++ | down | +| ++home++ | home | +| ++end++ | end | +| ++page-up++ | pageup | +| ++page-down++ | pagedown | +| ++tab++ | tab | +| ++backtab++ | backtab | +| ++delete++ | delete | +| ++insert++ | insert | +| ++esc++ | esc | +| ++f1++-++f12++ | f1-f12 | -### Mode +### Modifiers -The following modes are supported +The following modifiers are available. You can also combine them using `|` as a separator. Please +note that these modifiers are case-sensitive | Key | Config Name | |----------------|-------------| - | | `normal` | -| ++shift++ | `shift` | -| ++alt++ | `alt` | -| ++alt+shift++ | `altshift` | -| ++ctrl++ | `ctrl` | -| ++ctrl+shift++ | `ctrlshift` | -| ++ctrl+alt++ | `ctrlalt` | - -## Supported Actions - -| Action | Config Name | Default Keybinding | -|-------------------------|--------------|--------------------| -| Scroll Down | `down` | ++down++ | -| Scroll Up | `up` | ++up++ | -| Scroll / Select Left | `left` | ++left++ | -| Scroll / Select Right | `right` | ++right++ | -| Focus the next view | `focus_next` | ++tab++ | -| Focus the previous view | `focus_prev` | ++shift+tab++ | -| Go to Top | N/a | ++"g"++ ++"g"++ | -| Go to Bottom | N/a | ++"G"++ | -| Half page down | N/a | ++ctrl+d++ | -| Half page up | N/a | ++ctrl+u++ | -| Focus the previous view | `focus_prev` | ++shift+tab++ | -| Toggle the language selection | `toggle_language_selection` | ++f2++ | -| Toggle the article language selection | `toggle_article_language_selection` | ++f3++ | - -> When updating the language via the selection popup, existing search results and links in articles -> won't work until you've changed the language back to what it was then opening the article / -> starting the search - -## Sample Remap +| ++shift++ | `SHIFT` | +| ++ctrl++ | `CONTROL` | +| ++alt++ | `ALT` | + +## Default Bindings + +Below are the default bindings for all of the configurable actions + +### Global Bindings + +| Action | Description | Default Binding | +|------------------------------------|------------------------------------------------------|----------------------------| +| `scroll_down` | Scroll down | ++j++ | +| `scroll_up` | Scroll down | ++k++ | +| `scroll_to_top` | Scroll to the top | ++'g'++ / ++home++ | +| `scroll_to_bottom` | Scroll to the bottom | ++'G'++ / ++end++ | +| `pop_popup` | Remove the displayed popup | ++esc++ | +| `half_down` | Scroll half a page down | ++ctrl+d++ / ++page-down++ | +| `half-up` | Scroll half a page up | ++ctrl+u++ / ++page-up++ | +| `unselect_scroll` | Unselect the current selection | ++h++ | +| `submit` | Submit the selected form or open the selection | ++enter++ | +| `quit` | Quit the program | ++q++ | +| `enter_search_bar` | Focus the searchbar | ++i++ | +| `exit_search_bar` | Defocus the searchbar (return to the previous focus) | ++esc++ | +| `switch_context_search` | Switch to the search pane | ++s++ | +| `switch_context_page` | Switch to the page pane | ++p++ | +| `toggle_search_language_selection` | Toggle the search language selection popup | ++f2++ | +| `toggle_logger` | Toggle the logger view | ++l++ | + +The default configuration file for the global bindings ```toml -[keybindings] -down.key = "j" -down.mode = "shift" -``` +[bindings.global] +scroll_down = "j" +scroll_up = "k" + +scroll_to_top = [ "g", "home" ] +scroll_to_bottom = [ + { code = "G", modifiers = "SHIFT" }, + "end", +] -[release-0.5.0]: https://github.com/Builditluc/wiki-tui/releases/tag/v0.5.0 -[release-0.6.0]: https://github.com/Builditluc/wiki-tui/releases/tag/v0.6.0 +pop_popup = "esc" + +half_down = [ + { code = "d", modifiers = "CONTROL" }, + "pagedown", +] +half_up = [ + { code = "u", modifiers = "CONTROL" }, + "pageup", +] + +unselect_scroll = "h" + +submit = "enter" +quit = "q" + +enter_search_bar = "i" +exit_search_bar = "esc" + +switch_context_search = "s" +switch_context_page = "p" + +toggle_search_language_selection = "f2" +toggle_logger = "l" +``` diff --git a/src/app.rs b/src/app.rs index d100a213..cb4e9837 100644 --- a/src/app.rs +++ b/src/app.rs @@ -140,43 +140,47 @@ impl Component for AppComponent { return result; } - match key.code { - KeyCode::Char('q') => Action::Quit.into(), - KeyCode::Esc => Action::PopPopup.into(), + let global_bindings = &self.config.bindings.global; + macro_rules! match_bindings { + ($($bind:ident => $action:expr),+) => { + $(if global_bindings.$bind.matches_event(key) { + return $action.into(); + })+ + }; + } - KeyCode::Char('l') => Action::ToggleShowLogger.into(), + match_bindings!( + quit => Action::Quit, + pop_popup => Action::PopPopup, - KeyCode::Char('s') => Action::SwitchContextSearch.into(), - KeyCode::Char('p') => Action::SwitchContextPage.into(), + toggle_logger => Action::ToggleShowLogger, - KeyCode::Char('j') => Action::ScrollDown(1).into(), - KeyCode::Char('k') => Action::ScrollUp(1).into(), + switch_context_search => Action::SwitchContextSearch, + switch_context_page => Action::SwitchContextPage, - KeyCode::Char('g') | KeyCode::Home => Action::ScrollToTop.into(), - KeyCode::Char('G') | KeyCode::End => Action::ScrollToBottom.into(), + scroll_down => Action::ScrollDown(1), + scroll_up => Action::ScrollUp(1), - KeyCode::Char('d') if has_modifier!(key, Modifier::CONTROL) => { - Action::ScrollHalfDown.into() - } - KeyCode::Char('u') if has_modifier!(key, Modifier::CONTROL) => { - Action::ScrollHalfUp.into() - } - KeyCode::PageDown => Action::ScrollHalfDown.into(), - KeyCode::PageUp => Action::ScrollHalfUp.into(), + scroll_to_top => Action::ScrollToTop, + scroll_to_bottom => Action::ScrollToBottom, - KeyCode::Char('h') => Action::UnselectScroll.into(), + half_up => Action::ScrollHalfUp, + half_down => Action::ScrollHalfDown, - KeyCode::Char('i') => Action::EnterSearchBar.into(), + unselect_scroll => Action::UnselectScroll, + enter_search_bar => Action::EnterSearchBar, - KeyCode::F(2) => { + toggle_search_language_selection => { self.popups .push(Box::new(SearchLanguageSelectionComponent::new( + self.config.clone(), self.theme.clone(), ))); ActionResult::consumed() } - _ => ActionResult::Ignored, - } + ); + + ActionResult::Ignored } fn update(&mut self, action: Action) -> ActionResult { diff --git a/src/components/page_language_popup.rs b/src/components/page_language_popup.rs index be76b022..3440ec7f 100644 --- a/src/components/page_language_popup.rs +++ b/src/components/page_language_popup.rs @@ -11,7 +11,7 @@ use wiki_api::page::LanguageLink; use crate::{ action::{Action, ActionPacket, ActionResult}, - config::Theme, + config::{Config, Theme}, terminal::Frame, ui::{centered_rect, StatefulList}, }; @@ -27,17 +27,19 @@ pub struct PageLanguageSelectionComponent { list: StatefulList, language_links: Vec, + config: Arc, theme: Arc, } impl PageLanguageSelectionComponent { - pub fn new(language_links: Vec, theme: Arc) -> Self { + pub fn new(language_links: Vec, config: Arc, theme: Arc) -> Self { Self { input: Input::default(), list: StatefulList::with_items(language_links.clone()), language_links, focus: 0, + config, theme, } } @@ -60,22 +62,27 @@ impl PageLanguageSelectionComponent { impl Component for PageLanguageSelectionComponent { fn handle_key_events(&mut self, key: crossterm::event::KeyEvent) -> ActionResult { - match key.code { - KeyCode::Enter => { - if let Some(link) = self.list.selected() { - return ActionPacket::single(Action::PopPopup) - .action(Action::PopupMessage( - "Information".to_string(), - format!( - "Changing the language of the page to '{}'", - link.language.name() - ), - )) - .action(Action::LoadLangaugeLink(link.to_owned())) - .into(); - } - ActionResult::Ignored + if self.config.bindings.global.submit.matches_event(key) { + if let Some(link) = self.list.selected() { + return ActionPacket::single(Action::PopPopup) + .action(Action::PopupMessage( + "Information".to_string(), + format!( + "Changing the language of the page to '{}'", + link.language.name() + ), + )) + .action(Action::LoadLangaugeLink(link.to_owned())) + .into(); } + return ActionResult::Ignored; + } + + if self.config.bindings.global.pop_popup.matches_event(key) { + return Action::PopPopup.into(); + } + + match key.code { KeyCode::Tab | KeyCode::BackTab => { if self.focus == FOCUS_INPUT { self.focus = FOCUS_LIST; @@ -91,7 +98,7 @@ impl Component for PageLanguageSelectionComponent { self.focus = FOCUS_INPUT; ActionResult::consumed() } - KeyCode::Esc | KeyCode::F(3) => Action::PopPopup.into(), + KeyCode::F(3) => Action::PopPopup.into(), _ if self.focus == FOCUS_INPUT => { self.input.handle_event(&crossterm::event::Event::Key(key)); self.update_list(); diff --git a/src/components/page_viewer.rs b/src/components/page_viewer.rs index ec029d63..a5f4f1eb 100644 --- a/src/components/page_viewer.rs +++ b/src/components/page_viewer.rs @@ -66,7 +66,7 @@ impl PageViewer { .current_page() .and_then(|x| x.page.language_links.to_owned()) .unwrap_or_default(); - PageLanguageSelectionComponent::new(language_links, self.theme.clone()) + PageLanguageSelectionComponent::new(language_links, self.config.clone(), self.theme.clone()) } } diff --git a/src/components/search.rs b/src/components/search.rs index 6b61063b..3620b563 100644 --- a/src/components/search.rs +++ b/src/components/search.rs @@ -251,7 +251,9 @@ impl Component for SearchComponent { } } Mode::FinishedSearch => match key.code { - KeyCode::Enter if self.search_results.is_selected() => { + _ if self.search_results.is_selected() + && self.config.bindings.global.submit.matches_event(key) => + { Action::Search(SearchAction::OpenSearchResult).into() } KeyCode::Char('c') => Action::Search(SearchAction::ContinueSearch).into(), diff --git a/src/components/search_bar.rs b/src/components/search_bar.rs index 87e504bb..7b92718a 100644 --- a/src/components/search_bar.rs +++ b/src/components/search_bar.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{KeyEvent}; use ratatui::{ prelude::Rect, style::{Color, Modifier, Style}, @@ -53,14 +53,22 @@ impl Component for SearchBarComponent { } fn handle_key_events(&mut self, key: KeyEvent) -> ActionResult { - match key.code { - KeyCode::Enter => Action::SubmitSearchBar.into(), - KeyCode::Esc => Action::ExitSearchBar.into(), - _ => { - self.input.handle_event(&crossterm::event::Event::Key(key)); - ActionResult::consumed() - } + if self.config.bindings.global.submit.matches_event(key) { + return Action::SubmitSearchBar.into(); } + + if self + .config + .bindings + .global + .exit_search_bar + .matches_event(key) + { + return Action::ExitSearchBar.into(); + } + + self.input.handle_event(&crossterm::event::Event::Key(key)); + ActionResult::consumed() } fn render(&mut self, f: &mut Frame<'_>, area: Rect) { diff --git a/src/components/search_language_popup.rs b/src/components/search_language_popup.rs index 2241f9bb..9edf3a1f 100644 --- a/src/components/search_language_popup.rs +++ b/src/components/search_language_popup.rs @@ -11,7 +11,7 @@ use wiki_api::languages::{Language, LANGUAGES}; use crate::{ action::{Action, ActionPacket, ActionResult, SearchAction}, - config::Theme, + config::{Config, Theme}, terminal::Frame, ui::{centered_rect, StatefulList}, }; @@ -26,16 +26,18 @@ pub struct SearchLanguageSelectionComponent { focus: u8, list: StatefulList, + config: Arc, theme: Arc, } impl SearchLanguageSelectionComponent { - pub fn new(theme: Arc) -> Self { + pub fn new(config: Arc, theme: Arc) -> Self { Self { input: Input::default(), list: StatefulList::with_items(Vec::new()), focus: 0, + config, theme, } } @@ -57,22 +59,27 @@ impl SearchLanguageSelectionComponent { impl Component for SearchLanguageSelectionComponent { fn handle_key_events(&mut self, key: crossterm::event::KeyEvent) -> ActionResult { - match key.code { - KeyCode::Enter => { - if let Some(lang) = self.list.selected() { - return ActionPacket::single(Action::SwitchContextSearch) - .action(Action::PopPopup) - .action(Action::PopupMessage( - "Information".to_string(), - format!("Changed the language for searches to '{}'", lang.name()), - )) - .action(Action::Search(SearchAction::ChangeLanguage( - lang.to_owned(), - ))) - .into(); - } - ActionResult::Ignored + if self.config.bindings.global.submit.matches_event(key) { + if let Some(lang) = self.list.selected() { + return ActionPacket::single(Action::SwitchContextSearch) + .action(Action::PopPopup) + .action(Action::PopupMessage( + "Information".to_string(), + format!("Changed the language for searches to '{}'", lang.name()), + )) + .action(Action::Search(SearchAction::ChangeLanguage( + lang.to_owned(), + ))) + .into(); } + return ActionResult::Ignored; + } + + if self.config.bindings.global.pop_popup.matches_event(key) { + return Action::PopPopup.into(); + } + + match key.code { KeyCode::Tab | KeyCode::BackTab => { if self.focus == FOCUS_INPUT { self.focus = FOCUS_LIST; @@ -89,7 +96,7 @@ impl Component for SearchLanguageSelectionComponent { ActionResult::consumed() } - KeyCode::Esc | KeyCode::F(2) => Action::PopPopup.into(), + KeyCode::F(2) => Action::PopPopup.into(), _ if self.focus == FOCUS_INPUT => { self.input.handle_event(&crossterm::event::Event::Key(key)); diff --git a/src/config.rs b/src/config.rs index ba4919ce..291f904d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Context, Result}; use bitflags::bitflags; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use directories::ProjectDirs; use ratatui::{ layout::Constraint, @@ -72,6 +73,10 @@ pub fn load_config() -> Result { override_page_config(&mut default_config.page, user_page_config) } + if let Some(user_bindings_config) = user_config.bindings { + override_bindings_config(&mut default_config.bindings, user_bindings_config) + } + Ok(default_config) } @@ -101,6 +106,35 @@ fn override_page_config(config: &mut PageConfig, user_config: UserPageConfig) { } } +fn override_bindings_config(config: &mut Keybindings, user_config: UserKeybindingsConfig) { + if let Some(user_global_bindings) = user_config.global { + override_options!(config.global, user_global_bindings::{ + scroll_down, + scroll_up, + + scroll_to_top, + scroll_to_bottom, + + pop_popup, + + half_down, + half_up, + unselect_scroll, + + submit, + quit, + enter_search_bar, + exit_search_bar, + + switch_context_search, + switch_context_page, + + toggle_search_language_selection, + toggle_logger + }); + } +} + fn load_user_config() -> Result { let path = config_dir() .context("failed retrieving the config dir")? @@ -118,6 +152,7 @@ fn load_user_config() -> Result { pub struct Config { pub page: PageConfig, + pub bindings: Keybindings, } pub struct PageConfig { @@ -192,8 +227,80 @@ pub struct TocConfig { pub enable_scrolling: bool, } +#[derive(Deserialize)] +struct Binding { + code: KeyCode, + modifiers: KeyModifiers, +} + +#[derive(Deserialize)] +pub struct Keybinding { + bindings: Vec, +} + +impl Keybinding { + fn new() -> Self { + Self { + bindings: Vec::new(), + } + } + + fn binding(mut self, code: KeyCode, modifiers: KeyModifiers) -> Self { + self.bindings.push(Binding { code, modifiers }); + self + } + + pub fn matches_event(&self, event: KeyEvent) -> bool { + return self + .bindings + .iter() + .any(|x| x.code == event.code && x.modifiers == event.modifiers); + } +} + +pub struct GlobalKeybindings { + pub scroll_down: Keybinding, + pub scroll_up: Keybinding, + + pub scroll_to_top: Keybinding, + pub scroll_to_bottom: Keybinding, + + pub pop_popup: Keybinding, + + pub half_down: Keybinding, + pub half_up: Keybinding, + pub unselect_scroll: Keybinding, + + pub submit: Keybinding, + pub quit: Keybinding, + pub enter_search_bar: Keybinding, + pub exit_search_bar: Keybinding, + + pub switch_context_search: Keybinding, + pub switch_context_page: Keybinding, + + pub toggle_search_language_selection: Keybinding, + pub toggle_logger: Keybinding, +} + +pub struct Keybindings { + pub global: GlobalKeybindings, +} + impl Config { pub fn new() -> Self { + macro_rules! keybinding { + ([$($ch:expr; $($md:ident)|*),+]) => { + { + Keybinding::new() + $(.binding( + $ch, + KeyModifiers::NONE$(|KeyModifiers::$md)* + ))+ + } + }; + } + Self { page: PageConfig { toc: TocConfig { @@ -213,6 +320,34 @@ impl Config { zen_horizontal: Constraint::Percentage(80), zen_vertical: Constraint::Percentage(90), }, + bindings: Keybindings { + global: GlobalKeybindings { + scroll_down: keybinding!([KeyCode::Char('j');]), + scroll_up: keybinding!([KeyCode::Char('k');]), + + scroll_to_top: keybinding!([KeyCode::Char('g');, KeyCode::Home;]), + scroll_to_bottom: keybinding!([KeyCode::Char('G'); SHIFT, KeyCode::End;]), + + pop_popup: keybinding!([KeyCode::Esc;]), + + half_down: keybinding!([KeyCode::Char('d'); CONTROL, KeyCode::PageDown;]), + half_up: keybinding!([KeyCode::Char('u'); CONTROL, KeyCode::PageUp;]), + + unselect_scroll: keybinding!([KeyCode::Char('h');]), + + submit: keybinding!([KeyCode::Enter;]), + quit: keybinding!([KeyCode::Char('q');, KeyCode::Char('c'); CONTROL]), + + enter_search_bar: keybinding!([KeyCode::Char('i');]), + exit_search_bar: keybinding!([KeyCode::Esc;]), + + switch_context_search: keybinding!([KeyCode::Char('s');]), + switch_context_page: keybinding!([KeyCode::Char('p');]), + + toggle_search_language_selection: keybinding!([KeyCode::F(2);]), + toggle_logger: keybinding!([KeyCode::Char('l');]), + }, + }, } } } @@ -237,6 +372,7 @@ impl Default for Config { #[derive(Deserialize)] struct UserConfig { page: Option, + bindings: Option, } #[derive(Deserialize)] @@ -289,6 +425,162 @@ struct UserTocConfig { enable_scrolling: Option, } +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +enum UserKeyCodeInner { + Backspace, + Enter, + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + Tab, + BackTab, + Delete, + Insert, + Esc, + + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum UserKeyCode { + Char(char), + NonChar(UserKeyCodeInner), +} + +#[allow(clippy::from_over_into)] +impl Into for UserKeyCode { + fn into(self) -> KeyCode { + match self { + Self::Char(char) => KeyCode::Char(char), + Self::NonChar(inner) => match inner { + UserKeyCodeInner::Backspace => KeyCode::Backspace, + UserKeyCodeInner::Enter => KeyCode::Enter, + UserKeyCodeInner::Left => KeyCode::Left, + UserKeyCodeInner::Right => KeyCode::Right, + UserKeyCodeInner::Up => KeyCode::Up, + UserKeyCodeInner::Down => KeyCode::Down, + UserKeyCodeInner::Home => KeyCode::Home, + UserKeyCodeInner::End => KeyCode::End, + UserKeyCodeInner::PageUp => KeyCode::PageUp, + UserKeyCodeInner::PageDown => KeyCode::PageDown, + UserKeyCodeInner::Tab => KeyCode::Tab, + UserKeyCodeInner::BackTab => KeyCode::BackTab, + UserKeyCodeInner::Delete => KeyCode::Delete, + UserKeyCodeInner::Insert => KeyCode::Insert, + UserKeyCodeInner::Esc => KeyCode::Esc, + UserKeyCodeInner::F1 => KeyCode::F(1), + UserKeyCodeInner::F2 => KeyCode::F(2), + UserKeyCodeInner::F3 => KeyCode::F(3), + UserKeyCodeInner::F4 => KeyCode::F(4), + UserKeyCodeInner::F5 => KeyCode::F(5), + UserKeyCodeInner::F6 => KeyCode::F(6), + UserKeyCodeInner::F7 => KeyCode::F(7), + UserKeyCodeInner::F8 => KeyCode::F(8), + UserKeyCodeInner::F9 => KeyCode::F(9), + UserKeyCodeInner::F10 => KeyCode::F(10), + UserKeyCodeInner::F11 => KeyCode::F(12), + UserKeyCodeInner::F12 => KeyCode::F(13), + }, + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum UserBinding { + CodeOnly(UserKeyCode), + Binding { + code: UserKeyCode, + modifiers: Option, + }, +} + +#[allow(clippy::from_over_into)] +impl Into for UserBinding { + fn into(self) -> Binding { + match self { + UserBinding::CodeOnly(code) => UserBinding::Binding { + code, + modifiers: None, + } + .into(), + UserBinding::Binding { code, modifiers } => Binding { + code: code.into(), + modifiers: modifiers.unwrap_or(KeyModifiers::empty()), + }, + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum UserKeybinding { + SingleBinding(UserBinding), + MultipleBindings(Vec), +} + +#[allow(clippy::from_over_into)] +impl Into for UserKeybinding { + fn into(self) -> Keybinding { + match self { + UserKeybinding::SingleBinding(binding) => Keybinding { + bindings: vec![binding.into()], + }, + UserKeybinding::MultipleBindings(bindings) => Keybinding { + bindings: bindings.into_iter().map(|x| x.into()).collect(), + }, + } + } +} + +#[derive(Deserialize)] +struct UserGlobalKeybindings { + scroll_down: Option, + scroll_up: Option, + + scroll_to_top: Option, + scroll_to_bottom: Option, + + pop_popup: Option, + + half_down: Option, + half_up: Option, + unselect_scroll: Option, + + submit: Option, + quit: Option, + enter_search_bar: Option, + exit_search_bar: Option, + + switch_context_search: Option, + switch_context_page: Option, + + toggle_search_language_selection: Option, + toggle_logger: Option, +} + +#[derive(Deserialize)] +struct UserKeybindingsConfig { + global: Option, +} pub fn load_theme() -> Result { let mut default_theme = Theme::default();