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

View file

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

View file

@ -6,13 +6,14 @@ mod widgets;
use std::sync::Arc;
use async_trait::async_trait;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::KeyCode;
use parking_lot::FairMutex;
use tokio::sync::Mutex;
use toss::frame::{Frame, Pos, Size};
use toss::terminal::Terminal;
use crate::store::{Msg, MsgStore};
use crate::ui::input::{key, KeyEvent};
use crate::ui::widgets::editor::EditorState;
use crate::ui::widgets::Widget;
@ -66,101 +67,61 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
coming_from: Option<M::Id>,
parent: Option<M::Id>,
) -> Reaction<M> {
let harmless_char = (event.modifiers - KeyModifiers::SHIFT).is_empty();
// TODO Tab-completion
match event.code {
KeyCode::Esc => {
match event {
key!(Esc) => {
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();
if content.trim().is_empty() {
Reaction::Handled
} else {
if !content.trim().is_empty() {
self.cursor = Cursor::Pseudo {
coming_from,
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
// work, maybe alt does
self.editor.insert_char('\n');
self.correction = Some(Correction::MakeCursorVisible);
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,
// Enter with *any* modifier pressed - if ctrl and shift don't
// work, maybe alt does
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);
Reaction::Handled
}
async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool {
let chat_height = frame.size().height - 3;
let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty();
match event.code {
KeyCode::Char('k') | KeyCode::Up if shift_only => self.move_cursor_up().await,
KeyCode::Char('j') | KeyCode::Down if shift_only => self.move_cursor_down().await,
KeyCode::Char('h') | KeyCode::Left if shift_only => self.move_cursor_older().await,
KeyCode::Char('l') | KeyCode::Right if shift_only => self.move_cursor_newer().await,
KeyCode::Char('g') | KeyCode::Home if shift_only => self.move_cursor_to_top().await,
KeyCode::Char('G') | KeyCode::End if shift_only => self.move_cursor_to_bottom().await,
KeyCode::Char('y') if event.modifiers == KeyModifiers::CONTROL => self.scroll_up(1),
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1),
KeyCode::Char('u') if event.modifiers == KeyModifiers::CONTROL => {
let delta = chat_height / 2;
self.scroll_up(delta.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());
}
match event {
key!('k') | key!(Up) => self.move_cursor_up().await,
key!('j') | key!(Down) => self.move_cursor_down().await,
key!('h') | key!(Left) => self.move_cursor_older().await,
key!('l') | key!(Right) => self.move_cursor_newer().await,
key!('g') | key!(Home) => self.move_cursor_to_top().await,
key!('G') | key!(End) => self.move_cursor_to_bottom().await,
key!(Ctrl + 'y') => self.scroll_up(1),
key!(Ctrl + 'e') => self.scroll_down(1),
key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()),
key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).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()),
_ => return false,
}
@ -172,35 +133,21 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
event: KeyEvent,
id: Option<M::Id>,
) -> bool {
let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty();
if !shift_only {
return false;
}
match event.code {
KeyCode::Char('r') => {
match event {
key!('r') => {
if let Some(parent) = self.parent_for_normal_reply().await {
self.cursor = Cursor::Editor {
coming_from: id,
parent,
};
self.cursor = Cursor::editor(id, parent);
self.correction = Some(Correction::MakeCursorVisible);
}
}
KeyCode::Char('R') => {
key!('R') => {
if let Some(parent) = self.parent_for_alternate_reply().await {
self.cursor = Cursor::Editor {
coming_from: id,
parent,
};
self.cursor = Cursor::editor(id, parent);
self.correction = Some(Correction::MakeCursorVisible);
}
}
KeyCode::Char('t' | 'T') => {
self.cursor = Cursor::Editor {
coming_from: id,
parent: None,
};
key!('t') | key!('T') => {
self.cursor = Cursor::editor(id, None);
self.correction = Some(Correction::MakeCursorVisible);
}
_ => 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> {
pub fn refers_to(&self, id: &I) -> bool {
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::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent};
use crossterm::event::KeyCode;
use crossterm::style::{Color, ContentStyle, Stylize};
use parking_lot::FairMutex;
use tokio::sync::oneshot::error::TryRecvError;
@ -14,6 +14,7 @@ use crate::euph::{self, Joined, Status};
use crate::vault::EuphVault;
use super::chat::{ChatState, Reaction};
use super::input::{key, KeyEvent};
use super::widgets::background::Background;
use super::widgets::border::Border;
use super::widgets::editor::EditorState;
@ -328,11 +329,7 @@ impl EuphRoom {
}
}
if !event.modifiers.is_empty() {
return false;
}
if let KeyCode::Char('n' | 'N') = event.code {
if let key!('n') | key!('N') = event {
self.state = State::ChooseNick(EditorState::with_initial_text(
joined.session.name.clone(),
));
@ -349,19 +346,19 @@ impl EuphRoom {
.handled()
}
State::ChooseNick(ed) => {
match event.code {
KeyCode::Esc => self.state = State::Normal,
KeyCode::Enter => {
match event {
key!(Esc) => self.state = State::Normal,
key!(Enter) => {
if let Some(room) = &self.room {
let _ = room.nick(ed.text());
}
self.state = State::Normal;
}
KeyCode::Backspace => ed.backspace(),
KeyCode::Left => ed.move_cursor_left(),
KeyCode::Right => ed.move_cursor_right(),
KeyCode::Delete => ed.delete(),
KeyCode::Char(ch) => ed.insert_char(ch),
key!(Char ch) => ed.insert_char(ch),
key!(Backspace) => ed.backspace(),
key!(Left) => ed.move_cursor_left(),
key!(Right) => ed.move_cursor_right(),
key!(Delete) => ed.delete(),
_ => return false,
}
true

View file

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