use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::domain::services::UrgencyWeights;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RecallConfig {
pub semantic: bool,
pub semantic_threshold: f32,
pub semantic_top_k: usize,
}
impl Default for RecallConfig {
fn default() -> Self {
RecallConfig {
semantic: false,
semantic_threshold: 0.30,
semantic_top_k: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TelemetryConfig {
pub enabled: bool,
pub endpoint: Option<String>,
pub token: Option<String>,
}
impl Default for TelemetryConfig {
fn default() -> Self {
TelemetryConfig {
enabled: true,
endpoint: None,
token: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TuiConfig {
pub theme: String,
}
impl Default for TuiConfig {
fn default() -> Self {
TuiConfig {
theme: "classic".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub default_project: String,
pub date_dialect: String,
pub urgency: UrgencyWeights,
pub vault_path: Option<PathBuf>,
pub recall: RecallConfig,
pub telemetry: TelemetryConfig,
pub tui: TuiConfig,
}
impl Default for Config {
fn default() -> Self {
Config {
default_project: "inbox".to_string(),
date_dialect: "uk".to_string(),
urgency: UrgencyWeights::default(),
vault_path: None,
recall: RecallConfig::default(),
telemetry: TelemetryConfig::default(),
tui: TuiConfig::default(),
}
}
}
pub fn project_dirs() -> Result<ProjectDirs> {
ProjectDirs::from("", "", "sara").context("Could not determine home directory")
}
fn legacy_tk_project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("", "", "tk")
}
pub fn config_path() -> Result<PathBuf> {
let dirs = project_dirs()?;
Ok(dirs.config_dir().join("config.toml"))
}
pub fn db_path() -> Result<PathBuf> {
let dirs = project_dirs()?;
Ok(dirs.data_dir().join("tasks.db"))
}
pub fn vault_path(cfg: &Config) -> Result<PathBuf> {
if let Some(ref p) = cfg.vault_path {
return Ok(p.clone());
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Ok(cwd.join("Sara"))
}
pub fn set_vault_path(cfg: &mut Config, path: PathBuf) -> Result<()> {
cfg.vault_path = Some(path);
save(cfg)
}
fn legacy_tk_config_path() -> Option<PathBuf> {
legacy_tk_project_dirs().map(|d| d.config_dir().join("config.toml"))
}
fn legacy_tk_db_path() -> Option<PathBuf> {
legacy_tk_project_dirs().map(|d| d.data_dir().join("tasks.db"))
}
pub fn migrate_from_tk_if_needed() -> Result<bool> {
let sara_cfg = config_path()?;
let sara_db = db_path()?;
let mut migrated = false;
if let Some(tk_cfg) = legacy_tk_config_path()
&& tk_cfg.exists()
&& !sara_cfg.exists()
{
if let Some(parent) = sara_cfg.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&tk_cfg, &sara_cfg)
.with_context(|| format!("Failed to copy config from {}", tk_cfg.display()))?;
migrated = true;
}
if let Some(tk_db) = legacy_tk_db_path()
&& tk_db.exists()
&& !sara_db.exists()
{
if let Some(parent) = sara_db.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&tk_db, &sara_db)
.with_context(|| format!("Failed to copy database from {}", tk_db.display()))?;
migrated = true;
}
if migrated {
eprintln!(
"Imported existing tk config and tasks into sara.\n\
You can remove the old tk binary with: cargo uninstall tk"
);
}
Ok(migrated)
}
pub fn load() -> Result<Config> {
migrate_from_tk_if_needed()?;
let path = config_path()?;
if !path.exists() {
return Ok(Config::default());
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config: {}", path.display()))?;
toml::from_str(&content).with_context(|| format!("Failed to parse config: {}", path.display()))
}
pub fn save(cfg: &Config) -> Result<()> {
let path = config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(cfg).context("Failed to serialize config")?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write config: {}", path.display()))
}
#[cfg(test)]
#[path = "../../tests/unit/infrastructure/config.rs"]
mod tests;