Skip to content

Commit

Permalink
Add global keybindings
Browse files Browse the repository at this point in the history
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 <git@builditluc.eu>
  • Loading branch information
Builditluc committed Nov 21, 2024
1 parent d713a9d commit 0c11c2d
Show file tree
Hide file tree
Showing 11 changed files with 508 additions and 123 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/changelog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
168 changes: 115 additions & 53 deletions docs/docs/configuration/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
50 changes: 27 additions & 23 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 25 additions & 18 deletions src/components/page_language_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand All @@ -27,17 +27,19 @@ pub struct PageLanguageSelectionComponent {
list: StatefulList<LanguageLink>,
language_links: Vec<LanguageLink>,

config: Arc<Config>,
theme: Arc<Theme>,
}

impl PageLanguageSelectionComponent {
pub fn new(language_links: Vec<LanguageLink>, theme: Arc<Theme>) -> Self {
pub fn new(language_links: Vec<LanguageLink>, config: Arc<Config>, theme: Arc<Theme>) -> Self {
Self {
input: Input::default(),
list: StatefulList::with_items(language_links.clone()),
language_links,
focus: 0,

config,
theme,
}
}
Expand All @@ -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;
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/components/page_viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/components/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit 0c11c2d

Please sign in to comment.