ftui-pty 0.3.1

PTY-backed test utilities for FrankenTUI.
Documentation
use ftui_pty::virtual_terminal::{QuirkSet, VirtualTerminal};
use std::env;
use std::error::Error;
use std::fs;
use std::path::PathBuf;

struct Config {
    input: PathBuf,
    output: PathBuf,
    cols: u16,
    rows: u16,
    quirks: QuirkSet,
}

fn print_usage() {
    eprintln!(
        "Usage: pty_canonicalize --input <file> --output <file> --cols <n> --rows <n> [--profile <name>] [--quirk <name>]\n\
         \n\
         Profiles:\n\
           none | tmux_nested | screen | windows_console\n\
         Quirks (repeatable):\n\
           tmux_nested_cursor | screen_immediate_wrap | windows_no_alt_screen\n\
         \n\
         Example:\n\
           pty_canonicalize --input /tmp/run.pty --output /tmp/run.canonical.txt --cols 80 --rows 24 --profile screen"
    );
}

fn parse_profile(name: &str) -> Result<QuirkSet, String> {
    match name {
        "none" => Ok(QuirkSet::empty()),
        "tmux_nested" | "tmux" => Ok(QuirkSet::tmux_nested()),
        "screen" | "gnu_screen" => Ok(QuirkSet::gnu_screen()),
        "windows" | "windows_console" => Ok(QuirkSet::windows_console()),
        _ => Err(format!("unknown --profile value: {name}")),
    }
}

fn apply_quirk(quirks: QuirkSet, name: &str) -> Result<QuirkSet, String> {
    match name {
        "tmux_nested_cursor" | "tmux_nested" => Ok(quirks.with_tmux_nested_cursor(true)),
        "screen_immediate_wrap" | "screen_wrap" => Ok(quirks.with_screen_immediate_wrap(true)),
        "windows_no_alt_screen" | "windows_console" => Ok(quirks.with_windows_no_alt_screen(true)),
        _ => Err(format!("unknown --quirk value: {name}")),
    }
}

fn parse_args() -> Result<Config, String> {
    let mut args = env::args().skip(1);
    let mut input: Option<PathBuf> = None;
    let mut output: Option<PathBuf> = None;
    let mut cols: Option<u16> = None;
    let mut rows: Option<u16> = None;
    let mut quirks = QuirkSet::empty();
    let mut positional: Vec<String> = Vec::new();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--input" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--input requires a value".to_string())?;
                input = Some(PathBuf::from(value));
            }
            "--output" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--output requires a value".to_string())?;
                output = Some(PathBuf::from(value));
            }
            "--cols" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--cols requires a value".to_string())?;
                cols = Some(
                    value
                        .parse::<u16>()
                        .map_err(|_| "invalid --cols value".to_string())?,
                );
            }
            "--rows" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--rows requires a value".to_string())?;
                rows = Some(
                    value
                        .parse::<u16>()
                        .map_err(|_| "invalid --rows value".to_string())?,
                );
            }
            "--profile" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--profile requires a value".to_string())?;
                quirks = parse_profile(&value)?;
            }
            "--quirk" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--quirk requires a value".to_string())?;
                quirks = apply_quirk(quirks, &value)?;
            }
            "-h" | "--help" => {
                print_usage();
                std::process::exit(0);
            }
            _ => positional.push(arg),
        }
    }

    if input.is_none() && positional.len() == 4 {
        input = Some(PathBuf::from(&positional[0]));
        output = Some(PathBuf::from(&positional[1]));
        cols = Some(
            positional[2]
                .parse::<u16>()
                .map_err(|_| "invalid cols positional value".to_string())?,
        );
        rows = Some(
            positional[3]
                .parse::<u16>()
                .map_err(|_| "invalid rows positional value".to_string())?,
        );
    }

    let input = input.ok_or_else(|| "missing --input".to_string())?;
    let output = output.ok_or_else(|| "missing --output".to_string())?;
    let cols = cols.ok_or_else(|| "missing --cols".to_string())?;
    let rows = rows.ok_or_else(|| "missing --rows".to_string())?;

    if cols == 0 || rows == 0 {
        return Err("cols/rows must be > 0".to_string());
    }

    Ok(Config {
        input,
        output,
        cols,
        rows,
        quirks,
    })
}

fn run() -> Result<(), Box<dyn Error>> {
    let cfg = parse_args().inspect_err(|_| {
        print_usage();
    })?;

    let bytes = fs::read(&cfg.input)?;
    let mut vt = VirtualTerminal::with_quirks(cfg.cols, cfg.rows, cfg.quirks);
    vt.feed(&bytes);
    let text = vt.screen_text();
    fs::write(&cfg.output, text)?;
    Ok(())
}

fn main() {
    if let Err(err) = run() {
        eprintln!("pty_canonicalize error: {err}");
        std::process::exit(1);
    }
}