Added base theme for defaults

This commit is contained in:
ManOfGoldForever 2026-04-01 13:03:39 -04:00
parent 31179d64e5
commit eb692b56e4
2 changed files with 101 additions and 3 deletions

View file

@ -103,13 +103,33 @@ pub fn plan_apply(
force: bool,
current_state: Option<&State>,
) -> Result<ApplyPlan> {
let theme_dir = themes_root()?.join(theme_name);
if theme_name == "base" {
anyhow::bail!("\"base\" is a reserved layer name and cannot be applied as a theme");
}
let root = themes_root()?;
let theme_dir = root.join(theme_name);
if !theme_dir.is_dir() {
anyhow::bail!("theme not found: {}", theme_name);
}
let home = effective_home()?;
let files = enumerate_theme_files(&theme_dir)?;
// Build a merged file map: base files first, theme files overlay (and win on conflict).
// Using BTreeMap keeps the iteration order deterministic.
let mut file_map: std::collections::BTreeMap<PathBuf, PathBuf> =
std::collections::BTreeMap::new();
let base_dir = root.join("base");
if base_dir.is_dir() {
for (rel, src) in enumerate_theme_files(&base_dir)? {
file_map.insert(rel, src);
}
}
for (rel, src) in enumerate_theme_files(&theme_dir)? {
file_map.insert(rel, src);
}
let managed_targets: HashSet<PathBuf> = current_state
.map(|s| s.entries.iter().map(|e| PathBuf::from(&e.target)).collect())
@ -117,7 +137,7 @@ pub fn plan_apply(
let mut actions = Vec::new();
for (relative, source) in files {
for (relative, source) in file_map {
let target = home.join(&relative);
match target.symlink_metadata() {
@ -724,4 +744,78 @@ mod tests {
let link = fs::read_link(env.home.path().join(".zshrc")).unwrap();
assert!(link.to_string_lossy().contains("catppuccin"));
}
// ---- base layer tests -----------------------------------------------
fn make_base_file(env: &TestEnv, rel: &str, content: &str) -> PathBuf {
make_theme_file(env, "base", rel, content)
}
#[test]
fn base_files_applied_alongside_theme() {
let env = TestEnv::new();
make_base_file(&env, ".config/hypr/keybinds.conf", "# base keybinds");
make_theme_file(&env, "dracula", ".zshrc", "# dracula");
let plan = plan_apply("dracula", false, None).unwrap();
assert_eq!(plan.actions.len(), 2);
let (_, summary) = execute_apply(&plan, false).unwrap();
assert_eq!(summary.created, 2);
let keybinds = fs::read_link(env.home.path().join(".config/hypr/keybinds.conf")).unwrap();
assert!(keybinds.to_string_lossy().contains("base"));
let zshrc = fs::read_link(env.home.path().join(".zshrc")).unwrap();
assert!(zshrc.to_string_lossy().contains("dracula"));
}
#[test]
fn theme_overrides_base_file() {
let env = TestEnv::new();
make_base_file(&env, ".zshrc", "# base zshrc");
make_theme_file(&env, "dracula", ".zshrc", "# dracula zshrc");
let plan = plan_apply("dracula", false, None).unwrap();
// Only one action — theme wins over base for same path.
assert_eq!(plan.actions.len(), 1);
execute_apply(&plan, false).unwrap();
let link = fs::read_link(env.home.path().join(".zshrc")).unwrap();
assert!(link.to_string_lossy().contains("dracula"));
}
#[test]
fn base_file_stays_when_theme_has_no_override() {
let env = TestEnv::new();
make_base_file(&env, ".config/hypr/keybinds.conf", "# base keybinds");
make_theme_file(&env, "dracula", ".zshrc", "# dracula");
make_theme_file(&env, "catppuccin", ".zshrc", "# catppuccin");
// Apply dracula
let plan = plan_apply("dracula", false, None).unwrap();
let (dracula_state, _) = execute_apply(&plan, false).unwrap();
// Switch to catppuccin — neither theme has keybinds.conf, so base symlink stays
let plan2 = plan_apply("catppuccin", false, Some(&dracula_state)).unwrap();
let keybinds_action = plan2
.actions
.iter()
.find(|a| match a {
PlannedAction::Skip { target, .. } => {
target.to_string_lossy().contains("keybinds")
}
_ => false,
});
assert!(keybinds_action.is_some(), "keybinds.conf should be skipped (already correct)");
}
#[test]
fn base_is_rejected_as_theme_name() {
let env = TestEnv::new();
make_base_file(&env, ".zshrc", "# base");
let result = plan_apply("base", false, None);
assert!(result.is_err());
}
}

View file

@ -31,6 +31,10 @@ pub fn discover_themes() -> Result<Vec<String>> {
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());
}
}