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",
"cookie",
"cove-config",
"cove-input",
"crossterm",
"directories",
"edit",

View file

@ -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.

View file

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

View file

@ -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"

View file

@ -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<UiEvent>,
mode: Mode,
rooms: Rooms,
log_chat: ChatState<LogMsg, Logger>,
key_bindings_list: Option<ListState<Infallible>>,
key_bindings_visible: bool,
key_bindings_list: ListState<Infallible>,
}
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;
}
}

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")
}