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",
|
"clap",
|
||||||
"cookie",
|
"cookie",
|
||||||
"cove-config",
|
"cove-config",
|
||||||
|
"cove-input",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
"edit",
|
"edit",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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