1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Deserialize, Serialize, Default)]
8pub struct ConfigFile {
9 #[serde(default)]
10 pub general: GeneralConfig,
11 #[serde(default)]
12 pub dev: DevConfig,
13}
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct GeneralConfig {
17 #[serde(default = "default_editor")]
18 pub editor: String,
19}
20
21impl Default for GeneralConfig {
22 fn default() -> Self {
23 Self {
24 editor: default_editor(),
25 }
26 }
27}
28
29fn default_editor() -> String {
30 std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string())
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize, Default)]
34pub struct DevConfig {
35 #[serde(default)]
36 pub packages: Vec<String>,
37}
38
39#[derive(Clone)]
40pub struct Config {
41 dkdc_dir: PathBuf,
42 config_file: Option<ConfigFile>,
43}
44
45impl Config {
46 pub fn new() -> Result<Self> {
47 let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE"))?;
48 let dkdc_dir = PathBuf::from(home).join(".dkdc");
49 let mut config = Self {
50 dkdc_dir,
51 config_file: None,
52 };
53
54 config.reload_config_file();
56
57 Ok(config)
58 }
59
60 pub fn from_path(dkdc_dir: PathBuf) -> Self {
61 let mut config = Self {
62 dkdc_dir,
63 config_file: None,
64 };
65 config.reload_config_file();
66 config
67 }
68
69 pub fn reload_config_file(&mut self) {
71 let config_path = self.config_file_path();
72 if config_path.exists() {
73 match fs::read_to_string(&config_path) {
74 Ok(content) => match toml::from_str(&content) {
75 Ok(config) => self.config_file = Some(config),
76 Err(_) => self.config_file = Some(ConfigFile::default()),
77 },
78 Err(_) => self.config_file = Some(ConfigFile::default()),
79 }
80 }
81 }
82
83 pub fn file(&self) -> ConfigFile {
85 self.config_file.clone().unwrap_or_default()
86 }
87
88 pub fn dkdc_dir(&self) -> &Path {
89 &self.dkdc_dir
90 }
91
92 pub fn lake_dir(&self) -> PathBuf {
93 self.dkdc_dir.join("dkdclake")
94 }
95
96 pub fn metadata_path(&self) -> PathBuf {
97 self.lake_dir().join("metadata.db")
98 }
99
100 pub fn data_path(&self) -> PathBuf {
101 self.lake_dir().join("data")
102 }
103
104 pub fn venv_path(&self) -> PathBuf {
105 self.dkdc_dir.join("venv")
106 }
107
108 pub fn python_path(&self) -> PathBuf {
109 let venv = self.venv_path();
110 if cfg!(windows) {
111 venv.join("Scripts").join("python.exe")
112 } else {
113 venv.join("bin").join("python")
114 }
115 }
116
117 pub fn config_file_path(&self) -> PathBuf {
118 let config_dir = if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
119 PathBuf::from(xdg_config)
120 } else if let Ok(home) = std::env::var("HOME") {
121 PathBuf::from(home).join(".config")
122 } else {
123 self.dkdc_dir.clone()
124 };
125
126 config_dir.join("dkdc").join("config.toml")
127 }
128
129 pub fn ensure_directories(&self) -> Result<()> {
130 fs::create_dir_all(&self.dkdc_dir)?;
131 fs::create_dir_all(self.lake_dir())?;
132 fs::create_dir_all(self.data_path())?;
133 Ok(())
134 }
135
136 pub fn ensure_metadata_db(&self) -> Result<()> {
137 self.ensure_directories()?;
138
139 if !self.metadata_path().exists() {
140 fs::File::create(self.metadata_path())?;
141 }
142
143 Ok(())
144 }
145}
146
147pub const SECRETS_TABLE_NAME: &str = "secrets";
148pub const FILES_TABLE_NAME: &str = "files";
149pub const ARCHIVES_TABLE_NAME: &str = "archives";
150
151pub const DUCKLAKE_EXTENSION: &str = "ducklake";
152pub const SQLITE_EXTENSION: &str = "sqlite";
153
154pub const ARCHIVE_FILENAME_TEMPLATE: &str = "archive_directory_{name}.zip";
155
156pub struct Colors;
157
158impl Colors {
159 pub const PRIMARY: &'static str = "#8b5cf6";
160 pub const SECONDARY: &'static str = "#06b6d4";
161 pub const SUCCESS: &'static str = "#10b981";
162 pub const WARNING: &'static str = "#f59e0b";
163 pub const ERROR: &'static str = "#ef4444";
164 pub const MUTED: &'static str = "#6b7280";
165 pub const ACCENT: &'static str = "#a855f7";
166}
167
168pub const DKDC_BANNER: &str = r#"
169▓█████▄ ██ ▄█▀▓█████▄ ▄████▄
170▒██▀ ██▌ ██▄█▒ ▒██▀ ██▌▒██▀ ▀█
171░██ █▌▓███▄░ ░██ █▌▒▓█ ▄
172░▓█▄ ▌▓██ █▄ ░▓█▄ ▌▒▓▓▄ ▄██▒
173░▒████▓ ▒██▒ █▄░▒████▓ ▒ ▓███▀ ░
174 ▒▒▓ ▒ ▒ ▒▒ ▓▒ ▒▒▓ ▒ ░ ░▒ ▒ ░
175 ░ ▒ ▒ ░ ░▒ ▒░ ░ ▒ ▒ ░ ▒
176 ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░
177 ░ ░ ░ ░ ░ ░
178 ░ ░ ░
179
180develop knowledge, develop code
181"#;
182
183#[allow(dead_code)]
184pub const DKDC_BANNER_V1: &str = r#"
185██████╗ ██╗ ██╗██████╗ ██████╗
186██╔══██╗██║ ██╔╝██╔══██╗██╔════╝
187██║ ██║█████╔╝ ██║ ██║██║
188██║ ██║██╔═██╗ ██║ ██║██║
189██████╔╝██║ ██╗██████╔╝╚██████╗
190╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝
191
192develop knowledge, develop code
193"#;
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_config_creation() {
201 let config = Config::new();
202 assert!(config.is_ok());
203 }
204
205 #[test]
206 fn test_paths() {
207 let config = Config::from_path(PathBuf::from("/home/test/.dkdc"));
208 assert_eq!(config.dkdc_dir().display().to_string(), "/home/test/.dkdc");
209 assert_eq!(
210 config.lake_dir().display().to_string(),
211 "/home/test/.dkdc/dkdclake"
212 );
213 assert_eq!(
214 config.metadata_path().display().to_string(),
215 "/home/test/.dkdc/dkdclake/metadata.db"
216 );
217 }
218}