Compose messages via external editor
This commit is contained in:
parent
9a351b5eb3
commit
36e1dbfa59
6 changed files with 236 additions and 17 deletions
71
Cargo.lock
generated
71
Cargo.lock
generated
|
|
@ -188,6 +188,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
|
"edit",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -277,6 +278,22 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -302,6 +319,15 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
@ -498,6 +524,15 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -763,6 +798,15 @@ version = "0.6.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
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]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
|
@ -1005,6 +1049,20 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -1132,7 +1190,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toss"
|
name = "toss"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
|
|
@ -1331,6 +1389,17 @@ dependencies = [
|
||||||
"untrusted",
|
"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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ async-trait = "0.1.56"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
crossterm = "0.23.2"
|
crossterm = "0.23.2"
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
|
edit = "0.1.4"
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
rusqlite = "0.27.0"
|
rusqlite = "0.27.0"
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
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" }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
|
use parking_lot::FairMutex;
|
||||||
use toss::frame::{Frame, Pos, Size};
|
use toss::frame::{Frame, Pos, Size};
|
||||||
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore};
|
use crate::store::{Msg, MsgStore};
|
||||||
|
|
||||||
|
|
@ -50,12 +54,30 @@ impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Handled<I> {
|
||||||
|
Ok,
|
||||||
|
NewMessage { parent: Option<I>, content: String },
|
||||||
|
}
|
||||||
|
|
||||||
impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
|
impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
|
||||||
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<FairMutex<()>>,
|
||||||
|
) -> Handled<M::Id> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Tree => {
|
Mode::Tree => {
|
||||||
self.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
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod action;
|
||||||
mod blocks;
|
mod blocks;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod layout;
|
mod layout;
|
||||||
|
|
@ -5,13 +6,16 @@ mod render;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use parking_lot::FairMutex;
|
||||||
use toss::frame::{Frame, Pos, Size};
|
use toss::frame::{Frame, Pos, Size};
|
||||||
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore};
|
use crate::store::{Msg, MsgStore};
|
||||||
|
|
||||||
use super::Cursor;
|
use super::{Cursor, Handled};
|
||||||
|
|
||||||
pub struct TreeView<M: Msg> {
|
pub struct TreeView<M: Msg> {
|
||||||
// pub focus: Option<M::Id>,
|
// pub focus: Option<M::Id>,
|
||||||
|
|
@ -29,23 +33,31 @@ impl<M: Msg> TreeView<M> {
|
||||||
|
|
||||||
pub async fn handle_key_event<S: MsgStore<M>>(
|
pub async fn handle_key_event<S: MsgStore<M>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
l: &Arc<FairMutex<()>>,
|
||||||
s: &mut S,
|
s: &mut S,
|
||||||
c: &mut Option<Cursor<M::Id>>,
|
c: &mut Option<Cursor<M::Id>>,
|
||||||
f: &mut Frame,
|
t: &mut Terminal,
|
||||||
z: Size,
|
z: Size,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) {
|
) -> Handled<M::Id> {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, f, z).await,
|
// Cursor movement
|
||||||
KeyCode::Char('k') => self.move_up(s, c, f, z).await,
|
KeyCode::Char('k') => self.move_up(s, c, t.frame(), z).await,
|
||||||
KeyCode::Char('j') => self.move_down(s, c, f, z).await,
|
KeyCode::Char('j') => self.move_down(s, c, t.frame(), z).await,
|
||||||
KeyCode::Char('K') => self.move_up_sibling(s, c, f, z).await,
|
KeyCode::Char('K') => self.move_up_sibling(s, c, t.frame(), z).await,
|
||||||
KeyCode::Char('J') => self.move_down_sibling(s, c, f, z).await,
|
KeyCode::Char('J') => self.move_down_sibling(s, c, t.frame(), z).await,
|
||||||
KeyCode::Char('g') => self.move_to_first(s, c, f, z).await,
|
KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, t.frame(), z).await,
|
||||||
KeyCode::Char('G') => self.move_to_last(s, c, f, 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?
|
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<S: MsgStore<M>>(
|
pub async fn render<S: MsgStore<M>>(
|
||||||
|
|
|
||||||
110
cove-tui/src/chat/tree/action.rs
Normal file
110
cove-tui/src/chat/tree/action.rs
Normal file
|
|
@ -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<M: Msg> TreeView<M> {
|
||||||
|
fn prompt_msg(crossterm_lock: &Arc<FairMutex<()>>, terminal: &mut Terminal) -> Option<String> {
|
||||||
|
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<S: MsgStore<M>>(
|
||||||
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
|
store: &S,
|
||||||
|
cursor: &Option<Cursor<M::Id>>,
|
||||||
|
terminal: &mut Terminal,
|
||||||
|
) -> Handled<M::Id> {
|
||||||
|
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<S: MsgStore<M>>(
|
||||||
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
|
store: &S,
|
||||||
|
cursor: &Option<Cursor<M::Id>>,
|
||||||
|
terminal: &mut Terminal,
|
||||||
|
) -> Handled<M::Id> {
|
||||||
|
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<FairMutex<()>>,
|
||||||
|
terminal: &mut Terminal,
|
||||||
|
) -> Handled<M::Id> {
|
||||||
|
if let Some(content) = Self::prompt_msg(crossterm_lock, terminal) {
|
||||||
|
Handled::NewMessage {
|
||||||
|
parent: None,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Handled::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -117,7 +117,8 @@ 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.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::Mouse(event)) => self.handle_mouse_event(event).await?,
|
||||||
UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue,
|
UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue,
|
||||||
|
|
@ -143,15 +144,19 @@ impl Ui {
|
||||||
async fn handle_key_event(
|
async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
frame: &mut Frame,
|
terminal: &mut Terminal,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
) -> EventHandleResult {
|
) -> EventHandleResult {
|
||||||
let shift_q = event.code == KeyCode::Char('Q');
|
let shift_q = event.code == KeyCode::Char('Q');
|
||||||
let ctrl_c = event.modifiers == KeyModifiers::CONTROL && event.code == KeyCode::Char('c');
|
let ctrl_c = event.modifiers == KeyModifiers::CONTROL && event.code == KeyCode::Char('c');
|
||||||
if shift_q || ctrl_c {
|
if shift_q || ctrl_c {
|
||||||
return EventHandleResult::Stop;
|
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
|
EventHandleResult::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue