diff options
author | Thang Pham <phamducthang1234@gmail.com> | 2022-04-28 17:39:01 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-28 17:39:01 -0400 |
commit | 7e4775d3425db584a0e32692cb60e679a2039756 (patch) | |
tree | 7507453f3481c8aecfb476978cb33bacd82461f0 | |
parent | f99380ed30a9f0897be67ba8a6f2e1145f4f6717 (diff) |
Support multiple keybindings to a single command (#70)
## Brief description of changes
- support multiple keybindings to a single command by specifying either **a key string** or **an array of key strings** in the config file
- add `[back]` button to the application's footer
## Breaking changes
- modify the default shortcuts for
- `global_keymap.quit`
- `global_keymap.goto_previous_view`
- `global_keymap.goto_search_view`
- `edit_keymap.move_cursor_left`
- `edit_keymap.move_cursor_right`
- `edit_keymap.move_cursor_to_begin`
- `edit_keymap.move_cursor_to_end`
-rw-r--r-- | README.md | 79 | ||||
-rw-r--r-- | config.md | 19 | ||||
-rw-r--r-- | examples/hn-tui.toml | 14 | ||||
-rw-r--r-- | hackernews_tui/src/config/keybindings.rs | 450 | ||||
-rw-r--r-- | hackernews_tui/src/utils.rs | 7 | ||||
-rw-r--r-- | hackernews_tui/src/view/article_view.rs | 4 | ||||
-rw-r--r-- | hackernews_tui/src/view/comment_view.rs | 4 | ||||
-rw-r--r-- | hackernews_tui/src/view/help_view.rs | 12 | ||||
-rw-r--r-- | hackernews_tui/src/view/search_view.rs | 10 | ||||
-rw-r--r-- | hackernews_tui/src/view/story_view.rs | 2 |
10 files changed, 339 insertions, 262 deletions
@@ -140,27 +140,27 @@ For more information about configuring the key mapping or defining custom shortc ### Global key shortcuts -| Command | Description | Default Shortcut | -| ----------------------- | ----------------------- | ---------------- | -| `open_help_dialog` | Open the help dialog | `?` | -| `close_dialog` | Close a dialog | `esc` | -| `quit` | Quit the application | `C-q` | -| `goto_previous_view` | Go to the previous view | `C-p` | -| `goto_search_view` | Go to search view | `C-s` | -| `goto_front_page_view` | Go to front page view | `F1` | -| `goto_all_stories_view` | Go to all stories view | `F2` | -| `goto_ask_hn_view` | Go to ask HN view | `F3` | -| `goto_show_hn_view` | Go to show HN view | `F4` | -| `goto_jobs_view` | Go to jobs view | `F5` | +| Command | Description | Default Shortcut | +| ----------------------- | ----------------------- | ------------------ | +| `open_help_dialog` | Open the help dialog | `?` | +| `close_dialog` | Close a dialog | `esc` | +| `quit` | Quit the application | `[q, C-c]` | +| `goto_previous_view` | Go to the previous view | `[backspace, C-p]` | +| `goto_search_view` | Go to search view | `[/, C-s]` | +| `goto_front_page_view` | Go to front page view | `F1` | +| `goto_all_stories_view` | Go to all stories view | `F2` | +| `goto_ask_hn_view` | Go to ask HN view | `F3` | +| `goto_show_hn_view` | Go to show HN view | `F4` | +| `goto_jobs_view` | Go to jobs view | `F5` | ### Edit key shortcuts | Command | Description | Default Shortcut | | ---------------------- | -------------------------------- | ---------------- | -| `move_cursor_left` | Move cursor to left | `left` | -| `move_cursor_right` | Move cursor to right | `right` | -| `move_cursor_to_begin` | Move cursor to the begin of line | `home` | -| `move_cursor_to_end` | Move cursor to the end of line | `end` | +| `move_cursor_left` | Move cursor to left | `[left, C-b]` | +| `move_cursor_right` | Move cursor to right | `[right, C-f]` | +| `move_cursor_to_begin` | Move cursor to the begin of line | `[home, C-a]` | +| `move_cursor_to_end` | Move cursor to the end of line | `[end, C-e]` | | `backward_delete_char` | Delete backward a character | `backspace` | ### Key shortcuts for each `View` @@ -208,26 +208,26 @@ For more information about configuring the key mapping or defining custom shortc #### Comment View shortcuts -| Command | Description | Default Shortcut | -| --------------------------- | -------------------------------------------------------------------- | ---------------- | -| `next_comment` | Focus the next comment | `j` | -| `prev_comment` | Focus the previous comment | `k` | -| `next_leq_level_comment` | Focus the next comment with smaller or equal level | `l` | -| `prev_leq_level_comment` | Focus the previous comment with smaller or equal level | `h` | -| `next_top_level_comment` | Focus the next top level comment | `n` | -| `prev_top_level_comment` | Focus the previous top level comment | `p` | -| `parent_comment` | Focus the parent comment (if exists) | `u` | -| `toggle_collapse_comment` | Toggle collapsing the focused comment | `tab` | -| `up` | Scroll up | `up` | -| `down` | Scroll down | `down` | -| `page_up` | Scroll up half a page | `page_up` | -| `page_down` | Scroll down half a page | `page_down` | -| `open_article_in_browser` | Open in browser the article associated with the discussed story | `o` | -| `open_link_in_article_view` | Open in article view the article associated with the discussed story | `O` | -| `open_story_in_browser` | Open in browser the discussed story | `s` | -| `open_comment_in_browser` | Open in browser the focused comment | `c` | -| `open_link_in_browser` | Open in browser the {link_id}-th link in the focused comment | `{link_id} f` | -| `open_link_in_article_view` | Open in article view the {link_id}-th link in the focused comment | `{link_id} F` | +| Command | Description | Default Shortcut | +| ------------------------------ | -------------------------------------------------------------------- | ---------------- | +| `next_comment` | Focus the next comment | `j` | +| `prev_comment` | Focus the previous comment | `k` | +| `next_leq_level_comment` | Focus the next comment with smaller or equal level | `l` | +| `prev_leq_level_comment` | Focus the previous comment with smaller or equal level | `h` | +| `next_top_level_comment` | Focus the next top level comment | `n` | +| `prev_top_level_comment` | Focus the previous top level comment | `p` | +| `parent_comment` | Focus the parent comment (if exists) | `u` | +| `toggle_collapse_comment` | Toggle collapsing the focused comment | `tab` | +| `up` | Scroll up | `up` | +| `down` | Scroll down | `down` | +| `page_up` | Scroll up half a page | `page_up` | +| `page_down` | Scroll down half a page | `page_down` | +| `open_article_in_browser` | Open in browser the article associated with the discussed story | `o` | +| `open_article_in_article_view` | Open in article view the article associated with the discussed story | `O` | +| `open_story_in_browser` | Open in browser the discussed story | `s` | +| `open_comment_in_browser` | Open in browser the focused comment | `c` | +| `open_link_in_browser` | Open in browser the {link_id}-th link in the focused comment | `{link_id} f` | +| `open_link_in_article_view` | Open in article view the {link_id}-th link in the focused comment | `{link_id} F` | #### Search View shortcuts @@ -246,7 +246,8 @@ In `SearchView`, there are two modes: `Navigation` and `Search`. The default mod ## Configuration -By default, `hackernews-tui` will look for the `hn-tui.toml` user-defined config file inside +By default, `hackernews-tui` will look for the `hn-tui.toml` user-defined config file inside + - the [user's config directory](https://docs.rs/dirs-next/latest/dirs_next/fn.config_dir.html) - `.config` directory inside the [user's home directory](https://docs.rs/dirs-next/latest/dirs_next/fn.home_dir.html) @@ -262,7 +263,7 @@ For further information about the application's configurations, please refer to ## Logging -`hackernews-tui` uses `RUST_LOG` environment variable to define the application's [logging level](https://docs.rs/log/0.4.14/log/enum.Level.html) (default to be `INFO`). +`hackernews-tui` uses `RUST_LOG` environment variable to define the application's [logging level](https://docs.rs/log/0.4.14/log/enum.Level.html) (default to be `INFO`). By default, the application creates the `hn-tui.log` log file inside the [user's cache directory](https://docs.rs/dirs-next/latest/dirs_next/fn.cache_dir.html), which can be configured by specifying the `-l` or `--log` option. @@ -287,7 +288,7 @@ By default, the application creates the `hn-tui.log` log file inside the [user's - [x] rewrite the theme parser to support more themes and allow to parse themes from known colorschemes - [ ] add some extra transition effects - improve the keybinding handler - - [ ] allow to bind multiple keys to a single command + - [x] allow to bind multiple keys to a single command - [ ] add prefix key support (emacs-like key chaining - `C-x C-c ...`) - [ ] improve the loading progress bar - [ ] snipe-like navigation, inspired by [vim-snipe](https://github.com/yangmillstheory/vim-snipe) @@ -20,13 +20,13 @@ An example config file (with some default config values) can be found in [exampl ## General -| Option | Description | Default | -| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -| `use_page_scrolling` | whether to enable page-like scrolling behavior, which automatically adjusts the view based on the scrolling direction | `true` | -| `use_pacman_loading` | whether to use a pacman loading screen or a plain loading screen | `true` | +| Option | Description | Default | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `use_page_scrolling` | whether to enable page-like scrolling behavior, which automatically adjusts the view based on the scrolling direction | `true` | +| `use_pacman_loading` | whether to use a pacman loading screen or a plain loading screen | `true` | | `url_open_command` | the command the application uses to open an url in browser | `{ command = 'open', options = [] }` | | `article_parse_command` | the command the application uses to parse an article into a readable text | `{ command = 'article_md', options = ['--format', 'html'] }` | -| `client_timeout` | the timeout (in seconds) when the application's client makes an API request | `32` | +| `client_timeout` | the timeout (in seconds) when the application's client makes an API request | `32` | ### Article Parse Command @@ -166,7 +166,7 @@ Specifying the 16-bit color's name will use **the theme palette's color** (as op ## Keymap -To modify the [default key mapping](https://github.com/aome510/hackernews-TUI#default-shortcuts), simply add new mapping entries to the corresponding keymap section. For example, to change the key shortcuts for the command `next_comment` to `J` and the command `prev_comment` to `K` in the comment view, add these 3 lines to the config file: +To modify the [default key mapping](https://github.com/aome510/hackernews-TUI#default-shortcuts), simply add new mapping entries to the corresponding keymap section. For example, to change the key shortcut for the command `next_comment` to `J` and the command `prev_comment` to `K` in the comment view, add the below lines to the config file: ```toml [keymap.comment_view_keymap] @@ -174,6 +174,13 @@ next_comment = "J" prev_comment = "K" ``` +It's possible for a command to have multiple keybindings. For example, to use either `j` or `J` for the `next_comment` command, add the below lines to the config file: + +```toml +[keymap.comment_view_keymap] +next_comment = ["j", "J"] +``` + ### Custom Keymap `custom_keymaps` is a config option used to define custom shortcuts to navigate between different story views with stories filtered by certain conditions. diff --git a/examples/hn-tui.toml b/examples/hn-tui.toml index c7a608c..f67c997 100644 --- a/examples/hn-tui.toml +++ b/examples/hn-tui.toml @@ -62,10 +62,10 @@ launch_hn = { front = "green", effect = "bold" } [keymap.global_keymap] open_help_dialog = "?" -quit = "C-q" +quit = ["q", "C-c"] close_dialog = "esc" -goto_previous_view = "C-p" -goto_search_view = "C-s" +goto_previous_view = ["backspace", "C-p"] +goto_search_view = ["/", "C-s"] goto_front_page_view = "f1" goto_all_stories_view = "f2" goto_ask_hn_view = "f3" @@ -73,10 +73,10 @@ goto_show_hn_view = "f4" goto_jobs_view = "f5" [keymap.edit_keymap] -move_cursor_left = "left" -move_cursor_right = "right" -move_cursor_to_begin = "home" -move_cursor_to_end = "end" +move_cursor_left = ["left", "C-b"] +move_cursor_right = ["right", "C-f"] +move_cursor_to_begin = ["home", "C-a"] +move_cursor_to_end = ["end", "C-e"] backward_delete_char = "backspace" [keymap.story_view_keymap] diff --git a/hackernews_tui/src/config/keybindings.rs b/hackernews_tui/src/config/keybindings.rs index 5d06650..269487c 100644 --- a/hackernews_tui/src/config/keybindings.rs +++ b/hackernews_tui/src/config/keybindings.rs @@ -17,7 +17,7 @@ pub struct KeyMap { #[derive(Debug, Clone, Deserialize)] pub struct CustomKeyMap { - pub key: Key, + pub key: Keys, pub tag: String, pub by_date: bool, pub numeric_filters: client::StoryNumericFilters, @@ -27,55 +27,69 @@ config_parser_impl!(CustomKeyMap); #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct EditKeyMap { - pub move_cursor_left: Key, - pub move_cursor_right: Key, - pub move_cursor_to_begin: Key, - pub move_cursor_to_end: Key, - pub backward_delete_char: Key, + pub move_cursor_left: Keys, + pub move_cursor_right: Keys, + pub move_cursor_to_begin: Keys, + pub move_cursor_to_end: Keys, + pub backward_delete_char: Keys, } impl Default for EditKeyMap { fn default() -> Self { EditKeyMap { - move_cursor_left: Key::new(event::Key::Left), - move_cursor_right: Key::new(event::Key::Right), - move_cursor_to_begin: Key::new(event::Key::Home), - move_cursor_to_end: Key::new(event::Key::End), - backward_delete_char: Key::new(event::Key::Backspace), + move_cursor_left: Keys::new(vec![event::Key::Left.into(), event::Event::CtrlChar('b')]), + move_cursor_right: Keys::new(vec![ + event::Key::Right.into(), + event::Event::CtrlChar('f'), + ]), + move_cursor_to_begin: Keys::new(vec![ + event::Key::Home.into(), + event::Event::CtrlChar('a'), + ]), + move_cursor_to_end: Keys::new(vec![ + event::Key::End.into(), + event::Event::CtrlChar('e'), + ]), + backward_delete_char: Keys::new(vec![event::Key::Backspace.into()]), } } } #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct GlobalKeyMap { - pub open_help_dialog: Key, - pub quit: Key, - pub close_dialog: Key, + pub open_help_dialog: Keys, + pub quit: Keys, + pub close_dialog: Keys, // view navigation keymaps - pub goto_previous_view: Key, - pub goto_front_page_view: Key, - pub goto_search_view: Key, - pub goto_all_stories_view: Key, - pub goto_ask_hn_view: Key, - pub goto_show_hn_view: Key, - pub goto_jobs_view: Key, + pub goto_previous_view: Keys, + pub goto_front_page_view: Keys, + pub goto_search_view: Keys, + pub goto_all_stories_view: Keys, + pub goto_ask_hn_view: Keys, + pub goto_show_hn_view: Keys, + pub goto_jobs_view: Keys, } impl Default for GlobalKeyMap { fn default() -> Self { GlobalKeyMap { - open_help_dialog: Key::new('?'), - quit: Key::new(event::Event::CtrlChar('q')), - close_dialog: Key::new(event::Key::Esc), - - goto_previous_view: Key::new(event::Event::CtrlChar('p')), - goto_search_view: Key::new(event::Event::CtrlChar('s')), - goto_front_page_view: Key::new(event::Key::F1), - goto_all_stories_view: Key::new(event::Key::F2), - goto_ask_hn_view: Key::new(event::Key::F3), - goto_show_hn_view: Key::new(event::Key::F4), - goto_jobs_view: Key::new(event::Key::F5), + open_help_dialog: Keys::new(vec!['?'.into()]), + quit: Keys::new(vec!['q'.into(), event::Event::CtrlChar('c')]), + close_dialog: Keys::new(vec![event::Key::Esc.into()]), + + goto_previous_view: Keys::new(vec![ + event::Key::Backspace.into(), + event::Event::CtrlChar('p'), + ]), + + goto_search_view: Keys::new(vec!['/'.into(), event::Event::CtrlChar('s')]), + + goto_front_page_view: Keys::new(vec![event::Key::F1.into()]), + goto_all_stories_view: Keys::new(vec![event::Key::F2.into()]), + goto_ask_hn_view: Keys::new(vec![event::Key::F3.into()]), + goto_show_hn_view: Keys::new(vec![event::Key::F4.into()]), + goto_jobs_view: Keys::new(vec![event::Key::F5.into()]), } } } @@ -83,45 +97,45 @@ impl Default for GlobalKeyMap { #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct StoryViewKeyMap { // story tags navigation keymaps - pub next_story_tag: Key, - pub prev_story_tag: Key, + pub next_story_tag: Keys, + pub prev_story_tag: Keys, // stories navigation keymaps - pub next_story: Key, - pub prev_story: Key, - pub goto_story: Key, + pub next_story: Keys, + pub prev_story: Keys, + pub goto_story: Keys, // stories paging/filtering keymaps - pub next_page: Key, - pub prev_page: Key, - pub toggle_sort_by_date: Key, + pub next_page: Keys, + pub prev_page: Keys, + pub toggle_sort_by_date: Keys, // link opening keymaps - pub open_article_in_browser: Key, - pub open_article_in_article_view: Key, - pub open_story_in_browser: Key, + pub open_article_in_browser: Keys, + pub open_article_in_article_view: Keys, + pub open_story_in_browser: Keys, - pub goto_story_comment_view: Key, + pub goto_story_comment_view: Keys, } impl Default for StoryViewKeyMap { fn default() -> Self { StoryViewKeyMap { - next_story_tag: Key::new('l'), - prev_story_tag: Key::new('h'), - next_story: Key::new('j'), - prev_story: Key::new('k'), - goto_story: Key::new('g'), + next_story_tag: Keys::new(vec!['l'.into()]), + prev_story_tag: Keys::new(vec!['h'.into()]), + next_story: Keys::new(vec!['j'.into()]), + prev_story: Keys::new(vec!['k'.into()]), + goto_story: Keys::new(vec!['g'.into()]), - next_page: Key::new('n'), - prev_page: Key::new('p'), - toggle_sort_by_date: Key::new('d'), + next_page: Keys::new(vec!['n'.into()]), + prev_page: Keys::new(vec!['p'.into()]), + toggle_sort_by_date: Keys::new(vec!['d'.into()]), - open_article_in_browser: Key::new('o'), - open_article_in_article_view: Key::new('O'), - open_story_in_browser: Key::new('s'), + open_article_in_browser: Keys::new(vec!['o'.into()]), + open_article_in_article_view: Keys::new(vec!['O'.into()]), + open_story_in_browser: Keys::new(vec!['s'.into()]), - goto_story_comment_view: Key::new(event::Key::Enter), + goto_story_comment_view: Keys::new(vec![event::Key::Enter.into()]), } } } @@ -129,15 +143,15 @@ impl Default for StoryViewKeyMap { #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct SearchViewKeyMap { // switch mode keymaps - pub to_navigation_mode: Key, - pub to_search_mode: Key, + pub to_navigation_mode: Keys, + pub to_search_mode: Keys, } impl Default for SearchViewKeyMap { fn default() -> Self { SearchViewKeyMap { - to_navigation_mode: Key::new(event::Key::Esc), - to_search_mode: Key::new('i'), + to_navigation_mode: Keys::new(vec![event::Key::Esc.into()]), + to_search_mode: Keys::new(vec!['i'.into()]), } } } @@ -145,186 +159,216 @@ impl Default for SearchViewKeyMap { #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct CommentViewKeyMap { // comments navigation keymaps - pub next_comment: Key, - pub prev_comment: Key, - pub next_top_level_comment: Key, - pub prev_top_level_comment: Key, - pub next_leq_level_comment: Key, - pub prev_leq_level_comment: Key, - pub parent_comment: Key, + pub next_comment: Keys, + pub prev_comment: Keys, + pub next_top_level_comment: Keys, + pub prev_top_level_comment: Keys, + pub next_leq_level_comment: Keys, + pub prev_leq_level_comment: Keys, + pub parent_comment: Keys, // link opening keymaps - pub open_comment_in_browser: Key, - pub open_link_in_browser: Key, - pub open_link_in_article_view: Key, + pub open_comment_in_browser: Keys, + pub open_link_in_browser: Keys, + pub open_link_in_article_view: Keys, // scrolling - pub down: Key, - pub up: Key, - pub page_down: Key, - pub page_up: Key, + pub down: Keys, + pub up: Keys, + pub page_down: Keys, + pub page_up: Keys, - pub toggle_collapse_comment: Key, + pub toggle_collapse_comment: Keys, } impl Default for CommentViewKeyMap { fn default() -> Self { CommentViewKeyMap { - next_comment: Key::new('j'), - prev_comment: Key::new('k'), - next_top_level_comment: Key::new('n'), - prev_top_level_comment: Key::new('p'), - next_leq_level_comment: Key::new('l'), - prev_leq_level_comment: Key::new('h'), - parent_comment: Key::new('u'), - - open_comment_in_browser: Key::new('c'), - open_link_in_browser: Key::new('f'), - open_link_in_article_view: Key::new('F'), - - up: Key::new(event::Key::Up), - down: Key::new(event::Key::Down), - page_up: Key::new(event::Key::PageUp), - page_down: Key::new(event::Key::PageDown), - - toggle_collapse_comment: Key::new(event::Key::Tab), + next_comment: Keys::new(vec!['j'.into()]), + prev_comment: Keys::new(vec!['k'.into()]), + next_top_level_comment: Keys::new(vec!['n'.into()]), + prev_top_level_comment: Keys::new(vec!['p'.into()]), + next_leq_level_comment: Keys::new(vec!['l'.into()]), + prev_leq_level_comment: Keys::new(vec!['h'.into()]), + parent_comment: Keys::new(vec!['u'.into()]), + + open_comment_in_browser: Keys::new(vec!['c'.into()]), + open_link_in_browser: Keys::new(vec!['f'.into()]), + open_link_in_article_view: Keys::new(vec!['F'.into()]), + + up: Keys::new(vec![event::Key::Up.into()]), + down: Keys::new(vec![event::Key::Down.into()]), + page_up: Keys::new(vec![event::Key::PageUp.into()]), + page_down: Keys::new(vec![event::Key::PageDown.into()]), + + toggle_collapse_comment: Keys::new(vec![event::Key::Tab.into()]), } } } #[derive(Debug, Clone, Deserialize, ConfigParse)] pub struct ArticleViewKeyMap { - pub down: Key, - pub up: Key, - pub page_down: Key, - pub page_up: Key, - pub top: Key, - pub bottom: Key, - - pub open_link_dialog: Key, - pub link_dialog_focus_next: Key, - pub link_dialog_focus_prev: Key, - - pub open_article_in_browser: Key, - pub open_link_in_browser: Key, - pub open_link_in_article_view: Key, + pub down: Keys, + pub up: Keys, + pub page_down: Keys, + pub page_up: Keys, + pub top: Keys, + pub bottom: Keys, + + pub open_link_dialog: Keys, + pub link_dialog_focus_next: Keys, + pub link_dialog_focus_prev: Keys, + + pub open_article_in_browser: Keys, + pub open_link_in_browser: Keys, + pub open_link_in_article_view: Keys, } impl Default for ArticleViewKeyMap { fn default() -> Self { ArticleViewKeyMap { - down: Key::new('j'), - up: Key::new('k'), - page_down: Key::new('d'), - page_up: Key::new('u'), - top: Key::new('g'), - bottom: Key::new('G'), - - open_link_dialog: Key::new('l'), - link_dialog_focus_next: Key::new('j'), - link_dialog_focus_prev: Key::new('k'), - - open_article_in_browser: Key::new('o'), - open_link_in_browser: Key::new('f'), - open_link_in_article_view: Key::new('F'), + down: Keys::new(vec!['j'.into()]), + up: Keys::new(vec!['k'.into()]), + page_down: Keys::new(vec!['d'.into()]), + page_up: Keys::new(vec!['u'.into()]), + top: Keys::new(vec!['g'.into()]), + bottom: Keys::new(vec!['G'.into()]), + + open_link_dialog: Keys::new(vec!['l'.into()]), + link_dialog_focus_next: Keys::new(vec!['j'.into()]), + link_dialog_focus_prev: Keys::new(vec!['k'.into()]), + + open_article_in_browser: Keys::new(vec!['o'.into()]), + open_link_in_browser: Keys::new(vec!['f'.into()]), + open_link_in_article_view: Keys::new(vec!['F'.into()]), } } } #[derive(Debug, Clone)] -pub struct Key { - event: event::Event, +pub struct Keys { + events: Vec<event::Event>, } -impl From<Key> for event::EventTrigger { - fn from(k: Key) -> Self { - k.event.into() +impl From<Keys> for event::EventTrigger { + fn from(k: Keys) -> Self { + event::EventTrigger::from_fn(move |e| k.has_event(e)) } } -impl From<Key> for event::Event { - fn from(k: Key) -> Self { - k.event - } -} - -impl std::fmt::Display for Key { +impl std::fmt::Display for Keys { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.event { - event::Event::Char(c) => write!(f, "{}", c), - event::Event::CtrlChar(c) => write!(f, "C-{}", c), - event::Event::AltChar(c) => write!(f, "M-{}", c), - event::Event::Key(k) => match k { - event::Key::Enter => write!(f, "enter"), - event::Key::Tab => write!(f, "tab"), - event::Key::Backspace => write!(f, "backspace"), - event::Key::Esc => write!(f, "esc"), - - event::Key::Left => write!(f, "left"), - event::Key::Right => write!(f, "right"), - event::Key::Up => write!(f, "up"), - event::Key::Down => write!(f, "down"), - - event::Key::Ins => write!(f, "ins"), - event::Key::Del => write!(f, "del"), - event::Key::Home => write!(f, "home"), - event::Key::End => write!(f, "end"), - event::Key::PageUp => write!(f, "page_up"), - event::Key::PageDown => write!(f, "page_down"), - - event::Key::F1 => write!(f, "f1"), - event::Key::F2 => write!(f, "f2"), - event::Key::F3 => write!(f, "f3"), - event::Key::F4 => write!(f, "f4"), - event::Key::F5 => write!(f, "f5"), - event::Key::F6 => write!(f, "f6"), - event::Key::F7 => write!(f, "f7"), - event::Key::F8 => write!(f, "f8"), - event::Key::F9 => write!(f, "f9"), - event::Key::F10 => write!(f, "f10"), - event::Key::F11 => write!(f, "f11"), - event::Key::F12 => write!(f, "f12"), - - _ => panic!("unknown key: {:?}", k), - }, - _ => panic!("unknown event: {:?}", self.event), + fn fmt_event(e: &event::Event, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match e { + event::Event::Char(c) => write!(f, "{}", c), + event::Event::CtrlChar(c) => write!(f, "C-{}", c), + event::Event::AltChar(c) => write!(f, "M-{}", c), + event::Event::Key(k) => match k { + event::Key::Enter => write!(f, "enter"), + event::Key::Tab => write!(f, "tab"), + event::Key::Backspace => write!(f, "backspace"), + event::Key::Esc => write!(f, "esc"), + + event::Key::Left => write!(f, "left"), + event::Key::Right => write!(f, "right"), + event::Key::Up => write!(f, "up"), + event::Key::Down => write!(f, "down"), + + event::Key::Ins => write!(f, "ins"), + event::Key::Del => write!(f, "del"), + event::Key::Home => write!(f, "home"), + event::Key::End => write!(f, "end"), + event::Key::PageUp => write!(f, "page_up"), + event::Key::PageDown => write!(f, "page_down"), + + event::Key::F1 => write!(f, "f1"), + event::Key::F2 => write!(f, "f2"), + event::Key::F3 => write!(f, "f3"), + event::Key::F4 => write!(f, "f4"), + event::Key::F5 => write!(f, "f5"), + event::Key::F6 => write!(f, "f6"), + event::Key::F7 => write!(f, "f7"), + event::Key::F8 => write!(f, "f8"), + event::Key::F9 => write!(f, "f9"), + event::Key::F10 => write!(f, "f10"), + event::Key::F11 => write!(f, "f11"), + event::Key::F12 => write!(f, "f12"), + + _ => panic!("unknown key: {:?}", k), + }, + _ => panic!("unknown event: {:?}", e), + } + } + + if self.events.is_empty() { + return Ok(()); + } + + if self.events.len() == 1 { + fmt_event(&self.events[0], f) + } else { + write!(f, "[")?; + fmt_event(&self.events[0], f)?; + for e in &self.events[1..] { + write!(f, ", ")?; + fmt_event(e, f)?; + } + write!(f, "]")?; + Ok(()) } } } -impl Key { - pub fn new<T: Into<event::Event>>(e: T) -> Self { - Key { event: e.into() } +impl Keys { + pub fn new(events: Vec<event::Event>) -> Self { + Keys { events } + } + + pub fn has_event(&self, e: &event::Event) -> bool { + self.events.iter().any(|x| *x == *e) } } -config_parser_impl!(Key); +config_parser_impl!(Keys); -impl<'de> de::Deserialize<'de> for Key { +impl<'de> de::Deserialize<'de> for Keys { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - let err = Err(de::Error::custom(format!( - "failed to parse key: unknown/invalid key {}", - s - ))); - - let chars: Vec<char> = s.chars().collect(); - let key = { - if chars.len() == 1 { + #[derive(Deserialize)] + #[serde(untagged)] + /// an enum representing either + /// - a single key string \[1\] + /// - an array of multiple key strings + /// + /// \[1\]: "key string" denotes the string representation of a key + enum StringOrVec { + String(String), + Vec(Vec<String>), + } + + /// a helper function that converts a key string into `cursive::event::Event` + fn from_key_string_to_event(ks: String) -> Result<event::Event> { + let chars: Vec<char> = ks.chars().collect(); + + let event = if chars.len() == 1 { // a single character - Key::new(chars[0]) + event::Event::Char(chars[0]) } else if chars.len() == 3 && chars[1] == '-' { // M-<c> for alt-<c> and C-<c> for ctrl-<c>, with <c> denotes a single character match chars[0] { - 'C' => Key::new(event::Event::CtrlChar(chars[2])), - 'M' => Key::new(event::Event::AltChar(chars[2])), - _ => return err, + 'C' => event::Event::CtrlChar(chars[2]), + 'M' => event::Event::AltChar(chars[2]), + _ => { + return Err(anyhow::anyhow!(format!( + "failed to parse key: unknown/invalid key {}", + ks + ))) + } } } else { - let key = match s.as_str() { + let key = match ks.as_str() { "enter" => event::Key::Enter, "tab" => event::Key::Tab, "backspace" => event::Key::Backspace, @@ -355,14 +399,32 @@ impl<'de> de::Deserialize<'de> for Key { "f11" => event::Key::F11, "f12" => event::Key::F12, - _ => return err, + _ => { + return Err(anyhow::anyhow!(format!( + "failed to parse key: unknown/invalid key {}", + ks + ))) + } }; - Key::new(key) - } + event::Event::Key(key) + }; + + Ok(event) + } + + let key_strings = match StringOrVec::deserialize(deserializer)? { + StringOrVec::String(v) => vec![v], + StringOrVec::Vec(v) => v, }; - Ok(key) + let events = key_strings + .into_iter() + .map(from_key_string_to_event) + .collect::<Result<Vec<_>>>() + .map_err(serde::de::Error::custom)?; |