1use std::io::{Read, Write};
8use std::path::PathBuf;
9use std::sync::mpsc::{self, Receiver, TryRecvError};
10use std::thread;
11
12use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
13use unicode_width::UnicodeWidthChar;
14
15pub struct Terminal {
16 pub open: bool,
17 pub full_panel: bool,
20 pub pane_bound: Option<usize>,
23 rows: Vec<Vec<Cell>>,
24 saved_primary: Option<SavedScreen>,
26 alt_screen: bool,
27 cursor_row: usize,
28 cursor_col: usize,
29 saved_cursor: (usize, usize),
30 cols: u16,
31 rows_count: u16,
32 scroll_offset: usize,
33 mouse_reporting: bool,
35 app_cursor_keys: bool,
37 bracketed_paste: bool,
39 master: Option<Box<dyn MasterPty + Send>>,
41 child: Option<Box<dyn portable_pty::Child + Send + Sync>>,
42 writer: Option<Box<dyn Write + Send>>,
43 rx: Option<Receiver<Vec<u8>>>,
44 scrollback: Vec<Vec<Cell>>,
45 fg: Color,
46 bg: Color,
47 bold: bool,
48 reverse: bool,
49 pending: Vec<u8>,
51 pub started: bool,
52 pub close_confirm: bool,
54}
55
56struct SavedScreen {
57 rows: Vec<Vec<Cell>>,
58 cursor_row: usize,
59 cursor_col: usize,
60 scrollback: Vec<Vec<Cell>>,
61}
62
63#[derive(Clone)]
64struct Cell {
65 ch: char,
66 fg: Option<Color>,
67 bg: Option<Color>,
68}
69
70impl Cell {
71 fn blank() -> Self {
72 Self {
73 ch: ' ',
74 fg: None,
75 bg: None,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Color {
82 Default,
83 Black,
84 Red,
85 Green,
86 Yellow,
87 Blue,
88 Magenta,
89 Cyan,
90 White,
91 BrightBlack,
92 BrightRed,
93 BrightGreen,
94 BrightYellow,
95 BrightBlue,
96 BrightMagenta,
97 BrightCyan,
98 BrightWhite,
99 Rgb(u8, u8, u8),
100}
101
102impl Color {
103 fn to_ratatui(self) -> ratatui::style::Color {
104 match self {
105 Color::Default => ratatui::style::Color::Rgb(0, 0, 0),
107 Color::Black => ratatui::style::Color::Rgb(0, 0, 0),
108 Color::Red => ratatui::style::Color::Red,
109 Color::Green => ratatui::style::Color::Green,
110 Color::Yellow => ratatui::style::Color::Yellow,
111 Color::Blue => ratatui::style::Color::Blue,
112 Color::Magenta => ratatui::style::Color::Magenta,
113 Color::Cyan => ratatui::style::Color::Cyan,
114 Color::White => ratatui::style::Color::White,
115 Color::BrightBlack => ratatui::style::Color::Gray,
116 Color::BrightRed => ratatui::style::Color::LightRed,
117 Color::BrightGreen => ratatui::style::Color::LightGreen,
118 Color::BrightYellow => ratatui::style::Color::LightYellow,
119 Color::BrightBlue => ratatui::style::Color::LightBlue,
120 Color::BrightMagenta => ratatui::style::Color::LightMagenta,
121 Color::BrightCyan => ratatui::style::Color::LightCyan,
122 Color::BrightWhite => ratatui::style::Color::White,
123 Color::Rgb(r, g, b) => ratatui::style::Color::Rgb(r, g, b),
124 }
125 }
126}
127
128fn blank_grid(cols: u16, rows: u16) -> Vec<Vec<Cell>> {
129 vec![vec![Cell::blank(); cols as usize]; rows as usize]
130}
131
132impl Default for Terminal {
133 fn default() -> Self {
134 let (cols, rows) = (80, 24);
135 Self {
136 open: false,
137 full_panel: false,
138 pane_bound: None,
139 rows: blank_grid(cols, rows),
140 saved_primary: None,
141 alt_screen: false,
142 cursor_row: 0,
143 cursor_col: 0,
144 saved_cursor: (0, 0),
145 cols,
146 rows_count: rows,
147 scroll_offset: 0,
148 mouse_reporting: false,
149 app_cursor_keys: false,
150 bracketed_paste: false,
151 master: None,
152 child: None,
153 writer: None,
154 rx: None,
155 scrollback: Vec::new(),
156 fg: Color::Default,
157 bg: Color::Default,
158 bold: false,
159 reverse: false,
160 pending: Vec::new(),
161 started: false,
162 close_confirm: false,
163 }
164 }
165}
166
167impl Terminal {
168 pub fn new() -> Self {
169 Self::default()
170 }
171
172 pub fn cols(&self) -> u16 {
173 self.cols
174 }
175 pub fn rows_count(&self) -> u16 {
176 self.rows_count
177 }
178
179 pub fn resize(&mut self, cols: u16, rows: u16) {
181 let cols = cols.max(2);
182 let rows = rows.max(2);
183 if cols == self.cols && rows == self.rows_count {
184 return;
187 }
188 self.resize_grid(cols, rows);
189 if let Some(ref master) = self.master {
190 let _ = master.resize(PtySize {
191 rows,
192 cols,
193 pixel_width: 0,
194 pixel_height: 0,
195 });
196 }
197 }
198
199 fn resize_grid(&mut self, cols: u16, rows: u16) {
200 let resize_buf = |grid: &mut Vec<Vec<Cell>>| {
201 let mut new_rows = Vec::with_capacity(rows as usize);
202 for r in 0..rows as usize {
203 let mut row = if r < grid.len() {
204 let mut old = grid[r].clone();
205 old.resize(cols as usize, Cell::blank());
206 old.truncate(cols as usize);
207 old
208 } else {
209 vec![Cell::blank(); cols as usize]
210 };
211 if row.len() != cols as usize {
212 row.resize(cols as usize, Cell::blank());
213 }
214 new_rows.push(row);
215 }
216 *grid = new_rows;
217 };
218 resize_buf(&mut self.rows);
219 if let Some(ref mut saved) = self.saved_primary {
220 resize_buf(&mut saved.rows);
221 saved.cursor_row = saved.cursor_row.min(rows as usize - 1);
222 saved.cursor_col = saved.cursor_col.min(cols as usize - 1);
223 }
224 self.cols = cols;
225 self.rows_count = rows;
226 self.cursor_row = self.cursor_row.min(rows as usize - 1);
227 self.cursor_col = self.cursor_col.min(cols as usize - 1);
228 }
229
230 pub fn start(&mut self, anchor: Option<&PathBuf>) {
232 if self.started {
233 return;
234 }
235
236 let cwd = anchor
237 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
238 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
239
240 let shell = std::env::var("SHELL").unwrap_or_else(|_| {
241 if cfg!(windows) {
242 "powershell.exe".into()
243 } else {
244 "/bin/zsh".into()
245 }
246 });
247
248 let pty_system = native_pty_system();
249 let pair = match pty_system.openpty(PtySize {
250 rows: self.rows_count,
251 cols: self.cols,
252 pixel_width: 0,
253 pixel_height: 0,
254 }) {
255 Ok(p) => p,
256 Err(_) => {
257 return;
259 }
260 };
261
262 let mut cmd = CommandBuilder::new(&shell);
263 cmd.cwd(cwd);
264 cmd.env("TERM", "xterm-256color");
265 cmd.env("COLORTERM", "truecolor");
266 cmd.env("COLUMNS", self.cols.to_string());
267 cmd.env("LINES", self.rows_count.to_string());
268 cmd.env_remove("KITTY_WINDOW_ID");
270 cmd.env_remove("WEZTERM_PANE");
271
272 let child = match pair.slave.spawn_command(cmd) {
273 Ok(c) => c,
274 Err(_) => return,
275 };
276
277 let mut reader = match pair.master.try_clone_reader() {
278 Ok(r) => r,
279 Err(_) => return,
280 };
281 let writer = match pair.master.take_writer() {
282 Ok(w) => w,
283 Err(_) => return,
284 };
285
286 let (tx, rx) = mpsc::channel::<Vec<u8>>();
287 thread::spawn(move || {
288 let mut buf = [0u8; 8192];
289 loop {
290 match reader.read(&mut buf) {
291 Ok(0) | Err(_) => break,
292 Ok(n) => {
293 if tx.send(buf[..n].to_vec()).is_err() {
294 break;
295 }
296 }
297 }
298 }
299 });
300
301 self.master = Some(pair.master);
302 self.child = Some(child);
303 self.writer = Some(writer);
304 self.rx = Some(rx);
305 self.open = true;
306 self.started = true;
307 self.pending.clear();
308 self.alt_screen = false;
309 self.saved_primary = None;
310 self.rows = blank_grid(self.cols, self.rows_count);
311 self.cursor_row = 0;
312 self.cursor_col = 0;
313 self.scrollback.clear();
314 self.scroll_offset = 0;
315 self.mouse_reporting = false;
316 self.app_cursor_keys = false;
317 self.bracketed_paste = false;
318 self.fg = Color::Default;
319 self.bg = Color::Default;
320 self.bold = false;
321 self.reverse = false;
322 }
323
324 pub fn shutdown(&mut self) {
325 self.writer = None;
327 if let Some(mut child) = self.child.take() {
328 let _ = child.kill();
329 let _ = child.wait();
330 }
331 self.master = None;
332 self.rx = None;
333 self.started = false;
334 self.open = false;
335 self.full_panel = false;
336 self.pane_bound = None;
337 self.close_confirm = false;
338 self.pending.clear();
339 self.alt_screen = false;
340 self.saved_primary = None;
341 self.rows = blank_grid(self.cols, self.rows_count);
342 self.cursor_row = 0;
343 self.cursor_col = 0;
344 self.scrollback.clear();
345 self.scroll_offset = 0;
346 self.mouse_reporting = false;
347 self.app_cursor_keys = false;
348 self.bracketed_paste = false;
349 self.fg = Color::Default;
350 self.bg = Color::Default;
351 self.bold = false;
352 self.reverse = false;
353 }
354
355 pub fn write_input(&mut self, bytes: &[u8]) {
356 self.scroll_offset = 0;
358 if let Some(ref mut w) = self.writer {
359 let _ = w.write_all(bytes);
360 let _ = w.flush();
361 }
362 }
363
364 pub fn paste_input(&mut self, text: &str) {
369 if text.is_empty() {
370 return;
371 }
372 if self.bracketed_paste {
373 let mut buf = Vec::with_capacity(text.len() + 12);
374 buf.extend_from_slice(b"\x1b[200~");
375 buf.extend_from_slice(text.as_bytes());
376 buf.extend_from_slice(b"\x1b[201~");
377 self.write_input(&buf);
378 } else {
379 self.write_input(text.as_bytes());
380 }
381 }
382
383 pub fn poll(&mut self) {
384 let data = if let Some(ref rx) = self.rx {
385 let mut all = Vec::new();
386 loop {
387 match rx.try_recv() {
388 Ok(part) => all.extend_from_slice(&part),
389 Err(TryRecvError::Empty) => break,
390 Err(TryRecvError::Disconnected) => break,
391 }
392 }
393 if all.is_empty() {
394 return;
395 }
396 all
397 } else {
398 return;
399 };
400 self.process_output(&data);
401 }
402
403 fn process_output(&mut self, data: &[u8]) {
404 self.pending.extend_from_slice(data);
405 let buf = std::mem::take(&mut self.pending);
406 let mut i = 0;
407 while i < buf.len() {
408 match self.try_consume(&buf, i) {
409 Consume::Advanced(n) => i = n,
410 Consume::NeedMore => {
411 self.pending = buf[i..].to_vec();
412 if self.pending.len() > 8192 {
413 self.pending.clear();
414 }
415 break;
416 }
417 }
418 }
419 }
420
421 fn try_consume(&mut self, data: &[u8], i: usize) -> Consume {
422 let b = data[i];
423 if b == 0x1b {
424 if i + 1 >= data.len() {
425 return Consume::NeedMore;
426 }
427 let n = data[i + 1];
428 match n {
429 b'[' => return self.consume_csi(data, i + 2),
430 b']' => return self.consume_osc(data, i + 2),
431 b'P' | b'X' | b'^' | b'_' => return self.consume_string_seq(data, i + 2),
432 b'\\' => return Consume::Advanced(i + 2),
433 b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' => {
434 if i + 2 >= data.len() {
435 return Consume::NeedMore;
436 }
437 return Consume::Advanced(i + 3);
438 }
439 b'7' => {
440 self.saved_cursor = (self.cursor_row, self.cursor_col);
441 return Consume::Advanced(i + 2);
442 }
443 b'8' => {
444 self.cursor_row = self.saved_cursor.0.min(self.rows_count as usize - 1);
445 self.cursor_col = self.saved_cursor.1.min(self.cols as usize - 1);
446 return Consume::Advanced(i + 2);
447 }
448 b'=' | b'>' | b'c' | b'M' | b'E' | b'D' | b'H' | b'Z' => {
449 return Consume::Advanced(i + 2);
450 }
451 _ if n >= 0x20 && n < 0x7f => return Consume::Advanced(i + 2),
452 _ => return Consume::Advanced(i + 1),
453 }
454 }
455
456 match b {
457 b'\n' => {
458 self.newline();
459 Consume::Advanced(i + 1)
460 }
461 b'\r' => {
462 self.cursor_col = 0;
463 Consume::Advanced(i + 1)
464 }
465 0x08 | 0x7f => {
466 if self.cursor_col > 0 {
467 self.cursor_col -= 1;
468 }
469 Consume::Advanced(i + 1)
470 }
471 b'\t' => {
472 let next = ((self.cursor_col / 8) + 1) * 8;
473 while self.cursor_col < next && self.cursor_col < self.cols as usize {
474 self.write_char(' ');
475 }
476 Consume::Advanced(i + 1)
477 }
478 0x07 | 0x0e | 0x0f => Consume::Advanced(i + 1),
479 b if b >= 0x80 => self.consume_utf8(data, i),
480 b if b >= 0x20 => {
481 self.write_char(b as char);
482 Consume::Advanced(i + 1)
483 }
484 _ => Consume::Advanced(i + 1),
485 }
486 }
487
488 fn consume_utf8(&mut self, data: &[u8], i: usize) -> Consume {
489 let b0 = data[i];
490 let need = if b0 & 0xE0 == 0xC0 {
491 2
492 } else if b0 & 0xF0 == 0xE0 {
493 3
494 } else if b0 & 0xF8 == 0xF0 {
495 4
496 } else {
497 return Consume::Advanced(i + 1);
498 };
499 if i + need > data.len() {
500 return Consume::NeedMore;
501 }
502 match std::str::from_utf8(&data[i..i + need]) {
503 Ok(s) => {
504 if let Some(ch) = s.chars().next() {
505 self.write_char(ch);
506 }
507 Consume::Advanced(i + need)
508 }
509 Err(_) => Consume::Advanced(i + 1),
510 }
511 }
512
513 fn consume_csi(&mut self, data: &[u8], start: usize) -> Consume {
514 let mut i = start;
515 let mut private = None;
516 if i < data.len() && matches!(data[i], b'?' | b'>' | b'=' | b'<') {
517 private = Some(data[i] as char);
518 i += 1;
519 }
520 let param_start = i;
521 while i < data.len() {
522 let b = data[i];
523 if b.is_ascii_digit() || b == b';' || b == b':' || b == b' ' {
524 i += 1;
525 continue;
526 }
527 if (0x20..=0x2F).contains(&b) {
528 i += 1;
529 continue;
530 }
531 if (0x40..=0x7E).contains(&b) {
532 let params = parse_csi_params(&data[param_start..i]);
533 self.apply_csi(b as char, ¶ms, private);
534 return Consume::Advanced(i + 1);
535 }
536 return Consume::Advanced(i + 1);
537 }
538 Consume::NeedMore
539 }
540
541 fn consume_osc(&mut self, data: &[u8], start: usize) -> Consume {
542 let mut i = start;
543 while i < data.len() {
544 if data[i] == 0x07 {
545 return Consume::Advanced(i + 1);
546 }
547 if data[i] == 0x1b {
548 if i + 1 >= data.len() {
549 return Consume::NeedMore;
550 }
551 if data[i + 1] == b'\\' {
552 return Consume::Advanced(i + 2);
553 }
554 return Consume::Advanced(i);
555 }
556 i += 1;
557 }
558 Consume::NeedMore
559 }
560
561 fn consume_string_seq(&mut self, data: &[u8], start: usize) -> Consume {
562 self.consume_osc(data, start)
563 }
564
565 fn apply_csi(&mut self, cmd: char, nums: &[i32], private: Option<char>) {
566 if private == Some('?') && (cmd == 'h' || cmd == 'l') {
568 let enable = cmd == 'h';
569 for &mode in nums {
570 self.apply_private_mode(mode, enable);
571 }
572 return;
573 }
574 if private.is_some() {
575 return;
577 }
578
579 let n = |i: usize, d: i32| -> i32 {
580 nums.get(i)
581 .copied()
582 .filter(|&v| v != 0)
583 .unwrap_or(d)
584 };
585 let n0 = |i: usize, d: i32| -> i32 { nums.get(i).copied().unwrap_or(d) };
586
587 match cmd {
588 'A' => {
589 self.cursor_row = self
590 .cursor_row
591 .saturating_sub(n(0, 1).max(1) as usize)
592 }
593 'B' => {
594 self.cursor_row = (self.cursor_row + n(0, 1).max(1) as usize)
595 .min(self.rows_count as usize - 1)
596 }
597 'C' => {
598 self.cursor_col = (self.cursor_col + n(0, 1).max(1) as usize)
599 .min(self.cols as usize - 1)
600 }
601 'D' => {
602 self.cursor_col = self
603 .cursor_col
604 .saturating_sub(n(0, 1).max(1) as usize)
605 }
606 'E' => {
607 self.cursor_row = (self.cursor_row + n(0, 1).max(1) as usize)
608 .min(self.rows_count as usize - 1);
609 self.cursor_col = 0;
610 }
611 'F' => {
612 self.cursor_row = self
613 .cursor_row
614 .saturating_sub(n(0, 1).max(1) as usize);
615 self.cursor_col = 0;
616 }
617 'G' => {
618 self.cursor_col = (n(0, 1).max(1) as usize - 1).min(self.cols as usize - 1);
619 }
620 'H' | 'f' => {
621 self.cursor_row =
622 (n(0, 1).max(1) as usize - 1).min(self.rows_count as usize - 1);
623 self.cursor_col =
624 (n(1, 1).max(1) as usize - 1).min(self.cols as usize - 1);
625 }
626 'd' => {
627 self.cursor_row =
628 (n(0, 1).max(1) as usize - 1).min(self.rows_count as usize - 1);
629 }
630 'J' => self.erase_display(n0(0, 0)),
631 'K' => self.erase_line(n0(0, 0)),
632 'S' => {
633 let n = n(0, 1).max(1) as usize;
634 for _ in 0..n {
635 self.scroll_up_one();
636 }
637 }
638 'T' => {
639 let n = n(0, 1).max(1) as usize;
640 for _ in 0..n {
641 self.scroll_down_one();
642 }
643 }
644 '@' => {
645 let n = n(0, 1).max(1) as usize;
646 let r = self.cursor_row;
647 let c = self.cursor_col;
648 let row = &mut self.rows[r];
649 for _ in 0..n {
650 if c < row.len() {
651 row.insert(c, Cell::blank());
652 if row.len() > self.cols as usize {
653 row.pop();
654 }
655 }
656 }
657 }
658 'P' => {
659 let n = n(0, 1).max(1) as usize;
660 let r = self.cursor_row;
661 let c = self.cursor_col;
662 let row = &mut self.rows[r];
663 for _ in 0..n {
664 if c < row.len() {
665 row.remove(c);
666 row.push(Cell::blank());
667 }
668 }
669 }
670 'X' => {
671 let n = n(0, 1).max(1) as usize;
672 let r = self.cursor_row;
673 for c in self.cursor_col..(self.cursor_col + n).min(self.cols as usize) {
674 self.rows[r][c] = Cell::blank();
675 }
676 }
677 's' => self.saved_cursor = (self.cursor_row, self.cursor_col),
678 'u' => {
679 self.cursor_row = self.saved_cursor.0.min(self.rows_count as usize - 1);
680 self.cursor_col = self.saved_cursor.1.min(self.cols as usize - 1);
681 }
682 'm' => self.apply_sgr(nums),
683 'n' | 'r' | 't' => {}
684 _ => {}
685 }
686 }
687
688 fn apply_private_mode(&mut self, mode: i32, enable: bool) {
689 match mode {
690 47 | 1047 | 1049 => {
692 if enable {
693 self.enter_alt_screen(mode == 1049 || mode == 1047);
694 } else {
695 self.leave_alt_screen(mode == 1049 || mode == 1047);
696 }
697 }
698 1 => self.app_cursor_keys = enable,
700 1000 | 1002 | 1003 => self.mouse_reporting = enable,
702 2004 => self.bracketed_paste = enable,
704 25 | 1006 | 1004 | 7 | 12 => {}
706 _ => {}
707 }
708 }
709
710 fn enter_alt_screen(&mut self, clear: bool) {
711 if self.alt_screen {
712 if clear {
713 self.rows = blank_grid(self.cols, self.rows_count);
714 self.cursor_row = 0;
715 self.cursor_col = 0;
716 }
717 return;
718 }
719 self.saved_primary = Some(SavedScreen {
720 rows: std::mem::replace(&mut self.rows, blank_grid(self.cols, self.rows_count)),
721 cursor_row: self.cursor_row,
722 cursor_col: self.cursor_col,
723 scrollback: std::mem::take(&mut self.scrollback),
724 });
725 self.alt_screen = true;
726 self.cursor_row = 0;
727 self.cursor_col = 0;
728 self.scroll_offset = 0;
729 if !clear {
730 }
732 }
733
734 fn leave_alt_screen(&mut self, _restore_cursor_style: bool) {
735 if !self.alt_screen {
736 return;
737 }
738 if let Some(saved) = self.saved_primary.take() {
739 self.rows = saved.rows;
740 self.cursor_row = saved.cursor_row.min(self.rows_count as usize - 1);
741 self.cursor_col = saved.cursor_col.min(self.cols as usize - 1);
742 self.scrollback = saved.scrollback;
743 } else {
744 self.rows = blank_grid(self.cols, self.rows_count);
745 self.cursor_row = 0;
746 self.cursor_col = 0;
747 }
748 self.alt_screen = false;
749 self.scroll_offset = 0;
750 }
751
752 fn erase_display(&mut self, mode: i32) {
753 if mode == 2 || mode == 3 {
754 for row in &mut self.rows {
755 for c in row.iter_mut() {
756 *c = Cell::blank();
757 }
758 }
759 if mode == 2 {
760 self.cursor_row = 0;
761 self.cursor_col = 0;
762 }
763 if mode == 3 {
764 self.scrollback.clear();
765 }
766 } else if mode == 0 {
767 for c in self.cursor_col..self.cols as usize {
768 self.rows[self.cursor_row][c] = Cell::blank();
769 }
770 for r in self.cursor_row + 1..self.rows_count as usize {
771 for c in 0..self.cols as usize {
772 self.rows[r][c] = Cell::blank();
773 }
774 }
775 } else if mode == 1 {
776 for r in 0..self.cursor_row {
777 for c in 0..self.cols as usize {
778 self.rows[r][c] = Cell::blank();
779 }
780 }
781 for c in 0..=self.cursor_col.min(self.cols as usize - 1) {
782 self.rows[self.cursor_row][c] = Cell::blank();
783 }
784 }
785 }
786
787 fn erase_line(&mut self, mode: i32) {
788 let r = self.cursor_row;
789 let range: Box<dyn Iterator<Item = usize>> = if mode == 0 {
790 Box::new(self.cursor_col..self.cols as usize)
791 } else if mode == 1 {
792 Box::new(0..=self.cursor_col.min(self.cols as usize - 1))
793 } else {
794 Box::new(0..self.cols as usize)
795 };
796 for c in range {
797 self.rows[r][c] = Cell::blank();
798 }
799 }
800
801 fn scroll_up_one(&mut self) {
802 if self.rows.is_empty() {
803 return;
804 }
805 if !self.alt_screen {
806 self.scrollback.push(self.rows[0].clone());
807 if self.scrollback.len() > 5000 {
808 let drain = self.scrollback.len() - 5000;
809 self.scrollback.drain(0..drain);
810 }
811 }
812 self.rows.remove(0);
813 self.rows
814 .push(vec![Cell::blank(); self.cols as usize]);
815 }
816
817 fn scroll_down_one(&mut self) {
818 self.rows
819 .insert(0, vec![Cell::blank(); self.cols as usize]);
820 if self.rows.len() > self.rows_count as usize {
821 self.rows.pop();
822 }
823 }
824
825 fn apply_sgr(&mut self, modes: &[i32]) {
826 let modes: Vec<i32> = if modes.is_empty() {
827 vec![0]
828 } else {
829 modes.to_vec()
830 };
831 let mut idx = 0;
832 while idx < modes.len() {
833 match modes[idx] {
834 0 => {
835 self.fg = Color::Default;
836 self.bg = Color::Default;
837 self.bold = false;
838 self.reverse = false;
839 }
840 1 => self.bold = true,
841 2 | 22 => self.bold = false,
842 7 => self.reverse = true,
843 27 => self.reverse = false,
844 30..=37 => self.fg = ansi_to_color(modes[idx] - 30),
845 38 => {
846 if let Some((c, skip)) = parse_ext_color(&modes[idx + 1..]) {
847 self.fg = c;
848 idx += skip;
849 }
850 }
851 39 => self.fg = Color::Default,
852 40..=47 => self.bg = ansi_to_color(modes[idx] - 40),
853 48 => {
854 if let Some((c, skip)) = parse_ext_color(&modes[idx + 1..]) {
855 self.bg = c;
856 idx += skip;
857 }
858 }
859 49 => self.bg = Color::Default,
860 90..=97 => self.fg = bright_to_color(modes[idx] - 90),
861 100..=107 => self.bg = bright_to_color(modes[idx] - 100),
862 _ => {}
863 }
864 idx += 1;
865 }
866 }
867
868 fn newline(&mut self) {
869 if self.cursor_row + 1 >= self.rows_count as usize {
870 self.scroll_up_one();
871 } else {
872 self.cursor_row += 1;
873 }
874 self.cursor_col = 0;
875 }
876
877 fn write_char(&mut self, ch: char) {
878 let w = UnicodeWidthChar::width(ch).unwrap_or(0);
879 if w == 0 {
880 return;
881 }
882 if self.cursor_col + w > self.cols as usize {
883 self.newline();
884 }
885 if self.cursor_row >= self.rows.len() || self.cursor_col >= self.cols as usize {
886 return;
887 }
888 let (mut fg, mut bg) = (self.fg, self.bg);
889 if self.reverse {
890 std::mem::swap(&mut fg, &mut bg);
891 }
892 let fg = if fg != Color::Default { Some(fg) } else { None };
893 let bg = if bg != Color::Default { Some(bg) } else { None };
894 self.rows[self.cursor_row][self.cursor_col] = Cell { ch, fg, bg };
895 if w >= 2 && self.cursor_col + 1 < self.cols as usize {
896 self.rows[self.cursor_row][self.cursor_col + 1] = Cell {
897 ch: ' ',
898 fg: None,
899 bg,
900 };
901 }
902 self.cursor_col += w;
903 }
904
905 fn row_cells_to_spans(
909 row: &[Cell],
910 force_black_bg: bool,
911 ) -> Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)> {
912 let mut out: Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)> =
915 Vec::new();
916 let mut i = 0;
917 while i < row.len() {
918 let cell = &row[i];
919 let w = UnicodeWidthChar::width(cell.ch).unwrap_or(1).max(1);
920 let fg = Some(cell.fg.unwrap_or(Color::Default).to_ratatui_fg());
922 let bg = if force_black_bg {
923 Some(Color::Default.to_ratatui())
924 } else {
925 Some(cell.bg.unwrap_or(Color::Default).to_ratatui())
926 };
927 match out.last_mut() {
928 Some((run, rfg, rbg)) if *rfg == fg && *rbg == bg => run.push(cell.ch),
929 _ => out.push((cell.ch.to_string(), fg, bg)),
930 }
931 i += w;
932 }
933 out
934 }
935
936 pub fn visible_rows(
937 &self,
938 ) -> Vec<Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)>> {
939 self.rows
940 .iter()
941 .map(|row| Self::row_cells_to_spans(row, false))
942 .collect()
943 }
944
945 pub fn wants_mouse(&self) -> bool {
947 self.mouse_reporting
948 }
949
950 pub fn arrow_seq(&self, dir: char) -> &'static [u8] {
952 match (self.app_cursor_keys, dir) {
953 (true, 'A') => b"\x1bOA",
954 (true, 'B') => b"\x1bOB",
955 (true, 'C') => b"\x1bOC",
956 (true, 'D') => b"\x1bOD",
957 (false, 'A') => b"\x1b[A",
958 (false, 'B') => b"\x1b[B",
959 (false, 'C') => b"\x1b[C",
960 _ => b"\x1b[D",
961 }
962 }
963
964 pub fn scroll(&self) -> usize {
965 if self.alt_screen {
967 0
968 } else {
969 self.scroll_offset
970 }
971 }
972 pub fn scroll_up(&mut self, a: usize) {
973 if self.alt_screen {
974 return;
975 }
976 self.scroll_offset = self
977 .scroll_offset
978 .saturating_add(a)
979 .min(self.scrollback.len());
980 }
981 pub fn scroll_down(&mut self, a: usize) {
982 if self.alt_screen {
983 return;
984 }
985 self.scroll_offset = self.scroll_offset.saturating_sub(a);
986 }
987 pub fn scrollback_len(&self) -> usize {
988 if self.alt_screen {
989 0
990 } else {
991 self.scrollback.len()
992 }
993 }
994
995 pub fn visible_scrollback(
996 &self,
997 ) -> Vec<Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)>> {
998 if self.alt_screen {
999 return Vec::new();
1000 }
1001 self.scrollback
1002 .iter()
1003 .map(|row| Self::row_cells_to_spans(row, true))
1004 .collect()
1005 }
1006
1007 pub fn cursor_position(&self) -> (u16, u16) {
1008 (self.cursor_col as u16, self.cursor_row as u16)
1009 }
1010
1011 pub fn is_alt_screen(&self) -> bool {
1012 self.alt_screen
1013 }
1014}
1015
1016impl Color {
1017 fn to_ratatui_fg(self) -> ratatui::style::Color {
1018 match self {
1019 Color::Default => ratatui::style::Color::Rgb(200, 200, 200),
1020 other => other.to_ratatui(),
1021 }
1022 }
1023}
1024
1025enum Consume {
1026 Advanced(usize),
1027 NeedMore,
1028}
1029
1030fn parse_csi_params(bytes: &[u8]) -> Vec<i32> {
1031 let s = String::from_utf8_lossy(bytes);
1032 if s.is_empty() {
1033 return Vec::new();
1034 }
1035 let mut out = Vec::new();
1036 for part in s.split(';') {
1037 if part.is_empty() {
1038 out.push(0);
1039 continue;
1040 }
1041 if part.contains(':') {
1042 for sub in part.split(':') {
1043 out.push(sub.parse::<i32>().unwrap_or(0));
1044 }
1045 } else {
1046 out.push(part.parse::<i32>().unwrap_or(0));
1047 }
1048 }
1049 out
1050}
1051
1052fn parse_ext_color(rest: &[i32]) -> Option<(Color, usize)> {
1053 if rest.is_empty() {
1054 return None;
1055 }
1056 match rest[0] {
1057 5 if rest.len() >= 2 => Some((index_to_color(rest[1]), 2)),
1058 2 if rest.len() >= 4 => {
1059 let r = rest[1].clamp(0, 255) as u8;
1060 let g = rest[2].clamp(0, 255) as u8;
1061 let b = rest[3].clamp(0, 255) as u8;
1062 Some((Color::Rgb(r, g, b), 4))
1063 }
1064 _ => Some((Color::Default, 1)),
1065 }
1066}
1067
1068fn ansi_to_color(i: i32) -> Color {
1069 match i {
1070 0 => Color::Black,
1071 1 => Color::Red,
1072 2 => Color::Green,
1073 3 => Color::Yellow,
1074 4 => Color::Blue,
1075 5 => Color::Magenta,
1076 6 => Color::Cyan,
1077 7 => Color::White,
1078 _ => Color::Default,
1079 }
1080}
1081fn bright_to_color(i: i32) -> Color {
1082 match i {
1083 0 => Color::BrightBlack,
1084 1 => Color::BrightRed,
1085 2 => Color::BrightGreen,
1086 3 => Color::BrightYellow,
1087 4 => Color::BrightBlue,
1088 5 => Color::BrightMagenta,
1089 6 => Color::BrightCyan,
1090 7 => Color::BrightWhite,
1091 _ => Color::Default,
1092 }
1093}
1094fn index_to_color(i: i32) -> Color {
1095 match i {
1096 0..=7 => ansi_to_color(i),
1097 8..=15 => bright_to_color(i - 8),
1098 16..=231 => {
1099 let n = i - 16;
1100 let r = ((n / 36) % 6) * 51;
1101 let g = ((n / 6) % 6) * 51;
1102 let b = (n % 6) * 51;
1103 Color::Rgb(r as u8, g as u8, b as u8)
1104 }
1105 232..=255 => {
1106 let v = ((i - 232) * 10 + 8).clamp(0, 255) as u8;
1107 Color::Rgb(v, v, v)
1108 }
1109 _ => Color::Default,
1110 }
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116
1117 #[test]
1118 fn utf8_box_drawing_not_mojibake() {
1119 let mut t = Terminal::new();
1120 t.process_output(&[0xe2, 0x94, 0x80]);
1121 assert_eq!(t.rows[0][0].ch, '─');
1122 }
1123
1124 #[test]
1125 fn osc_title_is_swallowed() {
1126 let mut t = Terminal::new();
1127 let mut seq = b"\x1b]0;hello\x07".to_vec();
1128 seq.extend_from_slice(b"ok");
1129 t.process_output(&seq);
1130 assert_eq!(t.rows[0][0].ch, 'o');
1131 assert_eq!(t.rows[0][1].ch, 'k');
1132 }
1133
1134 #[test]
1135 fn incomplete_utf8_held_across_chunks() {
1136 let mut t = Terminal::new();
1137 t.process_output(&[0xe2]);
1138 t.process_output(&[0x94, 0x80]);
1139 assert_eq!(t.rows[0][0].ch, '─');
1140 }
1141
1142 #[test]
1143 fn alt_screen_enter_leave() {
1144 let mut t = Terminal::new();
1145 t.process_output(b"hello");
1146 assert_eq!(t.rows[0][0].ch, 'h');
1147 t.process_output(b"\x1b[?1049h");
1149 assert!(t.alt_screen);
1150 assert_eq!(t.rows[0][0].ch, ' ');
1151 t.process_output(b"alt");
1152 assert_eq!(t.rows[0][0].ch, 'a');
1153 t.process_output(b"\x1b[?1049l");
1155 assert!(!t.alt_screen);
1156 assert_eq!(t.rows[0][0].ch, 'h');
1157 }
1158
1159 #[test]
1160 fn cup_and_clear() {
1161 let mut t = Terminal::new();
1162 t.process_output(b"\x1b[10;5H*");
1163 assert_eq!(t.cursor_row, 9);
1164 assert_eq!(t.cursor_col, 5); assert_eq!(t.rows[9][4].ch, '*');
1166 t.process_output(b"\x1b[2J");
1167 assert_eq!(t.rows[9][4].ch, ' ');
1168 assert_eq!(t.cursor_row, 0);
1169 }
1170}