use anyhow::{Context, Result};
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
pub fn confy_config_dir() -> PathBuf {
home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".config")
.join("confy")
}
pub fn confy_config_file() -> PathBuf {
confy_config_dir().join("config.json")
}
pub fn confy_legacy_file() -> PathBuf {
confy_config_dir().join("tracked.json")
}
pub fn mount_root() -> PathBuf {
home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".cache")
.join("confy")
.join("mounts")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorSettings {
pub bg: String,
pub fg: String,
pub highlight: String,
pub group: String,
pub border: String,
}
impl Default for ColorSettings {
fn default() -> Self {
Self {
bg: "default".into(),
fg: "default".into(),
highlight: "#cba6f7".into(),
group: "#89b4fa".into(),
border: "default".into(),
}
}
}
pub fn builtin_themes() -> HashMap<&'static str, ColorSettings> {
let mut m = HashMap::new();
m.insert("catppuccin", ColorSettings {
highlight: "#cba6f7".into(),
group: "#89b4fa".into(),
..Default::default()
});
m.insert("dracula", ColorSettings {
highlight: "#bd93f9".into(),
group: "#8be9fd".into(),
..Default::default()
});
m.insert("gruvbox", ColorSettings {
highlight: "#fabd2f".into(),
group: "#83a598".into(),
..Default::default()
});
m.insert("nord", ColorSettings {
highlight: "#88c0d0".into(),
group: "#81a1c1".into(),
..Default::default()
});
m.insert("tokyo-night", ColorSettings {
highlight: "#bb9af7".into(),
group: "#7aa2f7".into(),
..Default::default()
});
m.insert("one-dark", ColorSettings {
highlight: "#c678dd".into(),
group: "#61afef".into(),
..Default::default()
});
m
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default = "default_true")]
pub rollback: bool,
#[serde(default = "default_true")]
pub first_startup: bool,
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default)]
pub colors: ColorSettings,
#[serde(default)]
pub background_enable: bool,
#[serde(default = "default_bg_color")]
pub background_color: String,
#[serde(default)]
pub streamer_mode: bool,
#[serde(default = "default_true")]
pub streamer_hide_git: bool,
#[serde(default)]
pub editor: Option<String>,
#[serde(default = "default_true")]
pub ssh_allow: bool,
}
fn default_true() -> bool { true }
fn default_theme() -> String { "catppuccin".into() }
fn default_bg_color() -> String { "#1e1e2e".into() }
impl Default for Settings {
fn default() -> Self {
Self {
rollback: true,
first_startup: true,
theme: "catppuccin".into(),
colors: ColorSettings::default(),
background_enable: false,
background_color: default_bg_color(),
streamer_mode: false,
streamer_hide_git: true,
editor: None,
ssh_allow: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub groups: HashMap<String, Vec<String>>,
#[serde(default)]
pub last_opened: Option<String>,
#[serde(default)]
pub collapsed_groups: HashSet<String>,
#[serde(default = "default_sort_mode")]
pub sort_mode: String,
#[serde(default = "default_sort_order")]
pub sort_order: String,
#[serde(default = "default_config_dir")]
pub config_dir: String,
#[serde(default)]
pub settings: Settings,
#[serde(default)]
pub preview_enabled: bool,
#[serde(default = "default_git_dir")]
pub git_dir: String,
#[serde(default)]
pub git_blacklist: Vec<String>,
#[serde(default)]
pub git_blacklist_groups: Vec<String>,
#[serde(default)]
pub git_auto_push: bool,
#[serde(default)]
pub git_auto_commit: bool,
}
fn default_sort_mode() -> String { "name".into() }
fn default_sort_order() -> String { "asc".into() }
fn default_config_dir() -> String {
home_dir()
.unwrap_or_else(|| PathBuf::from("/home"))
.join(".config")
.to_string_lossy()
.into_owned()
}
impl Default for AppConfig {
fn default() -> Self {
let mut groups = HashMap::new();
groups.insert("ungrouped".into(), vec![]);
Self {
groups,
last_opened: None,
collapsed_groups: HashSet::new(),
sort_mode: default_sort_mode(),
sort_order: default_sort_order(),
config_dir: default_config_dir(),
settings: Settings::default(),
preview_enabled: false,
git_dir: default_git_dir(),
git_blacklist: vec![],
git_blacklist_groups: vec![],
git_auto_push: false,
git_auto_commit: false,
}
}
}
pub fn migrate_if_needed(config_dir: &Path) -> Option<String> {
let legacy = config_dir.join("tracked.json");
let current = config_dir.join("config.json");
if !current.exists() && legacy.exists() {
if fs::copy(&legacy, ¤t).is_ok() {
return Some("migrated tracked.json → config.json!".into());
}
}
None
}
pub fn load(path: &Path) -> Result<(AppConfig, bool)> {
if !path.exists() {
return Ok((AppConfig::default(), true)); }
let raw = fs::read_to_string(path)
.with_context(|| format!("couldn't read {}", path.display()))?;
if raw.trim().is_empty() {
return Ok((AppConfig::default(), true));
}
let mut val: serde_json::Value = serde_json::from_str(&raw)
.with_context(|| format!("couldn't parse {}", path.display()))?;
if let Some(files) = val.get("files").cloned() {
let mut groups = serde_json::Map::new();
groups.insert("ungrouped".into(), files);
val["groups"] = serde_json::Value::Object(groups);
}
let cfg: AppConfig = serde_json::from_value(val)
.with_context(|| format!("couldn't deserialise {}", path.display()))?;
let first = cfg.settings.first_startup;
Ok((cfg, first))
}
pub fn save(path: &Path, cfg: &AppConfig) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(cfg)?;
fs::write(path, json)?;
Ok(())
}
pub fn default_git_dir() -> String {
home_dir()
.unwrap_or_else(|| PathBuf::from("/home"))
.join("dotfiles-git")
.to_string_lossy()
.into_owned()
}