145 lines
4.5 KiB
Rust
145 lines
4.5 KiB
Rust
use anyhow::{Context, Result};
|
|
use dirs::home_dir;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
pub fn themes_root() -> Result<PathBuf> {
|
|
if let Ok(root) = std::env::var("DM_THEMES_ROOT") {
|
|
return Ok(PathBuf::from(root));
|
|
}
|
|
let home = home_dir().context("failed to find home directory")?;
|
|
Ok(home.join("dotfiles").join("themes"))
|
|
}
|
|
|
|
pub fn discover_themes() -> Result<Vec<String>> {
|
|
let root = themes_root()?;
|
|
let entries = fs::read_dir(&root)
|
|
.with_context(|| format!("failed to read themes directory: {}", root.display()))?;
|
|
|
|
let mut themes = Vec::new();
|
|
|
|
for entry in entries {
|
|
let entry = entry.with_context(|| {
|
|
format!(
|
|
"failed to read an entry from themes directory: {}",
|
|
root.display()
|
|
)
|
|
})?;
|
|
let path = entry.path();
|
|
|
|
if path.is_dir() {
|
|
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
|
continue;
|
|
};
|
|
// "base" is a reserved layer name, not a selectable theme.
|
|
if name == "base" {
|
|
continue;
|
|
}
|
|
themes.push(name.to_string());
|
|
}
|
|
}
|
|
|
|
themes.sort();
|
|
Ok(themes)
|
|
}
|
|
|
|
/// Recursively enumerate all files under `theme_dir`.
|
|
/// Returns `(theme_relative_path, absolute_source_path)` pairs sorted by path.
|
|
pub fn enumerate_theme_files(theme_dir: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
|
|
let mut files = Vec::new();
|
|
enumerate_recursive(theme_dir, theme_dir, &mut files)?;
|
|
files.sort_by(|a, b| a.0.cmp(&b.0));
|
|
Ok(files)
|
|
}
|
|
|
|
fn enumerate_recursive(
|
|
root: &Path,
|
|
current: &Path,
|
|
files: &mut Vec<(PathBuf, PathBuf)>,
|
|
) -> Result<()> {
|
|
let entries = fs::read_dir(current)
|
|
.with_context(|| format!("failed to read directory: {}", current.display()))?;
|
|
|
|
for entry in entries {
|
|
let entry =
|
|
entry.with_context(|| format!("failed to read entry from: {}", current.display()))?;
|
|
let path = entry.path();
|
|
|
|
// Skip symlinks inside a theme source tree
|
|
if path
|
|
.symlink_metadata()
|
|
.map(|m| m.file_type().is_symlink())
|
|
.unwrap_or(false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if path.is_dir() {
|
|
enumerate_recursive(root, &path, files)?;
|
|
} else if path.is_file() {
|
|
let relative = path
|
|
.strip_prefix(root)
|
|
.with_context(|| format!("failed to strip prefix from: {}", path.display()))?;
|
|
files.push((relative.to_path_buf(), path));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::test_utils::ENV_MUTEX;
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
fn make_theme(root: &Path, theme: &str, files: &[&str]) {
|
|
for rel in files {
|
|
let p = root.join(theme).join(rel);
|
|
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
fs::write(&p, "").unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn discover_themes_returns_directory_names() {
|
|
let _guard = ENV_MUTEX.lock().unwrap();
|
|
let tmp = TempDir::new().unwrap();
|
|
fs::create_dir(tmp.path().join("dracula")).unwrap();
|
|
fs::create_dir(tmp.path().join("catppuccin")).unwrap();
|
|
fs::write(tmp.path().join("not-a-dir.txt"), "").unwrap();
|
|
|
|
unsafe { std::env::set_var("DM_THEMES_ROOT", tmp.path()) };
|
|
let themes = discover_themes().unwrap();
|
|
unsafe { std::env::remove_var("DM_THEMES_ROOT") };
|
|
|
|
assert_eq!(themes, vec!["catppuccin", "dracula"]);
|
|
}
|
|
|
|
#[test]
|
|
fn enumerate_theme_files_returns_relative_paths() {
|
|
let tmp = TempDir::new().unwrap();
|
|
make_theme(tmp.path(), "dracula", &[".config/nvim/init.lua", ".zshrc"]);
|
|
|
|
let theme_dir = tmp.path().join("dracula");
|
|
let files = enumerate_theme_files(&theme_dir).unwrap();
|
|
let relatives: Vec<PathBuf> = files.into_iter().map(|(r, _)| r).collect();
|
|
|
|
assert!(relatives.contains(&PathBuf::from(".config/nvim/init.lua")));
|
|
assert!(relatives.contains(&PathBuf::from(".zshrc")));
|
|
}
|
|
|
|
#[test]
|
|
fn enumerate_theme_files_is_sorted() {
|
|
let tmp = TempDir::new().unwrap();
|
|
make_theme(tmp.path(), "t", &["b.txt", "a.txt", ".config/z.txt"]);
|
|
|
|
let theme_dir = tmp.path().join("t");
|
|
let files = enumerate_theme_files(&theme_dir).unwrap();
|
|
let relatives: Vec<PathBuf> = files.into_iter().map(|(r, _)| r).collect();
|
|
let mut sorted = relatives.clone();
|
|
sorted.sort();
|
|
assert_eq!(relatives, sorted);
|
|
}
|
|
}
|