practicode 0.2.5

Offline-first programming language lessons and coding practice in a Rust terminal UI.
Documentation
use crate::process::{run_capture, which};
use std::{env, process::Command, time::Duration};

pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const NPM_UPDATE_EXIT_CODE: i32 = 42;

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum UpdateCheck {
    Disabled,
    Current,
    Available(String),
    Failed,
}

pub fn check_latest_version() -> UpdateCheck {
    if env::var("PRACTICODE_NO_UPDATE_CHECK").ok().as_deref() == Some("1") {
        return UpdateCheck::Disabled;
    }
    let Some(npm) = which("npm") else {
        return UpdateCheck::Failed;
    };
    let mut command = Command::new(npm);
    command.args(["view", "practicode", "version", "--silent"]);
    match run_capture(&mut command, "", Duration::from_secs(5)) {
        Ok(output) if output.code == Some(0) => {
            let latest = output.stdout.trim();
            if is_newer(latest, CURRENT_VERSION) {
                UpdateCheck::Available(latest.to_string())
            } else {
                UpdateCheck::Current
            }
        }
        _ => UpdateCheck::Failed,
    }
}

pub fn is_newer(latest: &str, current: &str) -> bool {
    matches!(
        (version_parts(latest), version_parts(current)),
        (Some(latest), Some(current)) if latest > current
    )
}

pub fn update_is_eligible(latest: &str, current: &str, skipped: &str) -> bool {
    latest != skipped && is_newer(latest, current)
}

fn version_parts(version: &str) -> Option<[u64; 3]> {
    let mut parts = version.split('.');
    let version = [
        parts.next()?.parse().ok()?,
        parts.next()?.parse().ok()?,
        parts.next()?.parse().ok()?,
    ];
    parts.next().is_none().then_some(version)
}