1use std::collections::VecDeque;
8use std::ops::Range;
9
10use crate::cell::Cell;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ScrollbackLine {
19 pub cells: Vec<Cell>,
22 pub wrapped: bool,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct ScrollbackWindow {
32 pub total_lines: usize,
34 pub max_scroll_offset: usize,
36 pub scroll_offset_from_bottom: usize,
38 pub viewport_start: usize,
40 pub viewport_end: usize,
42 pub render_start: usize,
44 pub render_end: usize,
46}
47
48impl ScrollbackWindow {
49 #[inline]
51 #[must_use]
52 pub fn viewport_range(self) -> Range<usize> {
53 self.viewport_start..self.viewport_end
54 }
55
56 #[inline]
58 #[must_use]
59 pub fn render_range(self) -> Range<usize> {
60 self.render_start..self.render_end
61 }
62
63 #[inline]
65 #[must_use]
66 pub fn viewport_len(self) -> usize {
67 self.viewport_end.saturating_sub(self.viewport_start)
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn render_len(self) -> usize {
74 self.render_end.saturating_sub(self.render_start)
75 }
76}
77
78impl ScrollbackLine {
79 pub fn new(cells: &[Cell], wrapped: bool) -> Self {
81 Self {
82 cells: cells.to_vec(),
83 wrapped,
84 }
85 }
86
87 #[inline]
89 pub fn len(&self) -> usize {
90 self.cells.len()
91 }
92
93 #[inline]
95 pub fn is_empty(&self) -> bool {
96 self.cells.is_empty()
97 }
98}
99
100#[derive(Debug, Clone)]
105pub struct Scrollback {
106 lines: VecDeque<ScrollbackLine>,
107 capacity: usize,
108}
109
110impl Scrollback {
111 #[must_use]
115 pub fn new(capacity: usize) -> Self {
116 Self {
117 lines: VecDeque::with_capacity(capacity.min(4096)),
118 capacity,
119 }
120 }
121
122 #[inline]
124 #[must_use]
125 pub fn capacity(&self) -> usize {
126 self.capacity
127 }
128
129 pub fn set_capacity(&mut self, capacity: usize) {
134 self.capacity = capacity;
135 while self.lines.len() > capacity {
136 self.lines.pop_front();
137 }
138 }
139
140 #[inline]
142 #[must_use]
143 pub fn len(&self) -> usize {
144 self.lines.len()
145 }
146
147 #[inline]
149 #[must_use]
150 pub fn is_empty(&self) -> bool {
151 self.lines.is_empty()
152 }
153
154 pub fn push_row(&mut self, cells: &[Cell], wrapped: bool) -> Option<ScrollbackLine> {
159 if self.capacity == 0 {
160 return None;
161 }
162 let evicted = if self.lines.len() == self.capacity {
163 self.lines.pop_front()
164 } else {
165 None
166 };
167 self.lines.push_back(ScrollbackLine::new(cells, wrapped));
168 evicted
169 }
170
171 pub fn pop_newest(&mut self) -> Option<ScrollbackLine> {
176 self.lines.pop_back()
177 }
178
179 #[inline]
181 #[must_use]
182 pub fn peek_newest(&self) -> Option<&ScrollbackLine> {
183 self.lines.back()
184 }
185
186 #[inline]
188 #[must_use]
189 pub fn get(&self, index: usize) -> Option<&ScrollbackLine> {
190 self.lines.get(index)
191 }
192
193 pub fn iter(&self) -> impl Iterator<Item = &ScrollbackLine> {
195 self.lines.iter()
196 }
197
198 pub fn iter_range(&self, range: Range<usize>) -> impl Iterator<Item = &ScrollbackLine> {
203 let end = range.end.min(self.lines.len());
204 let start = range.start.min(end);
205 self.lines.range(start..end)
206 }
207
208 pub fn iter_rev(&self) -> impl Iterator<Item = &ScrollbackLine> {
210 self.lines.iter().rev()
211 }
212
213 pub fn clear(&mut self) {
215 self.lines.clear();
216 }
217
218 #[must_use]
224 pub fn virtualized_window(
225 &self,
226 scroll_offset_from_bottom: usize,
227 viewport_lines: usize,
228 overscan_lines: usize,
229 ) -> ScrollbackWindow {
230 let total_lines = self.lines.len();
231 let viewport_len = viewport_lines.min(total_lines);
232 let max_scroll_offset = total_lines.saturating_sub(viewport_len);
233 let scroll_offset_from_bottom = scroll_offset_from_bottom.min(max_scroll_offset);
234
235 if viewport_len == 0 {
236 return ScrollbackWindow {
237 total_lines,
238 max_scroll_offset,
239 scroll_offset_from_bottom,
240 viewport_start: total_lines,
241 viewport_end: total_lines,
242 render_start: total_lines,
243 render_end: total_lines,
244 };
245 }
246
247 let newest_viewport_start = total_lines.saturating_sub(viewport_len);
248 let viewport_start = newest_viewport_start.saturating_sub(scroll_offset_from_bottom);
249 let viewport_end = viewport_start.saturating_add(viewport_len);
250 let render_start = viewport_start.saturating_sub(overscan_lines);
251 let render_end = viewport_end.saturating_add(overscan_lines).min(total_lines);
252
253 ScrollbackWindow {
254 total_lines,
255 max_scroll_offset,
256 scroll_offset_from_bottom,
257 viewport_start,
258 viewport_end,
259 render_start,
260 render_end,
261 }
262 }
263}
264
265impl Default for Scrollback {
266 fn default() -> Self {
267 Self::new(0)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::cell::{Color, SgrAttrs, SgrFlags};
275
276 fn make_row(text: &str) -> Vec<Cell> {
277 text.chars().map(Cell::new).collect()
278 }
279
280 fn row_text(cells: &[Cell]) -> String {
281 cells.iter().map(|c| c.content()).collect()
282 }
283
284 #[test]
285 fn capacity_zero_drops_lines() {
286 let mut sb = Scrollback::new(0);
287 let _ = sb.push_row(&make_row("hello"), false);
288 assert!(sb.is_empty());
289 }
290
291 #[test]
292 fn push_and_retrieve() {
293 let mut sb = Scrollback::new(10);
294 let _ = sb.push_row(&make_row("first"), false);
295 let _ = sb.push_row(&make_row("second"), true);
296 assert_eq!(sb.len(), 2);
297
298 let line0 = sb.get(0).unwrap();
299 assert_eq!(row_text(&line0.cells), "first");
300 assert!(!line0.wrapped);
301
302 let line1 = sb.get(1).unwrap();
303 assert_eq!(row_text(&line1.cells), "second");
304 assert!(line1.wrapped);
305 }
306
307 #[test]
308 fn bounded_capacity_evicts_oldest() {
309 let mut sb = Scrollback::new(2);
310 let _ = sb.push_row(&make_row("a"), false);
311 let _ = sb.push_row(&make_row("b"), false);
312 let _ = sb.push_row(&make_row("c"), false);
313 assert_eq!(sb.len(), 2);
314 assert_eq!(row_text(&sb.get(0).unwrap().cells), "b");
315 assert_eq!(row_text(&sb.get(1).unwrap().cells), "c");
316 }
317
318 #[test]
319 fn pop_newest_returns_most_recent() {
320 let mut sb = Scrollback::new(10);
321 let _ = sb.push_row(&make_row("old"), false);
322 let _ = sb.push_row(&make_row("new"), false);
323 let popped = sb.pop_newest().unwrap();
324 assert_eq!(row_text(&popped.cells), "new");
325 assert_eq!(sb.len(), 1);
326 }
327
328 #[test]
329 fn pop_newest_empty_returns_none() {
330 let mut sb = Scrollback::new(10);
331 assert!(sb.pop_newest().is_none());
332 }
333
334 #[test]
335 fn peek_newest() {
336 let mut sb = Scrollback::new(10);
337 let _ = sb.push_row(&make_row("line"), false);
338 assert_eq!(row_text(&sb.peek_newest().unwrap().cells), "line");
339 assert_eq!(sb.len(), 1); }
341
342 #[test]
343 fn set_capacity_evicts_excess() {
344 let mut sb = Scrollback::new(10);
345 for i in 0..5 {
346 let _ = sb.push_row(&make_row(&format!("line{i}")), false);
347 }
348 sb.set_capacity(2);
349 assert_eq!(sb.len(), 2);
350 assert_eq!(row_text(&sb.get(0).unwrap().cells), "line3");
351 assert_eq!(row_text(&sb.get(1).unwrap().cells), "line4");
352 }
353
354 #[test]
355 fn iter_oldest_to_newest() {
356 let mut sb = Scrollback::new(10);
357 let _ = sb.push_row(&make_row("a"), false);
358 let _ = sb.push_row(&make_row("b"), false);
359 let _ = sb.push_row(&make_row("c"), false);
360 let texts: Vec<String> = sb.iter().map(|l| row_text(&l.cells)).collect();
361 assert_eq!(texts, vec!["a", "b", "c"]);
362 }
363
364 #[test]
365 fn iter_rev_newest_to_oldest() {
366 let mut sb = Scrollback::new(10);
367 let _ = sb.push_row(&make_row("a"), false);
368 let _ = sb.push_row(&make_row("b"), false);
369 let texts: Vec<String> = sb.iter_rev().map(|l| row_text(&l.cells)).collect();
370 assert_eq!(texts, vec!["b", "a"]);
371 }
372
373 #[test]
374 fn iter_range_is_clamped_and_ordered() {
375 let mut sb = Scrollback::new(10);
376 let _ = sb.push_row(&make_row("a"), false);
377 let _ = sb.push_row(&make_row("b"), false);
378 let _ = sb.push_row(&make_row("c"), false);
379 let _ = sb.push_row(&make_row("d"), false);
380
381 let texts: Vec<String> = sb.iter_range(1..3).map(|l| row_text(&l.cells)).collect();
382 assert_eq!(texts, vec!["b", "c"]);
383
384 let clamped: Vec<String> = sb.iter_range(3..99).map(|l| row_text(&l.cells)).collect();
385 assert_eq!(clamped, vec!["d"]);
386 }
387
388 #[test]
389 fn virtualized_window_from_bottom_with_overscan() {
390 let mut sb = Scrollback::new(32);
391 for i in 0..10 {
392 let _ = sb.push_row(&make_row(&format!("{i}")), false);
393 }
394
395 let window = sb.virtualized_window(0, 4, 1);
396 assert_eq!(window.total_lines, 10);
397 assert_eq!(window.max_scroll_offset, 6);
398 assert_eq!(window.viewport_range(), 6..10);
399 assert_eq!(window.render_range(), 5..10);
400 assert_eq!(window.viewport_len(), 4);
401 assert_eq!(window.render_len(), 5);
402 }
403
404 #[test]
405 fn virtualized_window_clamps_large_scroll_offset() {
406 let mut sb = Scrollback::new(32);
407 for i in 0..10 {
408 let _ = sb.push_row(&make_row(&format!("{i}")), false);
409 }
410
411 let window = sb.virtualized_window(999, 4, 2);
412 assert_eq!(window.scroll_offset_from_bottom, 6);
413 assert_eq!(window.viewport_range(), 0..4);
414 assert_eq!(window.render_range(), 0..6);
415 }
416
417 #[test]
418 fn virtualized_window_handles_small_history() {
419 let mut sb = Scrollback::new(8);
420 let _ = sb.push_row(&make_row("x"), false);
421 let _ = sb.push_row(&make_row("y"), false);
422
423 let window = sb.virtualized_window(3, 10, 5);
424 assert_eq!(window.max_scroll_offset, 0);
425 assert_eq!(window.viewport_range(), 0..2);
426 assert_eq!(window.render_range(), 0..2);
427 }
428
429 #[test]
430 fn clear_empties_buffer() {
431 let mut sb = Scrollback::new(10);
432 let _ = sb.push_row(&make_row("x"), false);
433 sb.clear();
434 assert!(sb.is_empty());
435 }
436
437 #[test]
438 fn preserves_cell_attributes() {
439 let mut sb = Scrollback::new(10);
440 let mut cells = make_row("AB");
441 cells[0].attrs = SgrAttrs {
442 flags: SgrFlags::BOLD,
443 fg: Color::Rgb(255, 0, 0),
444 bg: Color::Default,
445 underline_color: None,
446 };
447 cells[1].hyperlink = 42;
448 let _ = sb.push_row(&cells, false);
449
450 let stored = sb.get(0).unwrap();
451 assert!(stored.cells[0].attrs.flags.contains(SgrFlags::BOLD));
452 assert_eq!(stored.cells[0].attrs.fg, Color::Rgb(255, 0, 0));
453 assert_eq!(stored.cells[1].hyperlink, 42);
454 }
455
456 #[test]
457 fn scrollback_line_len_and_empty() {
458 let line = ScrollbackLine::new(&make_row("abc"), false);
459 assert_eq!(line.len(), 3);
460 assert!(!line.is_empty());
461
462 let empty = ScrollbackLine::new(&[], false);
463 assert_eq!(empty.len(), 0);
464 assert!(empty.is_empty());
465 }
466
467 #[test]
470 fn push_row_returns_evicted_line() {
471 let mut sb = Scrollback::new(2);
472 assert!(sb.push_row(&make_row("a"), false).is_none());
473 assert!(sb.push_row(&make_row("b"), false).is_none());
474
475 let evicted = sb.push_row(&make_row("c"), false);
477 assert!(evicted.is_some());
478 let evicted = evicted.unwrap();
479 assert_eq!(row_text(&evicted.cells), "a");
480 assert!(!evicted.wrapped);
481 }
482
483 #[test]
484 fn push_row_evicted_preserves_wrapped_flag() {
485 let mut sb = Scrollback::new(1);
486 sb.push_row(&make_row("first"), true);
487 let evicted = sb.push_row(&make_row("second"), false);
488 let evicted = evicted.unwrap();
489 assert_eq!(row_text(&evicted.cells), "first");
490 assert!(evicted.wrapped);
491 }
492
493 #[test]
494 fn capacity_one_push_evict_cycle() {
495 let mut sb = Scrollback::new(1);
496 sb.push_row(&make_row("x"), false);
497 assert_eq!(sb.len(), 1);
498 assert_eq!(row_text(&sb.get(0).unwrap().cells), "x");
499
500 let evicted = sb.push_row(&make_row("y"), false);
501 assert_eq!(row_text(&evicted.unwrap().cells), "x");
502 assert_eq!(sb.len(), 1);
503 assert_eq!(row_text(&sb.get(0).unwrap().cells), "y");
504 }
505
506 #[test]
507 fn default_scrollback_is_zero_capacity() {
508 let sb = Scrollback::default();
509 assert_eq!(sb.capacity(), 0);
510 assert!(sb.is_empty());
511 }
512
513 #[test]
514 fn clone_independence() {
515 let mut sb = Scrollback::new(10);
516 sb.push_row(&make_row("hello"), false);
517 let cloned = sb.clone();
518 sb.push_row(&make_row("world"), false);
519
520 assert_eq!(sb.len(), 2);
521 assert_eq!(cloned.len(), 1);
522 assert_eq!(row_text(&cloned.get(0).unwrap().cells), "hello");
523 }
524
525 #[test]
526 fn get_out_of_bounds_returns_none() {
527 let sb = Scrollback::new(10);
528 assert!(sb.get(0).is_none());
529 assert!(sb.get(999).is_none());
530
531 let mut sb2 = Scrollback::new(10);
532 sb2.push_row(&make_row("x"), false);
533 assert!(sb2.get(0).is_some());
534 assert!(sb2.get(1).is_none());
535 }
536
537 #[test]
538 fn set_capacity_to_zero_evicts_all() {
539 let mut sb = Scrollback::new(10);
540 for i in 0..5 {
541 sb.push_row(&make_row(&format!("{i}")), false);
542 }
543 sb.set_capacity(0);
544 assert!(sb.is_empty());
545 assert_eq!(sb.capacity(), 0);
546 }
547
548 #[test]
549 fn set_capacity_growing_preserves_lines() {
550 let mut sb = Scrollback::new(3);
551 for i in 0..3 {
552 sb.push_row(&make_row(&format!("{i}")), false);
553 }
554 sb.set_capacity(10);
555 assert_eq!(sb.len(), 3);
556 assert_eq!(sb.capacity(), 10);
557 assert_eq!(row_text(&sb.get(0).unwrap().cells), "0");
558 assert_eq!(row_text(&sb.get(2).unwrap().cells), "2");
559 }
560
561 #[test]
562 fn set_capacity_same_is_noop() {
563 let mut sb = Scrollback::new(3);
564 sb.push_row(&make_row("a"), false);
565 sb.push_row(&make_row("b"), false);
566 sb.set_capacity(3);
567 assert_eq!(sb.len(), 2);
568 }
569
570 #[test]
571 fn push_row_with_empty_cells() {
572 let mut sb = Scrollback::new(10);
573 sb.push_row(&[], false);
574 assert_eq!(sb.len(), 1);
575 let line = sb.get(0).unwrap();
576 assert!(line.is_empty());
577 assert!(!line.wrapped);
578 }
579
580 #[test]
581 fn multiple_evictions_correct_order() {
582 let mut sb = Scrollback::new(2);
583 sb.push_row(&make_row("a"), false);
585 sb.push_row(&make_row("b"), false);
586
587 let ev_a = sb.push_row(&make_row("c"), false).unwrap();
589 let ev_b = sb.push_row(&make_row("d"), false).unwrap();
590 let ev_c = sb.push_row(&make_row("e"), false).unwrap();
591
592 assert_eq!(row_text(&ev_a.cells), "a");
593 assert_eq!(row_text(&ev_b.cells), "b");
594 assert_eq!(row_text(&ev_c.cells), "c");
595
596 assert_eq!(row_text(&sb.get(0).unwrap().cells), "d");
598 assert_eq!(row_text(&sb.get(1).unwrap().cells), "e");
599 }
600
601 #[test]
602 fn iter_range_start_beyond_len() {
603 let mut sb = Scrollback::new(10);
604 sb.push_row(&make_row("a"), false);
605 sb.push_row(&make_row("b"), false);
606 sb.push_row(&make_row("c"), false);
607
608 let items: Vec<_> = sb.iter_range(10..20).collect();
610 assert!(items.is_empty());
611 }
612
613 #[test]
614 fn iter_range_empty_range() {
615 let mut sb = Scrollback::new(10);
616 sb.push_row(&make_row("a"), false);
617 sb.push_row(&make_row("b"), false);
618
619 let items: Vec<_> = sb.iter_range(1..1).collect();
621 assert!(items.is_empty());
622 }
623
624 #[test]
625 fn iter_range_full() {
626 let mut sb = Scrollback::new(10);
627 sb.push_row(&make_row("a"), false);
628 sb.push_row(&make_row("b"), false);
629 sb.push_row(&make_row("c"), false);
630
631 let texts: Vec<String> = sb.iter_range(0..3).map(|l| row_text(&l.cells)).collect();
632 assert_eq!(texts, vec!["a", "b", "c"]);
633 }
634
635 #[test]
636 fn iter_range_far_beyond_len() {
637 let mut sb = Scrollback::new(10);
638 sb.push_row(&make_row("x"), false);
639
640 let texts: Vec<String> = sb.iter_range(0..1000).map(|l| row_text(&l.cells)).collect();
641 assert_eq!(texts, vec!["x"]);
642 }
643
644 #[test]
645 fn pop_all_then_push() {
646 let mut sb = Scrollback::new(3);
647 sb.push_row(&make_row("a"), false);
648 sb.push_row(&make_row("b"), false);
649
650 sb.pop_newest();
652 sb.pop_newest();
653 assert!(sb.is_empty());
654 assert!(sb.pop_newest().is_none());
655
656 sb.push_row(&make_row("c"), false);
658 assert_eq!(sb.len(), 1);
659 assert_eq!(row_text(&sb.get(0).unwrap().cells), "c");
660 }
661
662 #[test]
663 fn peek_newest_does_not_consume() {
664 let mut sb = Scrollback::new(10);
665 sb.push_row(&make_row("a"), false);
666
667 assert_eq!(row_text(&sb.peek_newest().unwrap().cells), "a");
669 assert_eq!(row_text(&sb.peek_newest().unwrap().cells), "a");
670 assert_eq!(sb.len(), 1);
671 }
672
673 #[test]
674 fn peek_newest_empty_returns_none() {
675 let sb = Scrollback::new(10);
676 assert!(sb.peek_newest().is_none());
677 }
678
679 #[test]
680 fn virtualized_window_empty_scrollback() {
681 let sb = Scrollback::new(100);
682 let w = sb.virtualized_window(0, 10, 2);
683 assert_eq!(w.total_lines, 0);
684 assert_eq!(w.max_scroll_offset, 0);
685 assert_eq!(w.viewport_start, 0);
686 assert_eq!(w.viewport_end, 0);
687 assert_eq!(w.render_start, 0);
688 assert_eq!(w.render_end, 0);
689 assert_eq!(w.viewport_len(), 0);
690 assert_eq!(w.render_len(), 0);
691 }
692
693 #[test]
694 fn virtualized_window_zero_viewport() {
695 let mut sb = Scrollback::new(10);
696 for i in 0..5 {
697 sb.push_row(&make_row(&format!("{i}")), false);
698 }
699
700 let w = sb.virtualized_window(0, 0, 2);
701 assert_eq!(w.total_lines, 5);
702 assert_eq!(w.viewport_len(), 0);
703 }
704
705 #[test]
706 fn virtualized_window_zero_overscan() {
707 let mut sb = Scrollback::new(20);
708 for i in 0..10 {
709 sb.push_row(&make_row(&format!("{i}")), false);
710 }
711
712 let w = sb.virtualized_window(0, 4, 0);
713 assert_eq!(w.viewport_range(), 6..10);
714 assert_eq!(w.render_range(), 6..10);
715 assert_eq!(w.viewport_range(), w.render_range());
716 }
717
718 #[test]
719 fn virtualized_window_at_max_scroll_offset() {
720 let mut sb = Scrollback::new(20);
721 for i in 0..10 {
722 sb.push_row(&make_row(&format!("{i}")), false);
723 }
724
725 let w = sb.virtualized_window(6, 4, 0);
727 assert_eq!(w.scroll_offset_from_bottom, 6);
728 assert_eq!(w.viewport_range(), 0..4); }
730
731 #[test]
732 fn virtualized_window_overscan_clamped_to_bounds() {
733 let mut sb = Scrollback::new(20);
734 for i in 0..5 {
735 sb.push_row(&make_row(&format!("{i}")), false);
736 }
737
738 let w = sb.virtualized_window(0, 3, 100);
740 assert_eq!(w.render_start, 0); assert_eq!(w.render_end, 5); }
743
744 #[test]
745 fn virtualized_window_render_contains_viewport() {
746 let mut sb = Scrollback::new(50);
747 for i in 0..20 {
748 sb.push_row(&make_row(&format!("{i}")), false);
749 }
750
751 for offset in [0, 3, 8, 16] {
752 let w = sb.virtualized_window(offset, 5, 2);
753 assert!(w.render_start <= w.viewport_start);
754 assert!(w.render_end >= w.viewport_end);
755 }
756 }
757
758 #[test]
759 fn virtualized_window_single_line() {
760 let mut sb = Scrollback::new(10);
761 sb.push_row(&make_row("only"), false);
762
763 let w = sb.virtualized_window(0, 5, 2);
764 assert_eq!(w.total_lines, 1);
765 assert_eq!(w.viewport_range(), 0..1);
766 assert_eq!(w.max_scroll_offset, 0);
767 }
768
769 #[test]
770 fn scrollback_line_equality() {
771 let line1 = ScrollbackLine::new(&make_row("abc"), false);
772 let line2 = ScrollbackLine::new(&make_row("abc"), false);
773 let line3 = ScrollbackLine::new(&make_row("abc"), true);
774 let line4 = ScrollbackLine::new(&make_row("xyz"), false);
775
776 assert_eq!(line1, line2);
777 assert_ne!(line1, line3); assert_ne!(line1, line4); }
780
781 #[test]
782 fn scrollback_line_clone() {
783 let line = ScrollbackLine::new(&make_row("test"), true);
784 let cloned = line.clone();
785 assert_eq!(line, cloned);
786 assert!(cloned.wrapped);
787 }
788
789 #[test]
790 fn scrollback_window_copy_semantics() {
791 let w = ScrollbackWindow {
792 total_lines: 10,
793 max_scroll_offset: 6,
794 scroll_offset_from_bottom: 2,
795 viewport_start: 2,
796 viewport_end: 6,
797 render_start: 0,
798 render_end: 8,
799 };
800 let w2 = w; assert_eq!(w, w2);
802 }
803
804 #[test]
805 fn scrollback_window_viewport_range_and_len_consistent() {
806 let w = ScrollbackWindow {
807 total_lines: 20,
808 max_scroll_offset: 15,
809 scroll_offset_from_bottom: 3,
810 viewport_start: 12,
811 viewport_end: 17,
812 render_start: 10,
813 render_end: 19,
814 };
815 assert_eq!(w.viewport_range(), 12..17);
816 assert_eq!(w.viewport_len(), 5);
817 assert_eq!(w.render_range(), 10..19);
818 assert_eq!(w.render_len(), 9);
819 }
820
821 #[test]
822 fn large_capacity_capped_prealloc() {
823 let sb = Scrollback::new(1_000_000);
825 assert_eq!(sb.capacity(), 1_000_000);
826 assert!(sb.is_empty());
827 }
830
831 #[test]
832 fn clear_then_push() {
833 let mut sb = Scrollback::new(10);
834 sb.push_row(&make_row("a"), false);
835 sb.push_row(&make_row("b"), false);
836 sb.clear();
837 assert!(sb.is_empty());
838
839 sb.push_row(&make_row("c"), false);
840 assert_eq!(sb.len(), 1);
841 assert_eq!(row_text(&sb.get(0).unwrap().cells), "c");
842 }
843
844 #[test]
845 fn debug_format() {
846 let sb = Scrollback::new(5);
847 let dbg = format!("{sb:?}");
848 assert!(dbg.contains("Scrollback"));
849 }
850
851 #[test]
852 fn scrollback_line_debug_format() {
853 let line = ScrollbackLine::new(&make_row("x"), true);
854 let dbg = format!("{line:?}");
855 assert!(dbg.contains("ScrollbackLine"));
856 assert!(dbg.contains("wrapped: true"));
857 }
858}