Always show all key bindings in F1 menu

This commit is contained in:
Joscha 2023-04-28 14:33:11 +02:00
parent 51b1953207
commit 1ce31b6677
6 changed files with 132 additions and 55 deletions

1
Cargo.lock generated
View file

@ -256,6 +256,7 @@ dependencies = [
"clap", "clap",
"cookie", "cookie",
"cove-config", "cove-config",
"cove-input",
"crossterm", "crossterm",
"directories", "directories",
"edit", "edit",

View file

@ -154,16 +154,16 @@ impl Default for Scroll {
#[derive(Debug, Deserialize, Document, KeyGroup)] #[derive(Debug, Deserialize, Document, KeyGroup)]
pub struct Cursor { pub struct Cursor {
/// Move cursor up. /// Move up.
#[serde(default = "default::cursor::up")] #[serde(default = "default::cursor::up")]
pub up: KeyBinding, pub up: KeyBinding,
/// Move cursor down. /// Move down.
#[serde(default = "default::cursor::down")] #[serde(default = "default::cursor::down")]
pub down: KeyBinding, pub down: KeyBinding,
/// Move cursor to top. /// Move to top.
#[serde(default = "default::cursor::to_top")] #[serde(default = "default::cursor::to_top")]
pub to_top: KeyBinding, pub to_top: KeyBinding,
/// Move cursor to bottom. /// Move to bottom.
#[serde(default = "default::cursor::to_bottom")] #[serde(default = "default::cursor::to_bottom")]
pub to_bottom: KeyBinding, pub to_bottom: KeyBinding,
/// Center cursor. /// Center cursor.
@ -185,28 +185,28 @@ impl Default for Cursor {
#[derive(Debug, Deserialize, Document, KeyGroup)] #[derive(Debug, Deserialize, Document, KeyGroup)]
pub struct EditorCursor { pub struct EditorCursor {
/// Move cursor left. /// Move left.
#[serde(default = "default::editor_cursor::left")] #[serde(default = "default::editor_cursor::left")]
pub left: KeyBinding, pub left: KeyBinding,
/// Move cursor right. /// Move right.
#[serde(default = "default::editor_cursor::right")] #[serde(default = "default::editor_cursor::right")]
pub right: KeyBinding, pub right: KeyBinding,
/// Move cursor left a word. /// Move left a word.
#[serde(default = "default::editor_cursor::left_word")] #[serde(default = "default::editor_cursor::left_word")]
pub left_word: KeyBinding, pub left_word: KeyBinding,
/// Move cursor right a word. /// Move right a word.
#[serde(default = "default::editor_cursor::right_word")] #[serde(default = "default::editor_cursor::right_word")]
pub right_word: KeyBinding, pub right_word: KeyBinding,
/// Move cursor to start of line. /// Move to start of line.
#[serde(default = "default::editor_cursor::start")] #[serde(default = "default::editor_cursor::start")]
pub start: KeyBinding, pub start: KeyBinding,
/// Move cursor to end of line. /// Move to end of line.
#[serde(default = "default::editor_cursor::end")] #[serde(default = "default::editor_cursor::end")]
pub end: KeyBinding, pub end: KeyBinding,
/// Move cursor up. /// Move up.
#[serde(default = "default::editor_cursor::up")] #[serde(default = "default::editor_cursor::up")]
pub up: KeyBinding, pub up: KeyBinding,
/// Move cursor down. /// Move down.
#[serde(default = "default::editor_cursor::down")] #[serde(default = "default::editor_cursor::down")]
pub down: KeyBinding, pub down: KeyBinding,
} }
@ -266,30 +266,31 @@ pub struct Editor {
#[derive(Debug, Deserialize, Document, KeyGroup)] #[derive(Debug, Deserialize, Document, KeyGroup)]
pub struct TreeCursor { pub struct TreeCursor {
/// Move cursor to above sibling. /// Move to above sibling.
#[serde(default = "default::tree_cursor::to_above_sibling")] #[serde(default = "default::tree_cursor::to_above_sibling")]
pub to_above_sibling: KeyBinding, pub to_above_sibling: KeyBinding,
/// Move cursor to below sibling. /// Move to below sibling.
#[serde(default = "default::tree_cursor::to_below_sibling")] #[serde(default = "default::tree_cursor::to_below_sibling")]
pub to_below_sibling: KeyBinding, pub to_below_sibling: KeyBinding,
/// Move cursor to parent. /// Move to parent.
#[serde(default = "default::tree_cursor::to_parent")] #[serde(default = "default::tree_cursor::to_parent")]
pub to_parent: KeyBinding, pub to_parent: KeyBinding,
/// Move cursor to root. /// Move to root.
#[serde(default = "default::tree_cursor::to_root")] #[serde(default = "default::tree_cursor::to_root")]
pub to_root: KeyBinding, pub to_root: KeyBinding,
/// Move cursor to previous message. /// Move to previous message.
#[serde(default = "default::tree_cursor::to_prev_message")] #[serde(default = "default::tree_cursor::to_prev_message")]
pub to_prev_message: KeyBinding, pub to_prev_message: KeyBinding,
/// Move cursor to next message. /// Move to next message.
#[serde(default = "default::tree_cursor::to_next_message")] #[serde(default = "default::tree_cursor::to_next_message")]
pub to_next_message: KeyBinding, 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")] #[serde(default = "default::tree_cursor::to_prev_unseen_message")]
pub to_prev_unseen_message: KeyBinding, 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")] #[serde(default = "default::tree_cursor::to_next_unseen_message")]
pub to_next_unseen_message: KeyBinding, pub to_next_unseen_message: KeyBinding,
// TODO Bindings inspired by vim's ()/[]/{} bindings?
} }
impl Default for TreeCursor { impl Default for TreeCursor {
@ -309,10 +310,10 @@ impl Default for TreeCursor {
#[derive(Debug, Deserialize, Document, KeyGroup)] #[derive(Debug, Deserialize, Document, KeyGroup)]
pub struct TreeAction { pub struct TreeAction {
/// Reply to message (inline if possible). /// Reply to message, inline if possible.
#[serde(default = "default::tree_action::reply")] #[serde(default = "default::tree_action::reply")]
pub reply: KeyBinding, pub reply: KeyBinding,
/// Reply to message, opposite of normal reply. /// Reply opposite to normal reply.
#[serde(default = "default::tree_action::reply_alternate")] #[serde(default = "default::tree_action::reply_alternate")]
pub reply_alternate: KeyBinding, pub reply_alternate: KeyBinding,
/// Start a new thread. /// Start a new thread.

View file

@ -177,6 +177,10 @@ impl KeyBinding {
Self(vec![]) Self(vec![])
} }
pub fn keys(&self) -> &[KeyPress] {
&self.0
}
pub fn with_key(self, key: &str) -> Result<Self, ParseKeysError> { pub fn with_key(self, key: &str) -> Result<Self, ParseKeysError> {
self.with_keys([key]) self.with_keys([key])
} }

View file

@ -5,6 +5,7 @@ edition = { workspace = true }
[dependencies] [dependencies]
cove-config = { path = "../cove-config" } cove-config = { path = "../cove-config" }
cove-input = { path = "../cove-input" }
crossterm = { workspace = true } crossterm = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
@ -46,8 +47,8 @@ features = ["bot"]
git = "https://github.com/Garmelon/toss.git" git = "https://github.com/Garmelon/toss.git"
rev = "f414db40d526295c74cbcae6c3d194088da8f1d9" rev = "f414db40d526295c74cbcae6c3d194088da8f1d9"
# [patch."https://github.com/Garmelon/toss.git"] [patch."https://github.com/Garmelon/toss.git"]
# toss = { path = "../toss/" } toss = { path = "../../toss/" }
[dependencies.vault] [dependencies.vault]
git = "https://github.com/Garmelon/vault.git" git = "https://github.com/Garmelon/vault.git"

View file

@ -1,6 +1,7 @@
mod chat; mod chat;
mod euph; mod euph;
mod input; mod input;
mod key_bindings;
mod rooms; mod rooms;
mod util; mod util;
mod widgets; mod widgets;
@ -67,13 +68,16 @@ enum Mode {
} }
pub struct Ui { pub struct Ui {
config: &'static Config,
event_tx: UnboundedSender<UiEvent>, event_tx: UnboundedSender<UiEvent>,
mode: Mode, mode: Mode,
rooms: Rooms, rooms: Rooms,
log_chat: ChatState<LogMsg, Logger>, log_chat: ChatState<LogMsg, Logger>,
key_bindings_list: Option<ListState<Infallible>>,
key_bindings_visible: bool,
key_bindings_list: ListState<Infallible>,
} }
impl Ui { impl Ui {
@ -106,11 +110,13 @@ impl Ui {
// On the other hand, if the crossterm_event_task stops for any reason, // 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. // the rest of the UI is also shut down and the client stops.
let mut ui = Self { let mut ui = Self {
config,
event_tx: event_tx.clone(), event_tx: event_tx.clone(),
mode: Mode::Main, mode: Mode::Main,
rooms: Rooms::new(config, vault, event_tx.clone()).await, rooms: Rooms::new(config, vault, event_tx.clone()).await,
log_chat: ChatState::new(logger), log_chat: ChatState::new(logger),
key_bindings_list: None, key_bindings_visible: false,
key_bindings_list: ListState::new(),
}; };
tokio::select! { tokio::select! {
e = ui.run_main(terminal, event_rx, crossterm_lock) => e?, e = ui.run_main(terminal, event_rx, crossterm_lock) => e?,
@ -190,39 +196,19 @@ impl Ui {
} }
async fn widget(&mut self) -> BoxedAsync<'_, UiError> { 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 { let widget = match self.mode {
Mode::Main => self.rooms.widget().await, Mode::Main => self.rooms.widget().await,
Mode::Log => self.log_chat.widget(String::new(), true), Mode::Log => self.log_chat.widget(String::new(), true),
}; };
if let Some(key_bindings_list) = key_bindings_list { if self.key_bindings_visible {
// We checked whether this was Some earlier. let popup = key_bindings::widget(&mut self.key_bindings_list, self.config);
let list_state = self.key_bindings_list.as_mut().unwrap(); popup.desync().above(widget).boxed_async()
key_bindings_list
.widget(list_state)
.desync()
.above(widget)
.boxed_async()
} else { } else {
widget 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) { async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("ctrl+c", "quit cove"); bindings.binding("ctrl+c", "quit cove");
bindings.binding("F1, ?", "show this menu"); bindings.binding("F1, ?", "show this menu");
@ -275,11 +261,11 @@ impl Ui {
} }
// Key bindings list overrides any other bindings if visible // 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 { match event {
key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_list = None, key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_visible = false,
key!('k') | key!(Up) => key_bindings_list.scroll_up(1), key!('k') | key!(Up) => self.key_bindings_list.scroll_up(1),
key!('j') | key!(Down) => key_bindings_list.scroll_down(1), key!('j') | key!(Down) => self.key_bindings_list.scroll_down(1),
_ => return EventHandleResult::Continue, _ => return EventHandleResult::Continue,
} }
return EventHandleResult::Redraw; return EventHandleResult::Redraw;
@ -287,7 +273,7 @@ impl Ui {
match event { match event {
key!(F 1) => { key!(F 1) => {
self.key_bindings_list = Some(ListState::new()); self.key_bindings_visible = true;
return EventHandleResult::Redraw; return EventHandleResult::Redraw;
} }
key!(F 12) => { key!(F 12) => {
@ -321,7 +307,7 @@ impl Ui {
// text editor. // text editor.
if !handled { if !handled {
if let key!('?') = event { if let key!('?') = event {
self.show_key_bindings(); self.key_bindings_visible = true;
handled = true; handled = true;
} }
} }

View file

@ -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, Join2<Padding<Text>, 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<G: KeyGroup>(builder: &mut Builder, group: &G) {
for (binding, description) in group.bindings() {
render_binding(builder, binding, description);
}
}
pub fn widget<'a>(
list: &'a mut ListState<Infallible>,
config: &Config,
) -> impl Widget<UiError> + '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")
}