devpane 0.3.0

A terminal-first multi-process workspace and development dashboard
use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
use std::io::{self, Read, Write};
use std::path::Path;
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::thread::{self, JoinHandle};

pub fn default_shell() -> (&'static str, Vec<String>) {
    #[cfg(target_os = "macos")]
    {
        ("/bin/zsh", vec![])
    }

    #[cfg(target_os = "windows")]
    {
        ("cmd.exe", vec![])
    }
}

pub struct TerminalSession {
    master: Option<Box<dyn MasterPty + Send>>,
    writer: Box<dyn Write + Send>,
    output_rx: Receiver<Vec<u8>>,
    reader_thread: Option<JoinHandle<()>>,
    child: Box<dyn portable_pty::Child + Send + Sync>,
}

impl TerminalSession {
    pub fn spawn(
        program: &str,
        args: &[String],
        cwd: &Path,
        rows: u16,
        cols: u16,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let pty_system = native_pty_system();

        let pair = pty_system.openpty(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;

        let portable_pty::PtyPair { master, slave } = pair;

        let mut command = CommandBuilder::new(program);

        for arg in args {
            command.arg(arg);
        }

        command.cwd(cwd);

        let child = slave.spawn_command(command)?;
        drop(slave);

        let writer = master.take_writer()?;
        let mut reader = master.try_clone_reader()?;

        let (sender, output_rx) = mpsc::channel::<Vec<u8>>();

        let reader_thread = thread::spawn(move || {
            let mut buffer = [0_u8; 4096];

            loop {
                let bytes_read = match reader.read(&mut buffer) {
                    Ok(0) => break,
                    Ok(n) => n,
                    Err(error) if error.kind() == io::ErrorKind::Interrupted => {
                        continue;
                    }
                    Err(_) => break,
                };

                if sender.send(buffer[..bytes_read].to_vec()).is_err() {
                    break;
                }
            }
        });

        Ok(Self {
            master: Some(master),
            writer,
            output_rx,
            reader_thread: Some(reader_thread),
            child,
        })
    }

    pub fn send_input(&mut self, input: &[u8]) -> io::Result<()> {
        self.writer.write_all(input)?;
        self.writer.flush()
    }

    pub fn try_read_output(&self) -> Result<Vec<u8>, TryRecvError> {
        self.output_rx.try_recv()
    }

    pub fn resize(&self, rows: u16, cols: u16) -> Result<(), Box<dyn std::error::Error>> {
        let master = self
            .master
            .as_ref()
            .ok_or("PTY master has already been closed")?;

        master.resize(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;

        Ok(())
    }

    pub fn try_wait(&mut self) -> io::Result<Option<portable_pty::ExitStatus>> {
        self.child
            .try_wait()
            .map_err(|error| io::Error::other(error.to_string()))
    }

    pub fn terminate(&mut self) -> io::Result<()> {
        self.child
            .kill()
            .map_err(|error| io::Error::other(error.to_string()))?;

        Ok(())
    }
}

impl Drop for TerminalSession {
    fn drop(&mut self) {
        let _ = self.child.kill();

        self.master.take();

        if let Some(reader_thread) = self.reader_thread.take() {
            let _ = reader_thread.join();
        }
    }
}