use crate::config::{Config, ConfigError};
use std::path::{Path, PathBuf};
impl Config {
fn system_config_paths() -> Vec<PathBuf> {
let mut paths = Vec::with_capacity(2);
#[cfg(target_os = "macos")]
if let Some(home) = dirs::home_dir() {
let path = home.join(".config").join("fresh").join(Config::FILENAME);
if path.exists() {
paths.push(path);
}
}
if let Some(config_dir) = dirs::config_dir() {
let path = config_dir.join("fresh").join(Config::FILENAME);
if !paths.contains(&path) && path.exists() {
paths.push(path);
}
}
paths
}
fn config_search_paths(working_dir: &Path) -> Vec<PathBuf> {
let local = Self::local_config_path(working_dir);
let mut paths = Vec::with_capacity(3);
if local.exists() {
paths.push(local);
}
paths.extend(Self::system_config_paths());
paths
}
pub fn find_config_path(working_dir: &Path) -> Option<PathBuf> {
Self::config_search_paths(working_dir).into_iter().next()
}
pub fn load_for_working_dir(working_dir: &Path) -> Self {
for path in Self::config_search_paths(working_dir) {
match Self::load_from_file(&path) {
Ok(config) => {
tracing::info!("Loaded config from {}", path.display());
return config;
}
Err(e) => {
tracing::warn!(
"Failed to load config from {}: {}, trying next option",
path.display(),
e
);
}
}
}
tracing::debug!("No config file found, using defaults");
Self::default()
}
pub fn read_user_config_raw(working_dir: &Path) -> serde_json::Value {
for path in Self::config_search_paths(working_dir) {
if let Ok(contents) = std::fs::read_to_string(&path) {
match serde_json::from_str(&contents) {
Ok(value) => return value,
Err(e) => {
tracing::warn!("Failed to parse config from {}: {}", path.display(), e);
}
}
}
}
serde_json::Value::Object(serde_json::Map::new())
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
let current =
serde_json::to_value(self).map_err(|e| ConfigError::SerializeError(e.to_string()))?;
let defaults = serde_json::to_value(Self::default())
.map_err(|e| ConfigError::SerializeError(e.to_string()))?;
let diff = json_diff(&defaults, ¤t);
let contents = serde_json::to_string_pretty(&diff)
.map_err(|e| ConfigError::SerializeError(e.to_string()))?;
std::fs::write(path.as_ref(), contents).map_err(|e| ConfigError::IoError(e.to_string()))?;
Ok(())
}
}
fn json_diff(defaults: &serde_json::Value, current: &serde_json::Value) -> serde_json::Value {
use serde_json::Value;
match (defaults, current) {
(Value::Object(def_map), Value::Object(cur_map)) => {
let mut result = serde_json::Map::new();
for (key, cur_val) in cur_map {
if let Some(def_val) = def_map.get(key) {
let diff = json_diff(def_val, cur_val);
if !is_empty_diff(&diff) {
result.insert(key.clone(), diff);
}
} else {
result.insert(key.clone(), cur_val.clone());
}
}
Value::Object(result)
}
_ => {
if defaults == current {
Value::Object(serde_json::Map::new()) } else {
current.clone()
}
}
}
}
fn is_empty_diff(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(map) => map.is_empty(),
_ => false,
}
}
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub data_dir: std::path::PathBuf,
pub config_dir: std::path::PathBuf,
pub home_dir: Option<std::path::PathBuf>,
pub documents_dir: Option<std::path::PathBuf>,
pub downloads_dir: Option<std::path::PathBuf>,
}
impl DirectoryContext {
pub fn from_system() -> std::io::Result<Self> {
let data_dir = dirs::data_dir()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine data directory",
)
})?
.join("fresh");
#[allow(unused_mut)] let mut config_dir = dirs::config_dir()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine config directory",
)
})?
.join("fresh");
#[cfg(target_os = "macos")]
if let Some(home) = dirs::home_dir() {
let xdg_config = home.join(".config").join("fresh");
if xdg_config.exists() {
config_dir = xdg_config;
}
}
Ok(Self {
data_dir,
config_dir,
home_dir: dirs::home_dir(),
documents_dir: dirs::document_dir(),
downloads_dir: dirs::download_dir(),
})
}
pub fn for_testing(temp_dir: &std::path::Path) -> Self {
Self {
data_dir: temp_dir.join("data"),
config_dir: temp_dir.join("config"),
home_dir: Some(temp_dir.join("home")),
documents_dir: Some(temp_dir.join("documents")),
downloads_dir: Some(temp_dir.join("downloads")),
}
}
pub fn recovery_dir(&self) -> std::path::PathBuf {
self.data_dir.join("recovery")
}
pub fn sessions_dir(&self) -> std::path::PathBuf {
self.data_dir.join("sessions")
}
pub fn search_history_path(&self) -> std::path::PathBuf {
self.data_dir.join("search_history.json")
}
pub fn replace_history_path(&self) -> std::path::PathBuf {
self.data_dir.join("replace_history.json")
}
pub fn terminals_dir(&self) -> std::path::PathBuf {
self.data_dir.join("terminals")
}
pub fn terminal_dir_for(&self, working_dir: &std::path::Path) -> std::path::PathBuf {
let encoded = crate::session::encode_path_for_filename(working_dir);
self.terminals_dir().join(encoded)
}
pub fn config_path(&self) -> std::path::PathBuf {
self.config_dir.join(Config::FILENAME)
}
pub fn themes_dir(&self) -> std::path::PathBuf {
self.config_dir.join("themes")
}
pub fn grammars_dir(&self) -> std::path::PathBuf {
self.config_dir.join("grammars")
}
pub fn plugins_dir(&self) -> std::path::PathBuf {
self.config_dir.join("plugins")
}
}