Compose messages via external editor

This commit is contained in:
Joscha 2022-06-17 20:50:12 +02:00
parent 9a351b5eb3
commit 36e1dbfa59
6 changed files with 236 additions and 17 deletions

View file

@ -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" }

View file

@ -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<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> {
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 {
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
}
}

View file

@ -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<M: Msg> {
// pub focus: Option<M::Id>,
@ -29,23 +33,31 @@ impl<M: Msg> TreeView<M> {
pub async fn handle_key_event<S: MsgStore<M>>(
&mut self,
l: &Arc<FairMutex<()>>,
s: &mut S,
c: &mut Option<Cursor<M::Id>>,
f: &mut Frame,
t: &mut Terminal,
z: Size,
event: KeyEvent,
) {
) -> Handled<M::Id> {
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<S: MsgStore<M>>(

View 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
}
}
}

View file

@ -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<FairMutex<()>>,
) -> 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
}