nested_shell_async/
nested_shell_async.rs

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    // Wait for the child to complete
64    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            // Consume the output from the child
77            // Can't read the full buffer, since that would wait for EOF
78            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                    // Clear the processed portion of the buffer
91                    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    // Drop writer on purpose
102    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    // restore terminal
113    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        // Event read is non-blocking
129        if event::poll(Duration::from_millis(10))? {
130            // It's guaranteed that the `read()` won't block when the `poll()`
131            // function returns `true`
132            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}