1mod cell;
15mod copy_mode;
16mod grid;
17mod key_binding;
18mod keybindings;
19mod parser;
20mod screen;
21
22pub use cell::{Attrs, Cell};
23pub use copy_mode::{CopyMode, Pos};
24pub use grid::Grid;
25pub use key_binding::KeyBinding;
26pub use keybindings::VT100TermKeyBindings;
27pub use parser::Parser;
28pub use screen::Screen;
29
30use anyhow::Result;
31use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
32use ratatui::layout::Rect;
33use ratatui::style::{Color, Style};
34use ratatui::widgets::{Block, BorderType, Borders};
35use ratatui::Frame;
36use std::io::{Read, Write};
37use std::sync::{Arc, Mutex};
38
39pub struct VT100Term {
44 parser: Arc<Mutex<Parser>>,
46
47 pub title: String,
49
50 pub focused: bool,
52
53 pub copy_mode: CopyMode,
55
56 pub keybindings: VT100TermKeyBindings,
58
59 _master: Option<Arc<Mutex<Box<dyn MasterPty + Send>>>>,
61 _child: Option<Box<dyn Child + Send + Sync>>,
62 writer: Option<Arc<Mutex<Box<dyn Write + Send>>>>,
63
64 pub border_style: Style,
66 pub focused_border_style: Style,
67}
68
69impl VT100Term {
70 pub fn new(title: impl Into<String>) -> Self {
72 let parser = Parser::new(24, 80, 1000);
73
74 Self {
75 parser: Arc::new(Mutex::new(parser)),
76 title: title.into(),
77 focused: false,
78 copy_mode: CopyMode::None,
79 keybindings: VT100TermKeyBindings::default(),
80 _master: None,
81 _child: None,
82 writer: None,
83 border_style: Style::default().fg(Color::White),
84 focused_border_style: Style::default().fg(Color::Cyan),
85 }
86 }
87
88 pub fn with_keybindings(mut self, keybindings: VT100TermKeyBindings) -> Self {
90 self.keybindings = keybindings;
91 self
92 }
93
94 pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
96 use crossterm::event::KeyEventKind;
97
98 eprintln!(
99 "[VT100] handle_key called: focused={} key={:?}",
100 self.focused, key
101 );
102
103 if key.kind != KeyEventKind::Press {
105 eprintln!("[VT100] Ignoring non-Press event: {:?}", key.kind);
106 return false;
107 }
108
109 if self.copy_mode.is_active() {
111 return self.handle_copy_mode_key(key);
112 }
113
114 if self.keybindings.copy_selection.matches(&key) {
116 if let Some(text) = self.copy_mode.get_selected_text() {
117 if let Ok(mut clipboard) = arboard::Clipboard::new() {
118 let _ = clipboard.set_text(text);
119 }
120 return true;
121 }
122 }
123
124 if self.keybindings.enter_copy_mode.matches(&key) {
126 self.enter_copy_mode();
127 return true;
128 }
129
130 let text = self.key_to_terminal_input(key);
132 eprintln!(
133 "[VT100] key_to_terminal_input returned: {:?} (len={})",
134 text,
135 text.len()
136 );
137 if !text.is_empty() {
138 self.send_input(&text);
139 true
140 } else {
141 eprintln!("[VT100] ERROR: empty text from key_to_terminal_input!");
142 false
143 }
144 }
145
146 fn handle_copy_mode_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
148 use copy_mode::CopyMoveDir;
149
150 let code = key.code;
151
152 if self.keybindings.copy_exit.contains(&code) {
154 self.copy_mode = CopyMode::None;
155 return true;
156 }
157
158 if self.keybindings.copy_move_up.contains(&code) {
160 let (dx, dy) = CopyMoveDir::Up.delta();
161 self.copy_mode.move_cursor(dx, dy);
162 return true;
163 }
164
165 if self.keybindings.copy_move_down.contains(&code) {
167 let (dx, dy) = CopyMoveDir::Down.delta();
168 self.copy_mode.move_cursor(dx, dy);
169 return true;
170 }
171
172 if self.keybindings.copy_move_left.contains(&code) {
174 let (dx, dy) = CopyMoveDir::Left.delta();
175 self.copy_mode.move_cursor(dx, dy);
176 return true;
177 }
178
179 if self.keybindings.copy_move_right.contains(&code) {
181 let (dx, dy) = CopyMoveDir::Right.delta();
182 self.copy_mode.move_cursor(dx, dy);
183 return true;
184 }
185
186 if self.keybindings.copy_set_selection.contains(&code) {
188 self.copy_mode.set_end();
189 return true;
190 }
191
192 if self.keybindings.copy_and_exit.contains(&code) {
194 if let Some(text) = self.copy_mode.get_selected_text() {
195 if let Ok(mut clipboard) = arboard::Clipboard::new() {
196 let _ = clipboard.set_text(text);
197 }
198 }
199 self.copy_mode = CopyMode::None;
200 return true;
201 }
202
203 false
204 }
205
206 fn key_to_terminal_input(&self, key: crossterm::event::KeyEvent) -> String {
208 use crossterm::event::{KeyCode, KeyModifiers};
209
210 match key.code {
211 KeyCode::Char(c) => {
212 if key.modifiers.contains(KeyModifiers::CONTROL) {
213 match c.to_ascii_lowercase() {
214 'a'..='z' => {
215 let code = (c.to_ascii_lowercase() as u8 - b'a' + 1) as char;
216 code.to_string()
217 }
218 '@' => "\x00".to_string(),
219 '[' => "\x1b".to_string(),
220 '\\' => "\x1c".to_string(),
221 ']' => "\x1d".to_string(),
222 '^' => "\x1e".to_string(),
223 '_' => "\x1f".to_string(),
224 _ => c.to_string(),
225 }
226 } else if key.modifiers.contains(KeyModifiers::ALT) {
227 format!("\x1b{}", c)
228 } else {
229 c.to_string()
230 }
231 }
232 KeyCode::Enter => "\r".to_string(),
233 KeyCode::Backspace => "\x7f".to_string(),
234 KeyCode::Tab => "\t".to_string(),
235 KeyCode::Esc => "\x1b".to_string(),
236 KeyCode::Up => "\x1b[A".to_string(),
237 KeyCode::Down => "\x1b[B".to_string(),
238 KeyCode::Right => "\x1b[C".to_string(),
239 KeyCode::Left => "\x1b[D".to_string(),
240 KeyCode::Home => "\x1b[H".to_string(),
241 KeyCode::End => "\x1b[F".to_string(),
242 KeyCode::PageUp => "\x1b[5~".to_string(),
243 KeyCode::PageDown => "\x1b[6~".to_string(),
244 KeyCode::Delete => "\x1b[3~".to_string(),
245 _ => String::new(),
246 }
247 }
248
249 fn enter_copy_mode(&mut self) {
251 let parser = self.parser.lock().unwrap();
252 let screen = parser.screen().clone();
253 let size = screen.size();
254
255 let start = Pos::new(size.cols as i32 - 1, size.rows as i32 - 1);
257 self.copy_mode = CopyMode::enter(screen, start);
258 }
259
260 pub fn handle_mouse_down(&mut self, x: u16, y: u16) {
262 let content_x = x.saturating_sub(1) as i32;
264 let content_y = y.saturating_sub(1) as i32;
265
266 let parser = self.parser.lock().unwrap();
268 let screen = parser.screen().clone();
269
270 let start = Pos::new(content_x, content_y);
271 self.copy_mode = CopyMode::enter(screen, start);
272 }
273
274 pub fn handle_mouse_drag(&mut self, x: u16, y: u16) {
276 if self.copy_mode.is_active() {
277 let content_x = x.saturating_sub(1) as i32;
278 let content_y = y.saturating_sub(1) as i32;
279
280 if let CopyMode::Active { end, .. } = &mut self.copy_mode {
282 *end = Some(Pos::new(content_x, content_y));
283 }
284 }
285 }
286
287 pub fn handle_mouse_up(&mut self) {
289 }
291
292 pub fn scroll_up(&mut self, lines: usize) {
294 let mut parser = self.parser.lock().unwrap();
295 parser.screen_mut().scroll_screen_up(lines);
296 }
297
298 pub fn scroll_down(&mut self, lines: usize) {
300 let mut parser = self.parser.lock().unwrap();
301 parser.screen_mut().scroll_screen_down(lines);
302 }
303
304 pub fn clear_selection(&mut self) {
306 self.copy_mode = CopyMode::None;
307 }
308
309 pub fn has_selection(&self) -> bool {
311 self.copy_mode.is_active()
312 }
313
314 pub fn get_selected_text(&self) -> Option<String> {
316 self.copy_mode.get_selected_text()
317 }
318
319 pub fn spawn_with_command(
321 title: impl Into<String>,
322 command: &str,
323 args: &[&str],
324 ) -> Result<Self> {
325 let rows = 24;
326 let cols = 80;
327
328 let pty_system = native_pty_system();
329 let pty_size = PtySize {
330 rows,
331 cols,
332 pixel_width: 0,
333 pixel_height: 0,
334 };
335
336 let pair = pty_system.openpty(pty_size)?;
337
338 let mut cmd = CommandBuilder::new(command);
339 for arg in args {
340 cmd.arg(arg);
341 }
342
343 cmd.env("TERM", "xterm-256color");
346
347 cmd.env("fish_greeting", "");
349
350 let current_dir = std::env::current_dir()?;
351 cmd.cwd(current_dir);
352
353 let child = pair.slave.spawn_command(cmd)?;
354
355 #[cfg(unix)]
356 {
357 if let Some(fd) = pair.master.as_raw_fd() {
358 unsafe {
359 let flags = libc::fcntl(fd, libc::F_GETFL, 0);
360 libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
361 }
362 }
363 }
364
365 let reader = pair.master.try_clone_reader()?;
367 let writer = pair.master.take_writer()?;
368
369 let writer = Arc::new(Mutex::new(writer));
371
372 let parser = Parser::new(rows as usize, cols as usize, 1000);
373 let parser = Arc::new(Mutex::new(parser));
374 let parser_clone = Arc::clone(&parser);
375
376 tokio::spawn(async move {
378 let mut buf = [0u8; 8192];
379 let mut reader = reader;
380 loop {
381 match reader.read(&mut buf) {
382 Ok(0) => {
383 eprintln!("[VT100] PTY closed");
384 break;
385 }
386 Ok(n) => {
387 if let Ok(mut parser) = parser_clone.lock() {
388 parser.process(&buf[..n]);
389 }
390 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
391 }
392 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
393 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
394 }
395 Err(e) => {
396 eprintln!("[VT100] Read error: {}", e);
397 break;
398 }
399 }
400 }
401 });
402
403 Ok(Self {
404 parser,
405 title: title.into(),
406 focused: false,
407 copy_mode: CopyMode::None,
408 keybindings: VT100TermKeyBindings::default(),
409 _master: None, _child: Some(child),
411 writer: Some(writer),
412 border_style: Style::default().fg(Color::White),
413 focused_border_style: Style::default().fg(Color::Cyan),
414 })
415 }
416
417 pub fn send_input(&self, text: &str) {
419 if let Some(ref writer) = self.writer {
420 let mut writer = writer.lock().unwrap();
421 eprintln!("[VT100] Sending {} bytes: {:?}", text.len(), text);
422 match writer.write_all(text.as_bytes()) {
423 Ok(_) => match writer.flush() {
424 Ok(_) => eprintln!("[VT100] Flushed successfully"),
425 Err(e) => eprintln!("[VT100] Flush error: {}", e),
426 },
427 Err(e) => eprintln!("[VT100] Write error: {}", e),
428 }
429 } else {
430 eprintln!("[VT100] ERROR: No writer available!");
431 }
432 }
433
434 pub fn render_content(&mut self, frame: &mut Frame, area: Rect) {
436 let parser = self.parser.lock().unwrap();
438 let screen = parser.screen();
439
440 for row in 0..area.height.min(screen.size().rows as u16) {
442 for col in 0..area.width.min(screen.size().cols as u16) {
443 if let Some(cell) = screen.cell(row as usize, col as usize) {
444 let ratatui_cell = cell.to_ratatui();
445 let buf_cell = frame.buffer_mut().cell_mut((area.x + col, area.y + row));
446 if let Some(buf_cell) = buf_cell {
447 *buf_cell = ratatui_cell;
448 }
449 }
450 }
451 }
452
453 if let Some((start, end)) = self.copy_mode.get_selection() {
455 self.render_selection(frame, area, start, end, screen.scrollback());
456 }
457
458 let scrollback = screen.scrollback();
460 if scrollback > 0 {
461 let _indicator = format!(" -{} ", scrollback);
462 }
465 }
466
467 fn render_selection(
469 &self,
470 frame: &mut Frame,
471 area: Rect,
472 start: Pos,
473 end: Pos,
474 scrollback: usize,
475 ) {
476 let (low, high) = Pos::to_low_high(&start, &end);
477
478 let selection_style = Style::default()
479 .bg(Color::Rgb(70, 130, 180))
480 .fg(Color::White);
481
482 for row in low.y..=high.y {
483 let visible_row = (row + scrollback as i32) as u16;
484 if visible_row >= area.height {
485 continue;
486 }
487
488 let y = area.y + visible_row;
489 let start_col = if row == low.y { low.x as u16 } else { 0 };
490 let end_col = if row == high.y {
491 (high.x as u16).min(area.width)
492 } else {
493 area.width
494 };
495
496 for x in start_col..end_col {
497 if let Some(cell) = frame.buffer_mut().cell_mut((area.x + x, y)) {
498 cell.set_style(selection_style);
499 }
500 }
501 }
502 }
503
504 pub fn render(&mut self, frame: &mut Frame, area: Rect) {
506 let border_style = if self.focused {
507 self.focused_border_style
508 } else {
509 self.border_style
510 };
511
512 let block = Block::default()
513 .borders(Borders::ALL)
514 .border_type(BorderType::Rounded)
515 .border_style(border_style)
516 .title(&*self.title);
517
518 let inner = block.inner(area);
519 frame.render_widget(block, area);
520 self.render_content(frame, inner);
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
528
529 #[test]
530 fn test_vt100_term_creation() {
531 let term = VT100Term::new("Test Terminal");
532 assert_eq!(term.title, "Test Terminal");
533 assert!(!term.focused);
534 assert!(!term.copy_mode.is_active());
535 }
536
537 #[test]
538 fn test_vt100_term_focus_state() {
539 let mut term = VT100Term::new("Test");
540
541 assert!(!term.focused);
543
544 term.focused = true;
546 assert!(term.focused);
547
548 term.focused = false;
550 assert!(!term.focused);
551 }
552
553 #[test]
554 fn test_vt100_term_handle_key_when_not_focused() {
555 let mut term = VT100Term::new("Test");
556 term.focused = false;
557
558 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
559
560 let handled = term.handle_key(key);
563 assert!(handled);
564 }
565
566 #[test]
567 fn test_vt100_term_handle_key_when_focused() {
568 let mut term = VT100Term::new("Test");
569 term.focused = true;
570
571 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
572 let handled = term.handle_key(key);
573 assert!(handled);
574 }
575
576 #[test]
577 fn test_vt100_term_copy_mode_enter() {
578 let mut term = VT100Term::new("Test");
579
580 assert!(!term.copy_mode.is_active());
582
583 let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL);
585 term.handle_key(key);
586
587 assert!(term.copy_mode.is_active());
588 }
589
590 #[test]
591 fn test_vt100_term_copy_mode_exit() {
592 let mut term = VT100Term::new("Test");
593
594 let enter_key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL);
596 term.handle_key(enter_key);
597 assert!(term.copy_mode.is_active());
598
599 let esc_key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
601 term.handle_key(esc_key);
602
603 assert!(!term.copy_mode.is_active());
604 }
605
606 #[test]
607 fn test_vt100_term_key_to_terminal_input_char() {
608 let term = VT100Term::new("Test");
609
610 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
611 let input = term.key_to_terminal_input(key);
612 assert_eq!(input, "a");
613 }
614
615 #[test]
616 fn test_vt100_term_key_to_terminal_input_enter() {
617 let term = VT100Term::new("Test");
618
619 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
620 let input = term.key_to_terminal_input(key);
621 assert_eq!(input, "\r");
622 }
623
624 #[test]
625 fn test_vt100_term_key_to_terminal_input_ctrl() {
626 let term = VT100Term::new("Test");
627
628 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
630 let input = term.key_to_terminal_input(key);
631 assert_eq!(input, "\u{0003}"); }
633
634 #[test]
635 fn test_vt100_term_key_to_terminal_input_arrow_up() {
636 let term = VT100Term::new("Test");
637
638 let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
639 let input = term.key_to_terminal_input(key);
640 assert_eq!(input, "\x1b[A");
641 }
642
643 #[test]
644 fn test_vt100_term_key_to_terminal_input_arrow_down() {
645 let term = VT100Term::new("Test");
646
647 let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
648 let input = term.key_to_terminal_input(key);
649 assert_eq!(input, "\x1b[B");
650 }
651
652 #[test]
653 fn test_vt100_term_selection_mouse_handling() {
654 let mut term = VT100Term::new("Test");
655
656 term.handle_mouse_down(5, 5);
658
659 term.handle_mouse_drag(10, 5);
661
662 term.handle_mouse_up();
664
665 term.clear_selection();
667 assert!(!term.has_selection());
668 }
669
670 #[test]
671 fn test_vt100_term_scroll_up() {
672 let mut term = VT100Term::new("Test");
673
674 term.scroll_up(3);
676 }
677
678 #[test]
679 fn test_vt100_term_scroll_down() {
680 let mut term = VT100Term::new("Test");
681
682 term.scroll_down(3);
684 }
685
686 #[test]
687 fn test_spawn_command_sets_term_env() {
688 }
700}