summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThang Pham <phamducthang1234@gmail.com>2022-04-28 17:39:01 -0400
committerGitHub <noreply@github.com>2022-04-28 17:39:01 -0400
commit7e4775d3425db584a0e32692cb60e679a2039756 (patch)
tree7507453f3481c8aecfb476978cb33bacd82461f0
parentf99380ed30a9f0897be67ba8a6f2e1145f4f6717 (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.md79
-rw-r--r--config.md19
-rw-r--r--examples/hn-tui.toml14
-rw-r--r--hackernews_tui/src/config/keybindings.rs450
-rw-r--r--hackernews_tui/src/utils.rs7
-rw-r--r--hackernews_tui/src/view/article_view.rs4
-rw-r--r--hackernews_tui/src/view/comment_view.rs4
-rw-r--r--hackernews_tui/src/view/help_view.rs12
-rw-r--r--hackernews_tui/src/view/search_view.rs10
-rw-r--r--hackernews_tui/src/view/story_view.rs2
10 files changed, 339 insertions, 262 deletions
diff --git a/README.md b/README.md
index 93e65e3..f0835ab 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/config.md b/config.md
index c67d51a..86a0dea 100644
--- a/config.md
+++ b/config.md
@@ -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)?;