ao-cli 0.1.6

A unified administration tool for Linux systems
use crate::os::ExecutableCommand;
use anyhow::{Context, Result};
use std::io::Write;
use std::process::Command;

pub struct SystemCommand {
    pub binary: String,
    pub args: Vec<String>,
    pub stdin_data: Option<String>,
    pub ignore_exit_code: bool,
}

impl SystemCommand {
    pub fn new(binary: &str) -> Self {
        Self {
            binary: binary.to_string(),
            args: Vec::new(),
            stdin_data: None,
            ignore_exit_code: false,
        }
    }

    pub fn ignore_exit_code(mut self) -> Self {
        self.ignore_exit_code = true;
        self
    }

    pub fn arg(mut self, arg: &str) -> Self {
        self.args.push(arg.to_string());
        self
    }

    pub fn args(mut self, args: &[String]) -> Self {
        for arg in args {
            self.args.push(arg.clone());
        }
        self
    }

    pub fn stdin(mut self, data: &str) -> Self {
        self.stdin_data = Some(data.to_string());
        self
    }
}

impl ExecutableCommand for SystemCommand {
    fn execute(&self) -> Result<()> {
        let mut cmd = Command::new(&self.binary);
        cmd.args(&self.args);

        if let Some(data) = &self.stdin_data {
            cmd.stdin(std::process::Stdio::piped());
            let mut child = cmd
                .spawn()
                .with_context(|| format!("Failed to spawn {}", self.binary))?;
            if let Some(mut stdin) = child.stdin.take() {
                stdin
                    .write_all(data.as_bytes())
                    .with_context(|| format!("Failed to write to {} stdin", self.binary))?;
            }
            let status = child
                .wait()
                .with_context(|| format!("Failed to wait on {}", self.binary))?;
            if !self.ignore_exit_code && !status.success() {
                anyhow::bail!("{} failed with status {}", self.binary, status);
            }
        } else {
            let status = cmd
                .status()
                .with_context(|| format!("Failed to execute {}", self.binary))?;
            if !self.ignore_exit_code && !status.success() {
                anyhow::bail!("{} failed with status {}", self.binary, status);
            }
        }
        Ok(())
    }

    fn as_string(&self) -> String {
        format!("{} {}", self.binary, self.args.join(" "))
    }
}

pub struct CompoundCommand {
    pub commands: Vec<Box<dyn ExecutableCommand>>,
}

impl CompoundCommand {
    pub fn new(commands: Vec<Box<dyn ExecutableCommand>>) -> Self {
        Self { commands }
    }
}

impl ExecutableCommand for CompoundCommand {
    fn execute(&self) -> Result<()> {
        for cmd in &self.commands {
            cmd.execute()?;
        }
        Ok(())
    }

    fn as_string(&self) -> String {
        self.commands
            .iter()
            .map(|cmd| cmd.as_string())
            .collect::<Vec<String>>()
            .join(" && ")
    }
}

pub struct NoopCommand;
impl ExecutableCommand for NoopCommand {
    fn execute(&self) -> Result<()> {
        Ok(())
    }
    fn as_string(&self) -> String {
        "".to_string()
    }
}

pub fn is_completing_arg(
    words: &[&str],
    cmd_parts: &[&str],
    arg_pos: usize,
    _last_word_complete: bool,
) -> bool {
    if words.len() < cmd_parts.len() {
        return false;
    }
    if !words.starts_with(cmd_parts) {
        return false;
    }

    let words_after_cmd = words.len() - cmd_parts.len();
    words_after_cmd == arg_pos
}

pub fn format_duration(seconds: u64) -> String {
    let days = seconds / 86400;
    let hours = (seconds % 86400) / 3600;
    let minutes = (seconds % 3600) / 60;
    let secs = seconds % 60;

    let mut parts = Vec::new();
    if days > 0 {
        parts.push(format!("{}d", days));
    }
    if hours > 0 {
        parts.push(format!("{}h", hours));
    }
    if minutes > 0 {
        parts.push(format!("{}m", minutes));
    }
    if secs > 0 || parts.is_empty() {
        parts.push(format!("{}s", secs));
    }

    parts.join(" ")
}

pub fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    const TB: u64 = GB * 1024;

    if bytes >= TB {
        format!("{:.2} TB", bytes as f64 / TB as f64)
    } else if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

pub enum Emoji {
    Up,
    Down,
    Unknown,
    Physical,
    Wireless,
    Virtual,
    Cpu,
    Ram,
    Network,
    Disk,
    Used,
    Total,
    Pci,
    Usb,
    Loop,
    Nvme,
    Ssd,
    Hdd,
    Printer,
}

impl Emoji {
    pub fn get(&self) -> &'static str {
        match self {
            Emoji::Up => "🟢",
            Emoji::Down => "🔴",
            Emoji::Unknown => "🟡",
            Emoji::Physical => "🏗️",
            Emoji::Wireless => "📶",
            Emoji::Virtual => "☁️",
            Emoji::Cpu => "💻",
            Emoji::Ram => "🧠",
            Emoji::Network => "🌐",
            Emoji::Disk => "💾",
            Emoji::Used => "",
            Emoji::Total => "",
            Emoji::Pci => "🏗️",
            Emoji::Usb => "🔌",
            Emoji::Loop => "🔁",
            Emoji::Nvme => "🚀",
            Emoji::Ssd => "",
            Emoji::Hdd => "💾",
            Emoji::Printer => "🖨️",
        }
    }
}