taskers-runtime 0.7.0

PTY runtime and OSC signal parsing for taskers.
Documentation
use std::{
    collections::BTreeMap,
    io::{Read, Write},
    path::PathBuf,
};

use anyhow::Result;
use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandSpec {
    pub program: String,
    pub args: Vec<String>,
    pub cwd: Option<PathBuf>,
    pub env: BTreeMap<String, String>,
    pub cols: u16,
    pub rows: u16,
}

impl CommandSpec {
    pub fn shell() -> Self {
        Self {
            program: std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()),
            args: Vec::new(),
            cwd: None,
            env: BTreeMap::new(),
            cols: 120,
            rows: 40,
        }
    }

    pub fn new(program: impl Into<String>) -> Self {
        Self {
            program: program.into(),
            args: Vec::new(),
            cwd: None,
            env: BTreeMap::new(),
            cols: 120,
            rows: 40,
        }
    }
}

pub struct PtySession {
    child: Box<dyn portable_pty::Child + Send + Sync>,
    master: Box<dyn MasterPty + Send>,
    writer: Box<dyn Write + Send>,
}

pub struct PtyReader {
    reader: Box<dyn Read + Send>,
}

pub struct SpawnedPty {
    pub session: PtySession,
    pub reader: PtyReader,
}

impl PtySession {
    pub fn spawn(spec: &CommandSpec) -> Result<SpawnedPty> {
        let pty_system = native_pty_system();
        let pair = pty_system.openpty(PtySize {
            rows: spec.rows,
            cols: spec.cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;

        let mut command = CommandBuilder::new(&spec.program);
        for arg in &spec.args {
            command.arg(arg);
        }
        if let Some(cwd) = &spec.cwd {
            command.cwd(cwd);
        }
        for (key, value) in &spec.env {
            command.env(key, value);
        }

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

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

        Ok(SpawnedPty {
            session: Self {
                child,
                master: pair.master,
                writer,
            },
            reader: PtyReader { reader },
        })
    }

    pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
        self.master.resize(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;
        Ok(())
    }

    pub fn process_id(&self) -> Option<u32> {
        self.child.process_id()
    }

    pub fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> {
        self.writer.write_all(data)
    }

    pub fn kill(&mut self) -> Result<()> {
        self.child.kill()?;
        Ok(())
    }
}

impl PtyReader {
    pub fn read_into(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
        self.reader.read(buffer)
    }
}