use std::path::{Path, PathBuf};
use std::{fmt, fs, io, process};
use serde_json::{Value, json};
use crate::environment;
#[derive(Debug)]
pub struct SettingsError {
path: Option<PathBuf>,
error: io::Error,
}
impl fmt::Display for SettingsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.path {
Some(path) => write!(
formatter,
"{}: cannot write the setting: {}",
path.display(),
self.error
),
None => write!(
formatter,
"no configuration directory: {} is not set",
if cfg!(windows) { "APPDATA" } else { "HOME" }
),
}
}
}
fn directory() -> Option<PathBuf> {
let base = if cfg!(windows) {
variable("APPDATA")
} else if cfg!(target_os = "macos") {
variable("HOME").map(|home| home.join("Library/Application Support"))
} else {
variable("XDG_CONFIG_HOME")
.filter(|path| path.is_absolute())
.or_else(|| variable("HOME").map(|home| home.join(".config")))
};
base.map(|base| base.join("loft"))
}
fn path(name: &str) -> Option<PathBuf> {
directory().map(|directory| directory.join(name))
}
pub fn read(name: &str) -> Value {
path(name).map_or_else(|| json!({}), |path| read_at(&path))
}
pub fn read_at(path: &Path) -> Value {
fs::read(path)
.ok()
.and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
.filter(Value::is_object)
.unwrap_or_else(|| json!({}))
}
pub fn write(name: &str, value: &Value) -> Result<(), SettingsError> {
let path = path(name).ok_or(SettingsError {
path: None,
error: io::Error::from(io::ErrorKind::NotFound),
})?;
write_at(&path, value)
}
pub fn write_at(path: &Path, value: &Value) -> Result<(), SettingsError> {
let fail = |error| SettingsError {
path: Some(path.to_owned()),
error,
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(fail)?;
}
let name = path.file_name().unwrap_or_default().to_string_lossy();
let temporary = path.with_file_name(format!(".{name}.loft-{}", process::id()));
let bytes = serde_json::to_vec_pretty(value).expect("a JSON value serializes");
fs::write(&temporary, bytes).map_err(fail)?;
fs::rename(&temporary, path).map_err(fail)
}
fn variable(name: &str) -> Option<PathBuf> {
environment::value(name).map(PathBuf::from)
}