long_running/
long_running.rs

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