1use std::{
2 io::{self, Read, Write},
3 sync::{Arc, RwLock, mpsc::Sender},
4 time::Duration,
5};
6
7use bytes::Bytes;
8use crossterm::event::{self, Event, KeyCode, KeyEventKind};
9use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
10use ratatui::{
11 DefaultTerminal, Frame,
12 layout::Alignment,
13 style::{Modifier, Style},
14 widgets::{Block, Borders, Paragraph},
15};
16use tui_term::widget::PseudoTerminal;
17use vt100::Screen;
18
19#[derive(Debug)]
20struct Size {
21 cols: u16,
22 rows: u16,
23}
24
25fn main() -> std::io::Result<()> {
26 let mut terminal = ratatui::init();
27 let result = run_app(&mut terminal);
28 ratatui::restore();
29 result
30}
31
32fn run_app(terminal: &mut DefaultTerminal) -> std::io::Result<()> {
33 let size = Size {
34 rows: terminal.size()?.height,
35 cols: terminal.size()?.width,
36 };
37
38 let pty_system = NativePtySystem::default();
39 let cwd = std::env::current_dir().unwrap();
40 let mut cmd = CommandBuilder::new_default_prog();
41 cmd.cwd(cwd);
42
43 let pair = pty_system
44 .openpty(PtySize {
45 rows: size.rows,
46 cols: size.cols,
47 pixel_width: 0,
48 pixel_height: 0,
49 })
50 .unwrap();
51 std::thread::spawn(move || {
53 let mut child = pair.slave.spawn_command(cmd).unwrap();
54 let _child_exit_status = child.wait().unwrap();
55 drop(pair.slave);
56 });
57
58 let mut reader = pair.master.try_clone_reader().unwrap();
59 let parser = Arc::new(RwLock::new(vt100::Parser::new(size.rows, size.cols, 0)));
60
61 {
62 let parser = parser.clone();
63 std::thread::spawn(move || {
64 let mut buf = [0u8; 8192];
67 let mut processed_buf = Vec::new();
68 loop {
69 let size = reader.read(&mut buf).unwrap();
70 if size == 0 {
71 break;
72 }
73 if size > 0 {
74 processed_buf.extend_from_slice(&buf[..size]);
75 let mut parser = parser.write().unwrap();
76 parser.process(&processed_buf);
77
78 processed_buf.clear();
80 }
81 }
82 });
83 }
84
85 let (tx, rx) = std::sync::mpsc::channel::<Bytes>();
86
87 std::thread::spawn(move || {
89 let mut writer = pair.master.take_writer().unwrap();
90 while let Ok(bytes) = rx.recv() {
91 writer.write_all(&bytes).unwrap();
92 }
93 drop(pair.master);
94 });
95
96 let result = run(terminal, parser, tx);
97 println!("{size:?}");
98 result
99}
100
101fn run(
102 terminal: &mut DefaultTerminal,
103 parser: Arc<RwLock<vt100::Parser>>,
104 sender: Sender<Bytes>,
105) -> io::Result<()> {
106 loop {
107 terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
108
109 if event::poll(Duration::from_millis(10))? {
111 match event::read()? {
114 Event::Key(key) => {
115 if key.kind == KeyEventKind::Press {
116 match key.code {
117 KeyCode::Char('q') => return Ok(()),
118 KeyCode::Char(input) => sender
119 .send(Bytes::from(input.to_string().into_bytes()))
120 .unwrap(),
121 KeyCode::Backspace => {
122 sender.send(Bytes::from(vec![8])).unwrap();
123 }
124 KeyCode::Enter => {
125 #[cfg(unix)]
126 sender.send(Bytes::from(vec![b'\n'])).unwrap();
127 #[cfg(windows)]
128 sender.send(Bytes::from(vec![b'\r', b'\n'])).unwrap();
129 }
130 KeyCode::Left => sender.send(Bytes::from(vec![27, 91, 68])).unwrap(),
131 KeyCode::Right => sender.send(Bytes::from(vec![27, 91, 67])).unwrap(),
132 KeyCode::Up => sender.send(Bytes::from(vec![27, 91, 65])).unwrap(),
133 KeyCode::Down => sender.send(Bytes::from(vec![27, 91, 66])).unwrap(),
134 KeyCode::Home => sender.send(Bytes::from(vec![27, 91, 72])).unwrap(),
135 KeyCode::End => sender.send(Bytes::from(vec![27, 91, 70])).unwrap(),
136 KeyCode::PageUp => {
137 sender.send(Bytes::from(vec![27, 91, 53, 126])).unwrap()
138 }
139 KeyCode::PageDown => {
140 sender.send(Bytes::from(vec![27, 91, 54, 126])).unwrap()
141 }
142 KeyCode::Tab => sender.send(Bytes::from(vec![9])).unwrap(),
143 KeyCode::BackTab => sender.send(Bytes::from(vec![27, 91, 90])).unwrap(),
144 KeyCode::Delete => {
145 sender.send(Bytes::from(vec![27, 91, 51, 126])).unwrap()
146 }
147 KeyCode::Insert => {
148 sender.send(Bytes::from(vec![27, 91, 50, 126])).unwrap()
149 }
150 KeyCode::F(_) => todo!(),
151 KeyCode::Null => todo!(),
152 KeyCode::Esc => todo!(),
153 KeyCode::CapsLock => todo!(),
154 KeyCode::ScrollLock => todo!(),
155 KeyCode::NumLock => todo!(),
156 KeyCode::PrintScreen => todo!(),
157 KeyCode::Pause => todo!(),
158 KeyCode::Menu => todo!(),
159 KeyCode::KeypadBegin => todo!(),
160 KeyCode::Media(_) => todo!(),
161 KeyCode::Modifier(_) => todo!(),
162 }
163 }
164 }
165 Event::FocusGained => {}
166 Event::FocusLost => {}
167 Event::Mouse(_) => {}
168 Event::Paste(_) => todo!(),
169 Event::Resize(cols, rows) => {
170 parser.write().unwrap().screen_mut().set_size(rows, cols);
171 }
172 }
173 }
174 }
175}
176
177fn ui(f: &mut Frame, screen: &Screen) {
178 let chunks = ratatui::layout::Layout::default()
179 .direction(ratatui::layout::Direction::Vertical)
180 .margin(1)
181 .constraints(
182 [
183 ratatui::layout::Constraint::Percentage(100),
184 ratatui::layout::Constraint::Min(1),
185 ]
186 .as_ref(),
187 )
188 .split(f.area());
189 let block = Block::default()
190 .borders(Borders::ALL)
191 .style(Style::default().add_modifier(Modifier::BOLD));
192 let pseudo_term = PseudoTerminal::new(screen).block(block);
193 f.render_widget(pseudo_term, chunks[0]);
194 let explanation = "Press q to exit".to_string();
195 let explanation = Paragraph::new(explanation)
196 .style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED))
197 .alignment(Alignment::Center);
198 f.render_widget(explanation, chunks[1]);
199}