Migrate input handling to new bindings

This commit is contained in:
Joscha 2023-04-29 00:24:33 +02:00
parent 202969c7a9
commit 9bc6931fae
18 changed files with 748 additions and 1222 deletions

View file

@ -7,15 +7,14 @@ mod scroll;
mod widgets;
use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
use parking_lot::FairMutex;
use cove_config::Keys;
use cove_input::InputEvent;
use toss::widgets::EditorState;
use toss::{AsyncWidget, Frame, Pos, Size, Terminal, WidgetExt, WidthDb};
use toss::{AsyncWidget, Frame, Pos, Size, WidgetExt, WidthDb};
use crate::store::{Msg, MsgStore};
use crate::ui::input::{key, InputEvent, KeyBindingsList};
use crate::ui::{util, ChatMsg, UiError};
use crate::util::InfallibleExt;
@ -49,25 +48,10 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
}
}
pub fn list_movement_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("j/k, ↓/↑", "move cursor up/down");
bindings.binding("J/K, ctrl+↓/↑", "move cursor to prev/next sibling");
bindings.binding("p/P", "move cursor to parent/root");
bindings.binding("h/l, ←/→", "move cursor chronologically");
bindings.binding("H/L, ctrl+←/→", "move cursor to prev/next unseen message");
bindings.binding("g, home", "move cursor to top");
bindings.binding("G, end", "move cursor to bottom");
bindings.binding("ctrl+y/e", "scroll up/down a line");
bindings.binding("ctrl+u/d", "scroll up/down half a screen");
bindings.binding("ctrl+b/f, page up/down", "scroll up/down one screen");
bindings.binding("z", "center cursor on screen");
// TODO Bindings inspired by vim's ()/[]/{} bindings?
}
async fn handle_movement_input_event(
&mut self,
frame: &mut Frame,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
cursor: &mut Cursor<M::Id>,
editor: &mut EditorState,
) -> Result<bool, S::Error>
@ -77,152 +61,188 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
S: Send + Sync,
S::Error: Send,
{
let chat_height: i32 = (frame.size().height - 3).into();
let widthdb = frame.widthdb();
let chat_height: i32 = (event.frame().size().height - 3).into();
match event {
key!('k') | key!(Up) => cursor.move_up_in_tree(&self.store, &self.folded).await?,
key!('j') | key!(Down) => cursor.move_down_in_tree(&self.store, &self.folded).await?,
key!('K') | key!(Ctrl + Up) => cursor.move_to_prev_sibling(&self.store).await?,
key!('J') | key!(Ctrl + Down) => cursor.move_to_next_sibling(&self.store).await?,
key!('p') => cursor.move_to_parent(&self.store).await?,
key!('P') => cursor.move_to_root(&self.store).await?,
key!('h') | key!(Left) => cursor.move_to_older_msg(&self.store).await?,
key!('l') | key!(Right) => cursor.move_to_newer_msg(&self.store).await?,
key!('H') | key!(Ctrl + Left) => cursor.move_to_older_unseen_msg(&self.store).await?,
key!('L') | key!(Ctrl + Right) => cursor.move_to_newer_unseen_msg(&self.store).await?,
key!('g') | key!(Home) => cursor.move_to_top(&self.store).await?,
key!('G') | key!(End) => cursor.move_to_bottom(),
key!(Ctrl + 'y') => self.scroll_by(cursor, editor, widthdb, 1).await?,
key!(Ctrl + 'e') => self.scroll_by(cursor, editor, widthdb, -1).await?,
key!(Ctrl + 'u') => {
let delta = chat_height / 2;
self.scroll_by(cursor, editor, widthdb, delta).await?;
}
key!(Ctrl + 'd') => {
let delta = -(chat_height / 2);
self.scroll_by(cursor, editor, widthdb, delta).await?;
}
key!(Ctrl + 'b') | key!(PageUp) => {
let delta = chat_height.saturating_sub(1);
self.scroll_by(cursor, editor, widthdb, delta).await?;
}
key!(Ctrl + 'f') | key!(PageDown) => {
let delta = -chat_height.saturating_sub(1);
self.scroll_by(cursor, editor, widthdb, delta).await?;
}
key!('z') => self.center_cursor(cursor, editor, widthdb).await?,
_ => return Ok(false),
// Basic cursor movement
if event.matches(&keys.cursor.up) {
cursor.move_up_in_tree(&self.store, &self.folded).await?;
return Ok(true);
}
if event.matches(&keys.cursor.down) {
cursor.move_down_in_tree(&self.store, &self.folded).await?;
return Ok(true);
}
if event.matches(&keys.cursor.to_top) {
cursor.move_to_top(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.cursor.to_bottom) {
cursor.move_to_bottom();
return Ok(true);
}
Ok(true)
}
// Tree cursor movement
if event.matches(&keys.tree.cursor.to_above_sibling) {
cursor.move_to_prev_sibling(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_below_sibling) {
cursor.move_to_next_sibling(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_parent) {
cursor.move_to_parent(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_root) {
cursor.move_to_root(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_older_message) {
cursor.move_to_older_msg(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_newer_message) {
cursor.move_to_newer_msg(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_older_unseen_message) {
cursor.move_to_older_unseen_msg(&self.store).await?;
return Ok(true);
}
if event.matches(&keys.tree.cursor.to_newer_unseen_message) {
cursor.move_to_newer_unseen_msg(&self.store).await?;
return Ok(true);
}
pub fn list_action_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("space", "fold current message's subtree");
bindings.binding("s", "toggle current message's seen status");
bindings.binding("S", "mark all visible messages as seen");
bindings.binding("ctrl+s", "mark all older messages as seen");
// Scrolling
if event.matches(&keys.scroll.up_line) {
self.scroll_by(cursor, editor, event.widthdb(), 1).await?;
return Ok(true);
}
if event.matches(&keys.scroll.down_line) {
self.scroll_by(cursor, editor, event.widthdb(), -1).await?;
return Ok(true);
}
if event.matches(&keys.scroll.up_half) {
let delta = chat_height / 2;
self.scroll_by(cursor, editor, event.widthdb(), delta)
.await?;
return Ok(true);
}
if event.matches(&keys.scroll.down_half) {
let delta = -(chat_height / 2);
self.scroll_by(cursor, editor, event.widthdb(), delta)
.await?;
return Ok(true);
}
if event.matches(&keys.scroll.up_full) {
let delta = chat_height.saturating_sub(1);
self.scroll_by(cursor, editor, event.widthdb(), delta)
.await?;
return Ok(true);
}
if event.matches(&keys.scroll.down_full) {
let delta = -chat_height.saturating_sub(1);
self.scroll_by(cursor, editor, event.widthdb(), delta)
.await?;
return Ok(true);
}
if event.matches(&keys.scroll.center_cursor) {
self.center_cursor(cursor, editor, event.widthdb()).await?;
return Ok(true);
}
Ok(false)
}
async fn handle_action_input_event(
&mut self,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
id: Option<&M::Id>,
) -> Result<bool, S::Error> {
match event {
key!(' ') => {
if let Some(id) = id {
if !self.folded.remove(id) {
self.folded.insert(id.clone());
}
return Ok(true);
if event.matches(&keys.tree.action.fold_tree) {
if let Some(id) = id {
if !self.folded.remove(id) {
self.folded.insert(id.clone());
}
}
key!('s') => {
if let Some(id) = id {
if let Some(msg) = self.store.tree(id).await?.msg(id) {
self.store.set_seen(id, !msg.seen()).await?;
}
return Ok(true);
}
}
key!('S') => {
for id in &self.last_visible_msgs {
self.store.set_seen(id, true).await?;
}
return Ok(true);
}
key!(Ctrl + 's') => {
if let Some(id) = id {
self.store.set_older_seen(id, true).await?;
} else {
self.store
.set_older_seen(&M::last_possible_id(), true)
.await?;
}
return Ok(true);
}
_ => {}
return Ok(true);
}
Ok(false)
}
pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("r", "reply to message (inline if possible, else directly)");
bindings.binding("R", "reply to message (opposite of R)");
bindings.binding("t", "start a new thread");
if event.matches(&keys.tree.action.toggle_seen) {
if let Some(id) = id {
if let Some(msg) = self.store.tree(id).await?.msg(id) {
self.store.set_seen(id, !msg.seen()).await?;
}
}
return Ok(true);
}
if event.matches(&keys.tree.action.mark_visible_seen) {
for id in &self.last_visible_msgs {
self.store.set_seen(id, true).await?;
}
return Ok(true);
}
if event.matches(&keys.tree.action.mark_older_seen) {
if let Some(id) = id {
self.store.set_older_seen(id, true).await?;
} else {
self.store
.set_older_seen(&M::last_possible_id(), true)
.await?;
}
return Ok(true);
}
Ok(false)
}
async fn handle_edit_initiating_input_event(
&mut self,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
cursor: &mut Cursor<M::Id>,
id: Option<M::Id>,
) -> Result<bool, S::Error> {
match event {
key!('r') => {
if let Some(parent) = cursor.parent_for_normal_tree_reply(&self.store).await? {
*cursor = Cursor::Editor {
coming_from: id,
parent,
};
}
}
key!('R') => {
if let Some(parent) = cursor.parent_for_alternate_tree_reply(&self.store).await? {
*cursor = Cursor::Editor {
coming_from: id,
parent,
};
}
}
key!('t') | key!('T') => {
if event.matches(&keys.tree.action.reply) {
if let Some(parent) = cursor.parent_for_normal_tree_reply(&self.store).await? {
*cursor = Cursor::Editor {
coming_from: id,
parent: None,
parent,
};
}
_ => return Ok(false),
return Ok(true);
}
Ok(true)
}
pub fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
self.list_movement_key_bindings(bindings);
bindings.empty();
self.list_action_key_bindings(bindings);
if can_compose {
bindings.empty();
self.list_edit_initiating_key_bindings(bindings);
if event.matches(&keys.tree.action.reply_alternate) {
if let Some(parent) = cursor.parent_for_alternate_tree_reply(&self.store).await? {
*cursor = Cursor::Editor {
coming_from: id,
parent,
};
}
return Ok(true);
}
if event.matches(&keys.tree.action.new_thread) {
*cursor = Cursor::Editor {
coming_from: id,
parent: None,
};
return Ok(true);
}
Ok(false)
}
async fn handle_normal_input_event(
&mut self,
frame: &mut Frame,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
cursor: &mut Cursor<M::Id>,
editor: &mut EditorState,
can_compose: bool,
@ -234,102 +254,73 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
S: Send + Sync,
S::Error: Send,
{
#[allow(clippy::if_same_then_else)]
Ok(
if self
.handle_movement_input_event(frame, event, cursor, editor)
if self
.handle_movement_input_event(event, keys, cursor, editor)
.await?
{
return Ok(true);
}
if self
.handle_action_input_event(event, keys, id.as_ref())
.await?
{
return Ok(true);
}
if can_compose
&& self
.handle_edit_initiating_input_event(event, keys, cursor, id)
.await?
{
true
} else if self.handle_action_input_event(event, id.as_ref()).await? {
true
} else if can_compose {
self.handle_edit_initiating_input_event(event, cursor, id)
.await?
} else {
false
},
)
{
return Ok(true);
}
Ok(false)
}
fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) {
bindings.binding("esc", "close editor");
bindings.binding("enter", "send message");
util::list_editor_key_bindings_allowing_external_editing(bindings, |_| true);
}
#[allow(clippy::too_many_arguments)]
fn handle_editor_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
cursor: &mut Cursor<M::Id>,
editor: &mut EditorState,
coming_from: Option<M::Id>,
parent: Option<M::Id>,
) -> Reaction<M> {
// TODO Tab-completion
// Abort edit
if event.matches(&keys.general.abort) {
*cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
return Reaction::Handled;
}
match event {
key!(Esc) => {
*cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
// Send message
if event.matches(&keys.general.confirm) {
let content = editor.text().to_string();
if content.trim().is_empty() {
return Reaction::Handled;
}
key!(Enter) => {
let content = editor.text().to_string();
if !content.trim().is_empty() {
*cursor = Cursor::Pseudo {
coming_from,
parent: parent.clone(),
};
return Reaction::Composed { parent, content };
}
}
_ => {
let handled = util::handle_editor_input_event_allowing_external_editing(
editor,
terminal,
crossterm_lock,
event,
|_| true,
);
match handled {
Ok(true) => {}
Ok(false) => return Reaction::NotHandled,
Err(e) => return Reaction::ComposeError(e),
}
}
*cursor = Cursor::Pseudo {
coming_from,
parent: parent.clone(),
};
return Reaction::Composed { parent, content };
}
Reaction::Handled
}
// TODO Tab-completion
pub fn list_key_bindings(
&self,
bindings: &mut KeyBindingsList,
cursor: &Cursor<M::Id>,
can_compose: bool,
) {
bindings.heading("Chat");
match cursor {
Cursor::Bottom | Cursor::Msg(_) => {
self.list_normal_key_bindings(bindings, can_compose);
}
Cursor::Editor { .. } => self.list_editor_key_bindings(bindings),
Cursor::Pseudo { .. } => {
self.list_normal_key_bindings(bindings, false);
}
// Editing
if util::handle_editor_input_event(editor, event, keys, |_| true) {
return Reaction::Handled;
}
Reaction::NotHandled
}
pub async fn handle_input_event(
&mut self,
terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>,
event: &InputEvent,
event: &mut InputEvent<'_>,
keys: &Keys,
cursor: &mut Cursor<M::Id>,
editor: &mut EditorState,
can_compose: bool,
@ -343,14 +334,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
Ok(match cursor {
Cursor::Bottom => {
if self
.handle_normal_input_event(
terminal.frame(),
event,
cursor,
editor,
can_compose,
None,
)
.handle_normal_input_event(event, keys, cursor, editor, can_compose, None)
.await?
{
Reaction::Handled
@ -361,14 +345,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
Cursor::Msg(id) => {
let id = id.clone();
if self
.handle_normal_input_event(
terminal.frame(),
event,
cursor,
editor,
can_compose,
Some(id),
)
.handle_normal_input_event(event, keys, cursor, editor, can_compose, Some(id))
.await?
{
Reaction::Handled
@ -382,19 +359,11 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
} => {
let coming_from = coming_from.clone();
let parent = parent.clone();
self.handle_editor_input_event(
terminal,
crossterm_lock,
event,
cursor,
editor,
coming_from,
parent,
)
self.handle_editor_input_event(event, keys, cursor, editor, coming_from, parent)
}
Cursor::Pseudo { .. } => {
if self
.handle_movement_input_event(terminal.frame(), event, cursor, editor)
.handle_movement_input_event(event, keys, cursor, editor)
.await?
{
Reaction::Handled