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