diff --git a/core/kptui/Cargo.toml b/core/kptui/Cargo.toml index c89a8dd..a6db3b2 100644 --- a/core/kptui/Cargo.toml +++ b/core/kptui/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" anyhow = "1.0.93" crossterm = "0.28.1" kordophoned-client = { path = "../kordophoned-client" } -ratatui = "0.29.0" +ratatui = { version = "0.29.0", features = ["unstable-rendered-line-info"] } time = { version = "0.3.37", features = ["formatting"] } unicode-width = "0.2.0" diff --git a/core/kptui/src/main.rs b/core/kptui/src/main.rs index 25d99f0..783818b 100644 --- a/core/kptui/src/main.rs +++ b/core/kptui/src/main.rs @@ -177,7 +177,7 @@ fn ui(frame: &mut Frame, app: &AppState, requested_view: ViewMode) { render_status(frame, app, status_area, mode); } -fn render_conversations(frame: &mut Frame, app: &AppState, area: Rect, in_split: bool) { +fn render_conversations(frame: &mut Frame, app: &AppState, area: Rect, _in_split: bool) { let title = "Kordophone"; let items = app .conversations @@ -247,42 +247,7 @@ fn render_transcript(frame: &mut Frame, app: &AppState, area: Rect, in_split: bo "Chat".to_string() }; - let mut lines: Vec = Vec::new(); - for message in &app.messages { - let ts = time::OffsetDateTime::from_unix_timestamp(message.date_unix) - .unwrap_or(time::OffsetDateTime::UNIX_EPOCH) - .format(&time::format_description::well_known::Rfc3339) - .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()); - - lines.push(Line::from(vec![ - Span::styled( - message.sender.clone(), - Style::default().add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::styled(ts, Style::default().fg(Color::DarkGray)), - ])); - - let mut rendered_any_text = false; - for text_line in message.text.lines() { - rendered_any_text = true; - lines.push(Line::from(Span::raw(text_line.to_string()))); - } - if !rendered_any_text { - lines.push(Line::from(Span::styled( - "", - Style::default().fg(Color::DarkGray), - ))); - } - lines.push(Line::from(Span::raw(""))); - } - - if lines.is_empty() { - lines.push(Line::from(Span::styled( - "No messages.", - Style::default().fg(Color::DarkGray), - ))); - } + let lines = transcript_lines(&app.messages); let paragraph = Paragraph::new(Text::from(lines)) .block(Block::default().borders(Borders::ALL).title(title)) @@ -735,46 +700,55 @@ fn transcript_inner_width(size: Size, app: &AppState, requested_view: ViewMode) outer_width.saturating_sub(2).max(1) } -fn visual_line_count(s: &str, inner_width: u16) -> u16 { - let w = s.width(); - if w == 0 { - return 1; - } - let iw = inner_width.max(1) as usize; - ((w + iw - 1) / iw).min(u16::MAX as usize) as u16 -} - fn visual_width_u16(s: &str) -> u16 { s.width().min(u16::MAX as usize) as u16 } -fn transcript_content_visual_lines(messages: &[daemon::ChatMessage], inner_width: u16) -> u16 { - if messages.is_empty() { - return 1; - } - - let mut total: u32 = 0; +fn transcript_lines(messages: &[daemon::ChatMessage]) -> Vec> { + let mut lines: Vec> = Vec::new(); for message in messages { let ts = time::OffsetDateTime::from_unix_timestamp(message.date_unix) .unwrap_or(time::OffsetDateTime::UNIX_EPOCH) .format(&time::format_description::well_known::Rfc3339) .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()); - let header = format!("{} {}", message.sender, ts); - total += visual_line_count(&header, inner_width) as u32; + + lines.push(Line::from(vec![ + Span::styled( + message.sender.clone(), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled(ts, Style::default().fg(Color::DarkGray)), + ])); let mut rendered_any_text = false; for text_line in message.text.lines() { rendered_any_text = true; - total += visual_line_count(text_line, inner_width) as u32; + lines.push(Line::from(Span::raw(text_line.to_string()))); } if !rendered_any_text { - total += visual_line_count("", inner_width) as u32; + lines.push(Line::from(Span::styled( + "", + Style::default().fg(Color::DarkGray), + ))); } - - total += 1; // spacer line + lines.push(Line::from(Span::raw(""))); } - total.min(u16::MAX as u32) as u16 + if lines.is_empty() { + lines.push(Line::from(Span::styled( + "No messages.", + Style::default().fg(Color::DarkGray), + ))); + } + + lines +} + +fn transcript_content_visual_lines(messages: &[daemon::ChatMessage], inner_width: u16) -> u16 { + let paragraph = + Paragraph::new(Text::from(transcript_lines(messages))).wrap(Wrap { trim: false }); + paragraph.line_count(inner_width).min(u16::MAX as usize) as u16 } fn transcript_viewport_height(size: Size, app: &AppState, requested_view: ViewMode) -> u16 {