1use std::hash::{Hash, Hasher};
8use std::sync::Arc;
9
10use crate::cell::Cell;
11use crate::rect::Rect;
12use crate::style::Style;
13use unicode_width::UnicodeWidthChar;
14
15const MAX_CELL_SYMBOL_BYTES: usize = 32;
21
22pub(crate) const MAX_IMAGE_PIXELS: u64 = 16_777_216;
28
29#[inline]
37fn sanitize_cell_char(ch: char) -> char {
38 let c = ch as u32;
39 if c < 0x20 || c == 0x7f || (0x80..=0x9f).contains(&c) {
40 '\u{FFFD}'
41 } else {
42 ch
43 }
44}
45
46#[derive(Clone, Debug)]
52#[allow(dead_code)]
53pub(crate) struct KittyPlacement {
54 pub content_hash: u64,
56 pub rgba: Arc<Vec<u8>>,
58 pub src_width: u32,
60 pub src_height: u32,
62 pub x: u32,
64 pub y: u32,
65 pub cols: u32,
67 pub rows: u32,
68 pub crop_y: u32,
70 pub crop_h: u32,
72}
73
74pub(crate) fn hash_rgba(data: &[u8]) -> u64 {
76 let mut hasher = std::collections::hash_map::DefaultHasher::new();
77 data.hash(&mut hasher);
78 hasher.finish()
79}
80
81impl PartialEq for KittyPlacement {
82 fn eq(&self, other: &Self) -> bool {
83 self.content_hash == other.content_hash
84 && self.x == other.x
85 && self.y == other.y
86 && self.cols == other.cols
87 && self.rows == other.rows
88 && self.crop_y == other.crop_y
89 && self.crop_h == other.crop_h
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub(crate) struct KittyClipInfo {
100 pub top_clip_rows: u32,
102 pub original_height: u32,
104}
105
106pub struct Buffer {
115 pub area: Rect,
117 pub content: Vec<Cell>,
119 pub(crate) clip_stack: Vec<Rect>,
120 pub(crate) raw_sequences: Vec<(u32, u32, String)>,
121 pub(crate) kitty_placements: Vec<KittyPlacement>,
122 pub(crate) cursor_pos: Option<(u32, u32)>,
123 pub(crate) kitty_clip_info_stack: Vec<KittyClipInfo>,
127}
128
129impl Buffer {
130 pub fn empty(area: Rect) -> Self {
132 let size = area.area() as usize;
133 Self {
134 area,
135 content: vec![Cell::default(); size],
136 clip_stack: Vec::new(),
137 raw_sequences: Vec::new(),
138 kitty_placements: Vec::new(),
139 cursor_pos: None,
140 kitty_clip_info_stack: Vec::new(),
141 }
142 }
143
144 pub(crate) fn push_kitty_clip(&mut self, info: KittyClipInfo) {
146 self.kitty_clip_info_stack.push(info);
147 }
148
149 pub(crate) fn pop_kitty_clip(&mut self) -> Option<KittyClipInfo> {
151 self.kitty_clip_info_stack.pop()
152 }
153
154 pub(crate) fn current_kitty_clip(&self) -> Option<&KittyClipInfo> {
156 self.kitty_clip_info_stack.last()
157 }
158
159 pub(crate) fn set_cursor_pos(&mut self, x: u32, y: u32) {
160 self.cursor_pos = Some((x, y));
161 }
162
163 #[cfg(feature = "crossterm")]
164 pub(crate) fn cursor_pos(&self) -> Option<(u32, u32)> {
165 self.cursor_pos
166 }
167
168 pub fn raw_sequence(&mut self, x: u32, y: u32, seq: String) {
173 if let Some(clip) = self.effective_clip() {
174 if x >= clip.right() || y >= clip.bottom() {
175 return;
176 }
177 }
178 self.raw_sequences.push((x, y, seq));
179 }
180
181 pub(crate) fn kitty_place(&mut self, mut p: KittyPlacement) {
188 if let Some(clip) = self.effective_clip() {
190 if p.x >= clip.right()
191 || p.y >= clip.bottom()
192 || p.x + p.cols <= clip.x
193 || p.y + p.rows <= clip.y
194 {
195 return;
196 }
197 }
198
199 if let Some(info) = self.current_kitty_clip() {
201 let top_clip_rows = info.top_clip_rows;
202 let original_height = info.original_height;
203 if original_height > 0 && (top_clip_rows > 0 || p.rows < original_height) {
204 let ratio = p.src_height as f64 / original_height as f64;
205 p.crop_y = (top_clip_rows as f64 * ratio) as u32;
206 let bottom_clip = original_height.saturating_sub(top_clip_rows + p.rows);
207 let bottom_pixels = (bottom_clip as f64 * ratio) as u32;
208 p.crop_h = p.src_height.saturating_sub(p.crop_y + bottom_pixels);
209 }
210 }
211
212 self.kitty_placements.push(p);
213 }
214
215 pub fn push_clip(&mut self, rect: Rect) {
221 let effective = if let Some(current) = self.clip_stack.last() {
222 intersect_rects(*current, rect)
223 } else {
224 rect
225 };
226 self.clip_stack.push(effective);
227 }
228
229 pub fn pop_clip(&mut self) {
234 self.clip_stack.pop();
235 }
236
237 fn effective_clip(&self) -> Option<&Rect> {
238 self.clip_stack.last()
239 }
240
241 #[inline]
242 fn index_of(&self, x: u32, y: u32) -> usize {
243 ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
244 }
245
246 #[inline]
248 pub fn in_bounds(&self, x: u32, y: u32) -> bool {
249 x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom()
250 }
251
252 #[inline]
257 pub fn get(&self, x: u32, y: u32) -> &Cell {
258 assert!(
259 self.in_bounds(x, y),
260 "Buffer::get({x}, {y}) out of bounds for area {:?}",
261 self.area
262 );
263 &self.content[self.index_of(x, y)]
264 }
265
266 #[inline]
271 pub fn get_mut(&mut self, x: u32, y: u32) -> &mut Cell {
272 assert!(
273 self.in_bounds(x, y),
274 "Buffer::get_mut({x}, {y}) out of bounds for area {:?}",
275 self.area
276 );
277 let idx = self.index_of(x, y);
278 &mut self.content[idx]
279 }
280
281 #[inline]
287 pub fn try_get(&self, x: u32, y: u32) -> Option<&Cell> {
288 if self.in_bounds(x, y) {
289 Some(&self.content[self.index_of(x, y)])
290 } else {
291 None
292 }
293 }
294
295 #[inline]
300 pub fn try_get_mut(&mut self, x: u32, y: u32) -> Option<&mut Cell> {
301 if self.in_bounds(x, y) {
302 let idx = self.index_of(x, y);
303 Some(&mut self.content[idx])
304 } else {
305 None
306 }
307 }
308
309 pub fn set_string(&mut self, mut x: u32, y: u32, s: &str, style: Style) {
316 if y >= self.area.bottom() {
317 return;
318 }
319 let clip = self.effective_clip().copied();
320 for ch in s.chars() {
321 if x >= self.area.right() {
322 break;
323 }
324 let ch = sanitize_cell_char(ch);
325 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
326 if char_width == 0 {
327 if x > self.area.x {
330 let prev_in_clip = clip.map_or(true, |clip| {
331 (x - 1) >= clip.x
332 && (x - 1) < clip.right()
333 && y >= clip.y
334 && y < clip.bottom()
335 });
336 if prev_in_clip {
337 let prev = self.get_mut(x - 1, y);
338 if prev.symbol.len() + ch.len_utf8() <= MAX_CELL_SYMBOL_BYTES {
339 prev.symbol.push(ch);
340 }
341 }
342 }
343 continue;
344 }
345
346 let in_clip = clip.map_or(true, |clip| {
347 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
348 });
349
350 if !in_clip {
351 x = x.saturating_add(char_width);
352 continue;
353 }
354
355 let cell = self.get_mut(x, y);
356 cell.set_char(ch);
357 cell.set_style(style);
358
359 if char_width > 1 {
361 let next_x = x + 1;
362 if next_x < self.area.right() {
363 let next = self.get_mut(next_x, y);
364 next.symbol.clear();
365 next.style = style;
366 }
367 }
368
369 x = x.saturating_add(char_width);
370 }
371 }
372
373 pub fn set_string_linked(&mut self, mut x: u32, y: u32, s: &str, style: Style, url: &str) {
378 if y >= self.area.bottom() {
379 return;
380 }
381 let clip = self.effective_clip().copied();
382 let sanitized_url = sanitize_osc8_url(url);
383 let link = sanitized_url.map(compact_str::CompactString::new);
384 for ch in s.chars() {
385 if x >= self.area.right() {
386 break;
387 }
388 let ch = sanitize_cell_char(ch);
389 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
390 if char_width == 0 {
391 if x > self.area.x {
392 let prev_in_clip = clip.map_or(true, |clip| {
393 (x - 1) >= clip.x
394 && (x - 1) < clip.right()
395 && y >= clip.y
396 && y < clip.bottom()
397 });
398 if prev_in_clip {
399 let prev = self.get_mut(x - 1, y);
400 if prev.symbol.len() + ch.len_utf8() <= MAX_CELL_SYMBOL_BYTES {
401 prev.symbol.push(ch);
402 }
403 }
404 }
405 continue;
406 }
407
408 let in_clip = clip.map_or(true, |clip| {
409 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
410 });
411
412 if !in_clip {
413 x = x.saturating_add(char_width);
414 continue;
415 }
416
417 let cell = self.get_mut(x, y);
418 cell.set_char(ch);
419 cell.set_style(style);
420 cell.hyperlink = link.clone();
421
422 if char_width > 1 {
423 let next_x = x + 1;
424 if next_x < self.area.right() {
425 let next = self.get_mut(next_x, y);
426 next.symbol.clear();
427 next.style = style;
428 next.hyperlink = link.clone();
429 }
430 }
431
432 x = x.saturating_add(char_width);
433 }
434 }
435
436 pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
440 let in_clip = self.effective_clip().map_or(true, |clip| {
441 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
442 });
443 if !self.in_bounds(x, y) || !in_clip {
444 return;
445 }
446 let cell = self.get_mut(x, y);
447 cell.set_char(ch);
448 cell.set_style(style);
449 }
450
451 pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u32, u32, &'a Cell)> {
457 let mut updates = Vec::new();
458 for y in self.area.y..self.area.bottom() {
459 for x in self.area.x..self.area.right() {
460 let cur = self.get(x, y);
461 let prev = other.get(x, y);
462 if cur != prev {
463 updates.push((x, y, cur));
464 }
465 }
466 }
467 updates
468 }
469
470 pub fn reset(&mut self) {
472 for cell in &mut self.content {
473 cell.reset();
474 }
475 self.clip_stack.clear();
476 self.raw_sequences.clear();
477 self.kitty_placements.clear();
478 self.cursor_pos = None;
479 self.kitty_clip_info_stack.clear();
480 }
481
482 pub fn reset_with_bg(&mut self, bg: crate::style::Color) {
484 for cell in &mut self.content {
485 cell.reset();
486 cell.style.bg = Some(bg);
487 }
488 self.clip_stack.clear();
489 self.raw_sequences.clear();
490 self.kitty_placements.clear();
491 self.cursor_pos = None;
492 self.kitty_clip_info_stack.clear();
493 }
494
495 pub fn resize(&mut self, area: Rect) {
500 self.area = area;
501 let size = area.area() as usize;
502 self.content.resize(size, Cell::default());
503 self.reset();
504 }
505}
506
507pub(crate) fn sanitize_osc8_url(url: &str) -> Option<String> {
516 const MAX_URL_BYTES: usize = 2048;
517 if url.is_empty() || url.len() > MAX_URL_BYTES {
518 return None;
519 }
520 let bytes = url.as_bytes();
521 let mut i = 0;
522 while i < bytes.len() {
523 let b = bytes[i];
524 if b < 0x20 || b == 0x7f {
526 return None;
527 }
528 if b == 0x1b {
530 return None;
531 }
532 i += 1;
533 }
534 Some(url.to_string())
535}
536
537fn intersect_rects(a: Rect, b: Rect) -> Rect {
538 let x = a.x.max(b.x);
539 let y = a.y.max(b.y);
540 let right = a.right().min(b.right());
541 let bottom = a.bottom().min(b.bottom());
542 let width = right.saturating_sub(x);
543 let height = bottom.saturating_sub(y);
544 Rect::new(x, y, width, height)
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 #[test]
552 fn clip_stack_intersects_nested_regions() {
553 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
554 buf.push_clip(Rect::new(1, 1, 6, 3));
555 buf.push_clip(Rect::new(4, 0, 6, 4));
556
557 buf.set_char(3, 2, 'x', Style::new());
558 buf.set_char(4, 2, 'y', Style::new());
559
560 assert_eq!(buf.get(3, 2).symbol, " ");
561 assert_eq!(buf.get(4, 2).symbol, "y");
562 }
563
564 #[test]
565 fn set_string_advances_even_when_clipped() {
566 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
567 buf.push_clip(Rect::new(2, 0, 6, 1));
568
569 buf.set_string(0, 0, "abcd", Style::new());
570
571 assert_eq!(buf.get(2, 0).symbol, "c");
572 assert_eq!(buf.get(3, 0).symbol, "d");
573 }
574
575 #[test]
576 fn pop_clip_restores_previous_clip() {
577 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
578 buf.push_clip(Rect::new(0, 0, 2, 1));
579 buf.push_clip(Rect::new(4, 0, 2, 1));
580
581 buf.set_char(1, 0, 'a', Style::new());
582 buf.pop_clip();
583 buf.set_char(1, 0, 'b', Style::new());
584
585 assert_eq!(buf.get(1, 0).symbol, "b");
586 }
587
588 #[test]
589 fn reset_clears_clip_stack() {
590 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
591 buf.push_clip(Rect::new(0, 0, 0, 0));
592 buf.reset();
593 buf.set_char(0, 0, 'z', Style::new());
594
595 assert_eq!(buf.get(0, 0).symbol, "z");
596 }
597
598 #[test]
599 fn set_string_replaces_control_chars_with_replacement() {
600 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
601 buf.set_string(0, 0, "a\x1bbc", Style::new());
604 assert_eq!(buf.get(0, 0).symbol, "a");
605 assert_eq!(buf.get(1, 0).symbol, "\u{FFFD}");
606 assert_eq!(buf.get(2, 0).symbol, "b");
607 assert_eq!(buf.get(3, 0).symbol, "c");
608 }
609
610 #[test]
611 fn zero_width_combining_does_not_append_control_bytes() {
612 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
613 buf.set_char(0, 0, 'a', Style::new());
614 buf.set_string(1, 0, "\x07", Style::new());
618 let symbol = buf.get(1, 0).symbol.as_str();
619 assert!(!symbol.contains('\x07'), "BEL leaked into cell symbol");
620 }
621
622 #[test]
623 fn set_string_caps_combining_overflow() {
624 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
625 buf.set_char(0, 0, 'a', Style::new());
626 let combining: String = "\u{0301}".repeat(200);
630 buf.set_string(1, 0, &combining, Style::new());
631 assert!(
632 buf.get(0, 0).symbol.len() <= MAX_CELL_SYMBOL_BYTES,
633 "cell symbol exceeded MAX_CELL_SYMBOL_BYTES cap"
634 );
635 }
636
637 #[test]
638 fn sanitize_osc8_url_rejects_control_chars_and_esc() {
639 assert!(sanitize_osc8_url("https://example.com").is_some());
640 assert!(sanitize_osc8_url("https://example.com?q=1&r=2").is_some());
641 assert!(sanitize_osc8_url("https://example.com\x07attack").is_none());
643 assert!(sanitize_osc8_url("https://example.com\x1b]52;c;hi\x1b\\").is_none());
645 assert!(sanitize_osc8_url("").is_none());
647 assert!(sanitize_osc8_url(&"a".repeat(2049)).is_none());
648 }
649
650 #[test]
651 fn try_get_out_of_bounds_returns_none() {
652 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 2));
653 assert!(buf.try_get(0, 0).is_some());
654 assert!(buf.try_get(2, 0).is_none());
655 assert!(buf.try_get(0, 2).is_none());
656 assert!(buf.try_get_mut(5, 5).is_none());
657 }
658
659 #[test]
660 fn kitty_clip_stack_restores_outer_on_pop() {
661 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 4));
662 assert!(buf.current_kitty_clip().is_none());
663
664 let outer = KittyClipInfo {
665 top_clip_rows: 2,
666 original_height: 10,
667 };
668 let inner = KittyClipInfo {
669 top_clip_rows: 5,
670 original_height: 20,
671 };
672
673 buf.push_kitty_clip(outer);
674 assert_eq!(buf.current_kitty_clip(), Some(&outer));
675
676 buf.push_kitty_clip(inner);
678 assert_eq!(buf.current_kitty_clip(), Some(&inner));
679
680 let popped_inner = buf.pop_kitty_clip();
683 assert_eq!(popped_inner, Some(inner));
684 assert_eq!(buf.current_kitty_clip(), Some(&outer));
685
686 let popped_outer = buf.pop_kitty_clip();
687 assert_eq!(popped_outer, Some(outer));
688 assert!(buf.current_kitty_clip().is_none());
689 }
690
691 #[test]
692 fn kitty_clip_stack_cleared_on_reset() {
693 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 2));
694 buf.push_kitty_clip(KittyClipInfo {
695 top_clip_rows: 1,
696 original_height: 2,
697 });
698 buf.push_kitty_clip(KittyClipInfo {
699 top_clip_rows: 3,
700 original_height: 4,
701 });
702 buf.reset();
703 assert!(buf.kitty_clip_info_stack.is_empty());
704 assert!(buf.current_kitty_clip().is_none());
705 }
706
707 #[test]
708 fn kitty_clip_pop_on_empty_stack_is_none() {
709 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 2));
710 assert!(buf.pop_kitty_clip().is_none());
711 }
712}