Skip to main content

long_running/
long_running.rs

1use std::{
2    io::{self, Read},
3    sync::{Arc, RwLock},
4    time::Duration,
5};
6
7use crossterm::event::{self, Event, KeyCode, KeyEventKind};
8use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
9use ratatui::{
10    DefaultTerminal, Frame,
11    layout::Alignment,
12    style::{Modifier, Style},
13    text::Line,
14    widgets::{Block, Borders, Paragraph},
15};
16use tui_term::widget::PseudoTerminal;
17use vt100::Screen;
18
19fn main() -> std::io::Result<()> {
20    let mut terminal = ratatui::init();
21    let result = run_app(&mut terminal);
22    ratatui::restore();
23    result
24}
25
26fn run_app(terminal: &mut DefaultTerminal) -> std::io::Result<()> {
27    let pty_system = NativePtySystem::default();
28    let cwd = std::env::current_dir().unwrap();
29    let mut cmd = CommandBuilder::new("top");
30    cmd.cwd(cwd);
31
32    let pair = pty_system
33        .openpty(PtySize {
34            rows: 24,
35            cols: 80,
36            pixel_width: 0,
37            pixel_height: 0,
38        })
39        .unwrap();
40    // Wait for the child to complete
41    std::thread::spawn(move || {
42        let mut child = pair.slave.spawn_command(cmd).unwrap();
43        let _child_exit_status = child.wait().unwrap();
44        drop(pair.slave);
45    });
46
47    let mut reader = pair.master.try_clone_reader().unwrap();
48    let parser = Arc::new(RwLock::new(vt100::Parser::new(24, 80, 0)));
49
50    {
51        let parser = parser.clone();
52        std::thread::spawn(move || {
53            // Consume the output from the child
54            // Can't read the full buffer, since that would wait for EOF
55            let mut buf = [0u8; 8192];
56            let mut processed_buf = Vec::new();
57            loop {
58                let size = reader.read(&mut buf).unwrap();
59                if size == 0 {
60                    break;
61                }
62                if size > 0 {
63                    processed_buf.extend_from_slice(&buf[..size]);
64                    let mut parser = parser.write().unwrap();
65                    parser.process(&processed_buf);
66
67                    // Clear the processed portion of the buffer
68                    processed_buf.clear();
69                }
70            }
71        });
72    }
73
74    {
75        // Drop writer on purpose
76        let _writer = pair.master.take_writer().unwrap();
77    }
78    drop(pair.master);
79
80    run(terminal, parser)
81}
82
83fn run(terminal: &mut DefaultTerminal, parser: Arc<RwLock<vt100::Parser>>) -> io::Result<()> {
84    loop {
85        terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
86
87        // Event read is blocking
88        if event::poll(Duration::from_millis(10))? {
89            // It's guaranteed that the `read()` won't block when the `poll()`
90            // function returns `true`
91            if let Event::Key(key) = event::read()? {
92                if key.kind == KeyEventKind::Press {
93                    if let KeyCode::Char('q') = key.code {
94                        return Ok(());
95                    }
96                }
97            }
98        }
99    }
100}
101
102fn ui(f: &mut Frame, screen: &Screen) {
103    let chunks = ratatui::layout::Layout::default()
104        .direction(ratatui::layout::Direction::Vertical)
105        .margin(1)
106        .constraints(
107            [
108                ratatui::layout::Constraint::Percentage(0),
109                ratatui::layout::Constraint::Percentage(100),
110                ratatui::layout::Constraint::Min(1),
111            ]
112            .as_ref(),
113        )
114        .split(f.area());
115    let title = Line::from("[ Running: top ]");
116    let block = Block::default()
117        .borders(Borders::ALL)
118        .title(title)
119        .style(Style::default().add_modifier(Modifier::BOLD));
120    let pseudo_term = PseudoTerminal::new(screen).block(block);
121    f.render_widget(pseudo_term, chunks[1]);
122    let block = Block::default().borders(Borders::ALL);
123    f.render_widget(block, f.area());
124    let explanation = "Press q to exit".to_string();
125    let explanation = Paragraph::new(explanation)
126        .style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED))
127        .alignment(Alignment::Center);
128    f.render_widget(explanation, chunks[2]);
129}