1mod attrs;
15mod cell;
16mod copy_mode;
17mod grid;
18mod keybindings;
19mod parser;
20mod row;
21mod screen;
22mod size;
23mod widget;
24
25pub use attrs::{Attrs, Color};
26pub use cell::Cell;
27pub use copy_mode::{CopyMode, CopyMoveDir, CopyPos};
28pub use grid::{Grid, Pos};
29pub use keybindings::TermTuiKeyBindings;
30pub use parser::Parser;
31pub use row::Row;
32pub use screen::Screen;
33pub use size::Size;
34pub use widget::TermTuiWidget;
35
36use anyhow::Result;
37use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
38use ratatui::layout::Rect;
39use ratatui::style::{Color as RatatuiColor, Style};
40use ratatui::widgets::{Block, BorderType, Borders};
41use ratatui::Frame;
42use std::io::{Read, Write};
43use std::sync::{Arc, Mutex};
44
45pub struct TermTui {
53 parser: Arc<Mutex<Parser>>,
55
56 pub title: String,
58
59 pub focused: bool,
61
62 pub copy_mode: CopyMode,
64
65 _master: Option<Arc<Mutex<Box<dyn MasterPty + Send>>>>,
67 _child: Option<Box<dyn Child + Send + Sync>>,
68 writer: Option<Arc<Mutex<Box<dyn Write + Send>>>>,
69
70 pub border_style: Style,
72 pub focused_border_style: Style,
73
74 pub keybindings: TermTuiKeyBindings,
76}
77
78impl TermTui {
79 pub fn new(title: impl Into<String>) -> Self {
81 let parser = Parser::new(24, 80, 10000);
82
83 Self {
84 parser: Arc::new(Mutex::new(parser)),
85 title: title.into(),
86 focused: false,
87 copy_mode: CopyMode::None,
88 _master: None,
89 _child: None,
90 writer: None,
91 border_style: Style::default().fg(RatatuiColor::White),
92 focused_border_style: Style::default().fg(RatatuiColor::Cyan),
93 keybindings: TermTuiKeyBindings::default(),
94 }
95 }
96
97 pub fn with_keybindings(mut self, keybindings: TermTuiKeyBindings) -> Self {
99 self.keybindings = keybindings;
100 self
101 }
102
103 pub fn spawn_with_command(
105 title: impl Into<String>,
106 command: &str,
107 args: &[&str],
108 ) -> Result<Self> {
109 let rows = 24;
110 let cols = 80;
111
112 let pty_system = native_pty_system();
113 let pty_size = PtySize {
114 rows,
115 cols,
116 pixel_width: 0,
117 pixel_height: 0,
118 };
119
120 let pair = pty_system.openpty(pty_size)?;
121
122 let mut cmd = CommandBuilder::new(command);
123 for arg in args {
124 cmd.arg(arg);
125 }
126
127 cmd.env("TERM", "xterm-256color");
129
130 let current_dir = std::env::current_dir()?;
131 cmd.cwd(current_dir);
132
133 let child = pair.slave.spawn_command(cmd)?;
134
135 #[cfg(unix)]
137 {
138 if let Some(fd) = pair.master.as_raw_fd() {
139 unsafe {
140 let flags = libc::fcntl(fd, libc::F_GETFL, 0);
141 libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
142 }
143 }
144 }
145
146 let reader = pair.master.try_clone_reader()?;
147 let writer = pair.master.take_writer()?;
148
149 let writer = Arc::new(Mutex::new(writer));
150
151 let parser = Parser::new(rows as usize, cols as usize, 10000);
152 let parser = Arc::new(Mutex::new(parser));
153 let parser_clone = Arc::clone(&parser);
154
155 std::thread::spawn(move || {
157 let mut buf = [0u8; 8192];
158 let mut reader = reader;
159 loop {
160 match reader.read(&mut buf) {
161 Ok(0) => break,
162 Ok(n) => {
163 if let Ok(mut parser) = parser_clone.lock() {
164 parser.process(&buf[..n]);
165 }
166 std::thread::sleep(std::time::Duration::from_millis(10));
167 }
168 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
169 std::thread::sleep(std::time::Duration::from_millis(10));
170 }
171 Err(_) => break,
172 }
173 }
174 });
175
176 Ok(Self {
177 parser,
178 title: title.into(),
179 focused: false,
180 copy_mode: CopyMode::None,
181 _master: None,
182 _child: Some(child),
183 writer: Some(writer),
184 border_style: Style::default().fg(RatatuiColor::White),
185 focused_border_style: Style::default().fg(RatatuiColor::Cyan),
186 keybindings: TermTuiKeyBindings::default(),
187 })
188 }
189
190 pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
192 use crossterm::event::KeyEventKind;
193
194 if key.kind != KeyEventKind::Press {
196 return false;
197 }
198
199 if self.copy_mode.is_active() {
201 return self.handle_copy_mode_key(key);
202 }
203
204 if TermTuiKeyBindings::key_matches(&key, &self.keybindings.copy_selection) {
206 if let Some(text) = self.copy_mode.get_selected_text() {
207 if let Ok(mut clipboard) = arboard::Clipboard::new() {
208 let _ = clipboard.set_text(text);
209 }
210 return true;
211 }
212 }
213
214 if TermTuiKeyBindings::key_matches(&key, &self.keybindings.enter_copy_mode) {
216 self.enter_copy_mode();
217 return true;
218 }
219
220 let text = self.key_to_terminal_input(key);
222 if !text.is_empty() {
223 self.send_input(&text);
224 true
225 } else {
226 false
227 }
228 }
229
230 fn handle_copy_mode_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
232 let kb = &self.keybindings;
233
234 if TermTuiKeyBindings::key_matches(&key, &kb.copy_exit)
236 || TermTuiKeyBindings::key_matches(&key, &kb.copy_exit_alt)
237 {
238 self.copy_mode = CopyMode::None;
239 return true;
240 }
241
242 if TermTuiKeyBindings::key_matches(&key, &kb.copy_move_up)
244 || TermTuiKeyBindings::key_matches(&key, &kb.copy_move_up_alt)
245 {
246 self.copy_mode.move_dir(CopyMoveDir::Up);
247 return true;
248 }
249
250 if TermTuiKeyBindings::key_matches(&key, &kb.copy_move_down)
252 || TermTuiKeyBindings::key_matches(&key, &kb.copy_move_down_alt)
253 {
254 self.copy_mode.move_dir(CopyMoveDir::Down);
255 return true;
256 }
257
258 if TermTuiKeyBindings::key_matches(&key, &kb.copy_move_left)
260 || TermTuiKeyBindings::key_matches(&key, &kb.copy_move_left_alt)
261 {
262 self.copy_mode.move_dir(CopyMoveDir::Left);
263 return true;
264 }
265
266 if TermTuiKeyBindings::key_matches(&key, &kb.copy_move_right)
268 || TermTuiKeyBindings::key_matches(&key, &kb.copy_move_right_alt)
269 {
270 self.copy_mode.move_dir(CopyMoveDir::Right);
271 return true;
272 }
273
274 if TermTuiKeyBindings::key_matches(&key, &kb.copy_line_start)
276 || TermTuiKeyBindings::key_matches(&key, &kb.copy_line_start_alt)
277 {
278 self.copy_mode.move_dir(CopyMoveDir::LineStart);
279 return true;
280 }
281
282 if TermTuiKeyBindings::key_matches(&key, &kb.copy_line_end)
284 || TermTuiKeyBindings::key_matches(&key, &kb.copy_line_end_alt)
285 {
286 self.copy_mode.move_dir(CopyMoveDir::LineEnd);
287 return true;
288 }
289
290 if TermTuiKeyBindings::key_matches(&key, &kb.copy_page_up)
292 || TermTuiKeyBindings::key_matches(&key, &kb.copy_page_up_alt)
293 {
294 self.copy_mode.move_dir(CopyMoveDir::PageUp);
295 return true;
296 }
297
298 if TermTuiKeyBindings::key_matches(&key, &kb.copy_page_down)
300 || TermTuiKeyBindings::key_matches(&key, &kb.copy_page_down_alt)
301 {
302 self.copy_mode.move_dir(CopyMoveDir::PageDown);
303 return true;
304 }
305
306 if TermTuiKeyBindings::key_matches(&key, &kb.copy_top) {
308 self.copy_mode.move_dir(CopyMoveDir::Top);
309 return true;
310 }
311
312 if TermTuiKeyBindings::key_matches(&key, &kb.copy_bottom) {
314 self.copy_mode.move_dir(CopyMoveDir::Bottom);
315 return true;
316 }
317
318 if TermTuiKeyBindings::key_matches(&key, &kb.copy_word_left) {
320 self.copy_mode.move_dir(CopyMoveDir::WordLeft);
321 return true;
322 }
323
324 if TermTuiKeyBindings::key_matches(&key, &kb.copy_word_right) {
326 self.copy_mode.move_dir(CopyMoveDir::WordRight);
327 return true;
328 }
329
330 if TermTuiKeyBindings::key_matches(&key, &kb.copy_start_selection)
332 || TermTuiKeyBindings::key_matches(&key, &kb.copy_start_selection_alt)
333 {
334 self.copy_mode.set_anchor();
335 return true;
336 }
337
338 if TermTuiKeyBindings::key_matches(&key, &kb.copy_and_exit)
340 || TermTuiKeyBindings::key_matches(&key, &kb.copy_and_exit_alt)
341 {
342 if let Some(text) = self.copy_mode.get_selected_text() {
343 if let Ok(mut clipboard) = arboard::Clipboard::new() {
344 let _ = clipboard.set_text(text);
345 }
346 }
347 self.copy_mode = CopyMode::None;
348 return true;
349 }
350
351 false
352 }
353
354 fn key_to_terminal_input(&self, key: crossterm::event::KeyEvent) -> String {
356 use crossterm::event::{KeyCode, KeyModifiers};
357
358 match key.code {
359 KeyCode::Char(c) => {
360 if key.modifiers.contains(KeyModifiers::CONTROL) {
361 match c.to_ascii_lowercase() {
362 'a'..='z' => {
363 let code = (c.to_ascii_lowercase() as u8 - b'a' + 1) as char;
364 code.to_string()
365 }
366 '@' => "\x00".to_string(),
367 '[' => "\x1b".to_string(),
368 '\\' => "\x1c".to_string(),
369 ']' => "\x1d".to_string(),
370 '^' => "\x1e".to_string(),
371 '_' => "\x1f".to_string(),
372 _ => c.to_string(),
373 }
374 } else if key.modifiers.contains(KeyModifiers::ALT) {
375 format!("\x1b{}", c)
376 } else {
377 c.to_string()
378 }
379 }
380 KeyCode::Enter => "\r".to_string(),
381 KeyCode::Backspace => "\x7f".to_string(),
382 KeyCode::Tab => "\t".to_string(),
383 KeyCode::Esc => "\x1b".to_string(),
384 KeyCode::Up => "\x1b[A".to_string(),
385 KeyCode::Down => "\x1b[B".to_string(),
386 KeyCode::Right => "\x1b[C".to_string(),
387 KeyCode::Left => "\x1b[D".to_string(),
388 KeyCode::Home => "\x1b[H".to_string(),
389 KeyCode::End => "\x1b[F".to_string(),
390 KeyCode::PageUp => "\x1b[5~".to_string(),
391 KeyCode::PageDown => "\x1b[6~".to_string(),
392 KeyCode::Delete => "\x1b[3~".to_string(),
393 _ => String::new(),
394 }
395 }
396
397 pub fn enter_copy_mode(&mut self) {
399 let parser = self.parser.lock().unwrap();
400 let screen = parser.screen().clone();
401 let size = screen.size();
402
403 let start = CopyPos::new(size.cols as i32 - 1, size.rows as i32 - 1);
405 self.copy_mode = CopyMode::enter(screen, start);
406 }
407
408 pub fn handle_mouse(&mut self, event: crossterm::event::MouseEvent, area: Rect) -> bool {
422 use crossterm::event::{MouseButton, MouseEventKind};
423
424 let content_x = event.column.saturating_sub(area.x + 1) as i32;
426 let content_y = event.row.saturating_sub(area.y + 1) as i32;
427
428 match event.kind {
429 MouseEventKind::Down(MouseButton::Left) => {
430 if self.copy_mode.is_active() {
431 if let CopyMode::Active { cursor, .. } = &mut self.copy_mode {
433 cursor.x = content_x;
434 cursor.y = content_y;
435 }
436 } else {
437 let parser = self.parser.lock().unwrap();
439 let screen = parser.screen().clone();
440 drop(parser);
441
442 let start = CopyPos::new(content_x, content_y);
443 self.copy_mode = CopyMode::enter(screen, start);
444 }
445 true
446 }
447 MouseEventKind::Drag(MouseButton::Left) => {
448 if !self.copy_mode.is_active() {
449 let parser = self.parser.lock().unwrap();
451 let screen = parser.screen().clone();
452 drop(parser);
453
454 let start = CopyPos::new(content_x, content_y);
455 self.copy_mode = CopyMode::enter(screen, start);
456 self.copy_mode.set_anchor();
458 } else {
459 self.copy_mode.set_end();
461
462 if let CopyMode::Active { cursor, .. } = &mut self.copy_mode {
464 cursor.x = content_x;
465 cursor.y = content_y;
466 }
467 }
468 true
469 }
470 MouseEventKind::Up(MouseButton::Left) => {
471 true
473 }
474 MouseEventKind::ScrollUp => {
475 self.scroll_up(3);
476 true
477 }
478 MouseEventKind::ScrollDown => {
479 self.scroll_down(3);
480 true
481 }
482 _ => false,
483 }
484 }
485
486 #[deprecated(
490 since = "0.2.0",
491 note = "Use handle_mouse instead for comprehensive mouse handling"
492 )]
493 pub fn handle_mouse_down(&mut self, x: u16, y: u16) {
494 let content_x = x.saturating_sub(1) as i32;
495 let content_y = y.saturating_sub(1) as i32;
496
497 let parser = self.parser.lock().unwrap();
498 let screen = parser.screen().clone();
499
500 let start = CopyPos::new(content_x, content_y);
501 self.copy_mode = CopyMode::enter(screen, start);
502 }
503
504 #[deprecated(
508 since = "0.2.0",
509 note = "Use handle_mouse instead for comprehensive mouse handling"
510 )]
511 pub fn handle_mouse_drag(&mut self, x: u16, y: u16) {
512 if self.copy_mode.is_active() {
513 let content_x = x.saturating_sub(1) as i32;
514 let content_y = y.saturating_sub(1) as i32;
515
516 self.copy_mode.set_end();
518
519 if let CopyMode::Active { cursor, .. } = &mut self.copy_mode {
521 cursor.x = content_x;
522 cursor.y = content_y;
523 }
524 }
525 }
526
527 #[deprecated(
531 since = "0.2.0",
532 note = "Use handle_mouse instead for comprehensive mouse handling"
533 )]
534 pub fn handle_mouse_up(&mut self) {
535 }
537
538 pub fn scroll_up(&mut self, lines: usize) {
540 let mut parser = self.parser.lock().unwrap();
541 parser.screen_mut().scroll_screen_up(lines);
542 }
543
544 pub fn scroll_down(&mut self, lines: usize) {
546 let mut parser = self.parser.lock().unwrap();
547 parser.screen_mut().scroll_screen_down(lines);
548 }
549
550 pub fn clear_selection(&mut self) {
552 self.copy_mode = CopyMode::None;
553 }
554
555 pub fn has_selection(&self) -> bool {
557 self.copy_mode.is_active()
558 }
559
560 pub fn get_selected_text(&self) -> Option<String> {
562 self.copy_mode.get_selected_text()
563 }
564
565 pub fn send_input(&self, text: &str) {
567 if let Some(ref writer) = self.writer {
568 let mut writer = writer.lock().unwrap();
569 let _ = writer.write_all(text.as_bytes());
570 let _ = writer.flush();
571 }
572 }
573
574 pub fn resize(&mut self, rows: u16, cols: u16) {
576 let mut parser = self.parser.lock().unwrap();
577 parser.resize(rows as usize, cols as usize);
578 }
579
580 pub fn render_content(&mut self, frame: &mut Frame, area: Rect) {
582 let parser = self.parser.lock().unwrap();
583 let screen = if let Some(frozen) = self.copy_mode.frozen_screen() {
584 frozen
585 } else {
586 parser.screen()
587 };
588
589 let widget = TermTuiWidget::new(screen)
590 .scroll_offset(screen.scrollback())
591 .copy_mode(&self.copy_mode);
592
593 frame.render_widget(widget, area);
594 }
595
596 pub fn render(&mut self, frame: &mut Frame, area: Rect) {
598 use ratatui::layout::{Constraint, Direction, Layout};
599 use ratatui::text::{Line, Span};
600
601 let border_style = if self.focused {
602 self.focused_border_style
603 } else {
604 self.border_style
605 };
606
607 let chunks = Layout::default()
609 .direction(Direction::Vertical)
610 .constraints([Constraint::Min(3), Constraint::Length(1)])
611 .split(area);
612
613 let block = Block::default()
614 .borders(Borders::ALL)
615 .border_type(BorderType::Rounded)
616 .border_style(border_style)
617 .title(self.title.as_str());
618
619 let inner = block.inner(chunks[0]);
620 frame.render_widget(block, chunks[0]);
621 self.render_content(frame, inner);
622
623 let kb = &self.keybindings;
625 let hotkeys = if self.copy_mode.is_active() {
626 let move_keys = format!(
628 "{}/{}",
629 TermTuiKeyBindings::key_to_display_string(&kb.copy_move_up),
630 TermTuiKeyBindings::key_to_display_string(&kb.copy_move_down)
631 );
632 let select_key = TermTuiKeyBindings::key_to_display_string(&kb.copy_start_selection);
633 let copy_key = TermTuiKeyBindings::key_to_display_string(&kb.copy_and_exit);
634 let word_keys = format!(
635 "{}/{}",
636 TermTuiKeyBindings::key_to_display_string(&kb.copy_word_right),
637 TermTuiKeyBindings::key_to_display_string(&kb.copy_word_left)
638 );
639 let line_keys = format!(
640 "{}/{}",
641 TermTuiKeyBindings::key_to_display_string(&kb.copy_line_start),
642 TermTuiKeyBindings::key_to_display_string(&kb.copy_line_end)
643 );
644 let top_bot_keys = format!(
645 "{}/{}",
646 TermTuiKeyBindings::key_to_display_string(&kb.copy_top),
647 TermTuiKeyBindings::key_to_display_string(&kb.copy_bottom)
648 );
649 let exit_key = TermTuiKeyBindings::key_to_display_string(&kb.copy_exit);
650
651 Line::from(vec![
652 Span::styled(
653 " COPY ",
654 Style::default()
655 .fg(RatatuiColor::Black)
656 .bg(RatatuiColor::Yellow),
657 ),
658 Span::raw(" "),
659 Span::styled(move_keys, Style::default().fg(RatatuiColor::Cyan)),
660 Span::raw(" move "),
661 Span::styled(select_key, Style::default().fg(RatatuiColor::Cyan)),
662 Span::raw(" select "),
663 Span::styled(copy_key, Style::default().fg(RatatuiColor::Cyan)),
664 Span::raw(" copy "),
665 Span::styled(word_keys, Style::default().fg(RatatuiColor::Cyan)),
666 Span::raw(" word "),
667 Span::styled(line_keys, Style::default().fg(RatatuiColor::Cyan)),
668 Span::raw(" line "),
669 Span::styled(top_bot_keys, Style::default().fg(RatatuiColor::Cyan)),
670 Span::raw(" top/bot "),
671 Span::styled(exit_key, Style::default().fg(RatatuiColor::Cyan)),
672 Span::raw(" exit"),
673 ])
674 } else {
675 let enter_copy_key = TermTuiKeyBindings::key_to_display_string(&kb.enter_copy_mode);
676 let copy_selection_key = TermTuiKeyBindings::key_to_display_string(&kb.copy_selection);
677
678 Line::from(vec![
679 Span::styled(enter_copy_key, Style::default().fg(RatatuiColor::Cyan)),
680 Span::raw(" copy mode "),
681 Span::styled(copy_selection_key, Style::default().fg(RatatuiColor::Cyan)),
682 Span::raw(" copy "),
683 Span::styled("scroll", Style::default().fg(RatatuiColor::DarkGray)),
684 Span::raw(" mouse wheel"),
685 ])
686 };
687
688 frame.render_widget(hotkeys, chunks[1]);
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
696
697 #[test]
698 fn test_termtui_creation() {
699 let term = TermTui::new("Test Terminal");
700 assert_eq!(term.title, "Test Terminal");
701 assert!(!term.focused);
702 assert!(!term.copy_mode.is_active());
703 }
704
705 #[test]
706 fn test_termtui_focus() {
707 let mut term = TermTui::new("Test");
708
709 term.focused = true;
710 assert!(term.focused);
711
712 term.focused = false;
713 assert!(!term.focused);
714 }
715
716 #[test]
717 fn test_termtui_copy_mode_enter() {
718 let mut term = TermTui::new("Test");
719
720 assert!(!term.copy_mode.is_active());
721
722 let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
723 term.handle_key(key);
724
725 assert!(term.copy_mode.is_active());
726 }
727
728 #[test]
729 fn test_termtui_copy_mode_exit() {
730 let mut term = TermTui::new("Test");
731
732 let enter_key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
734 term.handle_key(enter_key);
735 assert!(term.copy_mode.is_active());
736
737 let esc_key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
739 term.handle_key(esc_key);
740 assert!(!term.copy_mode.is_active());
741 }
742
743 #[test]
744 fn test_termtui_key_conversion() {
745 let term = TermTui::new("Test");
746
747 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
749 assert_eq!(term.key_to_terminal_input(key), "a");
750
751 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
753 assert_eq!(term.key_to_terminal_input(key), "\r");
754
755 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
757 assert_eq!(term.key_to_terminal_input(key), "\x03");
758
759 let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
761 assert_eq!(term.key_to_terminal_input(key), "\x1b[A");
762 }
763
764 #[test]
765 fn test_termtui_selection() {
766 use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
767
768 let mut term = TermTui::new("Test");
769 let area = ratatui::layout::Rect::new(0, 0, 80, 24);
770
771 let mouse_event = MouseEvent {
773 kind: MouseEventKind::Down(MouseButton::Left),
774 column: 5,
775 row: 5,
776 modifiers: KeyModifiers::NONE,
777 };
778 term.handle_mouse(mouse_event, area);
779 assert!(term.has_selection());
780
781 term.clear_selection();
783 assert!(!term.has_selection());
784 }
785
786 #[test]
787 fn test_termtui_keybindings() {
788 let kb = TermTuiKeyBindings::default();
790 assert_eq!(kb.enter_copy_mode.code, KeyCode::Char('x'));
791 assert!(kb.enter_copy_mode.modifiers.contains(KeyModifiers::CONTROL));
792
793 let display = TermTuiKeyBindings::key_to_display_string(&kb.enter_copy_mode);
795 assert_eq!(display, "^X");
796
797 let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
799 assert!(TermTuiKeyBindings::key_matches(&key, &kb.enter_copy_mode));
800
801 let wrong_key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL);
802 assert!(!TermTuiKeyBindings::key_matches(
803 &wrong_key,
804 &kb.enter_copy_mode
805 ));
806 }
807
808 #[test]
809 fn test_termtui_with_keybindings() {
810 let custom_kb = TermTuiKeyBindings {
811 enter_copy_mode: KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
812 ..Default::default()
813 };
814
815 let term = TermTui::new("Test").with_keybindings(custom_kb);
816 assert_eq!(term.keybindings.enter_copy_mode.code, KeyCode::Char('c'));
817 }
818
819 #[test]
820 fn test_mouse_scroll() {
821 use crossterm::event::{MouseEvent, MouseEventKind};
822
823 let mut term = TermTui::new("Test");
824 let area = ratatui::layout::Rect::new(0, 0, 80, 24);
825
826 let scroll_up = MouseEvent {
828 kind: MouseEventKind::ScrollUp,
829 column: 10,
830 row: 10,
831 modifiers: KeyModifiers::NONE,
832 };
833 assert!(term.handle_mouse(scroll_up, area));
834
835 let scroll_down = MouseEvent {
837 kind: MouseEventKind::ScrollDown,
838 column: 10,
839 row: 10,
840 modifiers: KeyModifiers::NONE,
841 };
842 assert!(term.handle_mouse(scroll_down, area));
843 }
844}