// TODO Focusing on sub-trees mod cursor; mod layout; mod tree_blocks; mod widgets; use std::collections::HashSet; use std::fmt; use std::sync::Arc; use async_trait::async_trait; use parking_lot::FairMutex; use tokio::sync::Mutex; use toss::{Frame, Pos, Size, Terminal, WidthDb}; use crate::macros::logging_unwrap; use crate::store::{Msg, MsgStore}; use crate::ui::input::{key, InputEvent, KeyBindingsList}; use crate::ui::util; use crate::ui::widgets::editor::EditorState; use crate::ui::widgets::Widget; use self::cursor::Cursor; use super::{ChatMsg, Reaction}; /////////// // State // /////////// enum Correction { MakeCursorVisible, MoveCursorToVisibleArea, CenterCursor, } struct InnerTreeViewState> { store: S, last_cursor: Cursor, last_cursor_line: i32, last_visible_msgs: Vec, cursor: Cursor, editor: EditorState, /// Scroll the view on the next render. Positive values scroll up and /// negative values scroll down. scroll: i32, correction: Option, folded: HashSet, } impl> InnerTreeViewState { fn new(store: S) -> Self { Self { store, last_cursor: Cursor::Bottom, last_cursor_line: 0, last_visible_msgs: vec![], cursor: Cursor::Bottom, editor: EditorState::new(), scroll: 0, correction: None, folded: HashSet::new(), } } pub fn list_movement_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.binding("j/k, ↓/↑", "move cursor up/down"); bindings.binding("J/K, ctrl+↓/↑", "move cursor to prev/next sibling"); bindings.binding("p/P", "move cursor to parent/root"); bindings.binding("h/l, ←/→", "move cursor chronologically"); bindings.binding("H/L, ctrl+←/→", "move cursor to prev/next unseen message"); bindings.binding("g, home", "move cursor to top"); bindings.binding("G, end", "move cursor to bottom"); bindings.binding("ctrl+y/e", "scroll up/down a line"); bindings.binding("ctrl+u/d", "scroll up/down half a screen"); bindings.binding("ctrl+b/f, page up/down", "scroll up/down one screen"); bindings.binding("z", "center cursor on screen"); // TODO Bindings inspired by vim's ()/[]/{} bindings? } async fn handle_movement_input_event( &mut self, frame: &mut Frame, event: &InputEvent, ) -> Result { let chat_height = frame.size().height - 3; match event { key!('k') | key!(Up) => self.move_cursor_up().await?, key!('j') | key!(Down) => self.move_cursor_down().await?, key!('K') | key!(Ctrl + Up) => self.move_cursor_up_sibling().await?, key!('J') | key!(Ctrl + Down) => self.move_cursor_down_sibling().await?, key!('p') => self.move_cursor_to_parent().await?, key!('P') => self.move_cursor_to_root().await?, key!('h') | key!(Left) => self.move_cursor_older().await?, key!('l') | key!(Right) => self.move_cursor_newer().await?, key!('H') | key!(Ctrl + Left) => self.move_cursor_older_unseen().await?, key!('L') | key!(Ctrl + Right) => self.move_cursor_newer_unseen().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') | key!(PageUp) => self.scroll_up(chat_height.saturating_sub(1).into()), key!(Ctrl + 'f') | key!(PageDown) => { self.scroll_down(chat_height.saturating_sub(1).into()) } key!('z') => self.center_cursor(), _ => return Ok(false), } Ok(true) } pub fn list_action_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.binding("space", "fold current message's subtree"); bindings.binding("s", "toggle current message's seen status"); bindings.binding("S", "mark all visible messages as seen"); bindings.binding("ctrl+s", "mark all older messages as seen"); } async fn handle_action_input_event( &mut self, event: &InputEvent, id: Option<&M::Id>, ) -> Result { match event { key!(' ') => { if let Some(id) = id { if !self.folded.remove(id) { self.folded.insert(id.clone()); } return Ok(true); } } key!('s') => { if let Some(id) = id { if let Some(msg) = self.store.tree(id).await?.msg(id) { self.store.set_seen(id, !msg.seen()).await?; } return Ok(true); } } key!('S') => { for id in &self.last_visible_msgs { self.store.set_seen(id, true).await?; } return Ok(true); } key!(Ctrl + 's') => { if let Some(id) = id { self.store.set_older_seen(id, true).await?; } else { self.store .set_older_seen(&M::last_possible_id(), true) .await?; } return Ok(true); } _ => {} } Ok(false) } pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.binding("r", "reply to message (inline if possible, else directly)"); bindings.binding("R", "reply to message (opposite of R)"); bindings.binding("t", "start a new thread"); } async fn handle_edit_initiating_input_event( &mut self, event: &InputEvent, id: Option, ) -> Result { match event { key!('r') => { if let Some(parent) = self.parent_for_normal_reply().await? { self.cursor = Cursor::editor(id, parent); self.correction = Some(Correction::MakeCursorVisible); } } key!('R') => { if let Some(parent) = self.parent_for_alternate_reply().await? { self.cursor = Cursor::editor(id, parent); self.correction = Some(Correction::MakeCursorVisible); } } key!('t') | key!('T') => { self.cursor = Cursor::editor(id, None); self.correction = Some(Correction::MakeCursorVisible); } _ => return Ok(false), } Ok(true) } pub fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { self.list_movement_key_bindings(bindings); bindings.empty(); self.list_action_key_bindings(bindings); if can_compose { bindings.empty(); self.list_edit_initiating_key_bindings(bindings); } } async fn handle_normal_input_event( &mut self, frame: &mut Frame, event: &InputEvent, can_compose: bool, id: Option, ) -> Result { #[allow(clippy::if_same_then_else)] Ok(if self.handle_movement_input_event(frame, event).await? { true } else if self.handle_action_input_event(event, id.as_ref()).await? { true } else if can_compose { self.handle_edit_initiating_input_event(event, id).await? } else { false }) } fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) { bindings.binding("esc", "close editor"); bindings.binding("enter", "send message"); util::list_editor_key_bindings_allowing_external_editing(bindings, |_| true); } fn handle_editor_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: &InputEvent, coming_from: Option, parent: Option, ) -> Reaction { // TODO Tab-completion match event { key!(Esc) => { self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom); self.correction = Some(Correction::MakeCursorVisible); return Reaction::Handled; } key!(Enter) => { let content = self.editor.text(); if !content.trim().is_empty() { self.cursor = Cursor::Pseudo { coming_from, parent: parent.clone(), }; return Reaction::Composed { parent, content }; } } _ => { let handled = util::handle_editor_input_event_allowing_external_editing( &self.editor, terminal, crossterm_lock, event, |_| true, ); match handled { Ok(true) => {} Ok(false) => return Reaction::NotHandled, Err(e) => return Reaction::ComposeError(e), } } } self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { bindings.heading("Chat"); match &self.cursor { Cursor::Bottom | Cursor::Msg(_) => { self.list_normal_key_bindings(bindings, can_compose); } Cursor::Editor { .. } => self.list_editor_key_bindings(bindings), Cursor::Pseudo { .. } => { self.list_normal_key_bindings(bindings, false); } } } async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: &InputEvent, can_compose: bool, ) -> Result, S::Error> { Ok(match &self.cursor { Cursor::Bottom => { if self .handle_normal_input_event(terminal.frame(), event, can_compose, None) .await? { Reaction::Handled } else { Reaction::NotHandled } } Cursor::Msg(id) => { let id = id.clone(); if self .handle_normal_input_event(terminal.frame(), event, can_compose, Some(id)) .await? { Reaction::Handled } else { Reaction::NotHandled } } Cursor::Editor { coming_from, parent, } => self.handle_editor_input_event( terminal, crossterm_lock, event, coming_from.clone(), parent.clone(), ), Cursor::Pseudo { .. } => { if self .handle_movement_input_event(terminal.frame(), event) .await? { Reaction::Handled } else { Reaction::NotHandled } } }) } fn cursor(&self) -> Option { match &self.cursor { Cursor::Msg(id) => Some(id.clone()), Cursor::Bottom | Cursor::Editor { .. } | Cursor::Pseudo { .. } => None, } } fn sent(&mut self, id: Option) { if let Cursor::Pseudo { coming_from, .. } = &self.cursor { if let Some(id) = id { self.last_cursor = Cursor::Msg(id.clone()); self.cursor = Cursor::Msg(id); self.editor.clear(); } else { self.cursor = match coming_from { Some(id) => Cursor::Msg(id.clone()), None => Cursor::Bottom, }; }; } } } pub struct TreeViewState>(Arc>>); impl> TreeViewState { pub fn new(store: S) -> Self { Self(Arc::new(Mutex::new(InnerTreeViewState::new(store)))) } pub fn widget(&self, nick: String, focused: bool) -> TreeView { TreeView { inner: self.0.clone(), nick, focused, } } pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { self.0.lock().await.list_key_bindings(bindings, can_compose); } pub async fn handle_input_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: &InputEvent, can_compose: bool, ) -> Result, S::Error> { self.0 .lock() .await .handle_input_event(terminal, crossterm_lock, event, can_compose) .await } pub async fn cursor(&self) -> Option { self.0.lock().await.cursor() } pub async fn sent(&mut self, id: Option) { self.0.lock().await.sent(id) } } //////////// // Widget // //////////// pub struct TreeView> { inner: Arc>>, nick: String, focused: bool, } #[async_trait] impl Widget for TreeView where M: Msg + ChatMsg + Send + Sync, M::Id: Send + Sync, S: MsgStore + Send + Sync, S::Error: fmt::Display, { async fn size( &self, _widthdb: &mut WidthDb, _max_width: Option, _max_height: Option, ) -> Size { Size::ZERO } async fn render(self: Box, frame: &mut Frame) { let mut guard = self.inner.lock().await; let blocks = logging_unwrap!(guard.relayout(self.nick, self.focused, frame).await); let size = frame.size(); for block in blocks.into_blocks().blocks { frame.push( Pos::new(0, block.top_line), Size::new(size.width, block.height as u16), ); block.widget.render(frame).await; frame.pop(); } } }