rustpm 0.2.0

A fast, friendly APT frontend with kernel, desktop, and sources management
use anyhow::{Context, Result};
use std::io::{BufRead, BufReader};
use std::process::{Command, ExitStatus, Stdio};
use std::sync::mpsc;
use std::thread;

use super::parser::{parse_dry_run_output, PackageChange};

#[derive(Debug, Clone)]
pub enum AptOperation {
    Update,
    Upgrade { full: bool },
    Install(Vec<String>),
    Remove(Vec<String>),
    Purge(Vec<String>),
}

impl AptOperation {
    fn apt_args(&self) -> Vec<String> {
        match self {
            AptOperation::Update => vec!["update".into()],
            AptOperation::Upgrade { full } => {
                if *full {
                    vec!["full-upgrade".into(), "-y".into()]
                } else {
                    vec!["upgrade".into(), "-y".into()]
                }
            }
            AptOperation::Install(pkgs) => {
                let mut args = vec!["install".into(), "-y".into()];
                args.extend(pkgs.iter().cloned());
                args
            }
            AptOperation::Remove(pkgs) => {
                let mut args = vec!["remove".into(), "-y".into()];
                args.extend(pkgs.iter().cloned());
                args
            }
            AptOperation::Purge(pkgs) => {
                let mut args = vec!["purge".into(), "-y".into()];
                args.extend(pkgs.iter().cloned());
                args
            }
        }
    }

    fn dry_run_args(&self) -> Option<Vec<String>> {
        match self {
            AptOperation::Update => None,
            AptOperation::Upgrade { full } => {
                let sub = if *full { "full-upgrade" } else { "upgrade" };
                Some(vec![sub.into(), "--simulate".into()])
            }
            AptOperation::Install(pkgs) => {
                let mut args = vec!["install".into(), "--simulate".into()];
                args.extend(pkgs.iter().cloned());
                Some(args)
            }
            AptOperation::Remove(pkgs) => {
                let mut args = vec!["remove".into(), "--simulate".into()];
                args.extend(pkgs.iter().cloned());
                Some(args)
            }
            AptOperation::Purge(pkgs) => {
                let mut args = vec!["purge".into(), "--simulate".into()];
                args.extend(pkgs.iter().cloned());
                Some(args)
            }
        }
    }
}

/// Run a dry-run simulation and return the parsed package changes.
pub fn dry_run(op: &AptOperation) -> Result<Vec<PackageChange>> {
    let args = match op.dry_run_args() {
        Some(a) => a,
        None => return Ok(vec![]),
    };

    let output = Command::new("apt-get")
        .args(&args)
        .output()
        .context("failed to run apt-get --simulate")?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if !output.status.success() {
        let msg = if stderr.is_empty() {
            stdout.to_string()
        } else {
            stderr.to_string()
        };
        anyhow::bail!("{}", msg.trim());
    }

    Ok(parse_dry_run_output(&stdout))
}

/// Execute an apt-get operation, streaming output lines via `tx`.
/// Pass `None` for `tx` to inherit stdout/stderr directly (non-TUI mode).
pub fn execute(op: &AptOperation, tx: Option<mpsc::Sender<String>>) -> Result<ExitStatus> {
    let args = op.apt_args();

    match tx {
        None => {
            // Non-TUI: inherit terminal
            let status = Command::new("apt-get")
                .args(&args)
                .status()
                .context("failed to run apt-get")?;
            Ok(status)
        }
        Some(sender) => {
            // TUI: pipe and stream lines
            let mut child = Command::new("apt-get")
                .args(&args)
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
                .context("failed to spawn apt-get")?;

            let stdout = child.stdout.take().unwrap();
            let stderr = child.stderr.take().unwrap();

            let tx1 = sender.clone();
            let tx2 = sender;

            let t1 = thread::spawn(move || {
                let reader = BufReader::new(stdout);
                for line in reader.lines().map_while(Result::ok) {
                    let _ = tx1.send(line);
                }
            });

            let t2 = thread::spawn(move || {
                let reader = BufReader::new(stderr);
                for line in reader.lines().map_while(Result::ok) {
                    let _ = tx2.send(line);
                }
            });

            let status = child.wait().context("apt-get process error")?;
            let _ = t1.join();
            let _ = t2.join();

            Ok(status)
        }
    }
}