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 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}