feat(tui): wrap long chat messages instead of clipping them

This commit is contained in:
Michel de Graaf
2026-08-14 23:26:11 +02:00
parent 4232a1e223
commit 0aa6f33572
3 changed files with 95 additions and 28 deletions
+16 -9
View File
@@ -1,7 +1,7 @@
use crate::models::{AppState, MessageDirection};
use crate::ui::{
accent_style, border_style, dimmed_style, highlight_style, info_style,
primary_style,
primary_style, wrap_chat_line,
};
use ratatui::{
Frame,
@@ -98,25 +98,32 @@ fn render_messages(
peer: &str,
own_username: &str,
) {
let width = area.width as usize;
let lines: Vec<Line> = state
.messages
.iter()
.filter(|m| m.peer == peer)
.map(|m| {
.flat_map(|m| {
let (sender, sender_style) = match m.direction {
MessageDirection::Incoming => (peer, info_style()),
MessageDirection::Outgoing => (own_username, accent_style()),
};
Line::from(vec![
Span::styled(m.at.format("%H:%M ").to_string(), dimmed_style()),
Span::styled(format!("<{sender}> "), sender_style),
Span::styled(m.text.clone(), primary_style()),
])
wrap_chat_line(
vec![
Span::styled(
m.at.format("%H:%M ").to_string(),
dimmed_style(),
),
Span::styled(format!("<{sender}> "), sender_style),
],
&m.text,
primary_style(),
width,
)
})
.collect();
// Auto-scroll: one row per message (no wrapping), so tailing by the pane
// height keeps the newest message visible.
// Auto-scroll: tail by rendered rows so the newest message stays visible.
let start = lines.len().saturating_sub((area.height as usize).max(1));
frame.render_widget(Paragraph::new(lines[start..].to_vec()), area);
}
+20 -19
View File
@@ -2,7 +2,7 @@ use crate::models::{RoomsState, RoomsView};
use crate::ui::{
HIGHLIGHT_SYMBOL, PANE_PADDING, accent_style, dimmed_style,
highlight_style, info_style, pane_block, plain_title, primary_style,
row_highlight_style,
row_highlight_style, wrap_chat_line,
};
use ratatui::{
Frame,
@@ -174,31 +174,32 @@ fn render_messages(
area: Rect,
lines: &[crate::models::RoomLine],
) {
// Auto-scroll: show the most recent lines. Each RoomLine renders as exactly
// one row (no wrapping) so the tail count is exact and the newest message
// is always visible; over-long lines are clipped at the right edge rather
// than wrapping and pushing newer lines off the bottom.
let height = area.height as usize;
let start = lines.len().saturating_sub(height.max(1));
let rendered: Vec<Line> = lines[start..]
// Wrap each message to the pane width, then auto-scroll: tail by rendered
// rows so the newest message is always fully visible.
let width = area.width as usize;
let rendered: Vec<Line> = lines
.iter()
.map(|l| {
.flat_map(|l| {
let time =
Span::styled(l.at.format("%H:%M ").to_string(), dimmed_style());
match &l.username {
Some(user) => Line::from(vec![
time,
Span::styled(format!("<{user}> "), info_style()),
Span::styled(l.text.clone(), primary_style()),
]),
None => Line::from(vec![
time,
Span::styled(l.text.clone(), dimmed_style()),
]),
Some(user) => wrap_chat_line(
vec![
time,
Span::styled(format!("<{user}> "), info_style()),
],
&l.text,
primary_style(),
width,
),
None => {
wrap_chat_line(vec![time], &l.text, dimmed_style(), width)
}
}
})
.collect();
frame.render_widget(Paragraph::new(rendered), area);
let start = rendered.len().saturating_sub((area.height as usize).max(1));
frame.render_widget(Paragraph::new(rendered[start..].to_vec()), area);
}
fn render_users(