moon 0.1.1

universal linux-only tool.
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

use crate::paths::Dirs;
use crate::util::Res;

pub mod ico {
    pub const CHECK: &str = "\u{f00c}";
    pub const CROSS: &str = "\u{f00d}";
    pub const WARN: &str = "\u{f071}";
    pub const FOLDER: &str = "\u{f07b}";
    pub const COG: &str = "\u{f013}";
    pub const PACKAGE: &str = "\u{f487}";
    pub const ARROW: &str = "\u{f061}";
    pub const TRASH: &str = "\u{f1f8}";
    pub const LIST: &str = "\u{f03a}";
    pub const UNDO: &str = "\u{f0e2}";
    pub const BULLET: &str = "\u{f444}";
    pub const LINK: &str = "\u{f0c1}";
    pub const DRIVE: &str = "\u{f0a0}";
    pub const DOWNLOAD: &str = "\u{f019}";
    pub const CLOCK: &str = "\u{f017}";
    pub const INFO: &str = "\u{f05a}";
    pub const MAGIC: &str = "\u{f0d0}";
    pub const WRENCH: &str = "\u{f0ad}";
    pub const SEARCH: &str = "\u{f002}";
    pub const FILE: &str = "\u{f15b}";
    pub const SHIELD: &str = "\u{f132}";
    pub const REFRESH: &str = "\u{f021}";
    pub const SAVE: &str = "\u{f0c7}";
    pub const UP: &str = "\u{f077}";
    pub const MOON: &str = "\u{f186}";
    pub const EXAM: &str = "\u{f058}";
    pub const STEP: &str = "\u{f105}";
}

#[derive(Clone, Copy, PartialEq)]
pub enum IconMode {
    Auto,
    Always,
    Never,
}

pub struct Config {
    path: PathBuf,
    pub icons: IconMode,
    nerd_detected: bool,
}

impl Config {
    pub fn load() -> Self {
        let cfg = env::var_os("XDG_CONFIG_HOME")
            .map(PathBuf::from)
            .filter(|p| p.is_absolute())
            .unwrap_or_else(|| PathBuf::from(env::var_os("HOME").unwrap_or_default()).join(".config"));
        let path = cfg.join("moon").join("config");
        let mut c = Config { path, icons: IconMode::Auto, nerd_detected: has_nerd_font() };
        if let Ok(t) = fs::read_to_string(&c.path) {
            for l in t.lines() {
                if let Some(v) = l.trim().strip_prefix("icons=") {
                    c.icons = match v.trim() {
                        "always" | "on" | "yes" => IconMode::Always,
                        "never" | "off" | "no" => IconMode::Never,
                        _ => IconMode::Auto,
                    };
                }
            }
        }
        c
    }

    fn save(&self) -> Res<()> {
        if let Some(p) = self.path.parent() {
            fs::create_dir_all(p).map_err(|e| format!("cannot create {}: {e}", p.display()))?;
        }
        let s = format!("# moon config\n# icons: auto | always | never\nicons={}\n", match self.icons {
            IconMode::Auto => "auto",
            IconMode::Always => "always",
            IconMode::Never => "never",
        });
        fs::write(&self.path, s).map_err(|e| format!("cannot write {}: {e}", self.path.display()))
    }

    pub fn icons_on(&self) -> bool {
        match self.icons {
            IconMode::Always => true,
            IconMode::Never => false,
            IconMode::Auto => self.nerd_detected,
        }
    }

    pub fn icon<'a>(&'a self, c: &'a str) -> &'a str {
        if self.icons_on() { c } else { "" }
    }

    pub fn glyph<'a>(&'a self, c: &'a str, plain: &'a str) -> &'a str {
        if self.icons_on() { c } else { plain }
    }

    pub fn step<'a>(&'a self) -> &'a str {
        self.glyph(ico::STEP, "==>")
    }
}

fn apps_hint(cfg: &Config) -> String {
    cfg.path
        .parent()
        .and_then(|p| p.parent())
        .map(|p| p.join("data/moon").display().to_string())
        .unwrap_or_default()
}

pub fn has_nerd_font() -> bool {
    let home = env::var_os("HOME").map(PathBuf::from);
    let mut roots: Vec<PathBuf> = vec![
        PathBuf::from("/usr/share/fonts"),
        PathBuf::from("/usr/local/share/fonts"),
        PathBuf::from("/run/current-system/sw/share/fonts"),
    ];
    if let Some(h) = &home {
        roots.push(h.join(".local/share/fonts"));
        roots.push(h.join(".fonts"));
    }
    let mut stack: Vec<PathBuf> = roots;
    let mut seen = 0;
    while let Some(dir) = stack.pop() {
        let Ok(rd) = fs::read_dir(&dir) else { continue };
        for e in rd.flatten() {
            let p = e.path();
            let is_dir = e.file_type().map_or(false, |t| t.is_dir());
            if is_dir {
                if p.file_name().map_or(false, |n| n == "fonts.conf") {
                    continue;
                }
                if stack.len() < 512 {
                    stack.push(p);
                }
                continue;
            }
            seen += 1;
            if seen > 20_000 {
                return false;
            }
            let name = p.file_name().unwrap_or_default().to_string_lossy().to_ascii_lowercase();
            if (name.contains("nerd") || name.contains("powerline") || name.contains("nerdfont"))
                && [".ttf", ".otf", ".ttc", ".woff2"].iter().any(|e| name.ends_with(e))
            {
                return true;
            }
        }
    }
    false
}

pub fn cmd_config(cfg: &mut Config, icons_set: bool, args: &[String]) -> Res<()> {
    let interactive = {
        use std::io::IsTerminal;
        std::io::stdin().is_terminal()
    };
    let yes = args.iter().any(|a| a == "-y" || a == "--yes");
    let script = args
        .iter()
        .position(|a| a == "--script")
        .and_then(|i| args.get(i + 1))
        .map(String::as_str);
    let mode_str = |m: IconMode| match m {
        IconMode::Auto => "auto",
        IconMode::Always => "always",
        IconMode::Never => "never",
    };
    let detected = if cfg.nerd_detected { "found" } else { "not installed" };

    if args.iter().any(|a| a == "--check") {
        return crate::update::cmd_check(cfg);
    }
    if args.iter().any(|a| a == "--update") {
        return crate::update::cmd_update(&Dirs::new()?, cfg, script, yes);
    }

    if icons_set && !interactive {
        cfg.save()?;
        println!("{} Icons: {} ({})", cfg.icon(ico::CHECK), mode_str(cfg.icons), detected);
        println!("  {} saved to {}", cfg.icon(ico::SAVE), cfg.path.display());
        return Ok(());
    }

    if !interactive {
        println!();
        println!("  {}  moon settings", cfg.icon(ico::COG));
        println!("  {}", "\u{2500}".repeat(40));
        println!("  {:<14}{} ({})", "Icons:", mode_str(cfg.icons), detected);
        println!("  {:<14}{}", "Version:", crate::update::VERSION);
        println!("  {:<14}{}", "Config:", cfg.path.display());
        println!("  {:<14}{}", "Apps:", format!("{}/apps", apps_hint(cfg)));
        println!();
        println!("  {} set with:  moon config --icons auto|always|never", cfg.icon(ico::BULLET));
        println!("  {} check with: moon config --check", cfg.icon(ico::BULLET));
        println!("  {} update with: open `moon config`, pick 2", cfg.icon(ico::BULLET));
        println!();
        return Ok(());
    }

    if icons_set {
        cfg.save()?;
        println!("{} Icons: {} ({})", cfg.icon(ico::CHECK), mode_str(cfg.icons), detected);
        return Ok(());
    }

    loop {
        println!();
        println!("  {}  moon settings", cfg.icon(ico::COG));
        println!("  {}", "\u{2500}".repeat(40));
        println!("  {:<3}{:<20}{}", "1.", "Icons", format!("{} (Nerd Font {})", mode_str(cfg.icons), detected));
        println!("  {:<3}{:<20}{}", "2.", "Update moon", format!("now {}", crate::update::VERSION));
        println!("  {:<3}{}", "0.", "Cancel");
        println!();
        print!("  {} ", cfg.icon(ico::ARROW));
        let _ = std::io::stdout().flush();
        let mut s = String::new();
        if std::io::stdin().read_line(&mut s).is_err() {
            return Ok(());
        }
        match s.trim() {
            "1" => {
                println!();
                println!("  Icons:");
                println!("    1. auto     use them only if a Nerd Font is installed  ({detected})");
                println!("    2. always   use them even if none is installed");
                println!("    3. never    plain text, no glyphs");
                print!("  {} ", cfg.icon(ico::ARROW));
                let _ = std::io::stdout().flush();
                let mut c = String::new();
                if std::io::stdin().read_line(&mut c).is_err() {
                    return Ok(());
                }
                cfg.icons = match c.trim() {
                    "2" => IconMode::Always,
                    "3" => IconMode::Never,
                    _ => IconMode::Auto,
                };
                cfg.save()?;
                println!("  {} Saved: icons = {}", cfg.icon(ico::CHECK), mode_str(cfg.icons));
            }
            "2" => {
                let d = Dirs::new()?;
                crate::update::cmd_update(&d, cfg, script, yes)?;
            }
            "0" | "" => return Ok(()),
            _ => println!("  Pick 0-2."),
        }
    }
}