diff --git a/README.md b/README.md index 07f8beb..829a4e0 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,8 @@ Without the installer: `cargo install --git https://github.com/Neotoxic-off/Lim `cargo build --release` and copy `target/release/lim` (`lim.exe`) onto your `PATH`. Runs on Linux, macOS and Windows 10+ (Windows Terminal or the console, not mintty). A -[Nerd Font](https://www.nerdfonts.com) for the default icons (or `"iconset": "unicode"` / `"ascii"`). Truecolor is +[Nerd Font](https://www.nerdfonts.com) for the default icons; without one installed, lim starts with +`"iconset": "unicode"` (or set `"ascii"`). Truecolor is detected; other terminals get the 256-color palette. Optional, picked up when installed: `node`, `python3`, `btm`, `gdu`, and the TUI tools of `t…`. diff --git a/docs/configuration.md b/docs/configuration.md index 2237c8e..14b5f4c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,7 +57,7 @@ Names and defaults follow AstroNvim. | `cmdheight` | `0` | `0`: notifications and an on-demand command line; `1`: permanent command line | | `clipboard` | `"unnamedplus"` | `""` keeps yanks inside lim | | `colorscheme` | unset | A theme name; unset uses the last one picked (nvchad at first) | -| `icons` · `iconset` | `true` · `"nerd"` | Glyphs at all · `"nerd"`, `"unicode"`, `"emoji"` or `"ascii"` | +| `icons` · `iconset` | `true` · `"nerd"` | Glyphs at all · `"nerd"`, `"unicode"`, `"emoji"` or `"ascii"` (`"unicode"` when no Nerd Font is installed) | | `statusline_style` | `"rounded"` | Statusline section ends: `"rounded"`, `"slanted"`, `"arrow"` or `"flat"` (Nerd Font icon set only) | | `splash` | `"random"` | Start screen hydra: `"random"` (one that fits, each start), `"braille"` or `"small"` | | `mouse` | `true` | Click (text, tabs, tabline buttons), drag to select, wheel; `false` gives the terminal its own selection back (to copy) | diff --git a/docs/themes.md b/docs/themes.md index cb22e89..272dee9 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -38,7 +38,8 @@ top of any theme. ## Icon sets `nerd` (default, Nerd Font devicons as AstroNvim), `unicode` (any font), `emoji` (color emoji, -two cells each) and `ascii`. Switch in the settings (`,`), `fI`, or the `iconset` option. +two cells each) and `ascii`. When no Nerd Font file is found in the system or user font folders, lim starts with +`unicode` instead; an `iconset` in the config always wins. Switch in the settings (`,`), `fI`, or the `iconset` option. | | | |---|---| diff --git a/src/config/mod.rs b/src/config/mod.rs index 5465839..84e22f6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -412,9 +412,44 @@ fn user_dir(xdg: &str, windows: &str, home: &[&str]) -> Option { Some(base.join("lim")) } +/// Whether a Nerd Font is installed in the system or user font folders. +/// shortcut: looks at font file names, not the terminal's font; set `iconset` when the guess is wrong. +fn nerd_font_installed() -> bool { + let var = |name: &str, parts: &[&str]| { + env::var_os(name).map(|v| parts.iter().fold(PathBuf::from(v), |p, part| p.join(part))) + }; + let dirs: [Option; 3] = if cfg!(windows) { + [var("WINDIR", &["Fonts"]), var("LOCALAPPDATA", &["Microsoft", "Windows", "Fonts"]), None] + } else if cfg!(target_os = "macos") { + [Some(PathBuf::from("/Library/Fonts")), var("HOME", &["Library", "Fonts"]), None] + } else { + [Some(PathBuf::from("/usr/share/fonts")), var("HOME", &[".local", "share", "fonts"]), var("HOME", &[".fonts"])] + }; + dirs.iter().flatten().any(|dir| has_nerd_font(dir, 3)) +} + +/// A font file named like a Nerd Font (`JetBrainsMonoNerdFont-Regular.ttf`, `CascadiaCodeNF.ttf`) under `dir`. +fn has_nerd_font(dir: &Path, depth: usize) -> bool { + let Ok(entries): io::Result = fs::read_dir(dir) else { return false }; + entries.flatten().any(|entry| { + if entry.file_type().is_ok_and(|t| t.is_dir()) { + return depth > 0 && has_nerd_font(&entry.path(), depth - 1); + } + let name: String = entry.file_name().to_string_lossy().to_lowercase().replace(' ', ""); + let Some((stem, ext)): Option<(&str, &str)> = name.rsplit_once('.') else { return false }; + let family: &str = stem.split('-').next().unwrap_or(stem); + matches!(ext, "ttf" | "otf" | "ttc") + && (family.contains("nerdfont") || ["nf", "nfm", "nfp"].iter().any(|s| family.ends_with(s))) + }) +} + /// The config and every problem found. A missing file is not a problem. pub fn load() -> (Config, Vec) { let mut config: Config = Config::default(); + // Nerd Font glyphs show as boxes without the font (a fresh install); the config's `iconset` still wins. + if !nerd_font_installed() { + config.options.iconset = "unicode".into(); + } let mut warnings: Vec = Vec::new(); let Some(path): Option = path() else { return (config, warnings) }; match fs::read_to_string(&path) { @@ -784,6 +819,23 @@ mod tests { assert_eq!(config.options, Options::default()); } + #[test] + fn finds_nerd_fonts_by_file_name() { + let dir: PathBuf = env::temp_dir().join(format!("lim-fonts-{}", std::process::id())); + let nested: PathBuf = dir.join("truetype").join("jetbrains"); + fs::create_dir_all(&nested).unwrap(); + fs::write(dir.join("DejaVuSans.ttf"), "").unwrap(); + fs::write(dir.join("fonts.conf"), "").unwrap(); + assert!(!has_nerd_font(&dir, 3)); + for name in ["JetBrainsMonoNerdFont-Regular.ttf", "Hack Nerd Font Complete.otf", "CascadiaCodeNF.ttf"] { + fs::write(nested.join(name), "").unwrap(); + assert!(has_nerd_font(&dir, 3), "{name}"); + assert!(!has_nerd_font(&dir, 0), "{name} is two folders down"); + fs::remove_file(nested.join(name)).unwrap(); + } + fs::remove_dir_all(&dir).unwrap(); + } + /// Every option written by `save_options` reads back to the same value, and other sections survive. #[test] fn saved_options_read_back() {