1use super::scroll_buffer::ScrollBuffer;
20use crate::actor::Engine;
21use crate::buffer::{Buffer, Cell, Rgb};
22use crate::layout::Rect;
23use std::io::Write;
24use unicode_segmentation::UnicodeSegmentation;
25use unicode_width::UnicodeWidthStr;
26
27#[derive(Debug, Clone)]
29pub struct StreamConfig {
30 pub max_scrollback: usize,
32 pub default_fg: Rgb,
34 pub default_bg: Rgb,
36 pub auto_scroll: bool,
38 pub word_wrap: bool,
40}
41
42impl Default for StreamConfig {
43 fn default() -> Self {
44 Self {
45 max_scrollback: 10000,
46 default_fg: Rgb::new(220, 220, 220),
47 default_bg: Rgb::DEFAULT_BG,
48 auto_scroll: true,
49 word_wrap: true,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum AppendResult {
57 FastPath {
59 chars: usize,
61 start_col: u16,
63 row: u16,
65 },
66 SlowPath {
68 dirty_rect: Rect,
70 },
71 Empty,
73}
74
75pub struct StreamWidget {
83 bounds: Rect,
85 config: StreamConfig,
87 content: ScrollBuffer,
89 cursor_col: u16,
91 cursor_row: u16,
93 current_fg: Rgb,
95 current_bg: Rgb,
97 needs_full_redraw: bool,
99 dirty_rects: Vec<Rect>,
101}
102
103impl StreamWidget {
104 pub fn new(bounds: Rect) -> Self {
106 Self::with_config(bounds, StreamConfig::default())
107 }
108
109 pub fn with_config(bounds: Rect, config: StreamConfig) -> Self {
111 Self {
112 bounds,
113 current_fg: config.default_fg,
114 current_bg: config.default_bg,
115 content: ScrollBuffer::new(config.max_scrollback),
116 config,
117 cursor_col: 0,
118 cursor_row: 0,
119 needs_full_redraw: true,
120 dirty_rects: Vec::new(),
121 }
122 }
123
124 pub const fn bounds(&self) -> Rect {
126 self.bounds
127 }
128
129 pub fn set_bounds(&mut self, bounds: Rect) {
131 if bounds != self.bounds {
132 self.bounds = bounds;
133 self.needs_full_redraw = true;
134 }
135 }
136
137 pub const fn set_fg(&mut self, fg: Rgb) {
139 self.current_fg = fg;
140 }
141
142 pub const fn set_bg(&mut self, bg: Rgb) {
144 self.current_bg = bg;
145 }
146
147 pub const fn reset_colors(&mut self) {
149 self.current_fg = self.config.default_fg;
150 self.current_bg = self.config.default_bg;
151 }
152
153 fn can_fast_path(&self, text: &str) -> bool {
161 if !self.content.at_bottom() {
163 return false;
164 }
165
166 if text.contains('\n') {
168 return false;
169 }
170
171 let text_width = UnicodeWidthStr::width(text);
173 let available = (self.bounds.width as usize).saturating_sub(self.cursor_col as usize);
174
175 text_width <= available
176 }
177
178 fn append_fast_path(&mut self, text: &str) -> AppendResult {
183 let start_col = self.cursor_col;
184 let row = self.cursor_row;
185 let mut char_count = 0;
186
187 let cells = text.graphemes(true).filter_map(|g| {
189 Cell::from_grapheme(g).map(|mut c| {
190 c.set_fg(self.current_fg);
191 c.set_bg(self.current_bg);
192 c
193 })
194 });
195 self.content.append(cells);
196
197 for grapheme in text.graphemes(true) {
199 let width = UnicodeWidthStr::width(grapheme);
200 self.cursor_col += u16::try_from(width).unwrap_or(0);
202 char_count += 1;
203 }
204
205 AppendResult::FastPath {
206 chars: char_count,
207 start_col,
208 row,
209 }
210 }
211
212 fn append_slow_path(&mut self, text: &str) -> AppendResult {
217 let initial_row = self.cursor_row;
218 let mut max_row = self.cursor_row;
219 let initial_col = self.cursor_col;
220 let mut min_touched_col = self.cursor_col;
221 let mut max_col = self.cursor_col;
222
223 for ch in text.chars() {
224 match ch {
225 '\n' => {
226 let was_at_bottom = self.content.at_bottom();
228 self.content.newline(false);
229 if !was_at_bottom {
230 self.content.scroll_up(1);
231 }
232
233 max_col = max_col.max(self.cursor_col);
234 self.cursor_col = 0;
235 min_touched_col = 0; self.cursor_row += 1;
237
238 if self.cursor_row >= self.bounds.height {
240 self.handle_scroll(was_at_bottom);
241 }
242 }
243 '\r' => {
244 self.cursor_col = 0;
246 min_touched_col = 0;
247 }
248 '\t' => {
249 let spaces = 4 - (self.cursor_col % 4);
251 for _ in 0..spaces {
252 self.append_char(' ');
253 }
254 }
255 _ => {
256 self.append_char(ch);
257 }
258 }
259
260 max_row = max_row.max(self.cursor_row);
261 max_col = max_col.max(self.cursor_col);
262
263 if self.cursor_col < initial_col && self.cursor_row > initial_row {
265 min_touched_col = 0;
266 }
267 }
268
269 let dirty_rect = Rect {
271 x: self.bounds.x + min_touched_col,
272 y: self.bounds.y + initial_row,
273 width: self.bounds.width,
274 height: (max_row - initial_row + 1).max(1),
275 };
276
277 if !self.needs_full_redraw {
278 self.dirty_rects.push(dirty_rect);
279 }
280
281 AppendResult::SlowPath { dirty_rect }
282 }
283
284 #[allow(clippy::cast_possible_truncation)]
286 fn append_char(&mut self, ch: char) {
287 let char_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
288
289 if self.cursor_col + char_width > self.bounds.width {
291 if self.config.word_wrap {
292 let was_at_bottom = self.content.at_bottom();
293 self.content.newline(true);
294 if !was_at_bottom {
295 self.content.scroll_up(1);
296 }
297
298 self.cursor_col = 0;
299 self.cursor_row += 1;
300
301 if self.cursor_row >= self.bounds.height {
302 self.handle_scroll(was_at_bottom);
303 }
304 } else {
305 return;
307 }
308 }
309
310 let mut cell = Cell::from_char(ch);
312 cell.set_fg(self.current_fg);
313 cell.set_bg(self.current_bg);
314
315 self.content.append(std::iter::once(cell));
316 self.cursor_col += char_width;
317 }
318
319 const fn handle_scroll(&mut self, was_at_bottom: bool) {
321 self.cursor_row = self.bounds.height - 1;
323
324 if self.config.auto_scroll && was_at_bottom {
327 self.content.scroll_to_bottom();
328 }
329
330 self.needs_full_redraw = true;
332 }
333
334 pub fn append(&mut self, text: &str) -> AppendResult {
339 if text.is_empty() {
340 return AppendResult::Empty;
341 }
342
343 if self.can_fast_path(text) {
344 self.append_fast_path(text)
345 } else {
346 self.append_slow_path(text)
347 }
348 }
349
350 #[allow(clippy::cast_possible_truncation)]
354 pub fn render(&mut self, buffer: &mut Buffer) {
355 let viewport_height = self.bounds.height as usize;
356
357 let visible_lines: Vec<_> = self.content.visible_lines(viewport_height).collect();
359
360 for (row, line) in visible_lines.iter().enumerate() {
362 let y = self.bounds.y + row as u16;
363 if y >= self.bounds.y + self.bounds.height {
364 break;
365 }
366
367 let mut col = 0u16;
368 for cell in &line.content {
369 if col >= self.bounds.width {
370 break;
371 }
372
373 let x = self.bounds.x + col;
374 buffer.set(x, y, *cell);
377
378 col += u16::from(cell.display_width());
379 }
380
381 while col < self.bounds.width {
383 let x = self.bounds.x + col;
384 buffer.set(x, y, Cell::new(' ').with_fg(self.current_fg).with_bg(self.current_bg));
385 col += 1;
386 }
387 }
388
389 for row in visible_lines.len()..viewport_height {
391 let y = self.bounds.y + row as u16;
392 for col in 0..self.bounds.width {
393 let x = self.bounds.x + col;
394 buffer.set(x, y, Cell::new(' ').with_fg(self.current_fg).with_bg(self.current_bg));
395 }
396 }
397
398 self.needs_full_redraw = false;
399 self.dirty_rects.clear();
400 }
401
402 pub fn write_fast_path(
407 &self,
408 result: AppendResult,
409 text: &str,
410 output: &mut Vec<u8>,
411 ) {
412 if let AppendResult::FastPath { start_col, row, .. } = result {
413 let abs_x = self.bounds.x + start_col + 1; let abs_y = self.bounds.y + row + 1; let _ = write!(output, "\x1b[{abs_y};{abs_x}H");
418
419 let _ = write!(
421 output,
422 "\x1b[38;2;{};{};{}m\x1b[48;2;{};{};{}m",
423 self.current_fg.r, self.current_fg.g, self.current_fg.b,
424 self.current_bg.r, self.current_bg.g, self.current_bg.b
425 );
426
427 output.extend_from_slice(text.as_bytes());
429 }
430 }
431
432 pub fn append_fast_into(&mut self, text: &str, output: &mut Vec<u8>) -> bool {
438 let result = self.append(text);
439 if let AppendResult::FastPath { .. } = result {
440 self.write_fast_path(result, text, output);
441 true
442 } else {
443 false
444 }
445 }
446
447 pub fn push(&mut self, engine: &Engine, text: &str) {
466 let result = self.append(text);
467
468 if let AppendResult::FastPath { .. } = result {
469 let mut output = Vec::with_capacity(64);
471 self.write_fast_path(result, text, &mut output);
472 engine.write_raw(output);
473 }
474 }
477
478 pub const fn needs_redraw(&self) -> bool {
480 self.needs_full_redraw || !self.dirty_rects.is_empty()
481 }
482
483 pub fn dirty_rects(&self) -> &[Rect] {
485 &self.dirty_rects
486 }
487
488 pub const fn invalidate(&mut self) {
490 self.needs_full_redraw = true;
491 }
492
493 pub fn clear(&mut self) {
495 self.content.clear();
496 self.cursor_col = 0;
497 self.cursor_row = 0;
498 self.needs_full_redraw = true;
499 }
500
501 pub fn scroll_up(&mut self, lines: usize) {
503 self.content.scroll_up(lines);
504 self.needs_full_redraw = true;
505 }
506
507 pub const fn scroll_down(&mut self, lines: usize) {
509 self.content.scroll_down(lines);
510 self.needs_full_redraw = true;
511 }
512
513 pub const fn cursor_position(&self) -> (u16, u16) {
515 (self.cursor_col, self.cursor_row)
516 }
517
518 pub fn line_count(&self) -> usize {
520 self.content.len()
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_stream_widget_new() {
530 let widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
531 assert_eq!(widget.bounds().width, 80);
532 assert_eq!(widget.bounds().height, 24);
533 assert_eq!(widget.cursor_position(), (0, 0));
534 }
535
536 #[test]
537 fn test_stream_widget_append_fast_path() {
538 let mut widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
539 let result = widget.append("Hello");
540
541 match result {
542 AppendResult::FastPath { chars, start_col, row } => {
543 assert_eq!(chars, 5);
544 assert_eq!(start_col, 0);
545 assert_eq!(row, 0);
546 }
547 _ => panic!("Expected fast path"),
548 }
549
550 assert_eq!(widget.cursor_position(), (5, 0));
551 }
552
553 #[test]
554 fn test_stream_widget_append_slow_path_newline() {
555 let mut widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
556 let result = widget.append("Hello\nWorld");
557
558 match result {
559 AppendResult::SlowPath { .. } => {}
560 _ => panic!("Expected slow path due to newline"),
561 }
562
563 assert_eq!(widget.cursor_position(), (5, 1));
564 }
565
566 #[test]
567 fn test_stream_widget_wrap() {
568 let mut widget = StreamWidget::new(Rect::new(0, 0, 10, 24));
569
570 widget.append("12345678901234567890");
572
573 assert!(widget.cursor_row > 0);
575 }
576
577 #[test]
578 fn test_stream_widget_render() {
579 let mut widget = StreamWidget::new(Rect::new(0, 0, 10, 3));
580 widget.append("Line 1\nLine 2\nLine 3");
581
582 let mut buffer = Buffer::new(10, 3);
583 widget.render(&mut buffer);
584
585 let cell = buffer.get(0, 0).unwrap();
587 assert_eq!(cell.grapheme(), Some("L"));
588 }
589}