tui_term/
controller.rs

1//! This is an unstable interface, and can be activated with the following
2//! feature flag: `unstable`.
3//!
4//! The controller aims to help manage spawning and reading processes
5//! to simplify the usage of `tui-term`, with the tradeoff being less flexible.
6//!
7//! Please do test this interface out and submit feedback, improvements and bug reports.
8//!
9//!
10//! Currently only oneshot commands are supported by the controller:
11//! Commands like `ls`, `cat`.
12//! Commands like `htop`, that are persistent still need to be handled manually,
13//! please look at the examples for a better overview.
14
15use std::{
16    io::Result as IoResult,
17    sync::{Arc, RwLock},
18};
19
20use portable_pty::{CommandBuilder, ExitStatus, PtySystem};
21use vt100::{Parser, Screen};
22
23/// Controller, in charge of command dispatch
24pub struct Controller {
25    // Needs to be set
26    cmd: CommandBuilder,
27    size: Size,
28    parser: Option<Arc<RwLock<Parser>>>,
29    exit_status: Option<IoResult<ExitStatus>>,
30}
31
32impl Controller {
33    pub fn new(cmd: CommandBuilder, size: Option<Size>) -> Self {
34        Self {
35            cmd,
36            size: size.unwrap_or_default(),
37            parser: None,
38            exit_status: None,
39        }
40    }
41
42    /// This function is blocking while waiting for the command to end.
43    pub fn run(&mut self) {
44        let pair = self.init_pty();
45        let mut child = pair.slave.spawn_command(self.cmd.clone()).unwrap();
46        drop(pair.slave);
47        let mut reader = pair.master.try_clone_reader().unwrap();
48        let parser = Arc::new(RwLock::new(vt100::Parser::new(
49            self.size.rows,
50            self.size.cols,
51            0,
52        )));
53        {
54            let parser = parser.clone();
55            std::thread::spawn(move || {
56                // Consume the output from the child
57                let mut s = String::new();
58                reader.read_to_string(&mut s).unwrap();
59                if !s.is_empty() {
60                    let mut parser = parser.write().unwrap();
61                    parser.process(s.as_bytes());
62                }
63            });
64        }
65        // Wait for the child to complete
66        self.exit_status = Some(child.wait());
67        // Drop writer on purpose
68        let _writer = pair.master.take_writer().unwrap();
69
70        drop(pair.master);
71        self.parser = Some(parser);
72    }
73
74    fn init_pty(&self) -> portable_pty::PtyPair {
75        use portable_pty::{NativePtySystem, PtySize};
76        let pty_system = NativePtySystem::default();
77
78        pty_system
79            .openpty(PtySize {
80                rows: self.size.rows,
81                cols: self.size.cols,
82                pixel_width: self.size.pixel_width,
83                pixel_height: self.size.pixel_height,
84            })
85            .unwrap()
86    }
87
88    pub fn screen(&self) -> Option<Screen> {
89        if let Some(parser) = &self.parser {
90            // We convert the read error into an option, since we might call
91            // the read multiple times, but we only care that we can read at some point
92            let binding = parser.read().ok()?;
93            Some(binding.screen().clone())
94        } else {
95            None
96        }
97    }
98
99    /// Whether the command finished running
100    pub fn finished(&self) -> bool {
101        self.exit_status.is_some()
102    }
103
104    /// The exit status of the process
105    pub fn status(&self) -> Option<&IoResult<ExitStatus>> {
106        self.exit_status.as_ref()
107    }
108}
109
110#[derive(Default, Clone)]
111pub struct Size {
112    pub cols: u16,
113    pub rows: u16,
114    pixel_width: u16,
115    pixel_height: u16,
116}
117
118impl Size {
119    pub fn new(cols: u16, rows: u16, pixel_width: u16, pixel_height: u16) -> Self {
120        Self {
121            cols,
122            rows,
123            pixel_width,
124            pixel_height,
125        }
126    }
127}