diff --git a/Cargo.lock b/Cargo.lock index b93a0a2..f4709b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ dependencies = [ "chrono", "crossterm", "directories", + "edit", "parking_lot", "rusqlite", "tokio", @@ -277,6 +278,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "edit" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c562aa71f7bc691fde4c6bf5f93ae5a5298b617c2eb44c76c87832299a17fbb4" +dependencies = [ + "tempfile", + "which", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "env_logger" version = "0.9.0" @@ -302,6 +319,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "fnv" version = "1.0.7" @@ -498,6 +524,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.2" @@ -763,6 +798,15 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1005,6 +1049,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1132,7 +1190,7 @@ dependencies = [ [[package]] name = "toss" version = "0.1.0" -source = "git+https://github.com/Garmelon/toss.git?rev=333cf74fba56080043a13b9f55c0b62695e2fa4a#333cf74fba56080043a13b9f55c0b62695e2fa4a" +source = "git+https://github.com/Garmelon/toss.git?rev=761519c1a7cdc950eab70fd6539c71bf22919a50#761519c1a7cdc950eab70fd6539c71bf22919a50" dependencies = [ "crossterm", "unicode-linebreak", @@ -1331,6 +1389,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "which" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index e890e66..531845a 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -9,7 +9,8 @@ async-trait = "0.1.56" chrono = "0.4.19" crossterm = "0.23.2" directories = "4.0.1" +edit = "0.1.4" parking_lot = "0.12.1" rusqlite = "0.27.0" tokio = { version = "1.19.2", features = ["full"] } -toss = { git = "https://github.com/Garmelon/toss.git", rev = "333cf74fba56080043a13b9f55c0b62695e2fa4a" } +toss = { git = "https://github.com/Garmelon/toss.git", rev = "761519c1a7cdc950eab70fd6539c71bf22919a50" } diff --git a/cove-tui/src/chat.rs b/cove-tui/src/chat.rs index 694650b..1885273 100644 --- a/cove-tui/src/chat.rs +++ b/cove-tui/src/chat.rs @@ -1,7 +1,11 @@ mod tree; +use std::sync::Arc; + use crossterm::event::KeyEvent; +use parking_lot::FairMutex; use toss::frame::{Frame, Pos, Size}; +use toss::terminal::Terminal; use crate::store::{Msg, MsgStore}; @@ -50,12 +54,30 @@ impl> Chat { } } +pub enum Handled { + Ok, + NewMessage { parent: Option, content: String }, +} + impl> Chat { - pub async fn handle_key_event(&mut self, event: KeyEvent, frame: &mut Frame, size: Size) { + pub async fn handle_key_event( + &mut self, + event: KeyEvent, + terminal: &mut Terminal, + size: Size, + crossterm_lock: &Arc>, + ) -> Handled { match self.mode { Mode::Tree => { self.tree - .handle_key_event(&mut self.store, &mut self.cursor, frame, size, event) + .handle_key_event( + crossterm_lock, + &mut self.store, + &mut self.cursor, + terminal, + size, + event, + ) .await } } diff --git a/cove-tui/src/chat/tree.rs b/cove-tui/src/chat/tree.rs index 1fff340..54c41a8 100644 --- a/cove-tui/src/chat/tree.rs +++ b/cove-tui/src/chat/tree.rs @@ -1,3 +1,4 @@ +mod action; mod blocks; mod cursor; mod layout; @@ -5,13 +6,16 @@ mod render; mod util; use std::marker::PhantomData; +use std::sync::Arc; use crossterm::event::{KeyCode, KeyEvent}; +use parking_lot::FairMutex; use toss::frame::{Frame, Pos, Size}; +use toss::terminal::Terminal; use crate::store::{Msg, MsgStore}; -use super::Cursor; +use super::{Cursor, Handled}; pub struct TreeView { // pub focus: Option, @@ -29,23 +33,31 @@ impl TreeView { pub async fn handle_key_event>( &mut self, + l: &Arc>, s: &mut S, c: &mut Option>, - f: &mut Frame, + t: &mut Terminal, z: Size, event: KeyEvent, - ) { + ) -> Handled { match event.code { - KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, f, z).await, - KeyCode::Char('k') => self.move_up(s, c, f, z).await, - KeyCode::Char('j') => self.move_down(s, c, f, z).await, - KeyCode::Char('K') => self.move_up_sibling(s, c, f, z).await, - KeyCode::Char('J') => self.move_down_sibling(s, c, f, z).await, - KeyCode::Char('g') => self.move_to_first(s, c, f, z).await, - KeyCode::Char('G') => self.move_to_last(s, c, f, z).await, + // Cursor movement + KeyCode::Char('k') => self.move_up(s, c, t.frame(), z).await, + KeyCode::Char('j') => self.move_down(s, c, t.frame(), z).await, + KeyCode::Char('K') => self.move_up_sibling(s, c, t.frame(), z).await, + KeyCode::Char('J') => self.move_down_sibling(s, c, t.frame(), z).await, + KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, t.frame(), z).await, + KeyCode::Char('g') => self.move_to_first(s, c, t.frame(), z).await, + KeyCode::Char('G') => self.move_to_last(s, c, t.frame(), z).await, KeyCode::Esc => *c = None, // TODO Make 'G' do the same thing? + // Writing messages + KeyCode::Char('r') => return Self::reply_normal(l, s, c, t).await, + KeyCode::Char('R') => return Self::reply_alternate(l, s, c, t).await, + KeyCode::Char('t') | KeyCode::Char('T') => return Self::create_new_thread(l, t).await, _ => {} } + + Handled::Ok } pub async fn render>( diff --git a/cove-tui/src/chat/tree/action.rs b/cove-tui/src/chat/tree/action.rs new file mode 100644 index 0000000..0dce400 --- /dev/null +++ b/cove-tui/src/chat/tree/action.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use parking_lot::FairMutex; +use toss::terminal::Terminal; + +use crate::chat::{Cursor, Handled}; +use crate::store::{Msg, MsgStore}; + +use super::TreeView; + +impl TreeView { + fn prompt_msg(crossterm_lock: &Arc>, terminal: &mut Terminal) -> Option { + let content = { + let _guard = crossterm_lock.lock(); + terminal.suspend().expect("could not suspend"); + let content = edit::edit("").expect("could not edit"); + terminal.unsuspend().expect("could not unsuspend"); + content + }; + + if content.trim().is_empty() { + None + } else { + Some(content) + } + } + + pub async fn reply_normal>( + crossterm_lock: &Arc>, + store: &S, + cursor: &Option>, + terminal: &mut Terminal, + ) -> Handled { + if let Some(cursor) = cursor { + let tree = store.tree(store.path(&cursor.id).await.first()).await; + let parent_id = if tree.next_sibling(&cursor.id).is_some() { + // A reply to a message that has further siblings should be a + // direct reply. An indirect reply might end up a lot further + // down in the current conversation. + cursor.id.clone() + } else if let Some(parent) = tree.parent(&cursor.id) { + // A reply to a message without further siblings should be an + // indirect reply so as not to create unnecessarily deep + // threads. In the case that our message has children, this + // might get a bit confusing. I'm not sure yet how well this + // "smart" reply actually works in practice. + parent + } else { + // When replying to a top-level message, it makes sense to avoid + // creating unnecessary new threads. + cursor.id.clone() + }; + + if let Some(content) = Self::prompt_msg(crossterm_lock, terminal) { + return Handled::NewMessage { + parent: Some(parent_id), + content, + }; + } + } + + Handled::Ok + } + + /// Does approximately the opposite of [`Self::reply_normal`]. + pub async fn reply_alternate>( + crossterm_lock: &Arc>, + store: &S, + cursor: &Option>, + terminal: &mut Terminal, + ) -> Handled { + if let Some(cursor) = cursor { + let tree = store.tree(store.path(&cursor.id).await.first()).await; + let parent_id = if tree.next_sibling(&cursor.id).is_none() { + // The opposite of replying normally + cursor.id.clone() + } else if let Some(parent) = tree.parent(&cursor.id) { + // The opposite of replying normally + parent + } else { + // The same as replying normally, still to avoid creating + // unnecessary new threads + cursor.id.clone() + }; + + if let Some(content) = Self::prompt_msg(crossterm_lock, terminal) { + return Handled::NewMessage { + parent: Some(parent_id), + content, + }; + } + } + + Handled::Ok + } + + pub async fn create_new_thread( + crossterm_lock: &Arc>, + terminal: &mut Terminal, + ) -> Handled { + if let Some(content) = Self::prompt_msg(crossterm_lock, terminal) { + Handled::NewMessage { + parent: None, + content, + } + } else { + Handled::Ok + } + } +} diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index 8b47591..221dd4a 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -117,7 +117,8 @@ impl Ui { let result = match event { UiEvent::Redraw => EventHandleResult::Continue, UiEvent::Term(Event::Key(event)) => { - self.handle_key_event(event, terminal.frame(), size).await + self.handle_key_event(event, terminal, size, &crossterm_lock) + .await } UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?, UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue, @@ -143,15 +144,19 @@ impl Ui { async fn handle_key_event( &mut self, event: KeyEvent, - frame: &mut Frame, + terminal: &mut Terminal, size: Size, + crossterm_lock: &Arc>, ) -> EventHandleResult { let shift_q = event.code == KeyCode::Char('Q'); let ctrl_c = event.modifiers == KeyModifiers::CONTROL && event.code == KeyCode::Char('c'); if shift_q || ctrl_c { return EventHandleResult::Stop; } - self.chat.handle_key_event(event, frame, size).await; + // TODO Perform resulting action + self.chat + .handle_key_event(event, terminal, size, crossterm_lock) + .await; EventHandleResult::Continue }