Properly format day delimiter
This commit is contained in:
@ -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"
|
||||
|
@ -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,
|
||||
}),
|
||||
)
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}),
|
||||
)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user