asuskb 0.1.0

A tool for changing asus laptop keyboard light level
use std::{
    fmt::Display,
    io::Read,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{Context, Result, bail};

#[derive(Debug, Clone, clap::ValueEnum)]
pub(crate) enum KbLevel {
    #[clap(name = "off")]
    Off,
    #[clap(name = "low")]
    Low,
    #[clap(name = "med")]
    Med,
    #[clap(name = "high")]
    High,
}

#[derive(Debug)]
pub(crate) struct Kbu8Level(u8);

impl Display for KbLevel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match *self {
            KbLevel::Off => "off",
            KbLevel::Low => "low",
            KbLevel::Med => "med",
            KbLevel::High => "high",
        };
        write!(f, "{}", s)
    }
}

impl TryFrom<&str> for KbLevel {
    type Error = anyhow::Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value.to_lowercase().as_str() {
            "off" => Ok(KbLevel::Off),
            "low" => Ok(KbLevel::Low),
            "med" => Ok(KbLevel::Med),
            "high" => Ok(KbLevel::High),
            &_ => bail!("Invalid keyboard level, expected 'Off', Low', 'Med', or 'High'"),
        }
    }
}

impl TryFrom<Kbu8Level> for KbLevel {
    type Error = anyhow::Error;

    fn try_from(value: Kbu8Level) -> std::prelude::v1::Result<Self, Self::Error> {
        match value.0 {
            0 => Ok(KbLevel::Off),
            1 => Ok(KbLevel::Low),
            2 => Ok(KbLevel::Med),
            3 => Ok(KbLevel::High),
            _ => bail!("Invalid number keyboard level, expected '0', '1', '2' or '3'"),
        }
    }
}

impl From<KbLevel> for Kbu8Level {
    fn from(value: KbLevel) -> Self {
        match value {
            KbLevel::Off => Kbu8Level(0),
            KbLevel::Low => Kbu8Level(1),
            KbLevel::Med => Kbu8Level(2),
            KbLevel::High => Kbu8Level(3),
        }
    }
}

const ASUS_UTIL: &str = "asusctl";

pub(crate) fn get_asusctl() -> Result<PathBuf> {
    let asusctl_path = which::which_global(ASUS_UTIL)
        .context("Can not find asusctl on system, install asusctl first")?;

    if !asusctl_path.exists() {
        bail!("Asusctl is found but its path is invalid");
    }

    if asusctl_path.is_dir() {
        bail!("Asusctl path is a directory");
    }

    std::path::absolute(asusctl_path)
        .context("Failed to convert asusctl path into a system absolute path")
}

pub(crate) fn get_kb_light_level(asusctl: &Path) -> Result<KbLevel> {
    let get_cmd = Command::new(asusctl)
        .args(["leds", "get"])
        .output()
        .context("Failed to execute asusctl command")?;

    if !get_cmd.status.success() {
        let stderr = String::from_utf8_lossy(&get_cmd.stderr);
        bail!(
            "Asusctl exited with exit code {} \n {}",
            get_cmd.status.code().unwrap_or(1),
            stderr.trim()
        );
    }

    let kb_level = if let Some(level) = String::from_utf8_lossy(&get_cmd.stdout)
        .trim()
        .split(": ")
        .nth(1)
    {
        KbLevel::try_from(level).map_err(|e| anyhow::anyhow!(e))?
    } else {
        bail!("Can not parse output into a valid keyboard light level");
    };

    Ok(kb_level)
}

pub(crate) fn set_kb_light_level(asusctl: &Path, kb_level: KbLevel) -> Result<()> {
    let mut cmd = Command::new(asusctl);
    let set_cmd = cmd.args(["leds", "set"]);
    let kb_level_arg = kb_level.to_string();
    set_cmd.arg(kb_level_arg);

    if let Ok(mut child) = set_cmd.spawn() {
        if let Ok(exit_code) = child.wait() {
            if !exit_code.success() {
                let mut stderr_buffer = String::new();
                let mut stderr = child
                    .stderr
                    .ok_or_else(|| anyhow::anyhow!("Failed to get stderr"))?;
                stderr.read_to_string(&mut stderr_buffer)?;
                bail!(
                    "Asusctl exited with exit code {} \n {}",
                    exit_code.code().unwrap_or(1),
                    stderr_buffer.trim()
                );
            }
        } else {
            bail!("Can not run chile process")
        }
    } else {
        bail!("Failed to spawn child process")
    }

    Ok(())
}

fn change_kb_light_level(asusctl: &Path, step: i8) -> Result<()> {
    let cur_level = Kbu8Level::from(get_kb_light_level(asusctl)?);
    let new_level = (cur_level.0 as i8 + step).clamp(0, 3);
    set_kb_light_level(asusctl, KbLevel::try_from(Kbu8Level(new_level as u8))?)?;
    Ok(())
}

pub(crate) fn inc_kb_light_level(asusctl: &Path) -> Result<()> {
    change_kb_light_level(asusctl, 1)
}

pub(crate) fn dec_kb_light_level(asusctl: &Path) -> Result<()> {
    change_kb_light_level(asusctl, -1)
}

pub(crate) fn custom_kb_light_level(asusctl: &Path, step: i8) -> Result<()> {
    match step {
        (-3..=3) => change_kb_light_level(asusctl, step),
        _ => {
            bail!("Step have to be between -3 and 3 inclusive")
        }
    }
}