diff --git a/src/ui.rs b/src/ui.rs index d2de13d..b47b42b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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>, ) -> 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, _ => {} } diff --git a/src/ui/chat.rs b/src/ui/chat.rs index b841a02..da15108 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -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; /////////// diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index c24b787..549598f 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -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> InnerTreeViewState { coming_from: Option, parent: Option, ) -> Reaction { - 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> InnerTreeViewState { event: KeyEvent, id: Option, ) -> 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, diff --git a/src/ui/chat/tree/cursor.rs b/src/ui/chat/tree/cursor.rs index 79a0caf..7f0effc 100644 --- a/src/ui/chat/tree/cursor.rs +++ b/src/ui/chat/tree/cursor.rs @@ -18,6 +18,15 @@ pub enum Cursor { }, } +impl Cursor { + pub fn editor(coming_from: Option, parent: Option) -> Self { + Self::Editor { + coming_from, + parent, + } + } +} + impl Cursor { pub fn refers_to(&self, id: &I) -> bool { if let Self::Msg(own_id) = self { diff --git a/src/ui/input.rs b/src/ui/input.rs new file mode 100644 index 0000000..f01ad21 --- /dev/null +++ b/src/ui/input.rs @@ -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 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; diff --git a/src/ui/room.rs b/src/ui/room.rs index 0c1d3a5..a0a1a8b 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -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 diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs index 537f8ea..4860537 100644 --- a/src/ui/rooms.rs +++ b/src/ui/rooms.rs @@ -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(), _ => {} }, }