Skip to main content

lean_ctx/core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::sync::Mutex;
4use std::time::SystemTime;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(default)]
8pub struct Config {
9    pub ultra_compact: bool,
10    pub tee_on_error: bool,
11    pub checkpoint_interval: u32,
12    pub excluded_commands: Vec<String>,
13    pub custom_aliases: Vec<AliasEntry>,
14    /// Commands taking longer than this threshold (ms) are recorded in the slow log.
15    /// Set to 0 to disable slow logging.
16    pub slow_command_threshold_ms: u64,
17    #[serde(default = "default_theme")]
18    pub theme: String,
19    #[serde(default)]
20    pub cloud: CloudConfig,
21    #[serde(default)]
22    pub autonomy: AutonomyConfig,
23}
24
25fn default_theme() -> String {
26    "default".to_string()
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31pub struct AutonomyConfig {
32    pub enabled: bool,
33    pub auto_preload: bool,
34    pub auto_dedup: bool,
35    pub auto_related: bool,
36    pub silent_preload: bool,
37    pub dedup_threshold: usize,
38}
39
40impl Default for AutonomyConfig {
41    fn default() -> Self {
42        Self {
43            enabled: true,
44            auto_preload: true,
45            auto_dedup: true,
46            auto_related: true,
47            silent_preload: true,
48            dedup_threshold: 8,
49        }
50    }
51}
52
53impl AutonomyConfig {
54    pub fn from_env() -> Self {
55        let mut cfg = Self::default();
56        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
57            if v == "false" || v == "0" {
58                cfg.enabled = false;
59            }
60        }
61        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
62            cfg.auto_preload = v != "false" && v != "0";
63        }
64        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
65            cfg.auto_dedup = v != "false" && v != "0";
66        }
67        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
68            cfg.auto_related = v != "false" && v != "0";
69        }
70        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
71            cfg.silent_preload = v != "false" && v != "0";
72        }
73        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
74            if let Ok(n) = v.parse() {
75                cfg.dedup_threshold = n;
76            }
77        }
78        cfg
79    }
80
81    pub fn load() -> Self {
82        let file_cfg = Config::load().autonomy;
83        let mut cfg = file_cfg;
84        if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
85            if v == "false" || v == "0" {
86                cfg.enabled = false;
87            }
88        }
89        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
90            cfg.auto_preload = v != "false" && v != "0";
91        }
92        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
93            cfg.auto_dedup = v != "false" && v != "0";
94        }
95        if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
96            cfg.auto_related = v != "false" && v != "0";
97        }
98        if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
99            cfg.silent_preload = v != "false" && v != "0";
100        }
101        if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
102            if let Ok(n) = v.parse() {
103                cfg.dedup_threshold = n;
104            }
105        }
106        cfg
107    }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111#[serde(default)]
112pub struct CloudConfig {
113    pub contribute_enabled: bool,
114    pub last_contribute: Option<String>,
115    pub last_sync: Option<String>,
116    pub last_model_pull: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct AliasEntry {
121    pub command: String,
122    pub alias: String,
123}
124
125impl Default for Config {
126    fn default() -> Self {
127        Self {
128            ultra_compact: false,
129            tee_on_error: false,
130            checkpoint_interval: 15,
131            excluded_commands: Vec::new(),
132            custom_aliases: Vec::new(),
133            slow_command_threshold_ms: 5000,
134            theme: default_theme(),
135            cloud: CloudConfig::default(),
136            autonomy: AutonomyConfig::default(),
137        }
138    }
139}
140
141impl Config {
142    pub fn path() -> Option<PathBuf> {
143        dirs::home_dir().map(|h| h.join(".lean-ctx").join("config.toml"))
144    }
145
146    pub fn load() -> Self {
147        static CACHE: Mutex<Option<(Config, SystemTime)>> = Mutex::new(None);
148
149        let path = match Self::path() {
150            Some(p) => p,
151            None => return Self::default(),
152        };
153
154        let mtime = std::fs::metadata(&path)
155            .and_then(|m| m.modified())
156            .unwrap_or(SystemTime::UNIX_EPOCH);
157
158        if let Ok(guard) = CACHE.lock() {
159            if let Some((ref cfg, ref cached_mtime)) = *guard {
160                if *cached_mtime == mtime {
161                    return cfg.clone();
162                }
163            }
164        }
165
166        let cfg = match std::fs::read_to_string(&path) {
167            Ok(content) => toml::from_str(&content).unwrap_or_default(),
168            Err(_) => Self::default(),
169        };
170
171        if let Ok(mut guard) = CACHE.lock() {
172            *guard = Some((cfg.clone(), mtime));
173        }
174
175        cfg
176    }
177
178    pub fn save(&self) -> Result<(), String> {
179        let path = Self::path().ok_or("cannot determine home directory")?;
180        if let Some(parent) = path.parent() {
181            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
182        }
183        let content = toml::to_string_pretty(self).map_err(|e| e.to_string())?;
184        std::fs::write(&path, content).map_err(|e| e.to_string())
185    }
186
187    pub fn show(&self) -> String {
188        let path = Self::path()
189            .map(|p| p.to_string_lossy().to_string())
190            .unwrap_or_else(|| "~/.lean-ctx/config.toml".to_string());
191        let content = toml::to_string_pretty(self).unwrap_or_default();
192        format!("Config: {path}\n\n{content}")
193    }
194}