cryosnap-core 0.2.11

Core rendering library for cryosnap.
Documentation
use crate::{Config, Error, InputSource, Result};
use std::path::PathBuf;
use std::time::Duration;

#[derive(Debug, Clone)]
pub(crate) struct LoadedInput {
    pub(crate) text: String,
    pub(crate) path: Option<PathBuf>,
    pub(crate) kind: InputKind,
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum InputKind {
    Code,
    Ansi,
}

pub(crate) fn load_input(input: &InputSource, timeout: Duration) -> Result<LoadedInput> {
    match input {
        InputSource::Text(text) => Ok(LoadedInput {
            text: text.clone(),
            path: None,
            kind: InputKind::Code,
        }),
        InputSource::File(path) => {
            let text = std::fs::read_to_string(path)?;
            Ok(LoadedInput {
                text,
                path: Some(path.clone()),
                kind: InputKind::Code,
            })
        }
        InputSource::Command(cmd) => {
            let text = execute_command(cmd, timeout)?;
            Ok(LoadedInput {
                text,
                path: None,
                kind: InputKind::Ansi,
            })
        }
    }
}

pub(crate) fn is_ansi_input(loaded: &LoadedInput, config: &Config) -> bool {
    if let Some(lang) = &config.language {
        if lang.eq_ignore_ascii_case("ansi") {
            return true;
        }
    }
    if matches!(loaded.kind, InputKind::Ansi) {
        return true;
    }
    loaded.text.contains('\u{1b}')
}

pub(crate) fn execute_command(cmd: &str, timeout: Duration) -> Result<String> {
    use portable_pty::{native_pty_system, CommandBuilder, PtySize};
    use std::io::Read;
    use std::sync::mpsc;
    use std::thread;

    let args = shell_words::split(cmd)
        .map_err(|err| Error::InvalidInput(format!("command parse: {err}")))?;
    if args.is_empty() {
        return Err(Error::InvalidInput("empty command".to_string()));
    }

    let (cols, rows) = terminal_size::terminal_size()
        .map(|(w, h)| (w.0, h.0))
        .unwrap_or((80, 24));

    let pty_system = native_pty_system();
    let pair = pty_system
        .openpty(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })
        .map_err(|err| Error::Render(format!("open pty: {err}")))?;

    let mut command = CommandBuilder::new(&args[0]);
    if args.len() > 1 {
        command.args(&args[1..]);
    }

    let mut child = pair
        .slave
        .spawn_command(command)
        .map_err(|err| Error::Render(format!("spawn command: {err}")))?;
    drop(pair.slave);
    let mut killer = child.clone_killer();

    let mut reader = pair
        .master
        .try_clone_reader()
        .map_err(|err| Error::Render(format!("pty reader: {err}")))?;
    drop(pair.master);

    let read_handle = thread::spawn(move || {
        let mut buf = Vec::new();
        let _ = reader.read_to_end(&mut buf);
        buf
    });

    let (status_tx, status_rx) = mpsc::channel();
    thread::spawn(move || {
        let status = child.wait();
        let _ = status_tx.send(status);
    });

    let status = match status_rx.recv_timeout(timeout) {
        Ok(status) => status,
        Err(_) => {
            let _ = killer.kill();
            return Err(Error::Timeout);
        }
    };
    let output = read_handle.join().unwrap_or_default();
    let output_str = String::from_utf8_lossy(&output).to_string();

    match status {
        Ok(exit) => {
            if !exit.success() {
                return Err(Error::Render(format!("command exited with {exit}")));
            }
        }
        Err(err) => return Err(Error::Render(format!("command wait: {err}"))),
    }

    if output_str.is_empty() {
        return Err(Error::InvalidInput("no command output".to_string()));
    }

    Ok(output_str)
}