Skip to main content

ansiq_render/
diff.rs

1use std::io::{self, Write};
2
3use ansiq_core::{
4    Color, HistoryBlock, HistoryEntry, HistoryLine, HistoryRun, Style, history_block_from_text,
5};
6use crossterm::{
7    cursor::{Hide, MoveTo, Show},
8    queue,
9    style::{
10        Attribute, Color as CrosstermColor, Print, SetAttribute, SetBackgroundColor,
11        SetForegroundColor,
12    },
13};
14use unicode_width::UnicodeWidthChar;
15
16use crate::{Cell, FrameBuffer};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Patch {
20    pub x: u16,
21    pub y: u16,
22    pub text: String,
23    pub style: Style,
24}
25
26pub fn diff_buffers(prev: &FrameBuffer, next: &FrameBuffer) -> Vec<Patch> {
27    let width = prev.width().max(next.width());
28    let height = prev.height().max(next.height());
29    let mut patches = Vec::new();
30
31    for y in 0..height {
32        let mut x = 0;
33        while x < width {
34            let prev_cell = read_cell(prev, x, y);
35            let next_cell = read_cell(next, x, y);
36            if prev_cell == next_cell {
37                x += 1;
38                continue;
39            }
40
41            let style = next_cell.style;
42            let start = x;
43            let mut text = String::new();
44
45            while x < width {
46                let prev_cell = read_cell(prev, x, y);
47                let next_cell = read_cell(next, x, y);
48                if prev_cell == next_cell || next_cell.style != style {
49                    break;
50                }
51
52                text.push(next_cell.symbol);
53                x += 1;
54            }
55
56            patches.push(Patch {
57                x: start,
58                y,
59                text,
60                style,
61            });
62        }
63    }
64
65    patches
66}
67
68pub fn frame_patches(frame: &FrameBuffer) -> Vec<Patch> {
69    let mut patches = Vec::with_capacity(frame.height() as usize);
70
71    for y in 0..frame.height() {
72        let mut x = 0;
73        while x < frame.width() {
74            let first = frame.get(x, y);
75            let style = first.style;
76            let start = x;
77            let mut text = String::new();
78
79            while x < frame.width() {
80                let cell = frame.get(x, y);
81                if cell.style != style {
82                    break;
83                }
84                text.push(cell.symbol);
85                x += 1;
86            }
87
88            patches.push(Patch {
89                x: start,
90                y,
91                text,
92                style,
93            });
94        }
95    }
96
97    patches
98}
99
100pub fn diff_buffers_in_regions(
101    prev: &FrameBuffer,
102    next: &FrameBuffer,
103    regions: &[ansiq_core::Rect],
104) -> Vec<Patch> {
105    let mut patches = Vec::new();
106
107    for region in regions {
108        if region.is_empty() {
109            continue;
110        }
111
112        let max_y = region.bottom().min(prev.height().max(next.height()));
113        let max_x = region.right().min(prev.width().max(next.width()));
114
115        for y in region.y..max_y {
116            let mut x = region.x;
117            while x < max_x {
118                let prev_cell = read_cell(prev, x, y);
119                let next_cell = read_cell(next, x, y);
120                if prev_cell == next_cell {
121                    x += 1;
122                    continue;
123                }
124
125                let style = next_cell.style;
126                let start = x;
127                let mut text = String::new();
128
129                while x < max_x {
130                    let prev_cell = read_cell(prev, x, y);
131                    let next_cell = read_cell(next, x, y);
132                    if prev_cell == next_cell || next_cell.style != style {
133                        break;
134                    }
135
136                    text.push(next_cell.symbol);
137                    x += 1;
138                }
139
140                patches.push(Patch {
141                    x: start,
142                    y,
143                    text,
144                    style,
145                });
146            }
147        }
148    }
149
150    patches
151}
152
153pub fn render_patches<W: Write>(writer: &mut W, patches: &[Patch]) -> io::Result<()> {
154    render_patches_at_origin(writer, patches, 0)
155}
156
157pub fn render_patches_at_origin<W: Write>(
158    writer: &mut W,
159    patches: &[Patch],
160    origin_y: u16,
161) -> io::Result<()> {
162    for patch in patches {
163        let text = collapse_wide_continuations(&patch.text);
164        queue!(
165            writer,
166            MoveTo(patch.x, patch.y.saturating_add(origin_y)),
167            SetForegroundColor(map_color(patch.style.fg)),
168            SetBackgroundColor(map_color(patch.style.bg)),
169            SetAttribute(if patch.style.bold {
170                Attribute::Bold
171            } else {
172                Attribute::NormalIntensity
173            }),
174            SetAttribute(if patch.style.reversed {
175                Attribute::Reverse
176            } else {
177                Attribute::NoReverse
178            }),
179            Print(text)
180        )?;
181    }
182
183    queue!(
184        writer,
185        SetForegroundColor(CrosstermColor::Reset),
186        SetBackgroundColor(CrosstermColor::Reset),
187        SetAttribute(Attribute::Reset)
188    )?;
189
190    writer.flush()
191}
192
193pub fn render_cursor<W: Write>(writer: &mut W, cursor: Option<(u16, u16)>) -> io::Result<()> {
194    render_cursor_at_origin(writer, cursor, 0)
195}
196
197pub fn render_cursor_at_origin<W: Write>(
198    writer: &mut W,
199    cursor: Option<(u16, u16)>,
200    origin_y: u16,
201) -> io::Result<()> {
202    match cursor {
203        Some((x, y)) => {
204            queue!(writer, Show, MoveTo(x, y.saturating_add(origin_y)))?;
205        }
206        None => {
207            queue!(writer, Hide)?;
208        }
209    }
210
211    writer.flush()
212}
213
214pub fn history_block_from_buffer(buffer: &FrameBuffer) -> HistoryBlock {
215    let mut lines = Vec::with_capacity(buffer.height() as usize);
216
217    for y in 0..buffer.height() {
218        let mut last_visible_x = None;
219        for x in 0..buffer.width() {
220            if buffer.get(x, y).symbol != ' ' {
221                last_visible_x = Some(x);
222            }
223        }
224
225        let Some(end_x) = last_visible_x.map(|x| x + 1) else {
226            lines.push(HistoryLine { runs: Vec::new() });
227            continue;
228        };
229
230        let mut runs = Vec::new();
231        let mut current_style = buffer.get(0, y).style;
232        let mut current_text = String::new();
233
234        for x in 0..end_x {
235            let cell = buffer.get(x, y);
236            if x == 0 {
237                current_style = cell.style;
238            }
239            if cell.style != current_style {
240                push_history_run(&mut runs, &mut current_text, current_style);
241                current_style = cell.style;
242            }
243            current_text.push(cell.symbol);
244        }
245
246        push_history_run(&mut runs, &mut current_text, current_style);
247        lines.push(HistoryLine { runs });
248    }
249
250    HistoryBlock { lines }
251}
252
253pub fn render_history_entries<W: Write>(
254    writer: &mut W,
255    entries: &[HistoryEntry],
256    width: u16,
257) -> io::Result<u16> {
258    let mut wrote_any_line = false;
259    let mut rendered_rows = 0u16;
260
261    for (entry_index, entry) in entries.iter().enumerate() {
262        let owned_block;
263        let block = match entry {
264            HistoryEntry::Text(content) => {
265                owned_block = history_block_from_text(content, width);
266                &owned_block
267            }
268            HistoryEntry::Block(block) => block,
269        };
270
271        if entry_index > 0 && wrote_any_line {
272            write!(writer, "\r\n")?;
273            rendered_rows = rendered_rows.saturating_add(1);
274        }
275
276        for (line_index, line) in block.lines.iter().enumerate() {
277            if wrote_any_line || line_index > 0 {
278                write!(writer, "\r\n")?;
279            }
280            render_history_line(writer, line)?;
281            wrote_any_line = true;
282            rendered_rows = rendered_rows.saturating_add(1);
283        }
284    }
285
286    if rendered_rows > 0 {
287        write!(writer, "\r\n")?;
288    }
289    queue!(
290        writer,
291        SetForegroundColor(CrosstermColor::Reset),
292        SetBackgroundColor(CrosstermColor::Reset),
293        SetAttribute(Attribute::Reset)
294    )?;
295    writer.flush()?;
296    Ok(rendered_rows)
297}
298
299fn collapse_wide_continuations(text: &str) -> String {
300    let mut collapsed = String::new();
301    let mut skip = 0u16;
302
303    for ch in text.chars() {
304        if skip > 0 {
305            skip -= 1;
306            continue;
307        }
308
309        collapsed.push(ch);
310        let width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
311        if width > 1 {
312            skip = width - 1;
313        }
314    }
315
316    collapsed
317}
318
319fn push_history_run(runs: &mut Vec<HistoryRun>, text: &mut String, style: Style) {
320    if text.is_empty() {
321        return;
322    }
323
324    runs.push(HistoryRun {
325        text: collapse_wide_continuations(text),
326        style,
327    });
328    text.clear();
329}
330
331fn render_history_line<W: Write>(writer: &mut W, line: &HistoryLine) -> io::Result<()> {
332    if line.runs.is_empty() {
333        return Ok(());
334    }
335
336    for run in &line.runs {
337        queue!(
338            writer,
339            SetForegroundColor(map_color(run.style.fg)),
340            SetBackgroundColor(map_color(run.style.bg)),
341            SetAttribute(if run.style.bold {
342                Attribute::Bold
343            } else {
344                Attribute::NormalIntensity
345            }),
346            SetAttribute(if run.style.reversed {
347                Attribute::Reverse
348            } else {
349                Attribute::NoReverse
350            }),
351            Print(&run.text)
352        )?;
353    }
354
355    Ok(())
356}
357fn read_cell(buffer: &FrameBuffer, x: u16, y: u16) -> Cell {
358    if x >= buffer.width() || y >= buffer.height() {
359        Cell::default()
360    } else {
361        buffer.get(x, y)
362    }
363}
364
365fn map_color(color: Color) -> CrosstermColor {
366    match color {
367        Color::Reset => CrosstermColor::Reset,
368        Color::Black => CrosstermColor::Black,
369        Color::DarkGrey => CrosstermColor::DarkGrey,
370        Color::Grey => CrosstermColor::Grey,
371        Color::White => CrosstermColor::White,
372        Color::Blue => CrosstermColor::Blue,
373        Color::Cyan => CrosstermColor::Cyan,
374        Color::Green => CrosstermColor::Green,
375        Color::Yellow => CrosstermColor::Yellow,
376        Color::Magenta => CrosstermColor::Magenta,
377        Color::Red => CrosstermColor::Red,
378        Color::Indexed(index) => CrosstermColor::AnsiValue(index),
379        Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
380    }
381}