Properly format day delimiter

This commit is contained in:
2023-11-05 17:11:32 +01:00
parent 06e3228bbc
commit 1a34a4ce5e
7 changed files with 171 additions and 76 deletions

View File

@ -39,6 +39,7 @@ color-eyre = "0.6.2"
human-panic = "1.2.0"
# Internal
enum_dispatch = "0.3.12"
inventory = "0.3"
itertools = "0.11.0"
lazy_static = "1.4.0"
@ -60,6 +61,7 @@ matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev
# UI
ansi-to-tui = "3.1.0"
chrono = "0.4.31"
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
strip-ansi-escapes = "0.2.0"

View File

@ -24,7 +24,7 @@ use tokio::sync::mpsc::UnboundedReceiver;
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use super::{Buffer, BufferItem};
use super::{Buffer, BufferItem, BufferItemContent};
use crate::widgets::Prerender;
/// Maximum number of log lines to be stored in memory
@ -71,10 +71,10 @@ impl Buffer for LogBuffer {
.chain(slice2.into_iter())
.rev()
.map(|(line, prerender)| BufferItem {
text: line.clone().into_text().unwrap_or_else(|e| {
content: BufferItemContent::Text(line.clone().into_text().unwrap_or_else(|e| {
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
Text::raw(line)
}),
})),
prerender,
}),
)

View File

@ -26,8 +26,15 @@ pub use log::LogBuffer;
mod room;
pub use room::RoomBuffer;
#[derive(Debug, Clone)]
pub enum BufferItemContent<'buf> {
Text(Text<'buf>),
Divider(Text<'buf>),
Empty,
}
pub struct BufferItem<'buf> {
pub text: Text<'buf>,
pub content: BufferItemContent<'buf>,
pub prerender: &'buf Prerender,
}

View File

@ -17,6 +17,7 @@
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::{Arc, OnceLock};
use chrono::{offset::Local, DateTime};
use color_eyre::eyre::{eyre, Result};
use eyeball_im::VectorDiff;
use futures::{FutureExt, Stream, StreamExt};
@ -32,13 +33,28 @@ use matrix_sdk_ui::timeline::{
use ratatui::text::Text;
use smallvec::SmallVec;
use super::{Buffer, BufferItem};
use super::{Buffer, BufferItem, BufferItemContent};
use crate::widgets::Prerender;
/// Like [`BufferItemContent`] but owned.
#[derive(Debug, Clone)]
pub enum OwnedBufferItemContent {
Text(String),
Divider(String),
Empty,
}
/// Like `format!()` but returns OwnedBufferItemContent::Text
macro_rules! text {
($($tokens:tt)*) => {
OwnedBufferItemContent::Text(format!($($tokens)*))
}
}
pub struct SingleClientRoomBuffer {
room_id: OwnedRoomId,
client: Client,
items: imbl::vector::Vector<(String, Prerender)>,
items: imbl::vector::Vector<(OwnedBufferItemContent, Prerender)>,
// TODO: get rid of this trait object, we know it's matrix_sdk_ui::timeline::TimelineStream
stream: Box<dyn Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + Send + Sync + Unpin>,
timeline: Arc<Timeline>,
@ -61,7 +77,7 @@ impl SingleClientRoomBuffer {
}
}
fn format_timeline_item(&self, tl_item: impl AsRef<TimelineItem>) -> String {
fn format_timeline_item(&self, tl_item: impl AsRef<TimelineItem>) -> OwnedBufferItemContent {
match tl_item.as_ref().kind() {
TimelineItemKind::Event(event) => {
use matrix_sdk_ui::timeline::TimelineItemContent::*;
@ -71,54 +87,54 @@ impl SingleClientRoomBuffer {
.strip_prefix("@")
.expect("missing @ prefix");
match event.content() {
Message(message) => format!(" <{}> {}", sender, message.body().replace("\n", "\n ")),
RedactedMessage => format!("xx <{}> [redacted]", sender),
Sticker(sticker) => format!("st <{}> {}", sender, sticker.content().body),
UnableToDecrypt(_) => format!("xx <{}> [unable to decrypt]", sender),
Message(message) => text!(" <{}> {}", sender, message.body().replace("\n", "\n ")),
RedactedMessage => text!("xx <{}> [redacted]", sender),
Sticker(sticker) => text!("st <{}> {}", sender, sticker.content().body),
UnableToDecrypt(_) => text!("xx <{}> [unable to decrypt]", sender),
MembershipChange(change) => {
use matrix_sdk_ui::timeline::MembershipChange::*;
if change.user_id() == event.sender() {
let Some(change_kind) = change.change() else {
return format!("--- {} made incomprehensible changes to themselves", sender);
return text!("--- {} made incomprehensible changes to themselves", sender);
};
match change_kind {
None => format!("--- {} made no discernable changes to themselves", sender),
Error => format!(
None => text!("--- {} made no discernable changes to themselves", sender),
Error => text!(
"xxx {} made a change to themselves that made matrix-sdk-ui error",
sender
),
Joined => format!("--> {} joined", sender),
Left => format!("<-- {} left", sender),
Banned => format!("-x- {} banned themselves", sender),
Unbanned => format!("-x- {} unbanned themselves", sender),
Kicked => format!("<!- {} kicked themselves", sender),
Invited => format!("-o- {} invited themselves", sender),
KickedAndBanned => format!("<!x {} kicked and banned themselves", sender),
InvitationAccepted => format!("-o> {} accepted an invite", sender),
InvitationRejected => format!("-ox {} rejected an invite", sender),
InvitationRevoked => format!("--x {} revoked an invite", sender),
Knocked => format!("-?> {} knocked", sender),
KnockAccepted => format!("-?o {} accepted a knock", sender),
KnockRetracted => format!("-?x {} retracted a knock", sender),
KnockDenied => format!("-?x {} denied a knock", sender),
NotImplemented => format!(
Joined => text!("--> {} joined", sender),
Left => text!("<-- {} left", sender),
Banned => text!("-x- {} banned themselves", sender),
Unbanned => text!("-x- {} unbanned themselves", sender),
Kicked => text!("<!- {} kicked themselves", sender),
Invited => text!("-o- {} invited themselves", sender),
KickedAndBanned => text!("<!x {} kicked and banned themselves", sender),
InvitationAccepted => text!("-o> {} accepted an invite", sender),
InvitationRejected => text!("-ox {} rejected an invite", sender),
InvitationRevoked => text!("--x {} revoked an invite", sender),
Knocked => text!("-?> {} knocked", sender),
KnockAccepted => text!("-?o {} accepted a knock", sender),
KnockRetracted => text!("-?x {} retracted a knock", sender),
KnockDenied => text!("-?x {} denied a knock", sender),
NotImplemented => text!(
"xxx {} made a change matrix-sdk-ui does not support yet",
sender
),
}
} else if change.user_id() == "" {
let Some(change_kind) = change.change() else {
return format!("--- {} made incomprehensible changes", sender);
return text!("--- {} made incomprehensible changes", sender);
};
match change_kind {
None => format!("--- {} made no discernable changes", sender),
Error => format!("xxx {} made a change that made matrix-sdk-ui error", sender),
None => text!("--- {} made no discernable changes", sender),
Error => text!("xxx {} made a change that made matrix-sdk-ui error", sender),
Joined | Left | Banned | Unbanned | Kicked | Invited | KickedAndBanned
| InvitationAccepted | InvitationRejected | InvitationRevoked | Knocked
| KnockAccepted | KnockRetracted | KnockDenied => {
format!("--> {} made a non-sensical change: {:?}", sender, change)
text!("--> {} made a non-sensical change: {:?}", sender, change)
},
NotImplemented => format!(
NotImplemented => text!(
"xxx {} made a change matrix-sdk-ui does not support yet",
sender
),
@ -130,44 +146,48 @@ impl SingleClientRoomBuffer {
.strip_prefix("@")
.expect("missing @ prefix");
let Some(change_kind) = change.change() else {
return format!("--- {} made incomprehensible changes to {}", sender, target);
return text!("--- {} made incomprehensible changes to {}", sender, target);
};
match change_kind {
None => format!("--- {} made no discernable changes to {}", sender, target),
Error => format!(
None => text!("--- {} made no discernable changes to {}", sender, target),
Error => text!(
"xxx {} made a change to {} that made matrix-sdk-ui error",
sender, target
sender,
target
),
Joined | Left => format!(
Joined | Left => text!(
"--> {} made a non-sensical change to {}: {:?}",
sender, target, change
sender,
target,
change
),
Banned => format!("-x- {} banned {}", sender, target),
Unbanned => format!("-x- {} unbanned {}", sender, target),
Kicked => format!("<!- {} kicked {}", sender, target),
Invited => format!("-o- {} invited {}", sender, target),
KickedAndBanned => format!("<!x {} kicked and banned {}", sender, target),
InvitationAccepted => format!("-o> {} accepted an invite to {}", sender, target),
InvitationRejected => format!("-ox {} rejected an invite to {}", sender, target),
InvitationRevoked => format!("--x {} revoked an invite to {}", sender, target),
Knocked => format!("-?> {} made {} knock", sender, target),
KnockAccepted => format!("-?o {} accepted {}'s knock", sender, target),
KnockRetracted => format!("-?x {} retracted {}'s knock", sender, target),
KnockDenied => format!("-?x {} denied {}'s knock", sender, target),
NotImplemented => format!(
Banned => text!("-x- {} banned {}", sender, target),
Unbanned => text!("-x- {} unbanned {}", sender, target),
Kicked => text!("<!- {} kicked {}", sender, target),
Invited => text!("-o- {} invited {}", sender, target),
KickedAndBanned => text!("<!x {} kicked and banned {}", sender, target),
InvitationAccepted => text!("-o> {} accepted an invite to {}", sender, target),
InvitationRejected => text!("-ox {} rejected an invite to {}", sender, target),
InvitationRevoked => text!("--x {} revoked an invite to {}", sender, target),
Knocked => text!("-?> {} made {} knock", sender, target),
KnockAccepted => text!("-?o {} accepted {}'s knock", sender, target),
KnockRetracted => text!("-?x {} retracted {}'s knock", sender, target),
KnockDenied => text!("-?x {} denied {}'s knock", sender, target),
NotImplemented => text!(
"xxx {} made a change to {} that matrix-sdk-ui does not support yet",
sender, target
sender,
target
),
}
}
},
ProfileChange(_) => format!("--- {} updated their profile", sender),
ProfileChange(_) => text!("--- {} updated their profile", sender),
OtherState(state) => {
if state.state_key() == "" {
format!("--- {} changed the room: {:?}", sender, state.content())
text!("--- {} changed the room: {:?}", sender, state.content())
} else {
format!(
text!(
"--- {} changed {}: {:?}",
sender,
state.state_key(),
@ -175,28 +195,42 @@ impl SingleClientRoomBuffer {
)
}
},
FailedToParseMessageLike { event_type, error } => format!(
FailedToParseMessageLike { event_type, error } => text!(
"xxx {} sent a {} message that made matrix-sdk-ui error: {:?}",
sender, event_type, error
sender,
event_type,
error
),
FailedToParseState {
event_type,
state_key,
error,
} => {
format!(
text!(
"xxx {} made a {} change to {} that made matrix-sdk-ui error: {:?}",
sender, event_type, state_key, error
sender,
event_type,
state_key,
error
)
},
Poll(_) => format!("-?- {} acted on a poll", sender),
Poll(_) => text!("-?- {} acted on a poll", sender),
}
},
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => {
format!("---- read marker ----")
OwnedBufferItemContent::Divider(format!("read marker"))
},
TimelineItemKind::Virtual(VirtualTimelineItem::DayDivider(day_divider)) => {
format!("---- day divider: {:?} ----", day_divider)
match day_divider.to_system_time() {
Some(system_time) => {
let datetime: DateTime<Local> = system_time.into();
OwnedBufferItemContent::Divider(format!("{}", datetime.format("%Y-%m-%d")))
},
None => {
tracing::warn!("Could not convert {:?} to system time", day_divider);
OwnedBufferItemContent::Empty
},
}
},
}
}
@ -279,7 +313,7 @@ impl RoomBuffer {
timeline: Arc::new(timeline),
items: items // FIXME: it's always empty. why?
.into_iter()
.map(|item| (format!("Initial item: {:#?}", item), Prerender::new()))
.map(|item| (text!("Initial item: {:#?}", item), Prerender::new()))
.collect(),
stream: Box::new(stream),
back_pagination_request: AtomicU16::new(0),
@ -326,7 +360,11 @@ impl Buffer for RoomBuffer {
.iter()
.rev()
.map(|(line, prerender)| BufferItem {
text: Text::raw(line),
content: match line {
OwnedBufferItemContent::Text(text) => BufferItemContent::Text(Text::raw(text)),
OwnedBufferItemContent::Divider(text) => BufferItemContent::Divider(Text::raw(text)),
OwnedBufferItemContent::Empty => BufferItemContent::Empty,
},
prerender,
}),
)

View File

@ -18,14 +18,17 @@ use std::ops::DerefMut;
use color_eyre::eyre::{Result, WrapErr};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use enum_dispatch::enum_dispatch;
use ratatui::{prelude::*, widgets::*};
use crate::components::Action;
use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
use crate::widgets::{BottomAlignedParagraph, OverlappableWidget};
use crate::widgets::{
BacklogItemWidget, BottomAlignedParagraph, Divider, EmptyWidget, OverlappableWidget,
};
use super::Component;
use crate::buffers::BufferItem;
use crate::buffers::{BufferItem, BufferItemContent};
#[derive(Default)]
pub struct Backlog {
@ -83,6 +86,21 @@ impl Backlog {
pub fn scroll_down(&mut self, lines: u64) {
self.scroll = self.scroll.saturating_sub(20);
}
fn build_widget<'a>(&self, content: BufferItemContent<'a>, scroll: u64) -> BacklogItemWidget<'a> {
match content {
BufferItemContent::Text(text) => BottomAlignedParagraph::new(text).scroll(scroll).into(),
BufferItemContent::Divider(text) => {
if scroll == 0 {
Divider::new(Paragraph::new(text).alignment(Alignment::Center)).into()
} else {
EmptyWidget.into()
}
},
BufferItemContent::Empty => EmptyWidget.into(),
}
}
/// Returns how many lines are unused at the top of the area.
pub fn draw_items<'a>(
&mut self,
@ -111,7 +129,7 @@ impl Backlog {
value: PrerenderValue::NotRendered(height),
}) if *key == text_area.width => *height,
prerender => {
let widget = BottomAlignedParagraph::new(item.text.clone());
let widget = self.build_widget(item.content.clone(), 0);
let expected_height = widget.height(text_area.width);
*prerender = Some(PrerenderInner {
key: text_area.width,
@ -128,7 +146,7 @@ impl Backlog {
}
// TODO: cache this
let widget = BottomAlignedParagraph::new(item.text).scroll(scroll);
let widget = self.build_widget(item.content, scroll);
let (_, height) = widget.render_overlap(text_area, frame_buffer);
text_area.height = text_area.height.saturating_sub(height);
@ -173,7 +191,7 @@ impl Backlog {
buf.area().height
},
prerender => {
let widget = BottomAlignedParagraph::new(item.text);
let widget = self.build_widget(item.content, 0);
let (drawn_width, height) = widget.render_overlap(text_area, frame_buffer);
assert!(drawn_width <= text_area.width);

View File

@ -75,14 +75,13 @@ impl<'a> BottomAlignedParagraph<'a> {
.take(u16::MAX as usize) // Avoid overflows in ratatui for excessively long messages
.collect()
}
/// Returns how many lines it would actuall draw if rendered with the given height
pub fn height(&self, width: u16) -> u64 {
self.wrap_lines(width).len() as u64
}
}
impl<'a> OverlappableWidget for BottomAlignedParagraph<'a> {
fn height(&self, width: u16) -> u64 {
self.wrap_lines(width).len() as u64
}
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> (u16, u16) {
// Inspired by https://github.com/ratatui-org/ratatui/blob/9f371000968044e09545d66068c4ed4ea4b35d8a/src/widgets/paragraph.rs#L214-L275
let lines = self.wrap_lines(area.width);

View File

@ -14,6 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use enum_dispatch::enum_dispatch;
use ratatui::prelude::*;
use ratatui::widgets::Widget;
@ -23,15 +24,45 @@ pub use bottom_aligned_paragraph::BottomAlignedParagraph;
pub(crate) mod prerender;
pub use prerender::Prerender;
mod divider;
pub use divider::Divider;
#[rustfmt::skip] // reflow is vendored from ratatui, let's avoid changes
mod reflow;
/// A [`Widget`] that returns how many columns and lines it needs to draw everything
/// (which is the number of lines it actually drew if it fits on screen)
#[enum_dispatch]
pub trait OverlappableWidget {
/// Returns how many lines, from the bottom of the area, this widget would actually draw
/// if rendered with the given width
fn height(&self, width: u16) -> u64;
/// Render the widget from the bottom, and return the width and height of the area
/// actually drawn.
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> (u16, u16);
}
/// Enum of [`OverlappableWidget`] implementors
#[enum_dispatch(OverlappableWidget)]
pub enum BacklogItemWidget<'a> {
Paragraph(BottomAlignedParagraph<'a>),
Divider(Divider<'a>),
Empty(EmptyWidget),
}
#[derive(Clone)]
pub struct EmptyWidget;
impl OverlappableWidget for EmptyWidget {
fn height(&self, _width: u16) -> u64 {
0
}
fn render_overlap(self, _area: Rect, _buf: &mut Buffer) -> (u16, u16) {
(0, 0)
}
}
/*
impl<W: OverlappableWidget> Widget for W {
fn render(self, area: Rect, buf: &mut Buffer) {