ratatui_toolkit/primitives/termtui/
screen.rs1use crate::primitives::termtui::attrs::{Attrs, Color};
4use crate::primitives::termtui::grid::{Grid, Pos};
5use crate::primitives::termtui::size::Size;
6use termwiz::escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Mode, Sgr};
7use termwiz::escape::{Action, ControlCode, Esc, EscCode, OperatingSystemCommand};
8use unicode_width::UnicodeWidthChar;
9
10const MODE_CURSOR_VISIBLE: u8 = 1 << 0;
12const MODE_ALTERNATE_SCREEN: u8 = 1 << 1;
13#[allow(dead_code)]
14const MODE_APPLICATION_CURSOR: u8 = 1 << 2;
15#[allow(dead_code)]
16const MODE_BRACKETED_PASTE: u8 = 1 << 3;
17const MODE_AUTO_WRAP: u8 = 1 << 4;
18#[allow(dead_code)]
19const MODE_ORIGIN: u8 = 1 << 5;
20
21#[derive(Clone)]
23pub struct Screen {
24 grid: Grid,
26 alternate_grid: Grid,
28 attrs: Attrs,
30 modes: u8,
32 title: String,
34 icon_name: String,
36 pending_wrap: bool,
38}
39
40impl Screen {
41 pub fn new(rows: usize, cols: usize, scrollback: usize) -> Self {
43 let size = Size::new(cols as u16, rows as u16);
44
45 Self {
46 grid: Grid::new(size, scrollback),
47 alternate_grid: Grid::new(size, 0), attrs: Attrs::default(),
49 modes: MODE_CURSOR_VISIBLE | MODE_AUTO_WRAP,
50 title: String::new(),
51 icon_name: String::new(),
52 pending_wrap: false,
53 }
54 }
55
56 fn grid(&self) -> &Grid {
58 if self.mode(MODE_ALTERNATE_SCREEN) {
59 &self.alternate_grid
60 } else {
61 &self.grid
62 }
63 }
64
65 fn grid_mut(&mut self) -> &mut Grid {
67 if self.mode(MODE_ALTERNATE_SCREEN) {
68 &mut self.alternate_grid
69 } else {
70 &mut self.grid
71 }
72 }
73
74 pub fn primary_grid(&self) -> &Grid {
76 &self.grid
77 }
78
79 pub fn size(&self) -> Size {
81 self.grid().size()
82 }
83
84 pub fn cursor_pos(&self) -> Pos {
86 self.grid().pos()
87 }
88
89 pub fn cursor_visible(&self) -> bool {
91 self.mode(MODE_CURSOR_VISIBLE)
92 }
93
94 pub fn title(&self) -> &str {
96 &self.title
97 }
98
99 pub fn scrollback(&self) -> usize {
101 self.grid().scrollback()
102 }
103
104 pub fn set_scrollback(&mut self, offset: usize) {
106 self.grid_mut().set_scrollback(offset);
107 }
108
109 pub fn scroll_screen_up(&mut self, n: usize) {
111 let current = self.grid().scrollback();
112 self.grid_mut().set_scrollback(current + n);
113 }
114
115 pub fn scroll_screen_down(&mut self, n: usize) {
117 let current = self.grid().scrollback();
118 self.grid_mut().set_scrollback(current.saturating_sub(n));
119 }
120
121 pub fn get_selected_text(&self, low_x: i32, low_y: i32, high_x: i32, high_y: i32) -> String {
123 self.grid().get_selected_text(low_x, low_y, high_x, high_y)
124 }
125
126 fn mode(&self, mode: u8) -> bool {
128 self.modes & mode != 0
129 }
130
131 #[allow(dead_code)]
133 fn set_mode(&mut self, mode: u8) {
134 self.modes |= mode;
135 }
136
137 #[allow(dead_code)]
139 fn clear_mode(&mut self, mode: u8) {
140 self.modes &= !mode;
141 }
142
143 pub fn resize(&mut self, rows: usize, cols: usize) {
145 let size = Size::new(cols as u16, rows as u16);
146 self.grid.resize(size);
147 self.alternate_grid.resize(size);
148 }
149
150 pub fn handle_action(&mut self, action: Action) {
152 match action {
153 Action::Print(c) => self.text(c),
154 Action::PrintString(s) => {
155 for c in s.chars() {
156 self.text(c);
157 }
158 }
159 Action::Control(code) => self.handle_control(code),
160 Action::Esc(esc) => self.handle_esc(esc),
161 Action::CSI(csi) => self.handle_csi(csi),
162 Action::OperatingSystemCommand(osc) => self.handle_osc(*osc),
163 Action::DeviceControl(_) => {} Action::Sixel(_) => {} Action::XtGetTcap(_) => {} Action::KittyImage(_) => {} }
168 }
169
170 fn text(&mut self, c: char) {
172 let char_width = c.width().unwrap_or(0);
173 if char_width == 0 {
174 return;
177 }
178
179 let size = self.grid().size();
180
181 if self.pending_wrap {
183 self.pending_wrap = false;
184
185 if let Some(row) = self.grid_mut().current_row_mut() {
187 row.set_wrapped(true);
188 }
189
190 let pos = self.grid().pos();
192 if pos.row + 1 >= size.rows {
193 self.grid_mut().scroll_up(1);
194 } else {
195 self.grid_mut().set_row(pos.row + 1);
196 }
197 self.grid_mut().set_col(0);
198 }
199
200 let pos = self.grid().pos();
201 let attrs = self.attrs; if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
205 if let Some(cell) = row.get_mut(pos.col) {
206 cell.set_text(c.to_string());
207 cell.set_attrs(attrs);
208 }
209
210 if char_width == 2 && pos.col + 1 < size.cols {
212 if let Some(next_cell) = row.get_mut(pos.col + 1) {
213 next_cell.set_wide_continuation();
214 next_cell.set_attrs(attrs);
215 }
216 }
217 }
218
219 let new_col = pos.col + char_width as u16;
221 if new_col >= size.cols {
222 if self.mode(MODE_AUTO_WRAP) {
223 self.pending_wrap = true;
224 self.grid_mut().set_col(size.cols - 1);
225 }
226 } else {
227 self.grid_mut().set_col(new_col);
228 }
229 }
230
231 fn handle_control(&mut self, code: ControlCode) {
233 match code {
234 ControlCode::Bell => {} ControlCode::Backspace => {
236 let pos = self.grid().pos();
237 if pos.col > 0 {
238 self.grid_mut().set_col(pos.col - 1);
239 }
240 self.pending_wrap = false;
241 }
242 ControlCode::HorizontalTab => {
243 let pos = self.grid().pos();
244 let next_tab = ((pos.col / 8) + 1) * 8;
245 let size = self.grid().size();
246 self.grid_mut().set_col(next_tab.min(size.cols - 1));
247 self.pending_wrap = false;
248 }
249 ControlCode::LineFeed | ControlCode::VerticalTab | ControlCode::FormFeed => {
250 let pos = self.grid().pos();
251 let size = self.grid().size();
252 if pos.row + 1 >= size.rows {
253 self.grid_mut().scroll_up(1);
254 } else {
255 self.grid_mut().set_row(pos.row + 1);
256 }
257 self.pending_wrap = false;
258 }
259 ControlCode::CarriageReturn => {
260 self.grid_mut().set_col(0);
261 self.pending_wrap = false;
262 }
263 _ => {}
264 }
265 }
266
267 fn handle_esc(&mut self, esc: Esc) {
269 match esc {
270 Esc::Code(EscCode::DecSaveCursorPosition) => {
271 self.grid_mut().save_pos();
272 }
273 Esc::Code(EscCode::DecRestoreCursorPosition) => {
274 self.grid_mut().restore_pos();
275 }
276 Esc::Code(EscCode::ReverseIndex) => {
277 let pos = self.grid().pos();
279 if pos.row == 0 {
280 self.grid_mut().scroll_down(1);
281 } else {
282 self.grid_mut().set_row(pos.row - 1);
283 }
284 }
285 Esc::Code(EscCode::Index) => {
286 let pos = self.grid().pos();
288 let size = self.grid().size();
289 if pos.row + 1 >= size.rows {
290 self.grid_mut().scroll_up(1);
291 } else {
292 self.grid_mut().set_row(pos.row + 1);
293 }
294 }
295 Esc::Code(EscCode::NextLine) => {
296 let pos = self.grid().pos();
298 let size = self.grid().size();
299 if pos.row + 1 >= size.rows {
300 self.grid_mut().scroll_up(1);
301 } else {
302 self.grid_mut().set_row(pos.row + 1);
303 }
304 self.grid_mut().set_col(0);
305 }
306 Esc::Code(EscCode::FullReset) => {
307 self.grid_mut().clear();
308 self.grid_mut().set_pos(Pos::new(0, 0));
309 self.attrs = Attrs::default();
310 self.modes = MODE_CURSOR_VISIBLE | MODE_AUTO_WRAP;
311 }
312 _ => {}
313 }
314 }
315
316 fn handle_csi(&mut self, csi: termwiz::escape::csi::CSI) {
318 use termwiz::escape::csi::CSI;
319
320 match csi {
321 CSI::Cursor(cursor) => self.handle_cursor(cursor),
322 CSI::Edit(edit) => self.handle_edit(edit),
323 CSI::Sgr(sgr) => self.handle_sgr(sgr),
324 CSI::Mode(mode) => self.handle_mode(mode),
325 CSI::Window(_) => {} CSI::Keyboard(_) => {} CSI::Mouse(_) => {} CSI::Device(_) => {} _ => {}
330 }
331 }
332
333 fn handle_cursor(&mut self, cursor: Cursor) {
335 let size = self.grid().size();
336 let pos = self.grid().pos();
337
338 match cursor {
339 Cursor::Position { line, col } => {
340 let row = line.as_zero_based().min(size.rows.saturating_sub(1) as u32) as u16;
341 let col = col.as_zero_based().min(size.cols.saturating_sub(1) as u32) as u16;
342 self.grid_mut().set_pos(Pos::new(col, row));
343 }
344 Cursor::Up(n) => {
345 let new_row = pos.row.saturating_sub(n as u16);
346 self.grid_mut().set_row(new_row);
347 }
348 Cursor::Down(n) => {
349 let new_row = (pos.row + n as u16).min(size.rows - 1);
350 self.grid_mut().set_row(new_row);
351 }
352 Cursor::Left(n) => {
353 let new_col = pos.col.saturating_sub(n as u16);
354 self.grid_mut().set_col(new_col);
355 }
356 Cursor::Right(n) => {
357 let new_col = (pos.col + n as u16).min(size.cols - 1);
358 self.grid_mut().set_col(new_col);
359 }
360 Cursor::CharacterAbsolute(col) => {
361 let col = col.as_zero_based().min(size.cols.saturating_sub(1) as u32) as u16;
362 self.grid_mut().set_col(col);
363 }
364 Cursor::NextLine(n) => {
365 let new_row = (pos.row + n as u16).min(size.rows - 1);
366 self.grid_mut().set_pos(Pos::new(0, new_row));
367 }
368 Cursor::PrecedingLine(n) => {
369 let new_row = pos.row.saturating_sub(n as u16);
370 self.grid_mut().set_pos(Pos::new(0, new_row));
371 }
372 Cursor::SaveCursor => {
373 self.grid_mut().save_pos();
374 }
375 Cursor::RestoreCursor => {
376 self.grid_mut().restore_pos();
377 }
378 _ => {}
379 }
380 self.pending_wrap = false;
381 }
382
383 fn handle_edit(&mut self, edit: Edit) {
385 let size = self.grid().size();
386 let pos = self.grid().pos();
387
388 match edit {
389 Edit::EraseInDisplay(mode) => match mode {
390 EraseInDisplay::EraseToEndOfDisplay => {
391 self.grid_mut().clear_below();
392 }
393 EraseInDisplay::EraseToStartOfDisplay => {
394 self.grid_mut().clear_above();
395 }
396 EraseInDisplay::EraseDisplay => {
397 self.grid_mut().clear();
398 }
399 EraseInDisplay::EraseScrollback => {
400 }
402 },
403 Edit::EraseInLine(mode) => {
404 if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
405 match mode {
406 EraseInLine::EraseToEndOfLine => {
407 row.erase(pos.col, size.cols);
408 }
409 EraseInLine::EraseToStartOfLine => {
410 row.erase(0, pos.col + 1);
411 }
412 EraseInLine::EraseLine => {
413 row.clear();
414 }
415 }
416 }
417 }
418 Edit::InsertCharacter(n) => {
419 if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
420 for _ in 0..n {
421 row.insert(pos.col, Default::default());
422 }
423 }
424 }
425 Edit::DeleteCharacter(n) => {
426 if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
427 for _ in 0..n {
428 row.remove(pos.col);
429 }
430 }
431 }
432 Edit::EraseCharacter(n) => {
433 if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
434 row.erase(pos.col, pos.col + n as u16);
435 }
436 }
437 Edit::InsertLine(n) => {
438 for _ in 0..n {
439 self.grid_mut().scroll_down(1);
440 }
441 }
442 Edit::DeleteLine(n) => {
443 for _ in 0..n {
444 self.grid_mut().scroll_up(1);
445 }
446 }
447 Edit::ScrollDown(n) => {
448 self.grid_mut().scroll_down(n as usize);
449 }
450 Edit::ScrollUp(n) => {
451 self.grid_mut().scroll_up(n as usize);
452 }
453 _ => {}
454 }
455 }
456
457 fn handle_sgr(&mut self, sgr: Sgr) {
459 match sgr {
460 Sgr::Reset => {
461 self.attrs.reset();
462 }
463 Sgr::Intensity(intensity) => match intensity {
464 termwiz::cell::Intensity::Bold => {
465 self.attrs.set_bold(true);
466 }
467 termwiz::cell::Intensity::Normal => {
468 self.attrs.set_bold(false);
469 }
470 termwiz::cell::Intensity::Half => {
471 self.attrs.set_bold(false);
472 }
473 },
474 Sgr::Italic(on) => {
475 self.attrs.set_italic(on);
476 }
477 Sgr::Underline(underline) => {
478 self.attrs
479 .set_underline(underline != termwiz::cell::Underline::None);
480 }
481 Sgr::Inverse(on) => {
482 self.attrs.set_inverse(on);
483 }
484 Sgr::StrikeThrough(on) => {
485 self.attrs.set_strikethrough(on);
486 }
487 Sgr::Foreground(color) => {
488 self.attrs.fg = Color::from(color);
489 }
490 Sgr::Background(color) => {
491 self.attrs.bg = Color::from(color);
492 }
493 _ => {}
494 }
495 }
496
497 fn handle_mode(&mut self, mode: Mode) {
499 let _ = mode;
501 }
502
503 fn handle_osc(&mut self, osc: OperatingSystemCommand) {
505 match osc {
506 OperatingSystemCommand::SetWindowTitle(title)
507 | OperatingSystemCommand::SetWindowTitleSun(title) => {
508 self.title = title;
509 }
510 OperatingSystemCommand::SetIconName(name)
511 | OperatingSystemCommand::SetIconNameSun(name) => {
512 self.icon_name = name;
513 }
514 OperatingSystemCommand::SetIconNameAndWindowTitle(title) => {
515 self.title = title.clone();
516 self.icon_name = title;
517 }
518 _ => {}
519 }
520 }
521
522 pub fn visible_rows(&self) -> impl Iterator<Item = &crate::primitives::termtui::row::Row> {
524 self.grid().visible_rows()
525 }
526
527 pub fn is_alternate_screen(&self) -> bool {
529 self.mode(MODE_ALTERNATE_SCREEN)
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_screen_new() {
539 let screen = Screen::new(24, 80, 1000);
540 assert_eq!(screen.size().rows, 24);
541 assert_eq!(screen.size().cols, 80);
542 }
543
544 #[test]
545 fn test_screen_text() {
546 let mut screen = Screen::new(24, 80, 1000);
547
548 screen.text('H');
549 screen.text('i');
550
551 assert_eq!(screen.cursor_pos().col, 2);
552 }
553
554 #[test]
555 fn test_screen_newline() {
556 let mut screen = Screen::new(24, 80, 1000);
557
558 screen.text('A');
559 screen.handle_control(ControlCode::LineFeed);
560 screen.handle_control(ControlCode::CarriageReturn);
561 screen.text('B');
562
563 assert_eq!(screen.cursor_pos().row, 1);
564 assert_eq!(screen.cursor_pos().col, 1);
565 }
566
567 #[test]
568 fn test_screen_scroll() {
569 let mut screen = Screen::new(24, 80, 100);
570
571 for _ in 0..30 {
573 screen.handle_control(ControlCode::LineFeed);
574 }
575
576 assert!(screen.grid().scrollback_available() > 0);
577 }
578}