diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-29 00:48:10 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-29 00:48:10 +0300 |
| commit | 324e625c60658ab0de46928be300fc1804af48df (patch) | |
| tree | 4cafa5c8f6794c64863cd1755d93010875af68d3 /src/ui | |
| parent | 1bb2676388a3541360ca106354075d67376a6843 (diff) | |
fix: wrap update preview copy
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/release_notes.rs | 201 |
1 files changed, 194 insertions, 7 deletions
diff --git a/src/ui/release_notes.rs b/src/ui/release_notes.rs index e6d20885..1e0769e7 100644 --- a/src/ui/release_notes.rs +++ b/src/ui/release_notes.rs @@ -82,7 +82,12 @@ pub(super) fn render_release_notes_overlay(app: &AppState, frame: &mut Frame, ar .add_modifier(Modifier::BOLD), ); - let sections = release_notes_sections(stack.content, notes.preview); + let sections = release_notes_sections( + stack.content, + notes.preview, + &app.update_install_command, + &app.palette, + ); let metrics = crate::pane::ScrollMetrics { offset_from_bottom: app.release_notes_max_scroll().saturating_sub(notes.scroll) as usize, max_offset_from_bottom: app.release_notes_max_scroll() as usize, @@ -389,9 +394,17 @@ pub(crate) struct ReleaseNotesSections { pub notes_body: Rect, } -pub(crate) fn release_notes_sections(area: Rect, preview: bool) -> ReleaseNotesSections { +pub(crate) fn release_notes_sections( + area: Rect, + preview: bool, + install_command: &str, + p: &Palette, +) -> ReleaseNotesSections { if preview && area.height >= 6 { - let rows = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]).areas::<2>(area); + let preview_height = release_notes_preview_panel_height(area.width, install_command, p) + .min(area.height.saturating_sub(1)); + let rows = Layout::vertical([Constraint::Length(preview_height), Constraint::Min(0)]) + .areas::<2>(area); ReleaseNotesSections { instructions: Some(rows[0]), notes_body: rows[1], @@ -404,6 +417,14 @@ pub(crate) fn release_notes_sections(area: Rect, preview: bool) -> ReleaseNotesS } } +fn release_notes_preview_panel_height(area_width: u16, install_command: &str, p: &Palette) -> u16 { + let text_width = area_width.saturating_sub(2).max(1); + let text_height = Paragraph::new(release_notes_preview_lines("", install_command, p)) + .wrap(Wrap { trim: false }) + .line_count(text_width) as u16; + text_height.saturating_add(3) +} + pub(super) fn release_notes_preview_lines<'a>( _version: &str, install_command: &'a str, @@ -439,11 +460,15 @@ fn render_release_notes_preview_panel( install_command: &str, p: &Palette, ) { + let preview_lines = release_notes_preview_lines(_version, install_command, p); + let text_height = Paragraph::new(preview_lines.clone()) + .wrap(Wrap { trim: false }) + .line_count(area.width.saturating_sub(2).max(1)) as u16; let rows = Layout::vertical([ - Constraint::Length(2), - Constraint::Length(1), + Constraint::Length(text_height), Constraint::Length(1), Constraint::Length(1), + Constraint::Min(0), ]) .areas::<4>(area); @@ -454,8 +479,7 @@ fn render_release_notes_preview_panel( rows[0].height, ); frame.render_widget( - Paragraph::new(release_notes_preview_lines(_version, install_command, p)) - .wrap(Wrap { trim: false }), + Paragraph::new(preview_lines).wrap(Wrap { trim: false }), text_area, ); @@ -483,7 +507,170 @@ pub(crate) fn product_announcement_display_lines<'a>( release_notes_lines(announcement.body.as_str(), p) } +pub(crate) fn release_notes_wrapped_line_count(lines: &[(usize, Line<'_>)], width: u16) -> usize { + Paragraph::new( + lines + .iter() + .map(|(_, line)| line.clone()) + .collect::<Vec<_>>(), + ) + .wrap(Wrap { trim: false }) + .line_count(width.max(1)) +} + pub(crate) fn release_notes_close_button_rect(area: Rect) -> Rect { let width = action_button_width(Some("esc"), "close"); Rect::new(area.x + area.width.saturating_sub(width), area.y, width, 1) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::state::Palette; + use ratatui::{backend::TestBackend, Terminal}; + + fn line_text(line: &Line<'_>) -> String { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::<String>() + } + + fn buffer_text(buffer: &ratatui::buffer::Buffer, area: Rect) -> String { + (area.y..area.y + area.height) + .map(|row| { + (area.x..area.x + area.width) + .map(|x| buffer[(x, row)].symbol()) + .collect::<String>() + .trim_end() + .to_string() + }) + .collect::<Vec<_>>() + .join("\n") + } + + #[test] + fn release_notes_inline_code_spans_are_styled_without_backticks() { + let palette = Palette::catppuccin(); + let lines = release_notes_lines("- `herdr pane run ...` now works", &palette); + + assert_eq!(lines.len(), 1); + assert_eq!(line_text(&lines[0].1), " • herdr pane run ... now works"); + assert_eq!(lines[0].1.spans[1].content.as_ref(), "herdr pane run ..."); + assert_eq!(lines[0].1.spans[1].style.fg, Some(palette.accent)); + assert_eq!(lines[0].1.spans[1].style.bg, Some(palette.surface0)); + } + + #[test] + fn release_notes_config_inline_code_uses_nonbreaking_spaces() { + let palette = Palette::catppuccin(); + let lines = release_notes_lines("- After: `new_tab = \"prefix+c\"`", &palette); + + assert_eq!(lines.len(), 1); + assert_eq!( + lines[0].1.spans[2].content.as_ref(), + "new_tab\u{00a0}=\u{00a0}\"prefix+c\"" + ); + assert_eq!( + line_text(&lines[0].1).replace('\u{00a0}', " "), + " • After: new_tab = \"prefix+c\"" + ); + } + + #[test] + fn release_notes_preview_lines_show_update_steps() { + let palette = Palette::catppuccin(); + let lines = release_notes_preview_lines("0.5.0", "herdr update", &palette); + + assert_eq!(lines.len(), 2); + assert_eq!(line_text(&lines[0]), "● update ready"); + assert_eq!( + line_text(&lines[1]), + "detach from this session, then run herdr update in your shell" + ); + assert_eq!(lines[0].spans[0].style.fg, Some(palette.accent)); + assert_eq!(lines[0].spans[1].style.fg, Some(palette.text)); + } + + #[test] + fn release_notes_preview_section_expands_for_wrapped_install_command() { + let palette = Palette::catppuccin(); + let area = Rect::new(0, 0, 40, 12); + let sections = + release_notes_sections(area, true, "brew update && brew upgrade herdr", &palette); + + let instructions = sections + .instructions + .expect("preview should reserve an instructions panel"); + assert_eq!(instructions.height, 7); + assert_eq!(sections.notes_body.y, 7); + assert_eq!(sections.notes_body.height, 5); + } + + #[test] + fn release_notes_preview_section_keeps_short_install_command_compact() { + let palette = Palette::catppuccin(); + let area = Rect::new(0, 0, 80, 12); + let sections = release_notes_sections(area, true, "herdr update", &palette); + + let instructions = sections + .instructions + .expect("preview should reserve an instructions panel"); + assert_eq!(instructions.height, 5); + assert_eq!(sections.notes_body.y, 5); + assert_eq!(sections.notes_body.height, 7); + } + + #[test] + fn release_notes_preview_panel_renders_wrapped_install_command_suffix() { + let palette = Palette::catppuccin(); + let area = Rect::new(0, 0, 40, 7); + let backend = TestBackend::new(area.width, area.height); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|frame| { + render_release_notes_preview_panel( + frame, + area, + "0.6.4", + "brew update && brew upgrade herdr", + &palette, + ); + }) + .unwrap(); + + let rendered = buffer_text(terminal.backend().buffer(), area); + assert!(rendered.contains("your shell"), "{rendered}"); + } + + #[test] + fn release_notes_fenced_code_blocks_render_as_preformatted_lines() { + let palette = Palette::catppuccin(); + let lines = release_notes_lines( + "### Fixed\n```bash\njust check\n- not a bullet\n```\n- after", + &palette, + ); + + assert_eq!(lines.len(), 4); + assert_eq!(line_text(&lines[0].1), " FIXED"); + assert_eq!(line_text(&lines[1].1), "▏ just check"); + assert_eq!(line_text(&lines[2].1), "▏ - not a bullet"); + assert_eq!(line_text(&lines[3].1), " • after"); + assert_eq!(lines[1].1.spans[0].style.fg, Some(palette.accent)); + assert_eq!(lines[1].1.spans[0].style.bg, Some(palette.surface1)); + assert_eq!(lines[1].1.spans[1].style.bg, Some(palette.surface1)); + assert_eq!(lines[1].1.spans[2].style.bg, Some(palette.surface1)); + } + + #[test] + fn release_notes_fenced_code_blocks_preserve_blank_lines() { + let palette = Palette::catppuccin(); + let lines = release_notes_lines("```\nfirst\n\nsecond\n```", &palette); + + assert_eq!(lines.len(), 3); + assert_eq!(line_text(&lines[0].1), "▏ first"); + assert_eq!(line_text(&lines[1].1), "▏ "); + assert_eq!(line_text(&lines[2].1), "▏ second"); + } +} |
