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