From 1ce31b6677ab0cf47b5eeaf4adcfddd91ac77598 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Apr 2023 14:33:11 +0200 Subject: [PATCH] Always show all key bindings in F1 menu --- Cargo.lock | 1 + cove-config/src/keys.rs | 45 ++++++++++---------- cove-input/src/keys.rs | 4 ++ cove/Cargo.toml | 5 ++- cove/src/ui.rs | 48 ++++++++------------- cove/src/ui/key_bindings.rs | 84 +++++++++++++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 cove/src/ui/key_bindings.rs diff --git a/Cargo.lock b/Cargo.lock index 346690d..9f2999a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,6 +256,7 @@ dependencies = [ "clap", "cookie", "cove-config", + "cove-input", "crossterm", "directories", "edit", diff --git a/cove-config/src/keys.rs b/cove-config/src/keys.rs index b27a6c8..3b1930b 100644 --- a/cove-config/src/keys.rs +++ b/cove-config/src/keys.rs @@ -154,16 +154,16 @@ impl Default for Scroll { #[derive(Debug, Deserialize, Document, KeyGroup)] pub struct Cursor { - /// Move cursor up. + /// Move up. #[serde(default = "default::cursor::up")] pub up: KeyBinding, - /// Move cursor down. + /// Move down. #[serde(default = "default::cursor::down")] pub down: KeyBinding, - /// Move cursor to top. + /// Move to top. #[serde(default = "default::cursor::to_top")] pub to_top: KeyBinding, - /// Move cursor to bottom. + /// Move to bottom. #[serde(default = "default::cursor::to_bottom")] pub to_bottom: KeyBinding, /// Center cursor. @@ -185,28 +185,28 @@ impl Default for Cursor { #[derive(Debug, Deserialize, Document, KeyGroup)] pub struct EditorCursor { - /// Move cursor left. + /// Move left. #[serde(default = "default::editor_cursor::left")] pub left: KeyBinding, - /// Move cursor right. + /// Move right. #[serde(default = "default::editor_cursor::right")] pub right: KeyBinding, - /// Move cursor left a word. + /// Move left a word. #[serde(default = "default::editor_cursor::left_word")] pub left_word: KeyBinding, - /// Move cursor right a word. + /// Move right a word. #[serde(default = "default::editor_cursor::right_word")] pub right_word: KeyBinding, - /// Move cursor to start of line. + /// Move to start of line. #[serde(default = "default::editor_cursor::start")] pub start: KeyBinding, - /// Move cursor to end of line. + /// Move to end of line. #[serde(default = "default::editor_cursor::end")] pub end: KeyBinding, - /// Move cursor up. + /// Move up. #[serde(default = "default::editor_cursor::up")] pub up: KeyBinding, - /// Move cursor down. + /// Move down. #[serde(default = "default::editor_cursor::down")] pub down: KeyBinding, } @@ -266,30 +266,31 @@ pub struct Editor { #[derive(Debug, Deserialize, Document, KeyGroup)] pub struct TreeCursor { - /// Move cursor to above sibling. + /// Move to above sibling. #[serde(default = "default::tree_cursor::to_above_sibling")] pub to_above_sibling: KeyBinding, - /// Move cursor to below sibling. + /// Move to below sibling. #[serde(default = "default::tree_cursor::to_below_sibling")] pub to_below_sibling: KeyBinding, - /// Move cursor to parent. + /// Move to parent. #[serde(default = "default::tree_cursor::to_parent")] pub to_parent: KeyBinding, - /// Move cursor to root. + /// Move to root. #[serde(default = "default::tree_cursor::to_root")] pub to_root: KeyBinding, - /// Move cursor to previous message. + /// Move to previous message. #[serde(default = "default::tree_cursor::to_prev_message")] pub to_prev_message: KeyBinding, - /// Move cursor to next message. + /// Move to next message. #[serde(default = "default::tree_cursor::to_next_message")] pub to_next_message: KeyBinding, - /// Move cursor to previous unseen message. + /// Move to previous unseen message. #[serde(default = "default::tree_cursor::to_prev_unseen_message")] pub to_prev_unseen_message: KeyBinding, - /// Move cursor to next unseen message. + /// Move to next unseen message. #[serde(default = "default::tree_cursor::to_next_unseen_message")] pub to_next_unseen_message: KeyBinding, + // TODO Bindings inspired by vim's ()/[]/{} bindings? } impl Default for TreeCursor { @@ -309,10 +310,10 @@ impl Default for TreeCursor { #[derive(Debug, Deserialize, Document, KeyGroup)] pub struct TreeAction { - /// Reply to message (inline if possible). + /// Reply to message, inline if possible. #[serde(default = "default::tree_action::reply")] pub reply: KeyBinding, - /// Reply to message, opposite of normal reply. + /// Reply opposite to normal reply. #[serde(default = "default::tree_action::reply_alternate")] pub reply_alternate: KeyBinding, /// Start a new thread. diff --git a/cove-input/src/keys.rs b/cove-input/src/keys.rs index 4725a46..7cba2e8 100644 --- a/cove-input/src/keys.rs +++ b/cove-input/src/keys.rs @@ -177,6 +177,10 @@ impl KeyBinding { Self(vec![]) } + pub fn keys(&self) -> &[KeyPress] { + &self.0 + } + pub fn with_key(self, key: &str) -> Result { self.with_keys([key]) } diff --git a/cove/Cargo.toml b/cove/Cargo.toml index a15d8af..077d62e 100644 --- a/cove/Cargo.toml +++ b/cove/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } [dependencies] cove-config = { path = "../cove-config" } +cove-input = { path = "../cove-input" } crossterm = { workspace = true } thiserror = { workspace = true } @@ -46,8 +47,8 @@ features = ["bot"] git = "https://github.com/Garmelon/toss.git" rev = "f414db40d526295c74cbcae6c3d194088da8f1d9" -# [patch."https://github.com/Garmelon/toss.git"] -# toss = { path = "../toss/" } +[patch."https://github.com/Garmelon/toss.git"] +toss = { path = "../../toss/" } [dependencies.vault] git = "https://github.com/Garmelon/vault.git" diff --git a/cove/src/ui.rs b/cove/src/ui.rs index 1e9d55c..4443fe9 100644 --- a/cove/src/ui.rs +++ b/cove/src/ui.rs @@ -1,6 +1,7 @@ mod chat; mod euph; mod input; +mod key_bindings; mod rooms; mod util; mod widgets; @@ -67,13 +68,16 @@ enum Mode { } pub struct Ui { + config: &'static Config, event_tx: UnboundedSender, mode: Mode, rooms: Rooms, log_chat: ChatState, - key_bindings_list: Option>, + + key_bindings_visible: bool, + key_bindings_list: ListState, } impl Ui { @@ -106,11 +110,13 @@ impl Ui { // On the other hand, if the crossterm_event_task stops for any reason, // the rest of the UI is also shut down and the client stops. let mut ui = Self { + config, event_tx: event_tx.clone(), mode: Mode::Main, rooms: Rooms::new(config, vault, event_tx.clone()).await, log_chat: ChatState::new(logger), - key_bindings_list: None, + key_bindings_visible: false, + key_bindings_list: ListState::new(), }; tokio::select! { e = ui.run_main(terminal, event_rx, crossterm_lock) => e?, @@ -190,39 +196,19 @@ impl Ui { } async fn widget(&mut self) -> BoxedAsync<'_, UiError> { - let key_bindings_list = if self.key_bindings_list.is_some() { - let mut bindings = KeyBindingsList::new(); - self.list_key_bindings(&mut bindings).await; - Some(bindings) - } else { - None - }; - let widget = match self.mode { Mode::Main => self.rooms.widget().await, Mode::Log => self.log_chat.widget(String::new(), true), }; - if let Some(key_bindings_list) = key_bindings_list { - // We checked whether this was Some earlier. - let list_state = self.key_bindings_list.as_mut().unwrap(); - - key_bindings_list - .widget(list_state) - .desync() - .above(widget) - .boxed_async() + if self.key_bindings_visible { + let popup = key_bindings::widget(&mut self.key_bindings_list, self.config); + popup.desync().above(widget).boxed_async() } else { widget } } - fn show_key_bindings(&mut self) { - if self.key_bindings_list.is_none() { - self.key_bindings_list = Some(ListState::new()) - } - } - async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.binding("ctrl+c", "quit cove"); bindings.binding("F1, ?", "show this menu"); @@ -275,11 +261,11 @@ impl Ui { } // Key bindings list overrides any other bindings if visible - if let Some(key_bindings_list) = &mut self.key_bindings_list { + if self.key_bindings_visible { match event { - key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_list = None, - key!('k') | key!(Up) => key_bindings_list.scroll_up(1), - key!('j') | key!(Down) => key_bindings_list.scroll_down(1), + key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_visible = false, + key!('k') | key!(Up) => self.key_bindings_list.scroll_up(1), + key!('j') | key!(Down) => self.key_bindings_list.scroll_down(1), _ => return EventHandleResult::Continue, } return EventHandleResult::Redraw; @@ -287,7 +273,7 @@ impl Ui { match event { key!(F 1) => { - self.key_bindings_list = Some(ListState::new()); + self.key_bindings_visible = true; return EventHandleResult::Redraw; } key!(F 12) => { @@ -321,7 +307,7 @@ impl Ui { // text editor. if !handled { if let key!('?') = event { - self.show_key_bindings(); + self.key_bindings_visible = true; handled = true; } } diff --git a/cove/src/ui/key_bindings.rs b/cove/src/ui/key_bindings.rs new file mode 100644 index 0000000..86d01b1 --- /dev/null +++ b/cove/src/ui/key_bindings.rs @@ -0,0 +1,84 @@ +//! A scrollable popup showing the current key bindings. + +use std::convert::Infallible; + +use cove_config::Config; +use cove_input::{KeyBinding, KeyGroup}; +use crossterm::style::Stylize; +use toss::widgets::{Either2, Join2, Padding, Text}; +use toss::{Style, Styled, Widget, WidgetExt}; + +use super::widgets::{ListBuilder, ListState, Popup}; +use super::UiError; + +type Line = Either2, Text>>; +type Builder = ListBuilder<'static, Infallible, Line>; + +fn render_empty(builder: &mut Builder) { + builder.add_unsel(Text::new("").first2()); +} + +fn render_title(builder: &mut Builder, title: &str) { + let style = Style::new().bold(); + builder.add_unsel(Text::new(Styled::new(title, style)).first2()); +} + +fn render_binding(builder: &mut Builder, binding: &KeyBinding, description: &str) { + let style = Style::new().cyan(); + let mut keys = Styled::default(); + for key in binding.keys() { + if !keys.text().is_empty() { + keys = keys.then_plain(", "); + } + keys = keys.then(key.to_string(), style); + } + + builder.add_unsel( + Join2::horizontal( + Text::new(description) + .with_wrap(false) + .padding() + .with_right(2) + .with_stretch(true) + .segment(), + Text::new(keys).with_wrap(false).segment().with_fixed(true), + ) + .second2(), + ) +} + +fn render_group(builder: &mut Builder, group: &G) { + for (binding, description) in group.bindings() { + render_binding(builder, binding, description); + } +} + +pub fn widget<'a>( + list: &'a mut ListState, + config: &Config, +) -> impl Widget + 'a { + let mut list_builder = ListBuilder::new(); + + render_title(&mut list_builder, "General"); + render_group(&mut list_builder, &config.keys.general); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Scrolling"); + render_group(&mut list_builder, &config.keys.scroll); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Cursor movement"); + render_group(&mut list_builder, &config.keys.cursor); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Editor cursor movement"); + render_group(&mut list_builder, &config.keys.editor.cursor); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Editor actions"); + render_group(&mut list_builder, &config.keys.editor.action); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Tree cursor movement"); + render_group(&mut list_builder, &config.keys.tree.cursor); + render_empty(&mut list_builder); + render_title(&mut list_builder, "Tree actions"); + render_group(&mut list_builder, &config.keys.tree.action); + + Popup::new(list_builder.build(list), "Key bindings") +}