Always show all key bindings in F1 menu
This commit is contained in:
parent
51b1953207
commit
1ce31b6677
6 changed files with 132 additions and 55 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -256,6 +256,7 @@ dependencies = [
|
|||
"clap",
|
||||
"cookie",
|
||||
"cove-config",
|
||||
"cove-input",
|
||||
"crossterm",
|
||||
"directories",
|
||||
"edit",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
cove/src/ui/key_bindings.rs
Normal file
84
cove/src/ui/key_bindings.rs
Normal 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")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue