use super::model::{SettingsFile, normalize_ui_scale_percent};
use std::{
io,
path::{Path, PathBuf},
};
use tracing::{info, warn};
const APP_DIR_NAME: &str = "indicatrix-cut";
const LEGACY_APP_DIR_NAME: &str = "diagram-gui";
const SETTINGS_FILE_NAME: &str = "settings.toml";
fn settings_path_under(app_dir_name: &str) -> PathBuf {
platform_config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(app_dir_name)
.join(SETTINGS_FILE_NAME)
}
#[must_use]
pub fn default_settings_path() -> PathBuf {
settings_path_under(APP_DIR_NAME)
}
fn legacy_settings_path() -> PathBuf {
settings_path_under(LEGACY_APP_DIR_NAME)
}
pub fn migrate_legacy_settings_if_needed(new_path: &Path) {
copy_legacy_settings_if_absent(&legacy_settings_path(), new_path);
}
fn copy_legacy_settings_if_absent(old_path: &Path, new_path: &Path) {
if new_path.exists() {
return;
}
if !old_path.exists() {
return;
}
if let Some(parent) = new_path.parent()
&& let Err(e) = std::fs::create_dir_all(parent)
{
warn!(
"Could not create {} to migrate legacy indicatrix-cut settings into; \
starting with defaults instead: {e}",
parent.display()
);
return;
}
match std::fs::copy(old_path, new_path) {
Ok(_) => info!(
"Migrated settings from the legacy config at {} to {}.",
old_path.display(),
new_path.display()
),
Err(e) => warn!(
"Could not copy legacy settings from {} to {}; starting with defaults \
instead: {e}",
old_path.display(),
new_path.display()
),
}
}
#[cfg(target_os = "windows")]
fn platform_config_dir() -> Option<PathBuf> {
std::env::var_os("APPDATA").map(PathBuf::from)
}
#[cfg(target_os = "macos")]
fn platform_config_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(|home| {
PathBuf::from(home)
.join("Library")
.join("Application Support")
})
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_config_dir() -> Option<PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
return Some(PathBuf::from(xdg));
}
std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SettingsLoadOutcome {
Ok,
Corrupt {
renamed_to: PathBuf,
},
}
fn rename_aside(path: &Path) -> Option<PathBuf> {
let unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let mut renamed = path.as_os_str().to_owned();
renamed.push(format!(".corrupt-{unix_secs}"));
let renamed = PathBuf::from(renamed);
match std::fs::rename(path, &renamed) {
Ok(()) => Some(renamed),
Err(e) => {
warn!(
"Could not rename the corrupt settings file at {} aside to {}: {e}. \
Continuing with defaults; the corrupt file is still at its original path.",
path.display(),
renamed.display()
);
None
}
}
}
fn defaults_for_existing_install() -> SettingsFile {
let mut file = SettingsFile::default();
file.settings.treat_as_existing_install();
file
}
#[must_use]
pub fn peek_ui_scale_percent() -> u16 {
peek_ui_scale_percent_at(&default_settings_path())
}
fn peek_ui_scale_percent_at(path: &Path) -> u16 {
std::fs::read_to_string(path).map_or(0, |text| ui_scale_percent_in(&text))
}
fn ui_scale_percent_in(text: &str) -> u16 {
toml::from_str::<toml::Table>(text)
.ok()
.and_then(|document| {
document
.get("settings")
.and_then(toml::Value::as_table)
.and_then(|settings| settings.get("ui_scale_percent"))
.and_then(toml::Value::as_integer)
})
.map_or(0, normalize_ui_scale_percent)
}
#[must_use]
pub fn load_with_outcome(path: &Path) -> (SettingsFile, SettingsLoadOutcome) {
let (mut file, outcome) = match std::fs::read_to_string(path) {
Ok(contents) => match toml::from_str::<SettingsFile>(&contents) {
Ok(file) => (file, SettingsLoadOutcome::Ok),
Err(e) => {
warn!(
"Settings file at {} is corrupt ({e}); falling back to defaults.",
path.display()
);
let outcome = rename_aside(path).map_or(SettingsLoadOutcome::Ok, |renamed_to| {
SettingsLoadOutcome::Corrupt { renamed_to }
});
(defaults_for_existing_install(), outcome)
}
},
Err(e) if e.kind() == io::ErrorKind::NotFound => {
info!(
"No settings file at {} yet; using defaults.",
path.display()
);
(SettingsFile::default(), SettingsLoadOutcome::Ok)
}
Err(e) => {
warn!(
"Could not read settings file at {} ({e}); falling back to defaults.",
path.display()
);
(defaults_for_existing_install(), SettingsLoadOutcome::Ok)
}
};
file.ensure_built_in_presets();
file.settings.migrate_legacy_remote_workers();
(file, outcome)
}
#[cfg(test)]
#[must_use]
pub fn load_or_default(path: &Path) -> SettingsFile {
load_with_outcome(path).0
}
pub fn save(path: &Path, settings: &SettingsFile) -> io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let toml_str = toml::to_string_pretty(settings)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let tmp_path = path.with_extension("toml.tmp");
std::fs::write(&tmp_path, toml_str)?;
std::fs::rename(&tmp_path, path)?;
Ok(())
}
#[cfg(test)]
mod tests;