use std::fs;
use std::io;
use std::path::Path;
use super::ecosystem::file_name;
use super::preferences::hold_folder;
use super::{Ecosystem, SettingValue, Settings, Shared};
use crate::diagnostics::Severity;
use crate::icons::IconMode;
impl Ecosystem {
pub fn settle(&self, app: &str) -> io::Result<Vec<Shared>> {
match self.config_dir() {
Some(dir) => self.settle_in(&dir, app),
None => Err(io::Error::new(io::ErrorKind::NotFound, "no config directory found")),
}
}
pub fn settle_in(&self, config_dir: &Path, app: &str) -> io::Result<Vec<Shared>> {
fs::create_dir_all(config_dir)?;
let _held = hold_folder(config_dir)?;
let mut own = Settings::open(config_dir.join(file_name(app))).member_of(self);
if let Some(problem) = own.diagnostics().iter().find(|problem| problem.severity == Severity::Error) {
return Err(io::Error::new(io::ErrorKind::InvalidData, problem.to_string()));
}
if own.get::<bool>(Settings::SHARED_CHECKED) == Some(true) {
return Ok(Vec::new());
}
let shared_path = config_dir.join(file_name(self.id()));
let shared = shared_path.is_file().then(|| Settings::open(&shared_path));
let mut following = Vec::new();
for key in Shared::ALL {
let fixed = key.read(&own).filter(|text| *text != self.id());
let common = shared
.as_ref()
.and_then(|shared| key.read(shared))
.or_else(|| (key == Shared::ReducedMotion && shared.is_some()).then(|| false.to_string()));
if let (Some(fixed), Some(common)) = (fixed, common)
&& common != self.id()
&& same(key, &fixed, &common)
{
own.store(key.key(), SettingValue::Text(self.id().to_owned()));
following.push(key);
}
}
own.store(Settings::SHARED_CHECKED, SettingValue::Bool(true));
own.save()?;
Ok(following)
}
}
fn same(key: Shared, fixed: &str, common: &str) -> bool {
match key {
Shared::Icons => IconMode::from_name(fixed).is_some_and(|mode| IconMode::from_name(common) == Some(mode)),
Shared::ReducedMotion => fixed.parse::<bool>().is_ok_and(|reduced| common.parse() == Ok(reduced)),
Shared::Language | Shared::Theme => {
let fixed = fixed.trim();
!fixed.is_empty() && fixed == common.trim()
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn folder(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("quvyta-settle-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).expect("the folder");
path
}
fn read(folder: &Path, name: &str) -> String {
fs::read_to_string(folder.join(name)).unwrap_or_default()
}
const SHARED: &str = "language = \"tr\"\ntheme = \"nordic\"\nicons = \"unicode\"\n";
#[test]
fn a_value_equal_to_the_shared_one_follows_the_ecosystem() {
let folder = folder("equal");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
fs::write(
folder.join("desktop.conf"),
"language = \"tr\"\ntheme = \"nordic\"\nicons = \"unicode\"\ndock = 3\n",
)
.expect("own");
let following = Ecosystem::QUVYTA.settle_in(&folder, "desktop").expect("settle");
assert_eq!(following, [Shared::Language, Shared::Theme, Shared::Icons]);
assert_eq!(
read(&folder, "desktop.conf"),
"language = \"quvyta\"\ntheme = \"quvyta\"\nicons = \"quvyta\"\ndock = 3\nshared-checked = true\n"
);
assert_eq!(read(&folder, "quvyta.conf"), SHARED, "the shared file is only read");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_value_other_than_the_shared_one_stays() {
let folder = folder("different");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
fs::write(folder.join("code.conf"), "language = \"en\"\ntheme = \"nordic\"\nicons = \"nerd\"\n").expect("own");
let following = Ecosystem::QUVYTA.settle_in(&folder, "code").expect("settle");
assert_eq!(following, [Shared::Theme]);
let own = read(&folder, "code.conf");
assert!(own.contains("language = \"en\""), "the person's language stays: {own}");
assert!(own.contains("icons = \"nerd\""), "and so do the icons: {own}");
assert!(own.contains("theme = \"quvyta\""), "{own}");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn reduced_motion_kept_off_follows_and_reduced_stays_when_the_shared_file_never_held_it() {
let folder = folder("motion");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
fs::write(folder.join("code.conf"), "reduced-motion = false\n").expect("code");
fs::write(folder.join("focus.conf"), "reduced-motion = true\n").expect("focus");
assert_eq!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("code"), [Shared::ReducedMotion]);
assert_eq!(read(&folder, "code.conf"), "reduced-motion = \"quvyta\"\nshared-checked = true\n");
assert!(Ecosystem::QUVYTA.settle_in(&folder, "focus").expect("focus").is_empty());
assert!(read(&folder, "focus.conf").contains("reduced-motion = true"), "the person's need stays");
fs::write(folder.join("quvyta.conf"), format!("{SHARED}reduced-motion = true\n")).expect("shared");
fs::write(folder.join("focus.conf"), "reduced-motion = true\n").expect("focus");
assert_eq!(Ecosystem::QUVYTA.settle_in(&folder, "focus").expect("again"), [Shared::ReducedMotion]);
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn the_second_run_changes_nothing() {
let folder = folder("twice");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
fs::write(folder.join("code.conf"), "theme = \"iris\"\n").expect("own");
assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("first").is_empty());
let mut own = Settings::open(folder.join("code.conf")).member_of(&Ecosystem::QUVYTA);
own.set("theme", "nordic".to_owned());
own.save().expect("the choice");
let before = read(&folder, "code.conf");
assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("second").is_empty());
assert_eq!(read(&folder, "code.conf"), before, "the choice is not undone");
assert!(before.contains("theme = \"nordic\""), "{before}");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_missing_file_takes_only_the_mark() {
let folder = folder("missing");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("settle").is_empty());
assert_eq!(read(&folder, "code.conf"), "shared-checked = true\n");
let bare = folder.join("bare");
assert!(Ecosystem::QUVYTA.settle_in(&bare, "code").expect("no shared file either").is_empty());
assert_eq!(read(&bare, "code.conf"), "shared-checked = true\n");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_broken_file_is_left_as_it_was() {
let folder = folder("broken");
fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
fs::write(folder.join("code.conf"), "theme = \nicons = \"nerd\"\n").expect("own");
let error = Ecosystem::QUVYTA.settle_in(&folder, "code").expect_err("broken");
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert_eq!(read(&folder, "code.conf"), "theme = \nicons = \"nerd\"\n");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn the_mark_survives_a_member_s_self_healing() {
let text = "theme = \"nordic\"\nshared-checked = true\n";
let healed = Settings::parse_str("code.conf", text)
.member_of(&Ecosystem::QUVYTA)
.schema(super::super::Schema::builtin())
.self_heal(true);
assert_eq!(healed.get::<bool>(Settings::SHARED_CHECKED), Some(true));
assert!(healed.diagnostics().is_empty(), "{:?}", healed.diagnostics());
}
}