1mod edit;
2mod modes;
3mod report;
4
5use crate::cell::Cell;
6use crate::cursor::Cursor;
7use crate::mode::TerminalModes;
8use crate::parser::{CsiSequence, ParserState};
9use crate::region::Region;
10use crate::screen::{Screen, ScreenKind, default_tab_stops, plain_text_for_screen};
11use crate::style::Style;
12
13pub struct Terminal {
18 cols: usize,
19 rows: usize,
20 primary: Screen,
21 alternate: Screen,
22 active: ScreenKind,
23 current_style: Style,
24 state: ParserState,
25 csi_buffer: String,
26 csi_intermediate: u8,
27 osc_buffer: Vec<u8>,
28 utf8_buffer: Vec<u8>,
29 utf8_remaining: usize,
30 output: Vec<u8>,
31 clipboard: Vec<String>,
32 bell_count: usize,
33 title: Option<String>,
34 modes: TerminalModes,
35 tab_stops: Vec<bool>,
36 last_printed: Option<char>,
37 scroll_region: Region,
38 max_scrollback: usize,
39}
40
41impl Terminal {
42 pub fn new(cols: usize, rows: usize) -> Self {
44 let style = Style::default();
45 let scroll_region = Region {
46 top: 0,
47 bottom: rows.saturating_sub(1),
48 };
49 Self {
50 cols,
51 rows,
52 primary: Screen::new(cols, rows, style),
53 alternate: Screen::new(cols, rows, style),
54 active: ScreenKind::Primary,
55 current_style: style,
56 state: ParserState::Ground,
57 csi_buffer: String::new(),
58 csi_intermediate: 0,
59 osc_buffer: Vec::new(),
60 utf8_buffer: Vec::new(),
61 utf8_remaining: 0,
62 output: Vec::new(),
63 clipboard: Vec::new(),
64 bell_count: 0,
65 title: None,
66 modes: TerminalModes::default(),
67 tab_stops: default_tab_stops(cols),
68 last_printed: None,
69 scroll_region,
70 max_scrollback: 1000,
71 }
72 }
73
74 pub fn set_max_scrollback(&mut self, max: usize) {
76 self.max_scrollback = max;
77 }
78
79 pub fn write(&mut self, bytes: &[u8]) {
81 for &byte in bytes {
82 self.advance(byte);
83 }
84 }
85
86 pub fn take_output(&mut self) -> Vec<u8> {
88 std::mem::take(&mut self.output)
89 }
90
91 pub fn take_clipboard(&mut self) -> Vec<String> {
93 std::mem::take(&mut self.clipboard)
94 }
95
96 pub fn resize(&mut self, cols: usize, rows: usize) {
98 if cols == self.cols && rows == self.rows {
99 return;
100 }
101 let style = self.current_style;
102 self.primary.resize(self.cols, self.rows, cols, rows, style);
103 self.alternate
104 .resize(self.cols, self.rows, cols, rows, style);
105 self.cols = cols;
106 self.rows = rows;
107 self.scroll_region = Region {
108 top: 0,
109 bottom: rows.saturating_sub(1),
110 };
111 self.tab_stops = default_tab_stops(cols);
112 }
113
114 pub fn cols(&self) -> usize {
117 self.cols
118 }
119
120 pub fn rows(&self) -> usize {
121 self.rows
122 }
123
124 pub fn active_screen(&self) -> ScreenKind {
125 self.active
126 }
127
128 pub fn cursor(&self) -> Cursor {
129 self.screen().cursor
130 }
131
132 pub fn current_style(&self) -> &Style {
133 &self.current_style
134 }
135
136 pub fn bell_count(&self) -> usize {
137 self.bell_count
138 }
139
140 pub fn title(&self) -> Option<&str> {
141 self.title.as_deref()
142 }
143
144 pub fn cell(&self, col: usize, row: usize) -> Option<&Cell> {
146 if col >= self.cols || row >= self.rows {
147 return None;
148 }
149 let screen = self.screen();
150 let idx = Screen::index(self.cols, col, row);
151 screen.grid.get(idx)
152 }
153
154 pub fn grid(&self) -> &[Cell] {
156 &self.screen().grid
157 }
158
159 pub fn scrollback_len(&self) -> usize {
161 self.screen().scrollback.len()
162 }
163
164 pub fn scrollback_row(&self, index: usize) -> Option<String> {
166 self.screen().scrollback.get(index).map(|row| {
167 let end = row
168 .iter()
169 .rposition(|cell| !cell.is_blank())
170 .map_or(0, |idx| idx + 1);
171 let mut out = String::new();
172 for cell in &row[..end] {
173 if !cell.is_wide_continuation() {
174 out.push(cell.ch());
175 }
176 }
177 out
178 })
179 }
180
181 pub fn plain_text(&self) -> String {
183 plain_text_for_screen(self.screen(), self.cols, self.rows)
184 }
185
186 pub fn screen_dump(&self) -> String {
188 let screen = self.screen();
189 let mut out = String::new();
190
191 for row in &screen.scrollback {
192 if !out.is_empty() {
193 out.push('\n');
194 }
195 for cell in row {
196 if !cell.is_wide_continuation() {
197 out.push(cell.ch());
198 }
199 }
200 }
201
202 let visible = plain_text_for_screen(screen, self.cols, self.rows);
203 if !visible.is_empty() {
204 if !out.is_empty() {
205 out.push('\n');
206 }
207 out.push_str(&visible);
208 }
209
210 out
211 }
212
213 pub fn wraparound(&self) -> bool {
216 self.modes.wraparound
217 }
218 pub fn cursor_visible(&self) -> bool {
219 self.modes.cursor_visible
220 }
221 pub fn cursor_shape(&self) -> crate::mode::CursorShape {
222 self.modes.cursor_shape
223 }
224 pub fn application_cursor_keys(&self) -> bool {
225 self.modes.application_cursor_keys
226 }
227 pub fn bracketed_paste(&self) -> bool {
228 self.modes.bracketed_paste
229 }
230 pub fn focus_reporting(&self) -> bool {
231 self.modes.focus_reporting
232 }
233 pub fn mouse_tracking(&self) -> Option<crate::mode::MouseTracking> {
234 self.modes.mouse_tracking
235 }
236 pub fn sgr_mouse(&self) -> bool {
237 self.modes.sgr_mouse
238 }
239 pub fn scroll_region(&self) -> (usize, usize) {
240 (self.scroll_region.top, self.scroll_region.bottom)
241 }
242
243 fn screen(&self) -> &Screen {
246 match self.active {
247 ScreenKind::Primary => &self.primary,
248 ScreenKind::Alternate => &self.alternate,
249 }
250 }
251
252 fn screen_mut(&mut self) -> &mut Screen {
253 match self.active {
254 ScreenKind::Primary => &mut self.primary,
255 ScreenKind::Alternate => &mut self.alternate,
256 }
257 }
258
259 fn advance(&mut self, byte: u8) {
260 match self.state {
261 ParserState::Ground => self.advance_ground(byte),
262 ParserState::Escape => self.advance_escape(byte),
263 ParserState::Csi => self.advance_csi(byte),
264 ParserState::Osc => self.advance_osc(byte),
265 }
266 }
267
268 fn advance_ground(&mut self, byte: u8) {
269 match byte {
270 0x07 => self.bell_count += 1,
272 0x08 => self.backspace(),
273 0x09 => self.horizontal_tab(),
274 0x0A | 0x0B | 0x0C => self.linefeed(),
275 0x0D => self.carriage_return(),
276 0x1B => {
277 self.state = ParserState::Escape;
278 }
279 0x20..=0x7E => {
281 let ch = byte as char;
282 self.print_char(ch);
283 }
284 0xC0..=0xDF => {
286 self.utf8_buffer.clear();
287 self.utf8_buffer.push(byte);
288 self.utf8_remaining = 1;
289 }
290 0xE0..=0xEF => {
291 self.utf8_buffer.clear();
292 self.utf8_buffer.push(byte);
293 self.utf8_remaining = 2;
294 }
295 0xF0..=0xF7 => {
296 self.utf8_buffer.clear();
297 self.utf8_buffer.push(byte);
298 self.utf8_remaining = 3;
299 }
300 0x80..=0xBF if self.utf8_remaining > 0 => {
302 self.advance_utf8(byte);
303 }
304 _ => {} }
306 }
307
308 fn advance_escape(&mut self, byte: u8) {
309 match byte {
310 b'[' => {
311 self.csi_buffer.clear();
312 self.state = ParserState::Csi;
313 return; }
315 b']' => {
316 self.osc_buffer.clear();
317 self.state = ParserState::Osc;
318 return; }
320 b'7' => self.save_cursor(),
321 b'8' => self.restore_cursor(),
322 b'M' => self.reverse_index(),
323 b'c' => self.full_reset(),
324 b'H' => self.set_tab_stop(),
325 b'(' | b')' => {} _ => {}
327 }
328 self.state = ParserState::Ground;
329 }
330
331 fn advance_csi(&mut self, byte: u8) {
332 match byte {
333 0x30..=0x3F => {
335 self.csi_buffer.push(byte as char);
336 }
337 0x20..=0x2F => {
339 self.csi_intermediate = byte;
340 }
341 0x40..=0x7E => {
343 let raw = self.csi_buffer.clone();
344 let intermediate = self.csi_intermediate;
345 let csi = CsiSequence::parse(&raw);
346 self.dispatch_csi(byte, &csi, intermediate);
347 self.csi_buffer.clear();
348 self.csi_intermediate = 0;
349 self.state = ParserState::Ground;
350 }
351 _ => {
353 self.csi_buffer.clear();
354 self.csi_intermediate = 0;
355 self.state = ParserState::Ground;
356 }
357 }
358 }
359
360 fn advance_osc(&mut self, byte: u8) {
361 match byte {
362 0x07 => {
363 self.finish_osc();
365 self.state = ParserState::Ground;
366 }
367 0x1B => {
368 self.osc_buffer.push(byte);
370 }
371 b'\\' if self.osc_buffer.last() == Some(&0x1B) => {
372 self.osc_buffer.pop(); self.finish_osc();
375 self.state = ParserState::Ground;
376 }
377 _ => {
378 self.osc_buffer.push(byte);
379 }
380 }
381 }
382
383 fn dispatch_csi(&mut self, final_byte: u8, csi: &CsiSequence, intermediate: u8) {
384 match final_byte {
385 b'A' => self.cursor_up(csi.param_or(0, 1)),
387 b'B' => self.cursor_down(csi.param_or(0, 1)),
388 b'C' => self.cursor_right(csi.param_or(0, 1)),
389 b'D' => self.cursor_left(csi.param_or(0, 1)),
390 b'E' => self.cursor_next_line(csi.param_or(0, 1)),
391 b'F' => self.cursor_previous_line(csi.param_or(0, 1)),
392 b'G' | b'`' => {
393 let col = csi.one_based_to_zero(0).min(self.cols.saturating_sub(1));
394 self.set_cursor(col, self.cursor().row);
395 }
396 b'H' | b'f' => {
397 let row = csi.one_based_to_zero(0).min(self.rows.saturating_sub(1));
398 let col = csi.one_based_to_zero(1).min(self.cols.saturating_sub(1));
399 self.set_cursor(col, row);
400 }
401 b'd' => {
402 let row = csi.one_based_to_zero(0).min(self.rows.saturating_sub(1));
403 self.set_cursor(self.cursor().col, row);
404 }
405 b'J' => self.erase_display(csi.param_or(0, 0)),
407 b'K' => self.erase_line(csi.param_or(0, 0)),
408 b'X' => self.erase_chars(csi.param_or(0, 1)),
409 b'@' => self.insert_blank_chars(csi.param_or(0, 1)),
411 b'P' => self.delete_chars(csi.param_or(0, 1)),
412 b'L' => self.insert_lines(csi.param_or(0, 1)),
413 b'M' => self.delete_lines(csi.param_or(0, 1)),
414 b'S' => self.scroll_up_n(csi.param_or(0, 1)),
416 b'T' => self.scroll_down_n(csi.param_or(0, 1)),
417 b'm' => self.select_graphic_rendition(&csi.params),
419 b'h' => self.set_private_modes(&csi.params, true),
421 b'l' => self.set_private_modes(&csi.params, false),
422 b'q' => {
424 if intermediate == b' ' {
426 self.set_cursor_shape(csi.param_or(0, 0));
427 }
428 }
429 b'n' => self.device_status_report(csi.private, &csi.params),
431 b'c' => self.device_attributes(&csi.raw),
432 b'r' => self.set_scroll_region(&csi.params),
434 b'I' => self.horizontal_tab_n(csi.param_or(0, 1)),
436 b'Z' => self.horizontal_tab_back_n(csi.param_or(0, 1)),
437 b'g' => self.clear_tabs(&csi.params),
438 b'b' => self.repeat_preceding_char(csi.param_or(0, 1)),
439 _ => {}
441 }
442 }
443
444 fn finish_osc(&mut self) {
445 let raw = &self.osc_buffer;
446 if let Some(semi_pos) = raw.iter().position(|&b| b == b';') {
449 let ps = &raw[..semi_pos];
450 let pt = &raw[semi_pos + 1..];
451
452 match ps {
453 b"0" | b"2" => {
454 if let Ok(title) = std::str::from_utf8(pt) {
456 self.title = Some(title.to_string());
457 }
458 }
459 b"52" => {
460 let data_start = if pt.first().map_or(false, |b| b.is_ascii_alphabetic()) {
463 &pt[1..]
464 } else {
465 pt
466 };
467 if let Ok(text) = std::str::from_utf8(data_start) {
469 self.clipboard.push(text.to_string());
470 }
471 }
472 _ => {}
473 }
474 }
475 }
476
477 fn full_reset(&mut self) {
478 let style = Style::default();
479 self.current_style = style;
480 self.primary.reset(self.cols, self.rows, style);
481 self.alternate.reset(self.cols, self.rows, style);
482 self.active = ScreenKind::Primary;
483 self.modes = TerminalModes::default();
484 self.tab_stops = default_tab_stops(self.cols);
485 self.scroll_region = Region {
486 top: 0,
487 bottom: self.rows.saturating_sub(1),
488 };
489 self.state = ParserState::Ground;
490 self.csi_buffer.clear();
491 self.osc_buffer.clear();
492 self.utf8_buffer.clear();
493 self.utf8_remaining = 0;
494 self.last_printed = None;
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 fn term(cols: usize, rows: usize) -> Terminal {
503 Terminal::new(cols, rows)
504 }
505
506 #[test]
507 fn plain_ascii() {
508 let mut t = term(10, 2);
509 t.write(b"hello");
510 assert_eq!(t.plain_text(), "hello");
511 }
512
513 #[test]
514 fn newline() {
515 let mut t = term(10, 3);
516 t.write(b"line1\nline2");
517 assert_eq!(t.plain_text(), "line1\nline2");
518 }
519
520 #[test]
521 fn carriage_return_overwrite() {
522 let mut t = term(10, 1);
523 t.write(b"abc\rXY");
524 assert_eq!(t.plain_text(), "XYc");
525 }
526
527 #[test]
528 fn cursor_movement() {
529 let mut t = term(10, 3);
530 t.write(b"a\x1B[2;3Hb");
531 assert_eq!(t.cursor().row, 1);
532 assert_eq!(t.cursor().col, 3);
534 }
535
536 #[test]
537 fn erase_display() {
538 let mut t = term(10, 2);
539 t.write(b"hello\x1B[2J");
540 assert_eq!(t.plain_text(), "");
541 }
542
543 #[test]
544 fn sgr_bold_and_color() {
545 let mut t = term(10, 1);
546 t.write(b"\x1B[1;31mX");
547 let cell = t.cell(0, 0).unwrap();
548 assert!(cell.style().bold);
549 }
550
551 #[test]
552 fn alternate_screen() {
553 let mut t = term(10, 2);
554 t.write(b"primary");
555 t.write(b"\x1B[?1049h"); t.write(b"alternate");
557 assert_eq!(t.plain_text(), "alternate");
558 assert_eq!(t.active_screen(), ScreenKind::Alternate);
559 t.write(b"\x1B[?1049l"); assert_eq!(t.plain_text(), "primary");
561 }
562
563 #[test]
564 fn scrollback_simple() {
565 let mut t = term(10, 2);
566 t.set_max_scrollback(10);
567 t.write(b"line1\nline2");
568 assert_eq!(t.scrollback_len(), 0);
570 assert_eq!(t.plain_text(), "line1\nline2");
571 }
572
573 #[test]
574 fn scrollback() {
575 let mut t = term(10, 2);
576 t.set_max_scrollback(10);
577 t.write(b"line1\nline2\nline3");
578 assert_eq!(t.scrollback_len(), 1);
580 assert_eq!(t.scrollback_row(0), Some("line1".to_string()));
581 assert_eq!(t.plain_text(), "line2\nline3");
582 }
583
584 #[test]
585 fn window_title() {
586 let mut t = term(10, 2);
587 t.write(b"\x1B]0;My Title\x07");
588 assert_eq!(t.title(), Some("My Title"));
589 }
590
591 #[test]
592 fn wide_characters() {
593 let mut t = term(10, 1);
594 t.write("你".as_bytes());
595 assert_eq!(t.cursor().col, 2); }
597
598 #[test]
599 fn cursor_shape() {
600 let mut t = term(10, 1);
601 t.write(b"\x1B[5 q");
602 assert_eq!(t.cursor_shape(), crate::mode::CursorShape::Bar);
603 }
604
605 #[test]
606 fn resize() {
607 let mut t = term(10, 2);
608 t.write(b"hello");
609 t.resize(20, 5);
610 assert_eq!(t.cols(), 20);
611 assert_eq!(t.rows(), 5);
612 assert!(t.plain_text().contains("hello"));
613 }
614}