nested_shell/
nested_shell.rs

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