Skip to content

Commit 89ee744

Browse files
alankyshumclaude
andcommitted
fix: markdown preview text wrapping and stale image clearing (v0.7.1)
- Add text auto-flow to markdown preview via Paragraph::wrap() - Fix scroll height calculation to account for wrapped lines - Fix Kitty graphics delete command (add d=a for delete-all) - Overwrite all cells when clearing stale images to remove lingering pixels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1e1df55 commit 89ee744

File tree

3 files changed

+46
-12
lines changed

3 files changed

+46
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "semantic-diff"
3-
version = "0.7.0"
3+
version = "0.7.1"
44
edition = "2021"
55
description = "A terminal diff viewer with AI-powered semantic grouping (Claude CLI / Copilot)"
66
license = "MIT"

src/ui/preview_view.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::preview::mermaid::{ImageProtocol, ImageSupport, MermaidRenderState};
1111
use ratatui::layout::Rect;
1212
use ratatui::style::{Color, Modifier, Style};
1313
use ratatui::text::{Line, Span};
14-
use ratatui::widgets::Paragraph;
14+
use ratatui::widgets::{Paragraph, Wrap};
1515
use ratatui::Frame;
1616
use std::io::Write;
1717

@@ -117,7 +117,9 @@ pub fn render_preview(app: &App, frame: &mut Frame, area: Rect) -> Vec<PendingIm
117117

118118
match segment {
119119
Segment::Text(lines) => {
120-
let para = Paragraph::new(lines.clone()).scroll((clip_top, 0));
120+
let para = Paragraph::new(lines.clone())
121+
.wrap(Wrap { trim: false })
122+
.scroll((clip_top, 0));
121123
frame.render_widget(para, seg_area);
122124
}
123125
Segment::Image { ref path } => {
@@ -177,20 +179,36 @@ pub fn flush_images(
177179
let _ = stdout.flush();
178180
}
179181

180-
/// Clear any stale inline images by forcing a full terminal redraw.
182+
/// Clear any stale inline images by overwriting all cells.
181183
/// Call when previous frame had images but current frame does not.
184+
///
185+
/// `terminal.clear()` alone is insufficient: it resets ratatui's buffer and
186+
/// queues `\x1b[2J`, but ratatui's diff algorithm skips cells that are empty
187+
/// in both the old and new buffers. Image pixels in those cells persist.
188+
/// We explicitly write spaces to every cell to guarantee overwrite.
182189
pub fn clear_stale_images(
183190
protocol: ImageProtocol,
184191
terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>,
185192
) {
186-
// For Kitty: explicitly delete all placement images
193+
let mut stdout = std::io::stdout();
194+
195+
// For Kitty: explicitly delete all image placements
187196
if protocol == ImageProtocol::Kitty {
188-
let mut stdout = std::io::stdout();
189-
let _ = write!(stdout, "\x1b_Ga=d;\x1b\\");
190-
let _ = stdout.flush();
197+
let _ = write!(stdout, "\x1b_Ga=d,d=a;\x1b\\");
198+
}
199+
200+
// Write spaces to every cell to overwrite lingering image pixels.
201+
// Inline images (iTerm2 OSC 1337, Kitty) bypass ratatui's buffer,
202+
// so we must physically overwrite the cells they occupied.
203+
if let Ok(size) = terminal.size() {
204+
let blank_line = " ".repeat(size.width as usize);
205+
for row in 0..size.height {
206+
let _ = write!(stdout, "\x1b[{};1H{blank_line}", row + 1);
207+
}
191208
}
192-
// Force ratatui to redraw every cell on the next frame,
193-
// which overwrites any leftover image pixels.
209+
let _ = stdout.flush();
210+
211+
// Reset ratatui's buffer state so the next draw rewrites all content.
194212
let _ = terminal.clear();
195213
}
196214

@@ -248,7 +266,23 @@ enum Segment {
248266
impl Segment {
249267
fn height(&self, pane_width: u16) -> u16 {
250268
match self {
251-
Segment::Text(lines) => lines.len() as u16,
269+
Segment::Text(lines) => {
270+
if pane_width == 0 {
271+
return lines.len() as u16;
272+
}
273+
let w = pane_width as usize;
274+
lines
275+
.iter()
276+
.map(|line| {
277+
let char_width: usize = line.spans.iter().map(|s| s.content.len()).sum();
278+
if char_width == 0 {
279+
1
280+
} else {
281+
char_width.div_ceil(w)
282+
}
283+
})
284+
.sum::<usize>() as u16
285+
}
252286
Segment::Image { ref path } => estimate_image_height(path, pane_width),
253287
}
254288
}

0 commit comments

Comments
 (0)