1use crate::configs::get_config;
21use crossterm::style::Color;
22use std::{collections::VecDeque, io::BufWriter};
23
24const MIN_RENDER_INTERVAL: tokio::time::Duration = tokio::time::Duration::from_millis(33);
25
26enum EscapeState {
29 Normal,
31 Esc,
33 Csi,
35}
36
37#[non_exhaustive]
39#[derive(Clone, Debug)]
40pub enum UICommand {
41 ScrollUp(usize),
42 ScrollDown(usize),
43 ScrollBottom,
44 ScrollTop,
45 StartSelection(u16, u16),
46 UpdateSelection(u16, u16),
47 CopySelection,
48 ClearBuffer,
49}
50
51#[derive(Clone, Debug)]
55struct Cell {
56 character: char,
57 fg_color: Color,
58 bg_color: Color,
59 is_selected: bool,
60}
61
62impl Default for Cell {
63 fn default() -> Self {
64 let config = get_config();
65 Self {
66 character: ' ',
67 fg_color: Color::from(&config.appearance.fg),
68 bg_color: Color::from(&config.appearance.bg),
69 is_selected: false,
70 }
71 }
72}
73
74#[derive(Clone, Copy, Debug)]
76struct Position {
77 x: u16,
80 y: usize,
82}
83
84impl From<(u16, usize)> for Position {
85 fn from((x, y): (u16, usize)) -> Self {
86 Self { x, y }
87 }
88}
89
90impl From<(u16, u16)> for Position {
91 fn from((x, y): (u16, u16)) -> Self {
92 Self { x, y: y as usize }
93 }
94}
95
96impl From<Position> for (u16, usize) {
97 fn from(position: Position) -> Self {
98 (position.x, position.y)
99 }
100}
101
102impl From<Position> for (u16, u16) {
103 fn from(position: Position) -> Self {
104 (position.x, position.y as u16)
105 }
106}
107
108pub struct ScreenBuffer {
112 width: u16,
114 height: u16,
116 lines: VecDeque<Vec<Cell>>,
119 view_start: usize,
122 cursor_pos: Position,
124 selection_start: Option<(u16, usize)>,
126 selection_end: Option<(u16, usize)>,
128 max_scrollback: usize,
130 escape_state: EscapeState,
133 last_render: Option<tokio::time::Instant>,
134 needs_render: bool,
135}
136
137impl ScreenBuffer {
138 pub fn new(width: u16, height: u16, max_scrollback: usize) -> Self {
141 let mut buffer = Self {
142 width,
143 height,
144 lines: VecDeque::new(),
145 view_start: 0,
146 cursor_pos: Position { x: 0, y: 0 },
147 selection_start: None,
148 selection_end: None,
149 max_scrollback,
150 last_render: None,
151 needs_render: false,
152 escape_state: EscapeState::Normal,
153 };
154 buffer
156 .lines
157 .push_back(vec![Cell::default(); width as usize]);
158 buffer
159 }
160
161 fn set_cursor_pos<P: Into<Position>>(&mut self, position: P) {
162 self.cursor_pos = position.into();
163 }
164
165 fn move_cursor_left(&mut self) {
166 self.cursor_pos.x = self.cursor_pos.x.saturating_sub(1);
167 }
168
169 fn move_cursor_right(&mut self) {
170 self.cursor_pos.x = self.cursor_pos.x.saturating_add(1);
171 }
172
173 pub fn add_data(&mut self, data: &[u8]) {
177 let text = String::from_utf8_lossy(data);
178 let mut chars = text.chars().peekable();
179
180 while let Some(ch) = chars.next() {
181 match self.escape_state {
182 EscapeState::Normal => {
183 match ch {
184 '\r' => {
185 self.cursor_pos.x = 0;
186 if chars.peek() == Some(&'\n') {
187 chars.next();
188 self.new_line();
189 }
190 }
191 '\n' => {
192 self.new_line();
193 }
194 '\x07' => {}
195 '\x08' => {
196 let mut temp_chars = chars.clone();
197 if let (Some(' '), Some('\x08')) =
199 (temp_chars.next(), temp_chars.next())
200 {
201 chars.next();
203 chars.next();
204 self.move_cursor_left();
205 self.set_char_at_cursor(' ');
206 } else {
207 self.move_cursor_left();
210 }
211 }
212 '\x1B' => {
213 self.escape_state = EscapeState::Esc;
214 }
215 c => {
216 let mut batch = vec![c];
217 while let Some(&next_ch) = chars.peek() {
218 if next_ch.is_control()
219 || next_ch == '\x1B'
220 || self.cursor_pos.x + batch.len() as u16 >= self.width
221 {
222 break;
223 }
224 batch.push(chars.next().unwrap());
225 }
226 self.add_char_batch(&batch);
227 }
228 }
229 }
230 EscapeState::Esc => match ch {
231 '[' => {
232 self.escape_state = EscapeState::Csi;
233 }
234 _ => {
235 self.escape_state = EscapeState::Normal;
236 }
237 },
238 EscapeState::Csi => match ch {
239 'J' => {
240 self.clear_from_cursor_to_eol();
241 self.escape_state = EscapeState::Normal;
242 }
243 'K' => {
244 self.clear_from_cursor_to_eol();
245 self.escape_state = EscapeState::Normal;
246 }
247 'C' => {
248 self.move_cursor_left();
249 self.escape_state = EscapeState::Normal;
250 }
251 'D' => {
252 self.move_cursor_right();
253 self.escape_state = EscapeState::Normal;
254 }
255 _ => {
256 self.escape_state = EscapeState::Normal;
257 }
258 },
259 }
260 }
261 self.scroll_to_bottom();
262 self.needs_render = true;
263 }
264
265 fn add_char_batch(&mut self, chars: &[char]) {
266 while self.cursor_pos.y >= self.lines.len() {
267 self.lines
268 .push_back(vec![Cell::default(); self.width as usize]);
269 }
270
271 if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
272 for &ch in chars {
273 if (self.cursor_pos.x as usize) < line.len() {
274 line[self.cursor_pos.x as usize].character = ch;
275 self.cursor_pos.x += 1;
276 if self.cursor_pos.x >= self.width {
277 self.new_line();
278 break;
279 }
280 }
281 }
282 }
283 }
284
285 pub fn should_render_now(&self) -> bool {
287 use tokio::time::Instant;
288
289 if !self.needs_render {
290 return false;
291 }
292
293 let now = Instant::now();
294 match self.last_render {
295 Some(last) => now.duration_since(last) >= MIN_RENDER_INTERVAL,
296 None => true,
297 }
298 }
299
300 fn set_char_at_cursor(&mut self, ch: char) {
301 while self.cursor_pos.y >= self.lines.len() {
302 self.lines
303 .push_back(vec![Cell::default(); self.width as usize]);
304 }
305
306 if let Some(line) = self.lines.get_mut(self.cursor_pos.y)
307 && (self.cursor_pos.x as usize) < line.len()
308 {
309 line[self.cursor_pos.x as usize].character = ch;
310 }
311 }
312
313 #[allow(clippy::needless_range_loop)]
314 fn clear_from_cursor_to_eol(&mut self) {
315 if let Some(line) = self.lines.get_mut(self.cursor_pos.y) {
316 for x in (self.cursor_pos.x as usize)..line.len() {
317 line[x] = Cell::default();
318 }
319 }
320 }
321
322 fn new_line(&mut self) {
323 self.set_cursor_pos((0, self.cursor_pos.y + 1));
324
325 if self.cursor_pos.y >= self.lines.len() {
326 self.lines
327 .push_back(vec![Cell::default(); self.width as usize]);
328 }
329
330 while self.lines.len() > self.max_scrollback {
332 self.lines.pop_front();
333 if self.cursor_pos.y > 0 {
335 self.cursor_pos.y -= 1;
336 }
337 if self.view_start > 0 {
338 self.view_start -= 1;
339 }
340 }
341 }
342
343 pub fn scroll_up(&mut self, lines: usize) {
345 if self.view_start >= lines {
346 self.view_start -= lines;
347 } else {
348 self.view_start = 0;
349 }
350 self.clear_selection();
351 self.needs_render = true;
352 }
353
354 pub fn scroll_down(&mut self, lines: usize) {
356 let max_view_start = self.lines.len().saturating_sub(self.height as usize);
357 self.view_start = (self.view_start + lines).min(max_view_start);
358 self.clear_selection();
359 self.needs_render = true;
360 }
361
362 pub fn scroll_to_bottom(&mut self) {
365 self.view_start = self.lines.len().saturating_sub(self.height as usize);
366 self.needs_render = true;
367 }
368
369 pub fn scroll_to_top(&mut self) {
371 self.view_start = 0;
372 self.needs_render = true;
373 }
374
375 pub fn start_selection(&mut self, screen_x: u16, screen_y: u16) {
379 let absolute_line = self.view_start + screen_y as usize;
380 self.clear_selection();
381 self.selection_start = Some((screen_x, absolute_line));
382 self.needs_render = true;
383 }
384
385 pub fn update_selection(&mut self, screen_x: u16, screen_y: u16) {
388 let absolute_line = self.view_start + screen_y as usize;
389 self.selection_end = Some((screen_x, absolute_line));
390 self.update_selection_highlighting();
391 self.needs_render = true;
392 }
393
394 pub fn clear_selection(&mut self) {
396 for line in &mut self.lines {
397 for cell in line {
398 cell.is_selected = false;
399 }
400 }
401 self.selection_start = None;
402 self.selection_end = None;
403 self.needs_render = true;
404 }
405
406 fn update_selection_highlighting(&mut self) {
407 for line in &mut self.lines {
408 for cell in line {
409 cell.is_selected = false;
410 }
411 }
412
413 if let (Some((start_x, start_line)), Some((end_x, end_line))) =
414 (self.selection_start, self.selection_end)
415 {
416 let (start_line, start_x, end_line, end_x) =
417 if start_line < end_line || (start_line == end_line && start_x <= end_x) {
418 (start_line, start_x, end_line, end_x)
419 } else {
420 (end_line, end_x, start_line, start_x)
421 };
422
423 for line_idx in start_line..=end_line {
424 if let Some(line) = self.lines.get_mut(line_idx) {
425 let line_start_x = if line_idx == start_line { start_x } else { 0 };
426 let line_end_x = if line_idx == end_line {
427 end_x
428 } else {
429 self.width - 1
430 };
431
432 for x in line_start_x..=line_end_x.min(self.width - 1) {
433 if let Some(cell) = line.get_mut(x as usize) {
434 cell.is_selected = true;
435 }
436 }
437 }
438 }
439 }
440 }
441
442 fn get_selected_text(&self) -> String {
443 if let (Some((start_x, start_line)), Some((end_x, end_line))) =
444 (self.selection_start, self.selection_end)
445 {
446 let (start_line, start_x, end_line, end_x) =
447 if start_line < end_line || (start_line == end_line && start_x <= end_x) {
448 (start_line, start_x, end_line, end_x)
449 } else {
450 (end_line, end_x, start_line, start_x)
451 };
452
453 let mut result = String::new();
454
455 for line_idx in start_line..=end_line {
456 if let Some(line) = self.lines.get(line_idx) {
457 let line_start_x = if line_idx == start_line { start_x } else { 0 };
458 let line_end_x = if line_idx == end_line {
459 end_x
460 } else {
461 self.width - 1
462 };
463
464 for x in line_start_x..=line_end_x.min(self.width - 1) {
465 if let Some(cell) = line.get(x as usize) {
466 result.push(cell.character);
467 }
468 }
469
470 if line_idx < end_line {
471 result.push('\n');
472 }
473 }
474 }
475
476 result.trim_end().to_string()
477 } else {
478 String::new()
479 }
480 }
481
482 pub fn copy_to_clipboard(&mut self) -> std::io::Result<()> {
484 use crossterm::{clipboard, execute};
485
486 let selected_text = self.get_selected_text();
487 if !selected_text.is_empty() {
488 execute!(
489 std::io::stdout(),
490 clipboard::CopyToClipboard::to_clipboard_from(selected_text)
491 )?;
492 }
493 self.clear_selection();
494 Ok(())
495 }
496
497 #[allow(dead_code)]
498 fn get_stats(&self) -> BufferStats {
499 let total_lines = self.lines.len();
500 BufferStats {
501 total_lines,
502 view_start: self.view_start,
503 view_end: (self.view_start + self.height as usize).min(total_lines),
504 cursor_line: self.cursor_pos.y,
505 has_selection: self.selection_start.is_some() && self.selection_end.is_some(),
506 }
507 }
508
509 pub fn clear_buffer(&mut self) {
513 self.lines.clear();
514 self.view_start = 0;
515 self.set_cursor_pos((0_u16, 0_usize));
516 self.lines
517 .push_back(vec![Cell::default(); self.width as usize]);
518 self.needs_render = true;
519 }
520
521 pub fn render(&mut self) -> std::io::Result<()> {
531 use crossterm::{cursor, queue, style};
532 use std::io::{self, Write};
533 use tokio::time::Instant;
534
535 if !self.needs_render {
536 return Ok(());
537 }
538
539 let mut writer = BufWriter::new(io::stdout());
540 queue!(writer, cursor::Hide)?;
541
542 for screen_y in 0..self.height {
543 let line_idx = self.view_start + screen_y as usize;
544 queue!(writer, cursor::MoveTo(0, screen_y))?;
545
546 if let Some(line) = self.lines.get(line_idx) {
547 let config = get_config();
548 let mut current_fg = Color::from(&config.appearance.fg);
549 let mut current_bg = Color::from(&config.appearance.bg);
550 queue!(writer, style::SetForegroundColor(current_fg))?;
551 queue!(writer, style::SetBackgroundColor(current_bg))?;
552
553 for cell in line {
554 let fg = if cell.is_selected {
555 Color::from(&config.appearance.hl_fg)
556 } else {
557 cell.fg_color
558 };
559 let bg = if cell.is_selected {
560 Color::from(&config.appearance.hl_bg)
561 } else {
562 cell.bg_color
563 };
564 if fg != current_fg {
565 queue!(writer, style::SetForegroundColor(fg))?;
566 current_fg = fg;
567 }
568 if bg != current_bg {
569 queue!(writer, style::SetBackgroundColor(bg))?;
570 current_bg = bg;
571 }
572 queue!(writer, style::Print(cell.character))?;
573 }
574 } else {
575 queue!(writer, style::ResetColor)?;
576 queue!(writer, style::Print(" ".repeat(self.width as usize)))?;
577 }
578 }
579
580 let screen_cursor_y = if self.cursor_pos.y >= self.view_start
581 && self.cursor_pos.y < self.view_start + self.height as usize
582 {
583 (self.cursor_pos.y - self.view_start) as u16
584 } else {
585 self.height - 1
586 };
587
588 queue!(
589 writer,
590 cursor::MoveTo(self.cursor_pos.x, screen_cursor_y),
591 cursor::Show
592 )?;
593 writer.flush()?;
594 self.last_render = Some(Instant::now());
595 self.needs_render = false;
596 Ok(())
597 }
598}
599
600#[allow(dead_code)]
601#[derive(Debug)]
602struct BufferStats {
603 pub total_lines: usize,
604 pub view_start: usize,
605 pub view_end: usize,
606 pub cursor_line: usize,
607 pub has_selection: bool,
608}