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}