Match key events using macros

This commit is contained in:
Joscha 2022-08-04 01:05:04 +02:00
parent df0403a782
commit 20ea96f83e
7 changed files with 152 additions and 155 deletions

View file

@ -1,4 +1,5 @@
mod chat; mod chat;
mod input;
mod room; mod room;
mod rooms; mod rooms;
mod util; mod util;
@ -7,7 +8,7 @@ mod widgets;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent}; use crossterm::event::{Event, KeyCode, MouseEvent};
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};
@ -19,6 +20,7 @@ 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, KeyEvent};
use self::rooms::Rooms; use self::rooms::Rooms;
use self::widgets::BoxedWidget; use self::widgets::BoxedWidget;
@ -152,7 +154,7 @@ impl Ui {
let result = match event { let result = match event {
UiEvent::Redraw => EventHandleResult::Continue, UiEvent::Redraw => EventHandleResult::Continue,
UiEvent::Term(Event::Key(event)) => { UiEvent::Term(Event::Key(event)) => {
self.handle_key_event(event, terminal, &crossterm_lock) self.handle_key_event(event.into(), terminal, &crossterm_lock)
.await .await
} }
UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?, UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?,
@ -193,17 +195,13 @@ impl Ui {
terminal: &mut Terminal, terminal: &mut Terminal,
crossterm_lock: &Arc<FairMutex<()>>, crossterm_lock: &Arc<FairMutex<()>>,
) -> EventHandleResult { ) -> EventHandleResult {
// Always exit when ctrl+c is pressed. Previously, shift+q would also match event {
// unconditionally quit cove, but that interfered with typing text in // Exit unconditionally on ctrl+c. Previously, shift+q would also
// unconditionally exit, but that interfered with typing text in
// inline editors. // inline editors.
let ctrl_c = event.modifiers == KeyModifiers::CONTROL && event.code == KeyCode::Char('c'); key!(Ctrl + 'c') => return EventHandleResult::Stop,
if ctrl_c { key!(F 1) => self.mode = Mode::Main,
return EventHandleResult::Stop; key!(F 2) => self.mode = Mode::Log,
}
match event.code {
KeyCode::F(1) => self.mode = Mode::Main,
KeyCode::F(2) => self.mode = Mode::Log,
_ => {} _ => {}
} }

View file

@ -4,7 +4,6 @@ mod tree;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crossterm::event::KeyEvent;
use parking_lot::FairMutex; use parking_lot::FairMutex;
use time::OffsetDateTime; use time::OffsetDateTime;
use toss::frame::{Frame, Size}; use toss::frame::{Frame, Size};
@ -15,6 +14,7 @@ use crate::store::{Msg, MsgStore};
use self::tree::{TreeView, TreeViewState}; use self::tree::{TreeView, TreeViewState};
use super::input::KeyEvent;
use super::widgets::Widget; use super::widgets::Widget;
/////////// ///////////

View file

@ -6,13 +6,14 @@ mod widgets;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::KeyCode;
use parking_lot::FairMutex; use parking_lot::FairMutex;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use toss::frame::{Frame, Pos, Size}; use toss::frame::{Frame, Pos, Size};
use toss::terminal::Terminal; use toss::terminal::Terminal;
use crate::store::{Msg, MsgStore}; use crate::store::{Msg, MsgStore};
use crate::ui::input::{key, KeyEvent};
use crate::ui::widgets::editor::EditorState; use crate::ui::widgets::editor::EditorState;
use crate::ui::widgets::Widget; use crate::ui::widgets::Widget;
@ -66,101 +67,61 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
coming_from: Option<M::Id>, coming_from: Option<M::Id>,
parent: Option<M::Id>, parent: Option<M::Id>,
) -> Reaction<M> { ) -> Reaction<M> {
let harmless_char = (event.modifiers - KeyModifiers::SHIFT).is_empty();
// TODO Tab-completion // TODO Tab-completion
match event.code { match event {
KeyCode::Esc => { key!(Esc) => {
self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom); self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
Reaction::Handled return Reaction::Handled;
} }
KeyCode::Enter if event.modifiers.is_empty() => {
key!(Enter) => {
let content = self.editor.text(); let content = self.editor.text();
if content.trim().is_empty() { if !content.trim().is_empty() {
Reaction::Handled
} else {
self.cursor = Cursor::Pseudo { self.cursor = Cursor::Pseudo {
coming_from, coming_from,
parent: parent.clone(), parent: parent.clone(),
}; };
Reaction::Composed { parent, content } return Reaction::Composed { parent, content };
} }
} }
KeyCode::Enter => {
// Enter with *any* modifier pressed - if ctrl and shift don't // Enter with *any* modifier pressed - if ctrl and shift don't
// work, maybe alt does // work, maybe alt does
self.editor.insert_char('\n'); KeyEvent {
code: KeyCode::Enter,
..
} => self.editor.insert_char('\n'),
key!(Char ch) => self.editor.insert_char(ch),
key!(Backspace) => self.editor.backspace(),
key!(Left) => self.editor.move_cursor_left(),
key!(Right) => self.editor.move_cursor_right(),
key!(Delete) => self.editor.delete(),
key!(Ctrl + 'e') => self.editor.edit_externally(terminal, crossterm_lock),
key!(Ctrl + 'l') => self.editor.clear(),
_ => return Reaction::NotHandled,
}
self.correction = Some(Correction::MakeCursorVisible); self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled Reaction::Handled
} }
KeyCode::Backspace => {
self.editor.backspace();
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Left => {
self.editor.move_cursor_left();
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Right => {
self.editor.move_cursor_right();
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Delete => {
self.editor.delete();
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Char(ch) if harmless_char => {
self.editor.insert_char(ch);
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => {
self.editor.edit_externally(terminal, crossterm_lock);
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
KeyCode::Char('l') if event.modifiers == KeyModifiers::CONTROL => {
self.editor.clear();
self.correction = Some(Correction::MakeCursorVisible);
Reaction::Handled
}
_ => Reaction::NotHandled,
}
}
async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool { async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool {
let chat_height = frame.size().height - 3; let chat_height = frame.size().height - 3;
let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty();
match event.code { match event {
KeyCode::Char('k') | KeyCode::Up if shift_only => self.move_cursor_up().await, key!('k') | key!(Up) => self.move_cursor_up().await,
KeyCode::Char('j') | KeyCode::Down if shift_only => self.move_cursor_down().await, key!('j') | key!(Down) => self.move_cursor_down().await,
KeyCode::Char('h') | KeyCode::Left if shift_only => self.move_cursor_older().await, key!('h') | key!(Left) => self.move_cursor_older().await,
KeyCode::Char('l') | KeyCode::Right if shift_only => self.move_cursor_newer().await, key!('l') | key!(Right) => self.move_cursor_newer().await,
KeyCode::Char('g') | KeyCode::Home if shift_only => self.move_cursor_to_top().await, key!('g') | key!(Home) => self.move_cursor_to_top().await,
KeyCode::Char('G') | KeyCode::End if shift_only => self.move_cursor_to_bottom().await, key!('G') | key!(End) => self.move_cursor_to_bottom().await,
KeyCode::Char('y') if event.modifiers == KeyModifiers::CONTROL => self.scroll_up(1), key!(Ctrl + 'y') => self.scroll_up(1),
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1), key!(Ctrl + 'e') => self.scroll_down(1),
KeyCode::Char('u') if event.modifiers == KeyModifiers::CONTROL => { key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()),
let delta = chat_height / 2; key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).into()),
self.scroll_up(delta.into()); key!(Ctrl + 'b') => self.scroll_up(chat_height.saturating_sub(1).into()),
} key!(Ctrl + 'f') => self.scroll_down(chat_height.saturating_sub(1).into()),
KeyCode::Char('d') if event.modifiers == KeyModifiers::CONTROL => {
let delta = chat_height / 2;
self.scroll_down(delta.into());
}
KeyCode::Char('b') if event.modifiers == KeyModifiers::CONTROL => {
let delta = chat_height.saturating_sub(1);
self.scroll_up(delta.into());
}
KeyCode::Char('f') if event.modifiers == KeyModifiers::CONTROL => {
let delta = chat_height.saturating_sub(1);
self.scroll_down(delta.into());
}
_ => return false, _ => return false,
} }
@ -172,35 +133,21 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
event: KeyEvent, event: KeyEvent,
id: Option<M::Id>, id: Option<M::Id>,
) -> bool { ) -> bool {
let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); match event {
if !shift_only { key!('r') => {
return false;
}
match event.code {
KeyCode::Char('r') => {
if let Some(parent) = self.parent_for_normal_reply().await { if let Some(parent) = self.parent_for_normal_reply().await {
self.cursor = Cursor::Editor { self.cursor = Cursor::editor(id, parent);
coming_from: id,
parent,
};
self.correction = Some(Correction::MakeCursorVisible); self.correction = Some(Correction::MakeCursorVisible);
} }
} }
KeyCode::Char('R') => { key!('R') => {
if let Some(parent) = self.parent_for_alternate_reply().await { if let Some(parent) = self.parent_for_alternate_reply().await {
self.cursor = Cursor::Editor { self.cursor = Cursor::editor(id, parent);
coming_from: id,
parent,
};
self.correction = Some(Correction::MakeCursorVisible); self.correction = Some(Correction::MakeCursorVisible);
} }
} }
KeyCode::Char('t' | 'T') => { key!('t') | key!('T') => {
self.cursor = Cursor::Editor { self.cursor = Cursor::editor(id, None);
coming_from: id,
parent: None,
};
self.correction = Some(Correction::MakeCursorVisible); self.correction = Some(Correction::MakeCursorVisible);
} }
_ => return false, _ => return false,

View file

@ -18,6 +18,15 @@ pub enum Cursor<I> {
}, },
} }
impl<I> Cursor<I> {
pub fn editor(coming_from: Option<I>, parent: Option<I>) -> Self {
Self::Editor {
coming_from,
parent,
}
}
}
impl<I: Eq> Cursor<I> { impl<I: Eq> Cursor<I> {
pub fn refers_to(&self, id: &I) -> bool { pub fn refers_to(&self, id: &I) -> bool {
if let Self::Msg(own_id) = self { if let Self::Msg(own_id) = self {

48
src/ui/input.rs Normal file
View file

@ -0,0 +1,48 @@
use crossterm::event::{KeyCode, KeyModifiers};
/// 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]
macro_rules! key {
// key!('a')
( $key:literal ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: false, alt: false, } };
( Ctrl + $key:literal ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: true, alt: false, } };
( Alt + $key:literal ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: false, alt: true, } };
// key!(Char(xyz))
( Char $key:pat ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: false, alt: false, } };
( Ctrl + Char $key:pat ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: true, alt: false, } };
( Alt + Char $key:pat ) => { KeyEvent { code: KeyCode::Char($key), shift: _, ctrl: false, alt: true, } };
// key!(F(n))
( F $key:pat ) => { KeyEvent { code: KeyCode::F($key), shift: false, ctrl: false, alt: false, } };
( Shift + F $key:pat ) => { KeyEvent { code: KeyCode::F($key), shift: true, ctrl: false, alt: false, } };
( Ctrl + F $key:pat ) => { KeyEvent { code: KeyCode::F($key), shift: false, ctrl: true, alt: false, } };
( Alt + F $key:pat ) => { KeyEvent { code: KeyCode::F($key), shift: false, ctrl: false, alt: true, } };
// key!(other)
( $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: false, ctrl: false, alt: false, } };
( Shift + $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: true, ctrl: false, alt: false, } };
( Ctrl + $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: false, ctrl: true, alt: false, } };
( Alt + $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: false, ctrl: false, alt: true, } };
}
pub(crate) use key;

View file

@ -1,7 +1,7 @@
use std::iter; use std::iter;
use std::sync::Arc; use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::KeyCode;
use crossterm::style::{Color, ContentStyle, Stylize}; use crossterm::style::{Color, ContentStyle, Stylize};
use parking_lot::FairMutex; use parking_lot::FairMutex;
use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::oneshot::error::TryRecvError;
@ -14,6 +14,7 @@ use crate::euph::{self, Joined, Status};
use crate::vault::EuphVault; use crate::vault::EuphVault;
use super::chat::{ChatState, Reaction}; use super::chat::{ChatState, Reaction};
use super::input::{key, KeyEvent};
use super::widgets::background::Background; use super::widgets::background::Background;
use super::widgets::border::Border; use super::widgets::border::Border;
use super::widgets::editor::EditorState; use super::widgets::editor::EditorState;
@ -328,11 +329,7 @@ impl EuphRoom {
} }
} }
if !event.modifiers.is_empty() { if let key!('n') | key!('N') = event {
return false;
}
if let KeyCode::Char('n' | 'N') = event.code {
self.state = State::ChooseNick(EditorState::with_initial_text( self.state = State::ChooseNick(EditorState::with_initial_text(
joined.session.name.clone(), joined.session.name.clone(),
)); ));
@ -349,19 +346,19 @@ impl EuphRoom {
.handled() .handled()
} }
State::ChooseNick(ed) => { State::ChooseNick(ed) => {
match event.code { match event {
KeyCode::Esc => self.state = State::Normal, key!(Esc) => self.state = State::Normal,
KeyCode::Enter => { key!(Enter) => {
if let Some(room) = &self.room { if let Some(room) = &self.room {
let _ = room.nick(ed.text()); let _ = room.nick(ed.text());
} }
self.state = State::Normal; self.state = State::Normal;
} }
KeyCode::Backspace => ed.backspace(), key!(Char ch) => ed.insert_char(ch),
KeyCode::Left => ed.move_cursor_left(), key!(Backspace) => ed.backspace(),
KeyCode::Right => ed.move_cursor_right(), key!(Left) => ed.move_cursor_left(),
KeyCode::Delete => ed.delete(), key!(Right) => ed.move_cursor_right(),
KeyCode::Char(ch) => ed.insert_char(ch), key!(Delete) => ed.delete(),
_ => return false, _ => return false,
} }
true true

View file

@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use std::iter; use std::iter;
use std::sync::Arc; use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::KeyCode;
use crossterm::style::{ContentStyle, Stylize}; use crossterm::style::{ContentStyle, Stylize};
use parking_lot::FairMutex; use parking_lot::FairMutex;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -13,6 +13,7 @@ use crate::euph::api::SessionType;
use crate::euph::{Joined, Status}; use crate::euph::{Joined, Status};
use crate::vault::Vault; use crate::vault::Vault;
use super::input::{key, KeyEvent};
use super::room::EuphRoom; use super::room::EuphRoom;
use super::widgets::background::Background; use super::widgets::background::Background;
use super::widgets::border::Border; use super::widgets::border::Border;
@ -205,34 +206,31 @@ impl Rooms {
event: KeyEvent, event: KeyEvent,
) { ) {
match &self.state { match &self.state {
State::ShowList => match event.code { State::ShowList => match event {
KeyCode::Enter => { key!(Enter) => {
if let Some(name) = self.list.cursor() { if let Some(name) = self.list.cursor() {
self.state = State::ShowRoom(name); self.state = State::ShowRoom(name);
} }
} }
KeyCode::Char('k') | KeyCode::Up => self.list.move_cursor_up(), key!('k') | key!(Up) => self.list.move_cursor_up(),
KeyCode::Char('j') | KeyCode::Down => self.list.move_cursor_down(), key!('j') | key!(Down) => self.list.move_cursor_down(),
KeyCode::Char('g') | KeyCode::Home => self.list.move_cursor_to_top(), key!('g') | key!(Home) => self.list.move_cursor_to_top(),
KeyCode::Char('G') | KeyCode::End => self.list.move_cursor_to_bottom(), key!('G') | key!(End) => self.list.move_cursor_to_bottom(),
KeyCode::Char('y') if event.modifiers == KeyModifiers::CONTROL => { key!(Ctrl + 'y') => self.list.scroll_up(1),
self.list.scroll_up(1) key!(Ctrl + 'e') => self.list.scroll_down(1),
}
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => { key!('c') => {
self.list.scroll_down(1)
}
KeyCode::Char('c') => {
if let Some(name) = self.list.cursor() { if let Some(name) = self.list.cursor() {
self.get_or_insert_room(name).connect(); self.get_or_insert_room(name).connect();
} }
} }
KeyCode::Char('C') => self.state = State::Connect(EditorState::new()), key!('C') => self.state = State::Connect(EditorState::new()),
KeyCode::Char('d') => { key!('d') => {
if let Some(name) = self.list.cursor() { if let Some(name) = self.list.cursor() {
self.get_or_insert_room(name).disconnect(); self.get_or_insert_room(name).disconnect();
} }
} }
KeyCode::Char('D') => { key!('D') => {
// TODO Check whether user wanted this via popup // TODO Check whether user wanted this via popup
if let Some(name) = self.list.cursor() { if let Some(name) = self.list.cursor() {
self.euph_rooms.remove(&name); self.euph_rooms.remove(&name);
@ -250,24 +248,24 @@ impl Rooms {
return; return;
} }
if event.code == KeyCode::Esc { if let key!(Esc) = event {
self.state = State::ShowList; self.state = State::ShowList;
} }
} }
State::Connect(ed) => match event.code { State::Connect(ed) => match event {
KeyCode::Esc => self.state = State::ShowList, key!(Esc) => self.state = State::ShowList,
KeyCode::Enter => { key!(Enter) => {
let name = ed.text(); let name = ed.text();
if !name.is_empty() { if !name.is_empty() {
self.get_or_insert_room(name.clone()).connect(); self.get_or_insert_room(name.clone()).connect();
self.state = State::ShowRoom(name); self.state = State::ShowRoom(name);
} }
} }
KeyCode::Backspace => ed.backspace(), key!(Char ch) if ch.is_ascii_alphanumeric() || ch == '_' => ed.insert_char(ch),
KeyCode::Left => ed.move_cursor_left(), key!(Backspace) => ed.backspace(),
KeyCode::Right => ed.move_cursor_right(), key!(Left) => ed.move_cursor_left(),
KeyCode::Delete => ed.delete(), key!(Right) => ed.move_cursor_right(),
KeyCode::Char(ch) if ch.is_ascii_alphanumeric() || ch == '_' => ed.insert_char(ch), key!(Delete) => ed.delete(),
_ => {} _ => {}
}, },
} }