diff --git a/src/euph.rs b/src/euph.rs index 30d39ee..23d3f04 100644 --- a/src/euph.rs +++ b/src/euph.rs @@ -1,8 +1,10 @@ pub mod api; mod conn; +mod message; mod room; mod util; pub use conn::{Joined, Joining, Status}; +pub use message::Message; pub use room::Room; pub use util::{hue, nick_color, nick_style}; diff --git a/src/euph/message.rs b/src/euph/message.rs new file mode 100644 index 0000000..b383d1d --- /dev/null +++ b/src/euph/message.rs @@ -0,0 +1,62 @@ +use crossterm::style::{ContentStyle, Stylize}; +use time::OffsetDateTime; +use toss::styled::Styled; + +use crate::store::Msg; +use crate::ui::ChatMsg; + +use super::api::{Snowflake, Time}; +use super::util; + +#[derive(Debug, Clone)] +pub struct Message { + pub id: Snowflake, + pub parent: Option, + pub time: Time, + pub nick: String, + pub content: String, +} + +fn styled_nick(nick: &str) -> Styled { + Styled::new_plain("[") + .then(nick, util::nick_style(nick)) + .then_plain("]") +} + +fn styled_content(content: &str) -> Styled { + Styled::new_plain(content.trim()) +} + +impl Msg for Message { + type Id = Snowflake; + + fn id(&self) -> Self::Id { + self.id + } + + fn parent(&self) -> Option { + self.parent + } + + fn last_possible_id() -> Self::Id { + Snowflake::MAX + } +} + +impl ChatMsg for Message { + fn time(&self) -> OffsetDateTime { + self.time.0 + } + + fn styled(&self) -> (Styled, Styled) { + Self::pseudo(&self.nick, &self.content) + } + + fn edit(nick: &str, content: &str) -> (Styled, Styled) { + (styled_nick(nick), styled_content(content)) + } + + fn pseudo(nick: &str, content: &str) -> (Styled, Styled) { + (styled_nick(nick), styled_content(content)) + } +} diff --git a/src/export.rs b/src/export.rs index 8dfb8b6..3351450 100644 --- a/src/export.rs +++ b/src/export.rs @@ -8,9 +8,10 @@ use time::format_description::FormatItem; use time::macros::format_description; use unicode_width::UnicodeWidthStr; +use crate::euph; use crate::euph::api::Snowflake; use crate::store::{MsgStore, Tree}; -use crate::vault::{EuphMsg, Vault}; +use crate::vault::Vault; const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); @@ -43,7 +44,7 @@ pub async fn export(vault: &Vault, room: String, file: &Path) -> anyhow::Result< fn write_tree( file: &mut BufWriter, - tree: &Tree, + tree: &Tree, id: Snowflake, indent: usize, ) -> anyhow::Result<()> { @@ -64,7 +65,11 @@ fn write_tree( Ok(()) } -fn write_msg(file: &mut BufWriter, indent_string: &str, msg: &EuphMsg) -> anyhow::Result<()> { +fn write_msg( + file: &mut BufWriter, + indent_string: &str, + msg: &euph::Message, +) -> anyhow::Result<()> { let nick = &msg.nick; let nick_empty = " ".repeat(nick.width()); diff --git a/src/logger.rs b/src/logger.rs index 0aca9a4..bb68df7 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -10,6 +10,7 @@ use tokio::sync::mpsc; use toss::styled::Styled; use crate::store::{Msg, MsgStore, Path, Tree}; +use crate::ui::ChatMsg; #[derive(Debug, Clone)] pub struct LogMsg { @@ -30,28 +31,35 @@ impl Msg for LogMsg { None } + fn last_possible_id() -> Self::Id { + Self::Id::MAX + } +} + +impl ChatMsg for LogMsg { fn time(&self) -> OffsetDateTime { self.time } - fn nick(&self) -> Styled { - let style = match self.level { + fn styled(&self) -> (Styled, Styled) { + let nick_style = match self.level { Level::Error => ContentStyle::default().bold().red(), Level::Warn => ContentStyle::default().bold().yellow(), Level::Info => ContentStyle::default().bold().green(), Level::Debug => ContentStyle::default().bold().blue(), Level::Trace => ContentStyle::default().bold().magenta(), }; - let text = format!("{}", self.level); - Styled::new(text, style) + let nick = Styled::new(format!("{}", self.level), nick_style); + let content = Styled::new_plain(&self.content); + (nick, content) } - fn content(&self) -> Styled { - Styled::new_plain(&self.content) + fn edit(nick: &str, content: &str) -> (Styled, Styled) { + panic!("log is not editable") } - fn last_possible_id() -> Self::Id { - Self::Id::MAX + fn pseudo(nick: &str, content: &str) -> (Styled, Styled) { + panic!("log is not editable") } } diff --git a/src/store.rs b/src/store.rs index ceca76a..e31c526 100644 --- a/src/store.rs +++ b/src/store.rs @@ -4,18 +4,12 @@ use std::hash::Hash; use std::vec; use async_trait::async_trait; -use time::OffsetDateTime; -use toss::styled::Styled; pub trait Msg { type Id: Clone + Debug + Hash + Eq + Ord; fn id(&self) -> Self::Id; fn parent(&self) -> Option; - fn time(&self) -> OffsetDateTime; - fn nick(&self) -> Styled; - fn content(&self) -> Styled; - fn last_possible_id() -> Self::Id; } diff --git a/src/ui.rs b/src/ui.rs index fef6b3f..36e4413 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,6 +18,7 @@ use toss::terminal::Terminal; use crate::logger::{LogMsg, Logger}; use crate::vault::Vault; +pub use self::chat::ChatMsg; use self::chat::ChatState; use self::rooms::Rooms; use self::widgets::BoxedWidget; diff --git a/src/ui/chat.rs b/src/ui/chat.rs index 73acb30..9a5ecb3 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -6,7 +6,9 @@ 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}; +use toss::styled::Styled; use toss::terminal::Terminal; use crate::store::{Msg, MsgStore}; @@ -15,6 +17,17 @@ use self::tree::{TreeView, TreeViewState}; use super::widgets::Widget; +/////////// +// Trait // +/////////// + +pub trait ChatMsg { + fn time(&self) -> OffsetDateTime; + fn styled(&self) -> (Styled, Styled); + fn edit(nick: &str, content: &str) -> (Styled, Styled); + fn pseudo(nick: &str, content: &str) -> (Styled, Styled); +} + /////////// // State // /////////// @@ -87,7 +100,7 @@ pub enum Chat> { #[async_trait] impl Widget for Chat where - M: Msg, + M: Msg + ChatMsg, M::Id: Send + Sync, S: MsgStore + Send + Sync, { diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index b36cc85..b2916d5 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -18,6 +18,8 @@ use crate::ui::widgets::Widget; use self::cursor::Cursor; +use super::ChatMsg; + /////////// // State // /////////// @@ -115,7 +117,7 @@ pub struct TreeView>(Arc>> #[async_trait] impl Widget for TreeView where - M: Msg, + M: Msg + ChatMsg, M::Id: Send + Sync, S: MsgStore + Send + Sync, { diff --git a/src/ui/chat/tree/layout.rs b/src/ui/chat/tree/layout.rs index efd3f8a..3ed353d 100644 --- a/src/ui/chat/tree/layout.rs +++ b/src/ui/chat/tree/layout.rs @@ -4,11 +4,12 @@ use crate::store::{Msg, MsgStore, Path, Tree}; use crate::ui::chat::blocks::Block; use crate::ui::widgets::empty::Empty; use crate::ui::widgets::text::Text; +use crate::ui::ChatMsg; use super::tree_blocks::{BlockId, Root, TreeBlocks}; use super::{widgets, Cursor, InnerTreeViewState}; -impl> InnerTreeViewState { +impl> InnerTreeViewState { async fn cursor_path(&self, cursor: &Cursor) -> Path { match cursor { Cursor::Msg(id) => self.store.path(id).await, diff --git a/src/ui/chat/tree/widgets.rs b/src/ui/chat/tree/widgets.rs index ce08dbe..b509798 100644 --- a/src/ui/chat/tree/widgets.rs +++ b/src/ui/chat/tree/widgets.rs @@ -5,6 +5,7 @@ mod time; use crossterm::style::{ContentStyle, Stylize}; +use super::super::ChatMsg; use crate::store::Msg; use crate::ui::widgets::join::{HJoin, Segment}; use crate::ui::widgets::padding::Padding; @@ -19,7 +20,8 @@ pub fn style_placeholder() -> ContentStyle { ContentStyle::default().dark_grey() } -pub fn msg(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget { +pub fn msg(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget { + let (nick, content) = msg.styled(); HJoin::new(vec![ Segment::new( Padding::new(time::widget(Some(msg.time()), highlighted)) @@ -27,10 +29,10 @@ pub fn msg(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget { .right(1), ), Segment::new(Indent::new(indent, highlighted)), - Segment::new(Padding::new(Text::new(msg.nick())).right(1)), + Segment::new(Padding::new(Text::new(nick)).right(1)), // TODO Minimum content width // TODO Minimizing and maximizing messages - Segment::new(Text::new(msg.content()).wrap(true)).priority(1), + Segment::new(Text::new(content).wrap(true)).priority(1), ]) .into() } diff --git a/src/ui/room.rs b/src/ui/room.rs index 0a3d519..889df0b 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -10,7 +10,7 @@ use toss::terminal::Terminal; use crate::euph::api::{SessionType, SessionView}; use crate::euph::{self, Joined, Status}; -use crate::vault::{EuphMsg, EuphVault}; +use crate::vault::EuphVault; use super::chat::ChatState; use super::widgets::background::Background; @@ -37,7 +37,7 @@ pub struct EuphRoom { state: State, room: Option, - chat: ChatState, + chat: ChatState, nick_list: ListState, } diff --git a/src/vault.rs b/src/vault.rs index a97e6c9..037be83 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -9,7 +9,7 @@ use rusqlite::Connection; use tokio::sync::{mpsc, oneshot}; use self::euph::EuphRequest; -pub use self::euph::{EuphMsg, EuphVault}; +pub use self::euph::EuphVault; enum Request { Close(oneshot::Sender<()>), diff --git a/src/vault/euph.rs b/src/vault/euph.rs index 3a84cf9..c5b479b 100644 --- a/src/vault/euph.rs +++ b/src/vault/euph.rs @@ -7,11 +7,11 @@ use rusqlite::types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef}; use rusqlite::{named_params, params, Connection, OptionalExtension, ToSql, Transaction}; use time::OffsetDateTime; use tokio::sync::oneshot; -use toss::styled::Styled; use crate::euph; use crate::euph::api::{Message, Snowflake, Time}; use crate::store::{Msg, MsgStore, Path, Tree}; +use crate::ui::ChatMsg; use super::{Request, Vault}; @@ -43,43 +43,6 @@ impl FromSql for Time { } } -#[derive(Debug, Clone)] -pub struct EuphMsg { - pub id: Snowflake, - pub parent: Option, - pub time: Time, - pub nick: String, - pub content: String, -} - -impl Msg for EuphMsg { - type Id = Snowflake; - - fn id(&self) -> Self::Id { - self.id - } - - fn parent(&self) -> Option { - self.parent - } - - fn time(&self) -> OffsetDateTime { - self.time.0 - } - - fn nick(&self) -> Styled { - (&self.nick, euph::nick_style(&self.nick)).into() - } - - fn content(&self) -> Styled { - self.content.trim().into() - } - - fn last_possible_id() -> Self::Id { - Snowflake::MAX - } -} - impl From for Request { fn from(r: EuphRequest) -> Self { Self::Euph(r) @@ -168,7 +131,7 @@ impl EuphVault { } #[async_trait] -impl MsgStore for EuphVault { +impl MsgStore for EuphVault { async fn path(&self, id: &Snowflake) -> Path { // TODO vault::Error let (tx, rx) = oneshot::channel(); @@ -181,7 +144,7 @@ impl MsgStore for EuphVault { rx.await.unwrap() } - async fn tree(&self, tree_id: &Snowflake) -> Tree { + async fn tree(&self, tree_id: &Snowflake) -> Tree { // TODO vault::Error let (tx, rx) = oneshot::channel(); let request = EuphRequest::GetTree { @@ -290,7 +253,7 @@ pub(super) enum EuphRequest { GetTree { room: String, root: Snowflake, - result: oneshot::Sender>, + result: oneshot::Sender>, }, GetPrevTreeId { room: String, @@ -681,7 +644,7 @@ impl EuphRequest { conn: &Connection, room: String, root: Snowflake, - result: oneshot::Sender>, + result: oneshot::Sender>, ) -> rusqlite::Result<()> { let msgs = conn .prepare( @@ -703,7 +666,7 @@ impl EuphRequest { ", )? .query_map(params![room, root], |row| { - Ok(EuphMsg { + Ok(euph::Message { id: row.get(0)?, parent: row.get(1)?, time: row.get(2)?,