simple_ls_rw/
simple_ls_rw.rs

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