Migrate input handling to new bindings
This commit is contained in:
parent
202969c7a9
commit
9bc6931fae
18 changed files with 748 additions and 1222 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -259,7 +259,6 @@ dependencies = [
|
||||||
"cove-input",
|
"cove-input",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
"edit",
|
|
||||||
"euphoxide",
|
"euphoxide",
|
||||||
"linkify",
|
"linkify",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -294,6 +293,7 @@ version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cove-macro",
|
"cove-macro",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"edit",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_either",
|
"serde_either",
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ edition = { workspace = true }
|
||||||
cove-macro = { path = "../cove-macro" }
|
cove-macro = { path = "../cove-macro" }
|
||||||
|
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
parking_lot = {workspace = true}
|
parking_lot = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_either = { workspace = true }
|
serde_either = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toss = {workspace = true}
|
toss = { workspace = true }
|
||||||
|
|
||||||
|
edit = "0.1.4"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
mod keys;
|
mod keys;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use cove_macro::KeyGroup;
|
pub use cove_macro::KeyGroup;
|
||||||
use crossterm::event::{Event, KeyEvent};
|
use crossterm::event::{Event, KeyEvent};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use toss::Terminal;
|
use toss::{Frame, Terminal, WidthDb};
|
||||||
|
|
||||||
pub use crate::keys::*;
|
pub use crate::keys::*;
|
||||||
|
|
||||||
|
|
@ -53,4 +54,22 @@ impl<'a> InputEvent<'a> {
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn frame(&mut self) -> &mut Frame {
|
||||||
|
self.terminal.frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widthdb(&mut self) -> &mut WidthDb {
|
||||||
|
self.terminal.widthdb()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prompt(&mut self, initial_text: &str) -> io::Result<String> {
|
||||||
|
let guard = self.crossterm_lock.lock();
|
||||||
|
self.terminal.suspend().expect("failed to suspend");
|
||||||
|
let content = edit::edit(initial_text);
|
||||||
|
self.terminal.unsuspend().expect("fauled to unsuspend");
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ async-trait = "0.1.68"
|
||||||
clap = { version = "4.2.1", features = ["derive", "deprecated"] }
|
clap = { version = "4.2.1", features = ["derive", "deprecated"] }
|
||||||
cookie = "0.17.0"
|
cookie = "0.17.0"
|
||||||
directories = "5.0.0"
|
directories = "5.0.0"
|
||||||
edit = "0.1.4"
|
|
||||||
linkify = "0.9.0"
|
linkify = "0.9.0"
|
||||||
log = { version = "0.4.17", features = ["std"] }
|
log = { version = "0.4.17", features = ["std"] }
|
||||||
once_cell = "1.17.1"
|
once_cell = "1.17.1"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
mod chat;
|
mod chat;
|
||||||
mod euph;
|
mod euph;
|
||||||
mod input;
|
|
||||||
mod key_bindings;
|
mod key_bindings;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
@ -12,6 +11,7 @@ use std::sync::{Arc, Weak};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use cove_config::Config;
|
use cove_config::Config;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::mpsc::error::TryRecvError;
|
use tokio::sync::mpsc::error::TryRecvError;
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||||
|
|
@ -26,7 +26,6 @@ use crate::vault::Vault;
|
||||||
|
|
||||||
pub use self::chat::ChatMsg;
|
pub use self::chat::ChatMsg;
|
||||||
use self::chat::ChatState;
|
use self::chat::ChatState;
|
||||||
use self::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use self::rooms::Rooms;
|
use self::rooms::Rooms;
|
||||||
use self::widgets::ListState;
|
use self::widgets::ListState;
|
||||||
|
|
||||||
|
|
@ -209,17 +208,6 @@ impl Ui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
|
||||||
bindings.binding("ctrl+c", "quit cove");
|
|
||||||
bindings.binding("F1, ?", "show this menu");
|
|
||||||
bindings.binding("F12", "toggle log");
|
|
||||||
bindings.empty();
|
|
||||||
match self.mode {
|
|
||||||
Mode::Main => self.rooms.list_key_bindings(bindings).await,
|
|
||||||
Mode::Log => self.log_chat.list_key_bindings(bindings, false).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_event(
|
async fn handle_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
|
|
@ -232,7 +220,7 @@ impl Ui {
|
||||||
UiEvent::LogChanged => EventHandleResult::Continue,
|
UiEvent::LogChanged => EventHandleResult::Continue,
|
||||||
UiEvent::Term(crossterm::event::Event::Resize(_, _)) => EventHandleResult::Redraw,
|
UiEvent::Term(crossterm::event::Event::Resize(_, _)) => EventHandleResult::Redraw,
|
||||||
UiEvent::Term(event) => {
|
UiEvent::Term(event) => {
|
||||||
self.handle_term_event(terminal, crossterm_lock, event)
|
self.handle_term_event(terminal, crossterm_lock.clone(), event)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
UiEvent::Euph(event) => {
|
UiEvent::Euph(event) => {
|
||||||
|
|
@ -248,74 +236,60 @@ impl Ui {
|
||||||
async fn handle_term_event(
|
async fn handle_term_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: Arc<FairMutex<()>>,
|
||||||
event: crossterm::event::Event,
|
event: crossterm::event::Event,
|
||||||
) -> EventHandleResult {
|
) -> EventHandleResult {
|
||||||
let event = some_or_return!(InputEvent::from_event(event), EventHandleResult::Continue);
|
let mut event = InputEvent::new(event, terminal, crossterm_lock);
|
||||||
|
let keys = &self.config.keys;
|
||||||
|
|
||||||
if let key!(Ctrl + 'c') = event {
|
if event.matches(&keys.general.exit) {
|
||||||
// Exit unconditionally on ctrl+c. Previously, shift+q would also
|
|
||||||
// unconditionally exit, but that interfered with typing text in
|
|
||||||
// inline editors.
|
|
||||||
return EventHandleResult::Stop;
|
return EventHandleResult::Stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key bindings list overrides any other bindings if visible
|
// Key bindings list overrides any other bindings if visible
|
||||||
if self.key_bindings_visible {
|
if self.key_bindings_visible {
|
||||||
match event {
|
if event.matches(&keys.general.abort) {
|
||||||
key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_visible = false,
|
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;
|
return EventHandleResult::Redraw;
|
||||||
}
|
}
|
||||||
|
if util::handle_list_input_event(&mut self.key_bindings_list, &event, keys) {
|
||||||
|
return EventHandleResult::Redraw;
|
||||||
|
}
|
||||||
|
// ... and does not let anything below the popup receive events
|
||||||
|
return EventHandleResult::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
// Other general bindings that override any other bindings
|
||||||
key!(F 1) => {
|
if event.matches(&keys.general.help) {
|
||||||
self.key_bindings_visible = true;
|
self.key_bindings_visible = true;
|
||||||
return EventHandleResult::Redraw;
|
return EventHandleResult::Redraw;
|
||||||
}
|
}
|
||||||
key!(F 12) => {
|
if event.matches(&keys.general.log) {
|
||||||
self.mode = match self.mode {
|
self.mode = match self.mode {
|
||||||
Mode::Main => Mode::Log,
|
Mode::Main => Mode::Log,
|
||||||
Mode::Log => Mode::Main,
|
Mode::Log => Mode::Main,
|
||||||
};
|
};
|
||||||
return EventHandleResult::Redraw;
|
return EventHandleResult::Redraw;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut handled = match self.mode {
|
match self.mode {
|
||||||
Mode::Main => {
|
Mode::Main => {
|
||||||
self.rooms
|
if self.rooms.handle_input_event(&mut event, keys).await {
|
||||||
.handle_input_event(terminal, crossterm_lock, &event)
|
return EventHandleResult::Redraw;
|
||||||
.await
|
}
|
||||||
}
|
}
|
||||||
Mode::Log => {
|
Mode::Log => {
|
||||||
let reaction = self
|
let reaction = self
|
||||||
.log_chat
|
.log_chat
|
||||||
.handle_input_event(terminal, crossterm_lock, &event, false)
|
.handle_input_event(&mut event, keys, false)
|
||||||
.await;
|
.await;
|
||||||
let reaction = logging_unwrap!(reaction);
|
let reaction = logging_unwrap!(reaction);
|
||||||
reaction.handled()
|
if reaction.handled() {
|
||||||
|
return EventHandleResult::Redraw;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Pressing '?' should only open the key bindings list if it doesn't
|
|
||||||
// interfere with any part of the main UI, such as entering text in a
|
|
||||||
// text editor.
|
|
||||||
if !handled {
|
|
||||||
if let key!('?') = event {
|
|
||||||
self.key_bindings_visible = true;
|
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if handled {
|
|
||||||
EventHandleResult::Redraw
|
|
||||||
} else {
|
|
||||||
EventHandleResult::Continue
|
EventHandleResult::Continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,17 @@ mod renderer;
|
||||||
mod tree;
|
mod tree;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
use std::io;
|
use cove_config::Keys;
|
||||||
use std::sync::Arc;
|
use cove_input::InputEvent;
|
||||||
|
|
||||||
use parking_lot::FairMutex;
|
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use toss::widgets::{BoxedAsync, EditorState};
|
use toss::widgets::{BoxedAsync, EditorState};
|
||||||
use toss::{Styled, Terminal, WidgetExt};
|
use toss::{Styled, WidgetExt};
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore};
|
use crate::store::{Msg, MsgStore};
|
||||||
|
|
||||||
use self::cursor::Cursor;
|
use self::cursor::Cursor;
|
||||||
use self::tree::TreeViewState;
|
use self::tree::TreeViewState;
|
||||||
|
|
||||||
use super::input::{InputEvent, KeyBindingsList};
|
|
||||||
use super::UiError;
|
use super::UiError;
|
||||||
|
|
||||||
pub trait ChatMsg {
|
pub trait ChatMsg {
|
||||||
|
|
@ -76,19 +73,10 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
|
|
||||||
match self.mode {
|
|
||||||
Mode::Tree => self
|
|
||||||
.tree
|
|
||||||
.list_key_bindings(bindings, &self.cursor, can_compose),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_input_event(
|
pub async fn handle_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
keys: &Keys,
|
||||||
event: &InputEvent,
|
|
||||||
can_compose: bool,
|
can_compose: bool,
|
||||||
) -> Result<Reaction<M>, S::Error>
|
) -> Result<Reaction<M>, S::Error>
|
||||||
where
|
where
|
||||||
|
|
@ -101,9 +89,8 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
Mode::Tree => {
|
Mode::Tree => {
|
||||||
self.tree
|
self.tree
|
||||||
.handle_input_event(
|
.handle_input_event(
|
||||||
terminal,
|
|
||||||
crossterm_lock,
|
|
||||||
event,
|
event,
|
||||||
|
keys,
|
||||||
&mut self.cursor,
|
&mut self.cursor,
|
||||||
&mut self.editor,
|
&mut self.editor,
|
||||||
can_compose,
|
can_compose,
|
||||||
|
|
@ -147,7 +134,6 @@ pub enum Reaction<M: Msg> {
|
||||||
parent: Option<M::Id>,
|
parent: Option<M::Id>,
|
||||||
content: String,
|
content: String,
|
||||||
},
|
},
|
||||||
ComposeError(io::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Msg> Reaction<M> {
|
impl<M: Msg> Reaction<M> {
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,14 @@ mod scroll;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use parking_lot::FairMutex;
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use toss::widgets::EditorState;
|
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::store::{Msg, MsgStore};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::{util, ChatMsg, UiError};
|
use crate::ui::{util, ChatMsg, UiError};
|
||||||
use crate::util::InfallibleExt;
|
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(
|
async fn handle_movement_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
frame: &mut Frame,
|
event: &mut InputEvent<'_>,
|
||||||
event: &InputEvent,
|
keys: &Keys,
|
||||||
cursor: &mut Cursor<M::Id>,
|
cursor: &mut Cursor<M::Id>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
) -> Result<bool, S::Error>
|
) -> Result<bool, S::Error>
|
||||||
|
|
@ -77,83 +61,133 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
S::Error: Send,
|
S::Error: Send,
|
||||||
{
|
{
|
||||||
let chat_height: i32 = (frame.size().height - 3).into();
|
let chat_height: i32 = (event.frame().size().height - 3).into();
|
||||||
let widthdb = frame.widthdb();
|
|
||||||
|
|
||||||
match event {
|
// Basic cursor movement
|
||||||
key!('k') | key!(Up) => cursor.move_up_in_tree(&self.store, &self.folded).await?,
|
if event.matches(&keys.cursor.up) {
|
||||||
key!('j') | key!(Down) => cursor.move_down_in_tree(&self.store, &self.folded).await?,
|
cursor.move_up_in_tree(&self.store, &self.folded).await?;
|
||||||
key!('K') | key!(Ctrl + Up) => cursor.move_to_prev_sibling(&self.store).await?,
|
return Ok(true);
|
||||||
key!('J') | key!(Ctrl + Down) => cursor.move_to_next_sibling(&self.store).await?,
|
}
|
||||||
key!('p') => cursor.move_to_parent(&self.store).await?,
|
if event.matches(&keys.cursor.down) {
|
||||||
key!('P') => cursor.move_to_root(&self.store).await?,
|
cursor.move_down_in_tree(&self.store, &self.folded).await?;
|
||||||
key!('h') | key!(Left) => cursor.move_to_older_msg(&self.store).await?,
|
return Ok(true);
|
||||||
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?,
|
if event.matches(&keys.cursor.to_top) {
|
||||||
key!('L') | key!(Ctrl + Right) => cursor.move_to_newer_unseen_msg(&self.store).await?,
|
cursor.move_to_top(&self.store).await?;
|
||||||
key!('g') | key!(Home) => cursor.move_to_top(&self.store).await?,
|
return Ok(true);
|
||||||
key!('G') | key!(End) => cursor.move_to_bottom(),
|
}
|
||||||
key!(Ctrl + 'y') => self.scroll_by(cursor, editor, widthdb, 1).await?,
|
if event.matches(&keys.cursor.to_bottom) {
|
||||||
key!(Ctrl + 'e') => self.scroll_by(cursor, editor, widthdb, -1).await?,
|
cursor.move_to_bottom();
|
||||||
key!(Ctrl + 'u') => {
|
return 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
let delta = chat_height / 2;
|
||||||
self.scroll_by(cursor, editor, widthdb, delta).await?;
|
self.scroll_by(cursor, editor, event.widthdb(), delta)
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!(Ctrl + 'd') => {
|
if event.matches(&keys.scroll.down_half) {
|
||||||
let delta = -(chat_height / 2);
|
let delta = -(chat_height / 2);
|
||||||
self.scroll_by(cursor, editor, widthdb, delta).await?;
|
self.scroll_by(cursor, editor, event.widthdb(), delta)
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!(Ctrl + 'b') | key!(PageUp) => {
|
if event.matches(&keys.scroll.up_full) {
|
||||||
let delta = chat_height.saturating_sub(1);
|
let delta = chat_height.saturating_sub(1);
|
||||||
self.scroll_by(cursor, editor, widthdb, delta).await?;
|
self.scroll_by(cursor, editor, event.widthdb(), delta)
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!(Ctrl + 'f') | key!(PageDown) => {
|
if event.matches(&keys.scroll.down_full) {
|
||||||
let delta = -chat_height.saturating_sub(1);
|
let delta = -chat_height.saturating_sub(1);
|
||||||
self.scroll_by(cursor, editor, widthdb, delta).await?;
|
self.scroll_by(cursor, editor, event.widthdb(), delta)
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!('z') => self.center_cursor(cursor, editor, widthdb).await?,
|
if event.matches(&keys.scroll.center_cursor) {
|
||||||
_ => return Ok(false),
|
self.center_cursor(cursor, editor, event.widthdb()).await?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(false)
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_action_input_event(
|
async fn handle_action_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &InputEvent,
|
event: &mut InputEvent<'_>,
|
||||||
|
keys: &Keys,
|
||||||
id: Option<&M::Id>,
|
id: Option<&M::Id>,
|
||||||
) -> Result<bool, S::Error> {
|
) -> Result<bool, S::Error> {
|
||||||
match event {
|
if event.matches(&keys.tree.action.fold_tree) {
|
||||||
key!(' ') => {
|
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if !self.folded.remove(id) {
|
if !self.folded.remove(id) {
|
||||||
self.folded.insert(id.clone());
|
self.folded.insert(id.clone());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
key!('s') => {
|
if event.matches(&keys.tree.action.toggle_seen) {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if let Some(msg) = self.store.tree(id).await?.msg(id) {
|
if let Some(msg) = self.store.tree(id).await?.msg(id) {
|
||||||
self.store.set_seen(id, !msg.seen()).await?;
|
self.store.set_seen(id, !msg.seen()).await?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
key!('S') => {
|
if event.matches(&keys.tree.action.mark_visible_seen) {
|
||||||
for id in &self.last_visible_msgs {
|
for id in &self.last_visible_msgs {
|
||||||
self.store.set_seen(id, true).await?;
|
self.store.set_seen(id, true).await?;
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!(Ctrl + 's') => {
|
|
||||||
|
if event.matches(&keys.tree.action.mark_older_seen) {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
self.store.set_older_seen(id, true).await?;
|
self.store.set_older_seen(id, true).await?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -163,66 +197,52 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
Ok(false)
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_edit_initiating_input_event(
|
async fn handle_edit_initiating_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &InputEvent,
|
event: &mut InputEvent<'_>,
|
||||||
|
keys: &Keys,
|
||||||
cursor: &mut Cursor<M::Id>,
|
cursor: &mut Cursor<M::Id>,
|
||||||
id: Option<M::Id>,
|
id: Option<M::Id>,
|
||||||
) -> Result<bool, S::Error> {
|
) -> Result<bool, S::Error> {
|
||||||
match event {
|
if event.matches(&keys.tree.action.reply) {
|
||||||
key!('r') => {
|
|
||||||
if let Some(parent) = cursor.parent_for_normal_tree_reply(&self.store).await? {
|
if let Some(parent) = cursor.parent_for_normal_tree_reply(&self.store).await? {
|
||||||
*cursor = Cursor::Editor {
|
*cursor = Cursor::Editor {
|
||||||
coming_from: id,
|
coming_from: id,
|
||||||
parent,
|
parent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!('R') => {
|
|
||||||
|
if event.matches(&keys.tree.action.reply_alternate) {
|
||||||
if let Some(parent) = cursor.parent_for_alternate_tree_reply(&self.store).await? {
|
if let Some(parent) = cursor.parent_for_alternate_tree_reply(&self.store).await? {
|
||||||
*cursor = Cursor::Editor {
|
*cursor = Cursor::Editor {
|
||||||
coming_from: id,
|
coming_from: id,
|
||||||
parent,
|
parent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
key!('t') | key!('T') => {
|
|
||||||
|
if event.matches(&keys.tree.action.new_thread) {
|
||||||
*cursor = Cursor::Editor {
|
*cursor = Cursor::Editor {
|
||||||
coming_from: id,
|
coming_from: id,
|
||||||
parent: None,
|
parent: None,
|
||||||
};
|
};
|
||||||
}
|
return Ok(true);
|
||||||
_ => return Ok(false),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(false)
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_normal_input_event(
|
async fn handle_normal_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
frame: &mut Frame,
|
event: &mut InputEvent<'_>,
|
||||||
event: &InputEvent,
|
keys: &Keys,
|
||||||
cursor: &mut Cursor<M::Id>,
|
cursor: &mut Cursor<M::Id>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
can_compose: bool,
|
can_compose: bool,
|
||||||
|
|
@ -234,102 +254,73 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
S::Error: Send,
|
S::Error: Send,
|
||||||
{
|
{
|
||||||
#[allow(clippy::if_same_then_else)]
|
|
||||||
Ok(
|
|
||||||
if self
|
if self
|
||||||
.handle_movement_input_event(frame, event, cursor, editor)
|
.handle_movement_input_event(event, keys, cursor, editor)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
true
|
return Ok(true);
|
||||||
} else if self.handle_action_input_event(event, id.as_ref()).await? {
|
}
|
||||||
true
|
|
||||||
} else if can_compose {
|
if self
|
||||||
self.handle_edit_initiating_input_event(event, cursor, id)
|
.handle_action_input_event(event, keys, id.as_ref())
|
||||||
.await?
|
.await?
|
||||||
} else {
|
{
|
||||||
false
|
return Ok(true);
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
if can_compose
|
||||||
bindings.binding("esc", "close editor");
|
&& self
|
||||||
bindings.binding("enter", "send message");
|
.handle_edit_initiating_input_event(event, keys, cursor, id)
|
||||||
util::list_editor_key_bindings_allowing_external_editing(bindings, |_| true);
|
.await?
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn handle_editor_input_event(
|
fn handle_editor_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
keys: &Keys,
|
||||||
event: &InputEvent,
|
|
||||||
cursor: &mut Cursor<M::Id>,
|
cursor: &mut Cursor<M::Id>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
coming_from: Option<M::Id>,
|
coming_from: Option<M::Id>,
|
||||||
parent: Option<M::Id>,
|
parent: Option<M::Id>,
|
||||||
) -> Reaction<M> {
|
) -> Reaction<M> {
|
||||||
// TODO Tab-completion
|
// Abort edit
|
||||||
|
if event.matches(&keys.general.abort) {
|
||||||
match event {
|
|
||||||
key!(Esc) => {
|
|
||||||
*cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
|
*cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
|
||||||
return Reaction::Handled;
|
return Reaction::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
key!(Enter) => {
|
// Send message
|
||||||
|
if event.matches(&keys.general.confirm) {
|
||||||
let content = editor.text().to_string();
|
let content = editor.text().to_string();
|
||||||
if !content.trim().is_empty() {
|
if content.trim().is_empty() {
|
||||||
|
return Reaction::Handled;
|
||||||
|
}
|
||||||
*cursor = Cursor::Pseudo {
|
*cursor = Cursor::Pseudo {
|
||||||
coming_from,
|
coming_from,
|
||||||
parent: parent.clone(),
|
parent: parent.clone(),
|
||||||
};
|
};
|
||||||
return Reaction::Composed { parent, content };
|
return Reaction::Composed { parent, content };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Tab-completion
|
||||||
|
|
||||||
|
// Editing
|
||||||
|
if util::handle_editor_input_event(editor, event, keys, |_| true) {
|
||||||
|
return Reaction::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
Reaction::NotHandled
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reaction::Handled
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_input_event(
|
pub async fn handle_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
keys: &Keys,
|
||||||
event: &InputEvent,
|
|
||||||
cursor: &mut Cursor<M::Id>,
|
cursor: &mut Cursor<M::Id>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
can_compose: bool,
|
can_compose: bool,
|
||||||
|
|
@ -343,14 +334,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
Ok(match cursor {
|
Ok(match cursor {
|
||||||
Cursor::Bottom => {
|
Cursor::Bottom => {
|
||||||
if self
|
if self
|
||||||
.handle_normal_input_event(
|
.handle_normal_input_event(event, keys, cursor, editor, can_compose, None)
|
||||||
terminal.frame(),
|
|
||||||
event,
|
|
||||||
cursor,
|
|
||||||
editor,
|
|
||||||
can_compose,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Reaction::Handled
|
Reaction::Handled
|
||||||
|
|
@ -361,14 +345,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
Cursor::Msg(id) => {
|
Cursor::Msg(id) => {
|
||||||
let id = id.clone();
|
let id = id.clone();
|
||||||
if self
|
if self
|
||||||
.handle_normal_input_event(
|
.handle_normal_input_event(event, keys, cursor, editor, can_compose, Some(id))
|
||||||
terminal.frame(),
|
|
||||||
event,
|
|
||||||
cursor,
|
|
||||||
editor,
|
|
||||||
can_compose,
|
|
||||||
Some(id),
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Reaction::Handled
|
Reaction::Handled
|
||||||
|
|
@ -382,19 +359,11 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
} => {
|
} => {
|
||||||
let coming_from = coming_from.clone();
|
let coming_from = coming_from.clone();
|
||||||
let parent = parent.clone();
|
let parent = parent.clone();
|
||||||
self.handle_editor_input_event(
|
self.handle_editor_input_event(event, keys, cursor, editor, coming_from, parent)
|
||||||
terminal,
|
|
||||||
crossterm_lock,
|
|
||||||
event,
|
|
||||||
cursor,
|
|
||||||
editor,
|
|
||||||
coming_from,
|
|
||||||
parent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Cursor::Pseudo { .. } => {
|
Cursor::Pseudo { .. } => {
|
||||||
if self
|
if self
|
||||||
.handle_movement_input_event(terminal.frame(), event, cursor, editor)
|
.handle_movement_input_event(event, keys, cursor, editor)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Reaction::Handled
|
Reaction::Handled
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use euphoxide::api::PersonalAccountView;
|
use euphoxide::api::PersonalAccountView;
|
||||||
use euphoxide::conn;
|
use euphoxide::conn;
|
||||||
use toss::widgets::{EditorState, Empty, Join3, Join4, Text};
|
use toss::widgets::{EditorState, Empty, Join3, Join4, Join5, Text};
|
||||||
use toss::{Style, Terminal, Widget, WidgetExt};
|
use toss::{Style, Widget, WidgetExt};
|
||||||
|
|
||||||
use crate::euph::{self, Room};
|
use crate::euph::{self, Room};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::Popup;
|
use crate::ui::widgets::Popup;
|
||||||
use crate::ui::{util, UiError};
|
use crate::ui::{util, UiError};
|
||||||
|
|
||||||
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Focus {
|
enum Focus {
|
||||||
Email,
|
Email,
|
||||||
|
|
@ -65,7 +68,7 @@ pub struct LoggedIn(PersonalAccountView);
|
||||||
impl LoggedIn {
|
impl LoggedIn {
|
||||||
fn widget(&self) -> impl Widget<UiError> {
|
fn widget(&self) -> impl Widget<UiError> {
|
||||||
let bold = Style::new().bold();
|
let bold = Style::new().bold();
|
||||||
Join3::vertical(
|
Join5::vertical(
|
||||||
Text::new(("Logged in", bold.green())).segment(),
|
Text::new(("Logged in", bold.green())).segment(),
|
||||||
Empty::new().with_height(1).segment(),
|
Empty::new().with_height(1).segment(),
|
||||||
Join3::horizontal(
|
Join3::horizontal(
|
||||||
|
|
@ -76,6 +79,8 @@ impl LoggedIn {
|
||||||
Text::new((&self.0.email,)).segment(),
|
Text::new((&self.0.email,)).segment(),
|
||||||
)
|
)
|
||||||
.segment(),
|
.segment(),
|
||||||
|
Empty::new().with_height(1).segment(),
|
||||||
|
Text::new(("Log out", Style::new().black().on_white())).segment(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,12 +90,6 @@ pub enum AccountUiState {
|
||||||
LoggedIn(LoggedIn),
|
LoggedIn(LoggedIn),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum EventResult {
|
|
||||||
NotHandled,
|
|
||||||
Handled,
|
|
||||||
ResetState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountUiState {
|
impl AccountUiState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::LoggedOut(LoggedOut::new())
|
Self::LoggedOut(LoggedOut::new())
|
||||||
|
|
@ -121,94 +120,74 @@ impl AccountUiState {
|
||||||
Popup::new(inner, "Account")
|
Popup::new(inner, "Account")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
|
||||||
bindings.binding("esc", "close account ui");
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::LoggedOut(logged_out) => {
|
|
||||||
match logged_out.focus {
|
|
||||||
Focus::Email => bindings.binding("enter", "focus on password"),
|
|
||||||
Focus::Password => bindings.binding("enter", "log in"),
|
|
||||||
}
|
|
||||||
bindings.binding("tab", "switch focus");
|
|
||||||
util::list_editor_key_bindings(bindings, |c| c != '\n');
|
|
||||||
}
|
|
||||||
Self::LoggedIn(_) => bindings.binding("L", "log out"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(
|
pub fn handle_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
event: &InputEvent,
|
keys: &Keys,
|
||||||
room: &Option<Room>,
|
room: &Option<Room>,
|
||||||
) -> EventResult {
|
) -> PopupResult {
|
||||||
if let key!(Esc) = event {
|
if event.matches(&keys.general.abort) {
|
||||||
return EventResult::ResetState;
|
return PopupResult::Close;
|
||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::LoggedOut(logged_out) => {
|
Self::LoggedOut(logged_out) => {
|
||||||
if let key!(Tab) = event {
|
if event.matches(&keys.general.focus) {
|
||||||
logged_out.focus = match logged_out.focus {
|
logged_out.focus = match logged_out.focus {
|
||||||
Focus::Email => Focus::Password,
|
Focus::Email => Focus::Password,
|
||||||
Focus::Password => Focus::Email,
|
Focus::Password => Focus::Email,
|
||||||
};
|
};
|
||||||
return EventResult::Handled;
|
return PopupResult::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
match logged_out.focus {
|
match logged_out.focus {
|
||||||
Focus::Email => {
|
Focus::Email => {
|
||||||
if let key!(Enter) = event {
|
if event.matches(&keys.general.confirm) {
|
||||||
logged_out.focus = Focus::Password;
|
logged_out.focus = Focus::Password;
|
||||||
return EventResult::Handled;
|
return PopupResult::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if util::handle_editor_input_event(
|
if util::handle_editor_input_event(
|
||||||
&mut logged_out.email,
|
&mut logged_out.email,
|
||||||
terminal,
|
|
||||||
event,
|
event,
|
||||||
|
keys,
|
||||||
|c| c != '\n',
|
|c| c != '\n',
|
||||||
) {
|
) {
|
||||||
EventResult::Handled
|
return PopupResult::Handled;
|
||||||
} else {
|
|
||||||
EventResult::NotHandled
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::Password => {
|
Focus::Password => {
|
||||||
if let key!(Enter) = event {
|
if event.matches(&keys.general.confirm) {
|
||||||
if let Some(room) = room {
|
if let Some(room) = room {
|
||||||
let _ = room.login(
|
let _ = room.login(
|
||||||
logged_out.email.text().to_string(),
|
logged_out.email.text().to_string(),
|
||||||
logged_out.password.text().to_string(),
|
logged_out.password.text().to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return EventResult::Handled;
|
return PopupResult::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if util::handle_editor_input_event(
|
if util::handle_editor_input_event(
|
||||||
&mut logged_out.password,
|
&mut logged_out.password,
|
||||||
terminal,
|
|
||||||
event,
|
event,
|
||||||
|
keys,
|
||||||
|c| c != '\n',
|
|c| c != '\n',
|
||||||
) {
|
) {
|
||||||
EventResult::Handled
|
return PopupResult::Handled;
|
||||||
} else {
|
|
||||||
EventResult::NotHandled
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::LoggedIn(_) => {
|
Self::LoggedIn(_) => {
|
||||||
if let key!('L') = event {
|
if event.matches(&keys.general.confirm) {
|
||||||
if let Some(room) = room {
|
if let Some(room) = room {
|
||||||
let _ = room.logout();
|
let _ = room.logout();
|
||||||
}
|
}
|
||||||
EventResult::Handled
|
return PopupResult::Handled;
|
||||||
} else {
|
|
||||||
EventResult::NotHandled
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupResult::NotHandled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use toss::widgets::EditorState;
|
use toss::widgets::EditorState;
|
||||||
use toss::{Terminal, Widget};
|
use toss::Widget;
|
||||||
|
|
||||||
use crate::euph::Room;
|
use crate::euph::Room;
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::Popup;
|
use crate::ui::widgets::Popup;
|
||||||
use crate::ui::{util, UiError};
|
use crate::ui::{util, UiError};
|
||||||
|
|
||||||
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
pub fn new() -> EditorState {
|
pub fn new() -> EditorState {
|
||||||
EditorState::new()
|
EditorState::new()
|
||||||
}
|
}
|
||||||
|
|
@ -17,38 +20,26 @@ pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> + '_ {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_key_bindings(bindings: &mut KeyBindingsList) {
|
|
||||||
bindings.binding("esc", "abort");
|
|
||||||
bindings.binding("enter", "authenticate");
|
|
||||||
util::list_editor_key_bindings(bindings, |_| true);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum EventResult {
|
|
||||||
NotHandled,
|
|
||||||
Handled,
|
|
||||||
ResetState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(
|
pub fn handle_input_event(
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
event: &InputEvent,
|
keys: &Keys,
|
||||||
room: &Option<Room>,
|
room: &Option<Room>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
) -> EventResult {
|
) -> PopupResult {
|
||||||
match event {
|
if event.matches(&keys.general.abort) {
|
||||||
key!(Esc) => EventResult::ResetState,
|
return PopupResult::Close;
|
||||||
key!(Enter) => {
|
}
|
||||||
|
|
||||||
|
if event.matches(&keys.general.confirm) {
|
||||||
if let Some(room) = &room {
|
if let Some(room) = &room {
|
||||||
let _ = room.auth(editor.text().to_string());
|
let _ = room.auth(editor.text().to_string());
|
||||||
}
|
}
|
||||||
EventResult::ResetState
|
return PopupResult::Close;
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if util::handle_editor_input_event(editor, terminal, event, |_| true) {
|
|
||||||
EventResult::Handled
|
|
||||||
} else {
|
|
||||||
EventResult::NotHandled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if util::handle_editor_input_event(editor, event, keys, |_| true) {
|
||||||
|
return PopupResult::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupResult::NotHandled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use euphoxide::api::{Message, NickEvent, SessionView};
|
use euphoxide::api::{Message, NickEvent, SessionView};
|
||||||
use euphoxide::conn::SessionInfo;
|
use euphoxide::conn::SessionInfo;
|
||||||
use toss::widgets::Text;
|
use toss::widgets::Text;
|
||||||
use toss::{Style, Styled, Widget};
|
use toss::{Style, Styled, Widget};
|
||||||
|
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::Popup;
|
use crate::ui::widgets::Popup;
|
||||||
use crate::ui::UiError;
|
use crate::ui::UiError;
|
||||||
|
|
||||||
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
macro_rules! line {
|
macro_rules! line {
|
||||||
( $text:ident, $name:expr, $val:expr ) => {
|
( $text:ident, $name:expr, $val:expr ) => {
|
||||||
$text = $text
|
$text = $text
|
||||||
|
|
@ -122,18 +125,10 @@ pub fn message_widget(msg: &Message) -> impl Widget<UiError> {
|
||||||
Popup::new(Text::new(text), "Inspect message")
|
Popup::new(Text::new(text), "Inspect message")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_key_bindings(bindings: &mut KeyBindingsList) {
|
pub fn handle_input_event(event: &mut InputEvent<'_>, keys: &Keys) -> PopupResult {
|
||||||
bindings.binding("esc", "close");
|
if event.matches(&keys.general.abort) {
|
||||||
}
|
return PopupResult::Close;
|
||||||
|
|
||||||
pub enum EventResult {
|
|
||||||
NotHandled,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(event: &InputEvent) -> EventResult {
|
|
||||||
match event {
|
|
||||||
key!(Esc) => EventResult::Close,
|
|
||||||
_ => EventResult::NotHandled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupResult::NotHandled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,21 @@
|
||||||
use std::io;
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use linkify::{LinkFinder, LinkKind};
|
use linkify::{LinkFinder, LinkKind};
|
||||||
use toss::widgets::Text;
|
use toss::widgets::Text;
|
||||||
use toss::{Style, Styled, Widget};
|
use toss::{Style, Styled, Widget};
|
||||||
|
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::{ListBuilder, ListState, Popup};
|
use crate::ui::widgets::{ListBuilder, ListState, Popup};
|
||||||
use crate::ui::UiError;
|
use crate::ui::{util, UiError};
|
||||||
|
|
||||||
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
pub struct LinksState {
|
pub struct LinksState {
|
||||||
links: Vec<String>,
|
links: Vec<String>,
|
||||||
list: ListState<usize>,
|
list: ListState<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum EventResult {
|
|
||||||
NotHandled,
|
|
||||||
Handled,
|
|
||||||
Close,
|
|
||||||
ErrorOpeningLink { link: String, error: io::Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
const NUMBER_KEYS: [char; 10] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
const NUMBER_KEYS: [char; 10] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
||||||
|
|
||||||
impl LinksState {
|
impl LinksState {
|
||||||
|
|
@ -77,7 +72,7 @@ impl LinksState {
|
||||||
Popup::new(list_builder.build(&mut self.list), "Links")
|
Popup::new(list_builder.build(&mut self.list), "Links")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_link_by_id(&self, id: usize) -> EventResult {
|
fn open_link_by_id(&self, id: usize) -> PopupResult {
|
||||||
if let Some(link) = self.links.get(id) {
|
if let Some(link) = self.links.get(id) {
|
||||||
// The `http://` or `https://` schema is necessary for open::that to
|
// The `http://` or `https://` schema is necessary for open::that to
|
||||||
// successfully open the link in the browser.
|
// successfully open the link in the browser.
|
||||||
|
|
@ -88,53 +83,52 @@ impl LinksState {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = open::that(&link) {
|
if let Err(error) = open::that(&link) {
|
||||||
return EventResult::ErrorOpeningLink { link, error };
|
return PopupResult::ErrorOpeningLink { link, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventResult::Handled
|
PopupResult::Handled
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_link(&self) -> EventResult {
|
fn open_link(&self) -> PopupResult {
|
||||||
if let Some(id) = self.list.selected() {
|
if let Some(id) = self.list.selected() {
|
||||||
self.open_link_by_id(*id)
|
self.open_link_by_id(*id)
|
||||||
} else {
|
} else {
|
||||||
EventResult::Handled
|
PopupResult::Handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
pub fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> PopupResult {
|
||||||
bindings.binding("esc", "close links popup");
|
if event.matches(&keys.general.abort) {
|
||||||
bindings.binding("j/k, ↓/↑", "move cursor up/down");
|
return PopupResult::Close;
|
||||||
bindings.binding("g, home", "move cursor to top");
|
|
||||||
bindings.binding("G, end", "move cursor to bottom");
|
|
||||||
bindings.binding("ctrl+y/e", "scroll up/down");
|
|
||||||
bindings.empty();
|
|
||||||
bindings.binding("enter", "open selected link");
|
|
||||||
bindings.binding("1,2,...", "open link by position");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input_event(&mut self, event: &InputEvent) -> EventResult {
|
if event.matches(&keys.general.confirm) {
|
||||||
match event {
|
return self.open_link();
|
||||||
key!(Esc) => return EventResult::Close,
|
|
||||||
key!('k') | key!(Up) => self.list.move_cursor_up(),
|
|
||||||
key!('j') | key!(Down) => self.list.move_cursor_down(),
|
|
||||||
key!('g') | key!(Home) => self.list.move_cursor_to_top(),
|
|
||||||
key!('G') | key!(End) => self.list.move_cursor_to_bottom(),
|
|
||||||
key!(Ctrl + 'y') => self.list.scroll_up(1),
|
|
||||||
key!(Ctrl + 'e') => self.list.scroll_down(1),
|
|
||||||
key!(Enter) => return self.open_link(),
|
|
||||||
key!('1') => return self.open_link_by_id(0),
|
|
||||||
key!('2') => return self.open_link_by_id(1),
|
|
||||||
key!('3') => return self.open_link_by_id(2),
|
|
||||||
key!('4') => return self.open_link_by_id(3),
|
|
||||||
key!('5') => return self.open_link_by_id(4),
|
|
||||||
key!('6') => return self.open_link_by_id(5),
|
|
||||||
key!('7') => return self.open_link_by_id(6),
|
|
||||||
key!('8') => return self.open_link_by_id(7),
|
|
||||||
key!('9') => return self.open_link_by_id(8),
|
|
||||||
key!('0') => return self.open_link_by_id(9),
|
|
||||||
_ => return EventResult::NotHandled,
|
|
||||||
}
|
}
|
||||||
EventResult::Handled
|
|
||||||
|
if util::handle_list_input_event(&mut self.list, event, keys) {
|
||||||
|
return PopupResult::Handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Mention that this is possible in the UI
|
||||||
|
if let Some(key_event) = event.key_event() {
|
||||||
|
if key_event.modifiers.is_empty() {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Char('1') => return self.open_link_by_id(0),
|
||||||
|
KeyCode::Char('2') => return self.open_link_by_id(1),
|
||||||
|
KeyCode::Char('3') => return self.open_link_by_id(2),
|
||||||
|
KeyCode::Char('4') => return self.open_link_by_id(3),
|
||||||
|
KeyCode::Char('5') => return self.open_link_by_id(4),
|
||||||
|
KeyCode::Char('6') => return self.open_link_by_id(5),
|
||||||
|
KeyCode::Char('7') => return self.open_link_by_id(6),
|
||||||
|
KeyCode::Char('8') => return self.open_link_by_id(7),
|
||||||
|
KeyCode::Char('9') => return self.open_link_by_id(8),
|
||||||
|
KeyCode::Char('0') => return self.open_link_by_id(9),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupResult::NotHandled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use euphoxide::conn::Joined;
|
use euphoxide::conn::Joined;
|
||||||
use toss::widgets::EditorState;
|
use toss::widgets::EditorState;
|
||||||
use toss::{Style, Terminal, Widget};
|
use toss::{Style, Widget};
|
||||||
|
|
||||||
use crate::euph::{self, Room};
|
use crate::euph::{self, Room};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::Popup;
|
use crate::ui::widgets::Popup;
|
||||||
use crate::ui::{util, UiError};
|
use crate::ui::{util, UiError};
|
||||||
|
|
||||||
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
pub fn new(joined: Joined) -> EditorState {
|
pub fn new(joined: Joined) -> EditorState {
|
||||||
EditorState::with_initial_text(joined.session.name)
|
EditorState::with_initial_text(joined.session.name)
|
||||||
}
|
}
|
||||||
|
|
@ -19,42 +22,26 @@ pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> + '_ {
|
||||||
Popup::new(inner, "Choose nick")
|
Popup::new(inner, "Choose nick")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nick_char(c: char) -> bool {
|
|
||||||
c != '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_key_bindings(bindings: &mut KeyBindingsList) {
|
|
||||||
bindings.binding("esc", "abort");
|
|
||||||
bindings.binding("enter", "set nick");
|
|
||||||
util::list_editor_key_bindings(bindings, nick_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum EventResult {
|
|
||||||
NotHandled,
|
|
||||||
Handled,
|
|
||||||
ResetState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(
|
pub fn handle_input_event(
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
event: &InputEvent,
|
keys: &Keys,
|
||||||
room: &Option<Room>,
|
room: &Option<Room>,
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
) -> EventResult {
|
) -> PopupResult {
|
||||||
match event {
|
if event.matches(&keys.general.abort) {
|
||||||
key!(Esc) => EventResult::ResetState,
|
return PopupResult::Close;
|
||||||
key!(Enter) => {
|
}
|
||||||
|
|
||||||
|
if event.matches(&keys.general.confirm) {
|
||||||
if let Some(room) = &room {
|
if let Some(room) = &room {
|
||||||
let _ = room.nick(editor.text().to_string());
|
let _ = room.nick(editor.text().to_string());
|
||||||
}
|
}
|
||||||
EventResult::ResetState
|
return PopupResult::Close;
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if util::handle_editor_input_event(editor, terminal, event, nick_char) {
|
|
||||||
EventResult::Handled
|
|
||||||
} else {
|
|
||||||
EventResult::NotHandled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if util::handle_editor_input_event(editor, event, keys, |c| c != '\n') {
|
||||||
|
return PopupResult::Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupResult::NotHandled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use toss::widgets::Text;
|
use toss::widgets::Text;
|
||||||
use toss::{Style, Styled, Widget};
|
use toss::{Style, Styled, Widget};
|
||||||
|
|
@ -30,3 +32,10 @@ impl RoomPopup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum PopupResult {
|
||||||
|
NotHandled,
|
||||||
|
Handled,
|
||||||
|
Close,
|
||||||
|
ErrorOpeningLink { link: String, error: io::Error },
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
|
use cove_config::Keys;
|
||||||
|
use cove_input::InputEvent;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
|
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
|
||||||
use euphoxide::bot::instance::{Event, ServerConfig};
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
use euphoxide::conn::{self, Joined, Joining, SessionInfo};
|
use euphoxide::conn::{self, Joined, Joining, SessionInfo};
|
||||||
use parking_lot::FairMutex;
|
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use toss::widgets::{BoxedAsync, EditorState, Join2, Layer, Text};
|
use toss::widgets::{BoxedAsync, EditorState, Join2, Layer, Text};
|
||||||
use toss::{Style, Styled, Terminal, Widget, WidgetExt};
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
|
|
||||||
use crate::euph;
|
use crate::euph;
|
||||||
use crate::macros::logging_unwrap;
|
use crate::macros::logging_unwrap;
|
||||||
use crate::ui::chat::{ChatState, Reaction};
|
use crate::ui::chat::{ChatState, Reaction};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use crate::ui::widgets::ListState;
|
use crate::ui::widgets::ListState;
|
||||||
use crate::ui::{util, UiError, UiEvent};
|
use crate::ui::{util, UiError, UiEvent};
|
||||||
use crate::vault::EuphRoomVault;
|
use crate::vault::EuphRoomVault;
|
||||||
|
|
||||||
use super::account::{self, AccountUiState};
|
use super::account::AccountUiState;
|
||||||
use super::links::{self, LinksState};
|
use super::links::LinksState;
|
||||||
use super::popup::RoomPopup;
|
use super::popup::{PopupResult, RoomPopup};
|
||||||
use super::{auth, inspect, nick, nick_list};
|
use super::{auth, inspect, nick, nick_list};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -316,29 +315,13 @@ impl EuphRoom {
|
||||||
Text::new(info).padding().with_horizontal(1).border()
|
Text::new(info).padding().with_horizontal(1).border()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_chat_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
async fn handle_chat_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
let can_compose = matches!(
|
|
||||||
self.room_state(),
|
|
||||||
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
|
||||||
);
|
|
||||||
self.chat.list_key_bindings(bindings, can_compose).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_chat_input_event(
|
|
||||||
&mut self,
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
|
||||||
event: &InputEvent,
|
|
||||||
) -> bool {
|
|
||||||
let can_compose = matches!(
|
let can_compose = matches!(
|
||||||
self.room_state(),
|
self.room_state(),
|
||||||
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
||||||
);
|
);
|
||||||
|
|
||||||
let reaction = self
|
let reaction = self.chat.handle_input_event(event, keys, can_compose).await;
|
||||||
.chat
|
|
||||||
.handle_input_event(terminal, crossterm_lock, event, can_compose)
|
|
||||||
.await;
|
|
||||||
let reaction = logging_unwrap!(reaction);
|
let reaction = logging_unwrap!(reaction);
|
||||||
|
|
||||||
match reaction {
|
match reaction {
|
||||||
|
|
@ -353,19 +336,12 @@ impl EuphRoom {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reaction::ComposeError(e) => {
|
|
||||||
self.popups.push_front(RoomPopup::Error {
|
|
||||||
description: "Failed to use external editor".to_string(),
|
|
||||||
reason: format!("{e}"),
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_room_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
async fn handle_room_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
match self.room_state() {
|
match self.room_state() {
|
||||||
// Authenticating
|
// Authenticating
|
||||||
Some(euph::State::Connected(
|
Some(euph::State::Connected(
|
||||||
|
|
@ -374,83 +350,36 @@ impl EuphRoom {
|
||||||
bounce: Some(_), ..
|
bounce: Some(_), ..
|
||||||
}),
|
}),
|
||||||
)) => {
|
)) => {
|
||||||
bindings.binding("a", "authenticate");
|
if event.matches(&keys.room.action.authenticate) {
|
||||||
}
|
|
||||||
|
|
||||||
// Connected
|
|
||||||
Some(euph::State::Connected(_, conn::State::Joined(_))) => {
|
|
||||||
bindings.binding("n", "change nick");
|
|
||||||
bindings.binding("m", "download more messages");
|
|
||||||
bindings.binding("A", "show account ui");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspecting messages
|
|
||||||
bindings.binding("i", "inspect message");
|
|
||||||
bindings.binding("I", "show message links");
|
|
||||||
bindings.binding("ctrl+p", "open room's plugh.de/present page");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_room_input_event(&mut self, event: &InputEvent) -> bool {
|
|
||||||
match self.room_state() {
|
|
||||||
// Authenticating
|
|
||||||
Some(euph::State::Connected(
|
|
||||||
_,
|
|
||||||
conn::State::Joining(Joining {
|
|
||||||
bounce: Some(_), ..
|
|
||||||
}),
|
|
||||||
)) => {
|
|
||||||
if let key!('a') = event {
|
|
||||||
self.state = State::Auth(auth::new());
|
self.state = State::Auth(auth::new());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joined
|
// Joined
|
||||||
Some(euph::State::Connected(_, conn::State::Joined(joined))) => match event {
|
Some(euph::State::Connected(_, conn::State::Joined(joined))) => {
|
||||||
key!('n') | key!('N') => {
|
if event.matches(&keys.room.action.nick) {
|
||||||
self.state = State::Nick(nick::new(joined.clone()));
|
self.state = State::Nick(nick::new(joined.clone()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('m') => {
|
if event.matches(&keys.room.action.more_messages) {
|
||||||
if let Some(room) = &self.room {
|
if let Some(room) = &self.room {
|
||||||
let _ = room.log();
|
let _ = room.log();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('A') => {
|
if event.matches(&keys.room.action.account) {
|
||||||
self.state = State::Account(AccountUiState::new());
|
self.state = State::Account(AccountUiState::new());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// Otherwise
|
// Otherwise
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always applicable
|
// Always applicable
|
||||||
match event {
|
if event.matches(&keys.room.action.present) {
|
||||||
key!('i') => {
|
|
||||||
if let Some(id) = self.chat.cursor() {
|
|
||||||
if let Some(msg) = logging_unwrap!(self.vault().full_msg(*id).await) {
|
|
||||||
self.state = State::InspectMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
key!('I') => {
|
|
||||||
if let Some(id) = self.chat.cursor() {
|
|
||||||
if let Some(msg) = logging_unwrap!(self.vault().msg(*id).await) {
|
|
||||||
self.state = State::Links(LinksState::new(&msg.content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
key!(Ctrl + 'p') => {
|
|
||||||
let link = format!("https://plugh.de/present/{}/", self.name());
|
let link = format!("https://plugh.de/present/{}/", self.name());
|
||||||
if let Err(error) = open::that(&link) {
|
if let Err(error) = open::that(&link) {
|
||||||
self.popups.push_front(RoomPopup::Error {
|
self.popups.push_front(RoomPopup::Error {
|
||||||
|
|
@ -460,52 +389,56 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_chat_focus_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
|
||||||
self.list_room_key_bindings(bindings);
|
|
||||||
bindings.empty();
|
|
||||||
self.list_chat_key_bindings(bindings).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_chat_focus_input_event(
|
async fn handle_chat_focus_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
keys: &Keys,
|
||||||
event: &InputEvent,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// We need to handle chat input first, otherwise the other
|
// We need to handle chat input first, otherwise the other
|
||||||
// key bindings will shadow characters in the editor.
|
// key bindings will shadow characters in the editor.
|
||||||
if self
|
if self.handle_chat_input_event(event, keys).await {
|
||||||
.handle_chat_input_event(terminal, crossterm_lock, event)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.handle_room_input_event(event).await {
|
if self.handle_room_input_event(event, keys).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.matches(&keys.tree.action.inspect) {
|
||||||
|
if let Some(id) = self.chat.cursor() {
|
||||||
|
if let Some(msg) = logging_unwrap!(self.vault().full_msg(*id).await) {
|
||||||
|
self.state = State::InspectMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.matches(&keys.tree.action.links) {
|
||||||
|
if let Some(id) = self.chat.cursor() {
|
||||||
|
if let Some(msg) = logging_unwrap!(self.vault().msg(*id).await) {
|
||||||
|
self.state = State::Links(LinksState::new(&msg.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_nick_list_focus_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
fn handle_nick_list_focus_input_event(
|
||||||
util::list_list_key_bindings(bindings);
|
&mut self,
|
||||||
|
event: &mut InputEvent<'_>,
|
||||||
bindings.binding("i", "inspect session");
|
keys: &Keys,
|
||||||
}
|
) -> bool {
|
||||||
|
if util::handle_list_input_event(&mut self.nick_list, event, keys) {
|
||||||
fn handle_nick_list_focus_input_event(&mut self, event: &InputEvent) -> bool {
|
|
||||||
if util::handle_list_input_event(&mut self.nick_list, event) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let key!('i') = event {
|
if event.matches(&keys.tree.action.inspect) {
|
||||||
if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = self.room_state()
|
if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = self.room_state()
|
||||||
{
|
{
|
||||||
if let Some(id) = self.nick_list.selected() {
|
if let Some(id) = self.nick_list.selected() {
|
||||||
|
|
@ -523,58 +456,27 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
async fn handle_normal_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
// Handled in rooms list, not here
|
|
||||||
bindings.binding("esc", "leave room");
|
|
||||||
|
|
||||||
match self.focus {
|
match self.focus {
|
||||||
Focus::Chat => {
|
Focus::Chat => {
|
||||||
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
if self.handle_chat_focus_input_event(event, keys).await {
|
||||||
bindings.binding("tab", "focus on nick list");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list_chat_focus_key_bindings(bindings).await;
|
|
||||||
}
|
|
||||||
Focus::NickList => {
|
|
||||||
bindings.binding("tab, esc", "focus on chat");
|
|
||||||
bindings.empty();
|
|
||||||
bindings.heading("Nick list");
|
|
||||||
self.list_nick_list_focus_key_bindings(bindings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_normal_input_event(
|
|
||||||
&mut self,
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
|
||||||
event: &InputEvent,
|
|
||||||
) -> bool {
|
|
||||||
match self.focus {
|
|
||||||
Focus::Chat => {
|
|
||||||
// Needs to be handled first or the tab key may be shadowed
|
|
||||||
// during editing.
|
|
||||||
if self
|
|
||||||
.handle_chat_focus_input_event(terminal, crossterm_lock, event)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
||||||
if let key!(Tab) = event {
|
if event.matches(&keys.general.focus) {
|
||||||
self.focus = Focus::NickList;
|
self.focus = Focus::NickList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::NickList => {
|
Focus::NickList => {
|
||||||
if let key!(Tab) | key!(Esc) = event {
|
if event.matches(&keys.general.abort) || event.matches(&keys.general.focus) {
|
||||||
self.focus = Focus::Chat;
|
self.focus = Focus::Chat;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.handle_nick_list_focus_input_event(event) {
|
if self.handle_nick_list_focus_input_event(event, keys) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -583,101 +485,41 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
bindings.heading("Room");
|
|
||||||
|
|
||||||
if !self.popups.is_empty() {
|
if !self.popups.is_empty() {
|
||||||
bindings.binding("esc", "close popup");
|
if event.matches(&keys.general.abort) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self.state {
|
|
||||||
State::Normal => self.list_normal_key_bindings(bindings).await,
|
|
||||||
State::Auth(_) => auth::list_key_bindings(bindings),
|
|
||||||
State::Nick(_) => nick::list_key_bindings(bindings),
|
|
||||||
State::Account(account) => account.list_key_bindings(bindings),
|
|
||||||
State::Links(links) => links.list_key_bindings(bindings),
|
|
||||||
State::InspectMessage(_) | State::InspectSession(_) => {
|
|
||||||
inspect::list_key_bindings(bindings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_input_event(
|
|
||||||
&mut self,
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
|
||||||
event: &InputEvent,
|
|
||||||
) -> bool {
|
|
||||||
if !self.popups.is_empty() {
|
|
||||||
if matches!(event, key!(Esc)) {
|
|
||||||
self.popups.pop_back();
|
self.popups.pop_back();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Prevent event from reaching anything below the popup
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Use a common EventResult
|
let result = match &mut self.state {
|
||||||
|
State::Normal => return self.handle_normal_input_event(event, keys).await,
|
||||||
|
State::Auth(editor) => auth::handle_input_event(event, keys, &self.room, editor),
|
||||||
|
State::Nick(editor) => nick::handle_input_event(event, keys, &self.room, editor),
|
||||||
|
State::Account(account) => account.handle_input_event(event, keys, &self.room),
|
||||||
|
State::Links(links) => links.handle_input_event(event, keys),
|
||||||
|
State::InspectMessage(_) | State::InspectSession(_) => {
|
||||||
|
inspect::handle_input_event(event, keys)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match &mut self.state {
|
match result {
|
||||||
State::Normal => {
|
PopupResult::NotHandled => false,
|
||||||
self.handle_normal_input_event(terminal, crossterm_lock, event)
|
PopupResult::Handled => true,
|
||||||
.await
|
PopupResult::Close => {
|
||||||
}
|
|
||||||
State::Auth(editor) => {
|
|
||||||
match auth::handle_input_event(terminal, event, &self.room, editor) {
|
|
||||||
auth::EventResult::NotHandled => false,
|
|
||||||
auth::EventResult::Handled => true,
|
|
||||||
auth::EventResult::ResetState => {
|
|
||||||
self.state = State::Normal;
|
self.state = State::Normal;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
PopupResult::ErrorOpeningLink { link, error } => {
|
||||||
}
|
|
||||||
State::Nick(editor) => {
|
|
||||||
match nick::handle_input_event(terminal, event, &self.room, editor) {
|
|
||||||
nick::EventResult::NotHandled => false,
|
|
||||||
nick::EventResult::Handled => true,
|
|
||||||
nick::EventResult::ResetState => {
|
|
||||||
self.state = State::Normal;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Account(account) => {
|
|
||||||
match account.handle_input_event(terminal, event, &self.room) {
|
|
||||||
account::EventResult::NotHandled => false,
|
|
||||||
account::EventResult::Handled => true,
|
|
||||||
account::EventResult::ResetState => {
|
|
||||||
self.state = State::Normal;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Links(links) => match links.handle_input_event(event) {
|
|
||||||
links::EventResult::NotHandled => false,
|
|
||||||
links::EventResult::Handled => true,
|
|
||||||
links::EventResult::Close => {
|
|
||||||
self.state = State::Normal;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
links::EventResult::ErrorOpeningLink { link, error } => {
|
|
||||||
self.popups.push_front(RoomPopup::Error {
|
self.popups.push_front(RoomPopup::Error {
|
||||||
description: format!("Failed to open link: {link}"),
|
description: format!("Failed to open link: {link}"),
|
||||||
reason: format!("{error}"),
|
reason: format!("{error}"),
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
State::InspectMessage(_) | State::InspectSession(_) => {
|
|
||||||
match inspect::handle_input_event(event) {
|
|
||||||
inspect::EventResult::NotHandled => false,
|
|
||||||
inspect::EventResult::Close => {
|
|
||||||
self.state = State::Normal;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
|
||||||
use crossterm::style::Stylize;
|
|
||||||
use toss::widgets::{Empty, Join2, Text};
|
|
||||||
use toss::{Style, Styled, Widget, WidgetExt};
|
|
||||||
|
|
||||||
use super::widgets::{ListBuilder, ListState};
|
|
||||||
use super::UiError;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum InputEvent {
|
|
||||||
Key(KeyEvent),
|
|
||||||
Paste(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputEvent {
|
|
||||||
pub fn from_event(event: Event) -> Option<Self> {
|
|
||||||
match event {
|
|
||||||
crossterm::event::Event::Key(key) => Some(Self::Key(key.into())),
|
|
||||||
crossterm::event::Event::Paste(text) => Some(Self::Paste(text)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A key event data type that is a bit easier to pattern match on than
|
|
||||||
/// [`crossterm::event::KeyEvent`].
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct KeyEvent {
|
|
||||||
pub code: KeyCode,
|
|
||||||
pub shift: bool,
|
|
||||||
pub ctrl: bool,
|
|
||||||
pub alt: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crossterm::event::KeyEvent> for KeyEvent {
|
|
||||||
fn from(event: crossterm::event::KeyEvent) -> Self {
|
|
||||||
Self {
|
|
||||||
code: event.code,
|
|
||||||
shift: event.modifiers.contains(KeyModifiers::SHIFT),
|
|
||||||
ctrl: event.modifiers.contains(KeyModifiers::CONTROL),
|
|
||||||
alt: event.modifiers.contains(KeyModifiers::ALT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(unused_macro_rules)]
|
|
||||||
macro_rules! key {
|
|
||||||
// key!(Paste text)
|
|
||||||
( Paste $text:ident ) => { crate::ui::input::InputEvent::Paste($text) };
|
|
||||||
|
|
||||||
// key!('a')
|
|
||||||
( $key:literal ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: false, alt: false, }) };
|
|
||||||
( Ctrl + $key:literal ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: true, alt: false, }) };
|
|
||||||
( Alt + $key:literal ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: false, alt: true, }) };
|
|
||||||
|
|
||||||
// key!(Char c)
|
|
||||||
( Char $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: false, alt: false, }) };
|
|
||||||
( Ctrl + Char $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: true, alt: false, }) };
|
|
||||||
( Alt + Char $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::Char($key), shift: _, ctrl: false, alt: true, }) };
|
|
||||||
|
|
||||||
// key!(F n)
|
|
||||||
( F $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::F($key), shift: false, ctrl: false, alt: false, }) };
|
|
||||||
( Shift + F $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::F($key), shift: true, ctrl: false, alt: false, }) };
|
|
||||||
( Ctrl + F $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::F($key), shift: false, ctrl: true, alt: false, }) };
|
|
||||||
( Alt + F $key:pat ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::F($key), shift: false, ctrl: false, alt: true, }) };
|
|
||||||
|
|
||||||
// key!(other)
|
|
||||||
( $key:ident ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::$key, shift: false, ctrl: false, alt: false, }) };
|
|
||||||
( Shift + $key:ident ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::$key, shift: true, ctrl: false, alt: false, }) };
|
|
||||||
( Ctrl + $key:ident ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::$key, shift: false, ctrl: true, alt: false, }) };
|
|
||||||
( Alt + $key:ident ) => { crate::ui::input::InputEvent::Key(crate::ui::input::KeyEvent { code: crossterm::event::KeyCode::$key, shift: false, ctrl: false, alt: true, }) };
|
|
||||||
}
|
|
||||||
pub(crate) use key;
|
|
||||||
|
|
||||||
enum Row {
|
|
||||||
Empty,
|
|
||||||
Heading(String),
|
|
||||||
Binding(String, String),
|
|
||||||
BindingContd(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct KeyBindingsList(Vec<Row>);
|
|
||||||
|
|
||||||
impl KeyBindingsList {
|
|
||||||
/// Width of the left column of key bindings.
|
|
||||||
const BINDING_WIDTH: u16 = 24;
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binding_style() -> Style {
|
|
||||||
Style::new().cyan()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_widget(row: Row) -> impl Widget<UiError> {
|
|
||||||
match row {
|
|
||||||
Row::Empty => Text::new("").first3(),
|
|
||||||
|
|
||||||
Row::Heading(name) => Text::new((name, Style::new().bold())).first3(),
|
|
||||||
|
|
||||||
Row::Binding(binding, description) => Join2::horizontal(
|
|
||||||
Text::new((binding, Self::binding_style()))
|
|
||||||
.padding()
|
|
||||||
.with_right(1)
|
|
||||||
.resize()
|
|
||||||
.with_min_width(Self::BINDING_WIDTH)
|
|
||||||
.segment()
|
|
||||||
.with_fixed(true),
|
|
||||||
Text::new(description).segment(),
|
|
||||||
)
|
|
||||||
.second3(),
|
|
||||||
|
|
||||||
Row::BindingContd(description) => Join2::horizontal(
|
|
||||||
Empty::new()
|
|
||||||
.with_width(Self::BINDING_WIDTH)
|
|
||||||
.segment()
|
|
||||||
.with_fixed(true),
|
|
||||||
Text::new(description).segment(),
|
|
||||||
)
|
|
||||||
.third3(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn widget(self, list_state: &mut ListState<Infallible>) -> impl Widget<UiError> + '_ {
|
|
||||||
let binding_style = Self::binding_style();
|
|
||||||
|
|
||||||
let hint_text = Styled::new("jk/↓↑", binding_style)
|
|
||||||
.then_plain(" to scroll, ")
|
|
||||||
.then("esc", binding_style)
|
|
||||||
.then_plain(" to close");
|
|
||||||
|
|
||||||
let hint = Text::new(hint_text)
|
|
||||||
.padding()
|
|
||||||
.with_horizontal(1)
|
|
||||||
.float()
|
|
||||||
.with_horizontal(0.5)
|
|
||||||
.with_vertical(0.0);
|
|
||||||
|
|
||||||
let mut list_builder = ListBuilder::new();
|
|
||||||
for row in self.0 {
|
|
||||||
list_builder.add_unsel(Self::row_widget(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
list_builder
|
|
||||||
.build(list_state)
|
|
||||||
.padding()
|
|
||||||
.with_horizontal(1)
|
|
||||||
.border()
|
|
||||||
.below(hint)
|
|
||||||
.background()
|
|
||||||
.float()
|
|
||||||
.with_center()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty(&mut self) {
|
|
||||||
self.0.push(Row::Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn heading(&mut self, name: &str) {
|
|
||||||
self.0.push(Row::Heading(name.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn binding(&mut self, binding: &str, description: &str) {
|
|
||||||
self.0
|
|
||||||
.push(Row::Binding(binding.to_string(), description.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn binding_ctd(&mut self, description: &str) {
|
|
||||||
self.0.push(Row::BindingContd(description.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,22 +2,21 @@ use std::collections::{HashMap, HashSet};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use cove_config::{Config, RoomsSortOrder};
|
use cove_config::{Config, Keys, RoomsSortOrder};
|
||||||
|
use cove_input::InputEvent;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use euphoxide::api::SessionType;
|
use euphoxide::api::SessionType;
|
||||||
use euphoxide::bot::instance::{Event, ServerConfig};
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
use euphoxide::conn::{self, Joined};
|
use euphoxide::conn::{self, Joined};
|
||||||
use parking_lot::FairMutex;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use toss::widgets::{BoxedAsync, EditorState, Empty, Join2, Text};
|
use toss::widgets::{BoxedAsync, EditorState, Empty, Join2, Text};
|
||||||
use toss::{Style, Styled, Terminal, Widget, WidgetExt};
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
|
|
||||||
use crate::euph;
|
use crate::euph;
|
||||||
use crate::macros::logging_unwrap;
|
use crate::macros::logging_unwrap;
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
|
|
||||||
use super::euph::room::EuphRoom;
|
use super::euph::room::EuphRoom;
|
||||||
use super::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use super::widgets::{ListBuilder, ListState, Popup};
|
use super::widgets::{ListBuilder, ListState, Popup};
|
||||||
use super::{util, UiError, UiEvent};
|
use super::{util, UiError, UiEvent};
|
||||||
|
|
||||||
|
|
@ -357,6 +356,7 @@ impl Rooms {
|
||||||
order: Order,
|
order: Order,
|
||||||
) {
|
) {
|
||||||
if euph_rooms.is_empty() {
|
if euph_rooms.is_empty() {
|
||||||
|
// TODO Use configured key binding
|
||||||
list_builder.add_unsel(Text::new((
|
list_builder.add_unsel(Text::new((
|
||||||
"Press F1 for key bindings",
|
"Press F1 for key bindings",
|
||||||
Style::new().grey().italic(),
|
Style::new().grey().italic(),
|
||||||
|
|
@ -409,56 +409,42 @@ impl Rooms {
|
||||||
c.is_ascii_alphanumeric() || c == '_'
|
c.is_ascii_alphanumeric() || c == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_showlist_key_bindings(bindings: &mut KeyBindingsList) {
|
fn handle_showlist_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
bindings.heading("Rooms");
|
// Open room
|
||||||
util::list_list_key_bindings(bindings);
|
if event.matches(&keys.general.confirm) {
|
||||||
bindings.empty();
|
|
||||||
bindings.binding("enter", "enter selected room");
|
|
||||||
bindings.binding("c", "connect to selected room");
|
|
||||||
bindings.binding("C", "connect to all rooms");
|
|
||||||
bindings.binding("d", "disconnect from selected room");
|
|
||||||
bindings.binding("D", "disconnect from all rooms");
|
|
||||||
bindings.binding("a", "connect to all autojoin room");
|
|
||||||
bindings.binding("A", "disconnect from all non-autojoin rooms");
|
|
||||||
bindings.binding("n", "connect to new room");
|
|
||||||
bindings.binding("X", "delete room");
|
|
||||||
bindings.empty();
|
|
||||||
bindings.binding("s", "change sort order");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_showlist_input_event(&mut self, event: &InputEvent) -> bool {
|
|
||||||
if util::handle_list_input_event(&mut self.list, event) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match event {
|
|
||||||
key!(Enter) => {
|
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.state = State::ShowRoom(name.clone());
|
self.state = State::ShowRoom(name.clone());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('c') => {
|
|
||||||
|
// Move cursor and scroll
|
||||||
|
if util::handle_list_input_event(&mut self.list, event, keys) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room actions
|
||||||
|
if event.matches(&keys.rooms.action.connect) {
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.connect_to_room(name.clone());
|
self.connect_to_room(name.clone());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('C') => {
|
if event.matches(&keys.rooms.action.connect_all) {
|
||||||
self.connect_to_all_rooms();
|
self.connect_to_all_rooms();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('d') => {
|
if event.matches(&keys.rooms.action.disconnect) {
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.disconnect_from_room(&name.clone());
|
self.disconnect_from_room(&name.clone());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('D') => {
|
if event.matches(&keys.rooms.action.disconnect_all) {
|
||||||
self.disconnect_from_all_rooms();
|
self.disconnect_from_all_rooms();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('a') => {
|
if event.matches(&keys.rooms.action.connect_autojoin) {
|
||||||
for (name, options) in &self.config.euph.rooms {
|
for (name, options) in &self.config.euph.rooms {
|
||||||
if options.autojoin {
|
if options.autojoin {
|
||||||
self.connect_to_room(name.clone());
|
self.connect_to_room(name.clone());
|
||||||
|
|
@ -466,7 +452,7 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('A') => {
|
if event.matches(&keys.rooms.action.disconnect_non_autojoin) {
|
||||||
for (name, room) in &mut self.euph_rooms {
|
for (name, room) in &mut self.euph_rooms {
|
||||||
let autojoin = self
|
let autojoin = self
|
||||||
.config
|
.config
|
||||||
|
|
@ -481,126 +467,79 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('n') => {
|
if event.matches(&keys.rooms.action.new) {
|
||||||
self.state = State::Connect(EditorState::new());
|
self.state = State::Connect(EditorState::new());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('X') => {
|
if event.matches(&keys.rooms.action.delete) {
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.state = State::Delete(name.clone(), EditorState::new());
|
self.state = State::Delete(name.clone(), EditorState::new());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!('s') => {
|
if event.matches(&keys.rooms.action.change_sort_order) {
|
||||||
self.order = match self.order {
|
self.order = match self.order {
|
||||||
Order::Alphabet => Order::Importance,
|
Order::Alphabet => Order::Importance,
|
||||||
Order::Importance => Order::Alphabet,
|
Order::Importance => Order::Alphabet,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
match &self.state {
|
|
||||||
State::ShowList => Self::list_showlist_key_bindings(bindings),
|
|
||||||
State::ShowRoom(name) => {
|
|
||||||
// Key bindings for leaving the room are a part of the room's
|
|
||||||
// list_key_bindings function since they may be shadowed by the
|
|
||||||
// nick selector or message editor.
|
|
||||||
if let Some(room) = self.euph_rooms.get(name) {
|
|
||||||
room.list_key_bindings(bindings).await;
|
|
||||||
} else {
|
|
||||||
// There should always be a room here already but I don't
|
|
||||||
// really want to panic in case it is not. If I show a
|
|
||||||
// message like this, it'll hopefully be reported if
|
|
||||||
// somebody ever encounters it.
|
|
||||||
bindings.binding_ctd("oops, this text should never be visible")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Connect(_) => {
|
|
||||||
bindings.heading("Rooms");
|
|
||||||
bindings.binding("esc", "abort");
|
|
||||||
bindings.binding("enter", "connect to room");
|
|
||||||
util::list_editor_key_bindings(bindings, Self::room_char);
|
|
||||||
}
|
|
||||||
State::Delete(_, _) => {
|
|
||||||
bindings.heading("Rooms");
|
|
||||||
bindings.binding("esc", "abort");
|
|
||||||
bindings.binding("enter", "delete room");
|
|
||||||
util::list_editor_key_bindings(bindings, Self::room_char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_input_event(
|
|
||||||
&mut self,
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
|
||||||
event: &InputEvent,
|
|
||||||
) -> bool {
|
|
||||||
self.stabilize_rooms().await;
|
self.stabilize_rooms().await;
|
||||||
|
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
State::ShowList => {
|
State::ShowList => {
|
||||||
if self.handle_showlist_input_event(event) {
|
if self.handle_showlist_input_event(event, keys) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::ShowRoom(name) => {
|
State::ShowRoom(name) => {
|
||||||
if let Some(room) = self.euph_rooms.get_mut(name) {
|
if let Some(room) = self.euph_rooms.get_mut(name) {
|
||||||
if room
|
if room.handle_input_event(event, keys).await {
|
||||||
.handle_input_event(terminal, crossterm_lock, event)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if event.matches(&keys.general.abort) {
|
||||||
if let key!(Esc) = event {
|
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Connect(ed) => match event {
|
State::Connect(editor) => {
|
||||||
key!(Esc) => {
|
if event.matches(&keys.general.abort) {
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!(Enter) => {
|
if event.matches(&keys.general.confirm) {
|
||||||
let name = ed.text().to_string();
|
let name = editor.text().to_string();
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
self.connect_to_room(name.clone());
|
self.connect_to_room(name.clone());
|
||||||
self.state = State::ShowRoom(name);
|
self.state = State::ShowRoom(name);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {
|
if util::handle_editor_input_event(editor, event, keys, Self::room_char) {
|
||||||
if util::handle_editor_input_event(ed, terminal, event, Self::room_char) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
State::Delete(name, editor) => {
|
||||||
State::Delete(name, editor) => match event {
|
if event.matches(&keys.general.abort) {
|
||||||
key!(Esc) => {
|
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
key!(Enter) if editor.text() == *name => {
|
if event.matches(&keys.general.confirm) {
|
||||||
self.euph_rooms.remove(name);
|
self.euph_rooms.remove(name);
|
||||||
logging_unwrap!(self.vault.euph().room(name.clone()).delete().await);
|
logging_unwrap!(self.vault.euph().room(name.clone()).delete().await);
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {
|
if util::handle_editor_input_event(editor, event, keys, Self::room_char) {
|
||||||
if util::handle_editor_input_event(editor, terminal, event, Self::room_char) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -1,191 +1,191 @@
|
||||||
use std::io;
|
use cove_config::Keys;
|
||||||
use std::sync::Arc;
|
use cove_input::InputEvent;
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use parking_lot::FairMutex;
|
|
||||||
use toss::widgets::EditorState;
|
use toss::widgets::EditorState;
|
||||||
use toss::Terminal;
|
|
||||||
|
|
||||||
use super::input::{key, InputEvent, KeyBindingsList};
|
|
||||||
use super::widgets::ListState;
|
use super::widgets::ListState;
|
||||||
|
|
||||||
pub fn prompt(
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
|
||||||
initial_text: &str,
|
|
||||||
) -> io::Result<String> {
|
|
||||||
let content = {
|
|
||||||
let _guard = crossterm_lock.lock();
|
|
||||||
terminal.suspend().expect("could not suspend");
|
|
||||||
let content = edit::edit(initial_text);
|
|
||||||
terminal.unsuspend().expect("could not unsuspend");
|
|
||||||
content
|
|
||||||
};
|
|
||||||
|
|
||||||
content
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////
|
//////////
|
||||||
// List //
|
// List //
|
||||||
//////////
|
//////////
|
||||||
|
|
||||||
pub fn list_list_key_bindings(bindings: &mut KeyBindingsList) {
|
pub fn handle_list_input_event<Id: Clone>(
|
||||||
bindings.binding("j/k, ↓/↑", "move cursor up/down");
|
list: &mut ListState<Id>,
|
||||||
bindings.binding("g, home", "move cursor to top");
|
event: &InputEvent<'_>,
|
||||||
bindings.binding("G, end", "move cursor to bottom");
|
keys: &Keys,
|
||||||
bindings.binding("ctrl+y/e", "scroll up/down");
|
) -> bool {
|
||||||
}
|
// Cursor movement
|
||||||
|
if event.matches(&keys.cursor.up) {
|
||||||
pub fn handle_list_input_event<Id: Clone>(list: &mut ListState<Id>, event: &InputEvent) -> bool {
|
list.move_cursor_up();
|
||||||
match event {
|
return true;
|
||||||
key!('k') | key!(Up) => list.move_cursor_up(),
|
}
|
||||||
key!('j') | key!(Down) => list.move_cursor_down(),
|
if event.matches(&keys.cursor.down) {
|
||||||
key!('g') | key!(Home) => list.move_cursor_to_top(),
|
list.move_cursor_down();
|
||||||
key!('G') | key!(End) => list.move_cursor_to_bottom(),
|
return true;
|
||||||
key!(Ctrl + 'y') => list.scroll_up(1),
|
}
|
||||||
key!(Ctrl + 'e') => list.scroll_down(1),
|
if event.matches(&keys.cursor.to_top) {
|
||||||
_ => return false,
|
list.move_cursor_to_top();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.cursor.to_bottom) {
|
||||||
|
list.move_cursor_to_bottom();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
// Scrolling
|
||||||
|
if event.matches(&keys.scroll.up_line) {
|
||||||
|
list.scroll_up(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.down_line) {
|
||||||
|
list.scroll_down(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.up_half) {
|
||||||
|
list.scroll_up_half();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.down_half) {
|
||||||
|
list.scroll_down_half();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.up_full) {
|
||||||
|
list.scroll_up_full();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.down_full) {
|
||||||
|
list.scroll_down_full();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.scroll.center_cursor) {
|
||||||
|
list.center_cursor();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////
|
////////////
|
||||||
// Editor //
|
// Editor //
|
||||||
////////////
|
////////////
|
||||||
|
|
||||||
fn list_editor_editing_key_bindings(
|
|
||||||
bindings: &mut KeyBindingsList,
|
|
||||||
char_filter: impl Fn(char) -> bool,
|
|
||||||
) {
|
|
||||||
if char_filter('\n') {
|
|
||||||
bindings.binding("enter+<any modifier>", "insert newline");
|
|
||||||
}
|
|
||||||
|
|
||||||
bindings.binding("ctrl+h, backspace", "delete before cursor");
|
|
||||||
bindings.binding("ctrl+d, delete", "delete after cursor");
|
|
||||||
bindings.binding("ctrl+l", "clear editor contents");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_editor_cursor_movement_key_bindings(bindings: &mut KeyBindingsList) {
|
|
||||||
bindings.binding("ctrl+b, ←", "move cursor left");
|
|
||||||
bindings.binding("ctrl+f, →", "move cursor right");
|
|
||||||
bindings.binding("alt+b, ctrl+←", "move cursor left a word");
|
|
||||||
bindings.binding("alt+f, ctrl+→", "move cursor right a word");
|
|
||||||
bindings.binding("ctrl+a, home", "move cursor to start of line");
|
|
||||||
bindings.binding("ctrl+e, end", "move cursor to end of line");
|
|
||||||
bindings.binding("↑/↓", "move cursor up/down");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_editor_key_bindings(
|
|
||||||
bindings: &mut KeyBindingsList,
|
|
||||||
char_filter: impl Fn(char) -> bool,
|
|
||||||
) {
|
|
||||||
list_editor_editing_key_bindings(bindings, char_filter);
|
|
||||||
bindings.empty();
|
|
||||||
list_editor_cursor_movement_key_bindings(bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_editor_input_event(
|
|
||||||
editor: &mut EditorState,
|
|
||||||
terminal: &mut Terminal,
|
|
||||||
event: &InputEvent,
|
|
||||||
char_filter: impl Fn(char) -> bool,
|
|
||||||
) -> bool {
|
|
||||||
match event {
|
|
||||||
// Enter with *any* modifier pressed - if ctrl and shift don't
|
|
||||||
// work, maybe alt does
|
|
||||||
key!(Enter) => return false,
|
|
||||||
InputEvent::Key(crate::ui::input::KeyEvent {
|
|
||||||
code: crossterm::event::KeyCode::Enter,
|
|
||||||
..
|
|
||||||
}) if char_filter('\n') => editor.insert_char(terminal.widthdb(), '\n'),
|
|
||||||
|
|
||||||
// Editing
|
|
||||||
key!(Char ch) if char_filter(*ch) => editor.insert_char(terminal.widthdb(), *ch),
|
|
||||||
key!(Paste str) => {
|
|
||||||
// It seems that when pasting, '\n' are converted into '\r' for some
|
|
||||||
// reason. I don't really know why, or at what point this happens.
|
|
||||||
// Vim converts any '\r' pasted via the terminal into '\n', so I
|
|
||||||
// decided to mirror that behaviour.
|
|
||||||
let str = str.replace('\r', "\n");
|
|
||||||
if str.chars().all(char_filter) {
|
|
||||||
editor.insert_str(terminal.widthdb(), &str);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key!(Ctrl + 'h') | key!(Backspace) => editor.backspace(terminal.widthdb()),
|
|
||||||
key!(Ctrl + 'd') | key!(Delete) => editor.delete(),
|
|
||||||
key!(Ctrl + 'l') => editor.clear(),
|
|
||||||
// TODO Key bindings to delete words
|
|
||||||
|
|
||||||
// Cursor movement
|
|
||||||
key!(Ctrl + 'b') | key!(Left) => editor.move_cursor_left(terminal.widthdb()),
|
|
||||||
key!(Ctrl + 'f') | key!(Right) => editor.move_cursor_right(terminal.widthdb()),
|
|
||||||
key!(Alt + 'b') | key!(Ctrl + Left) => editor.move_cursor_left_a_word(terminal.widthdb()),
|
|
||||||
key!(Alt + 'f') | key!(Ctrl + Right) => editor.move_cursor_right_a_word(terminal.widthdb()),
|
|
||||||
key!(Ctrl + 'a') | key!(Home) => editor.move_cursor_to_start_of_line(terminal.widthdb()),
|
|
||||||
key!(Ctrl + 'e') | key!(End) => editor.move_cursor_to_end_of_line(terminal.widthdb()),
|
|
||||||
key!(Up) => editor.move_cursor_up(terminal.widthdb()),
|
|
||||||
key!(Down) => editor.move_cursor_down(terminal.widthdb()),
|
|
||||||
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit_externally(
|
fn edit_externally(
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
char_filter: impl Fn(char) -> bool,
|
||||||
) -> io::Result<()> {
|
) {
|
||||||
let text = prompt(terminal, crossterm_lock, editor.text())?;
|
let Ok(text) = event.prompt(editor.text()) else {
|
||||||
|
// Something went wrong during editing, let's abort the edit.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if text.trim().is_empty() {
|
if text.trim().is_empty() {
|
||||||
// The user likely wanted to abort the edit and has deleted the
|
// The user likely wanted to abort the edit and has deleted the
|
||||||
// entire text (bar whitespace left over by some editors).
|
// entire text (bar whitespace left over by some editors).
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(text) = text.strip_suffix('\n') {
|
let text = text
|
||||||
// Some editors like vim add a trailing newline that would look out of
|
.strip_suffix('\n')
|
||||||
// place in cove's editors. To intentionally add a trailing newline,
|
.unwrap_or(&text)
|
||||||
// simply add two in-editor.
|
.chars()
|
||||||
editor.set_text(terminal.widthdb(), text.to_string());
|
.filter(|c| char_filter(*c))
|
||||||
} else {
|
.collect::<String>();
|
||||||
editor.set_text(terminal.widthdb(), text);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
editor.set_text(event.widthdb(), text);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_editor_key_bindings_allowing_external_editing(
|
fn char_modifier(modifiers: KeyModifiers) -> bool {
|
||||||
bindings: &mut KeyBindingsList,
|
modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
|
||||||
char_filter: impl Fn(char) -> bool,
|
|
||||||
) {
|
|
||||||
list_editor_editing_key_bindings(bindings, char_filter);
|
|
||||||
bindings.binding("ctrl+x", "edit in external editor");
|
|
||||||
bindings.empty();
|
|
||||||
list_editor_cursor_movement_key_bindings(bindings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_editor_input_event_allowing_external_editing(
|
pub fn handle_editor_input_event(
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
terminal: &mut Terminal,
|
event: &mut InputEvent<'_>,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
keys: &Keys,
|
||||||
event: &InputEvent,
|
|
||||||
char_filter: impl Fn(char) -> bool,
|
char_filter: impl Fn(char) -> bool,
|
||||||
) -> io::Result<bool> {
|
) -> bool {
|
||||||
if let key!(Ctrl + 'x') = event {
|
// Cursor movement
|
||||||
edit_externally(editor, terminal, crossterm_lock)?;
|
if event.matches(&keys.editor.cursor.left) {
|
||||||
Ok(true)
|
editor.move_cursor_left(event.widthdb());
|
||||||
} else {
|
return true;
|
||||||
Ok(handle_editor_input_event(
|
|
||||||
editor,
|
|
||||||
terminal,
|
|
||||||
event,
|
|
||||||
char_filter,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.right) {
|
||||||
|
editor.move_cursor_right(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.left_word) {
|
||||||
|
editor.move_cursor_left_a_word(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.right_word) {
|
||||||
|
editor.move_cursor_right_a_word(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.start) {
|
||||||
|
editor.move_cursor_to_start_of_line(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.end) {
|
||||||
|
editor.move_cursor_to_end_of_line(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.up) {
|
||||||
|
editor.move_cursor_up(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.cursor.down) {
|
||||||
|
editor.move_cursor_down(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editing
|
||||||
|
if event.matches(&keys.editor.action.backspace) {
|
||||||
|
editor.backspace(event.widthdb());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.action.delete) {
|
||||||
|
editor.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.action.clear) {
|
||||||
|
editor.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if event.matches(&keys.editor.action.external) {
|
||||||
|
edit_externally(editor, event, char_filter);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting individual characters
|
||||||
|
if let Some(key_event) = event.key_event() {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Enter if char_filter('\n') => {
|
||||||
|
editor.insert_char(event.widthdb(), '\n');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if char_modifier(key_event.modifiers) && char_filter(c) => {
|
||||||
|
editor.insert_char(event.widthdb(), c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pasting text
|
||||||
|
if let Some(text) = event.paste_event() {
|
||||||
|
// It seems that when pasting, '\n' are converted into '\r' for some
|
||||||
|
// reason. I don't really know why, or at what point this happens. Vim
|
||||||
|
// converts any '\r' pasted via the terminal into '\n', so I decided to
|
||||||
|
// mirror that behaviour.
|
||||||
|
let text = text
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c == '\r' { '\n' } else { c })
|
||||||
|
.filter(|c| char_filter(*c))
|
||||||
|
.collect::<String>();
|
||||||
|
editor.insert_str(event.widthdb(), &text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,22 @@ impl<Id: Clone> ListState<Id> {
|
||||||
self.scroll_to(self.offset.saturating_add(lines));
|
self.scroll_to(self.offset.saturating_add(lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up_half(&mut self) {
|
||||||
|
self.scroll_up((self.last_height / 2).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down_half(&mut self) {
|
||||||
|
self.scroll_down((self.last_height / 2).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up_full(&mut self) {
|
||||||
|
self.scroll_up(self.last_height.saturating_sub(1).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down_full(&mut self) {
|
||||||
|
self.scroll_down(self.last_height.saturating_sub(1).into());
|
||||||
|
}
|
||||||
|
|
||||||
/// Scroll so that the cursor is in the center of the widget, or at least as
|
/// Scroll so that the cursor is in the center of the widget, or at least as
|
||||||
/// close as possible.
|
/// close as possible.
|
||||||
pub fn center_cursor(&mut self) {
|
pub fn center_cursor(&mut self) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue