1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4const DEFAULT_CDN_URL: &str = "https://cdn.aichub.org/v1";
5const DEFAULT_TELEMETRY_URL: &str = "https://api.aichub.org/v1";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SourceConfig {
9 pub name: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub url: Option<String>,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub path: Option<String>,
14}
15
16#[derive(Debug, Clone)]
17pub struct Config {
18 pub sources: Vec<SourceConfig>,
19 pub output_dir: String,
20 pub refresh_interval: u64,
21 pub output_format: String,
22 pub source: String,
23 pub telemetry: bool,
24 pub feedback: bool,
25 pub telemetry_url: String,
26}
27
28impl Default for Config {
29 fn default() -> Self {
30 Self {
31 sources: vec![SourceConfig {
32 name: "default".to_string(),
33 url: Some(DEFAULT_CDN_URL.to_string()),
34 path: None,
35 }],
36 output_dir: ".context".to_string(),
37 refresh_interval: 21600,
38 output_format: "human".to_string(),
39 source: "official,maintainer,community".to_string(),
40 telemetry: true,
41 feedback: true,
42 telemetry_url: DEFAULT_TELEMETRY_URL.to_string(),
43 }
44 }
45}
46
47#[derive(Debug, Deserialize, Default)]
49struct FileConfig {
50 #[serde(default)]
51 sources: Option<Vec<SourceConfig>>,
52 #[serde(default)]
53 cdn_url: Option<String>,
54 #[serde(default)]
55 output_dir: Option<String>,
56 #[serde(default)]
57 refresh_interval: Option<u64>,
58 #[serde(default)]
59 output_format: Option<String>,
60 #[serde(default)]
61 source: Option<String>,
62 #[serde(default)]
63 telemetry: Option<bool>,
64 #[serde(default)]
65 feedback: Option<bool>,
66 #[serde(default)]
67 telemetry_url: Option<String>,
68}
69
70pub fn chub_dir() -> PathBuf {
72 if let Ok(dir) = std::env::var("CHUB_DIR") {
73 PathBuf::from(dir)
74 } else {
75 dirs::home_dir()
76 .unwrap_or_else(|| PathBuf::from("."))
77 .join(".chub")
78 }
79}
80
81pub fn load_config() -> Config {
86 let defaults = Config::default();
87 let config_path = chub_dir().join("config.yaml");
88
89 let file_config: FileConfig = std::fs::read_to_string(&config_path)
91 .ok()
92 .and_then(|raw| serde_yaml::from_str(&raw).ok())
93 .unwrap_or_default();
94
95 let project_config: FileConfig = find_project_chub_dir()
97 .map(|d| d.join("config.yaml"))
98 .and_then(|p| std::fs::read_to_string(&p).ok())
99 .and_then(|raw| serde_yaml::from_str(&raw).ok())
100 .unwrap_or_default();
101
102 let sources = project_config
104 .sources
105 .or(file_config.sources)
106 .unwrap_or_else(|| {
107 let url = std::env::var("CHUB_BUNDLE_URL")
108 .ok()
109 .or(project_config.cdn_url)
110 .or(file_config.cdn_url)
111 .unwrap_or_else(|| DEFAULT_CDN_URL.to_string());
112 vec![SourceConfig {
113 name: "default".to_string(),
114 url: Some(url),
115 path: None,
116 }]
117 });
118
119 Config {
120 sources,
121 output_dir: project_config
122 .output_dir
123 .or(file_config.output_dir)
124 .unwrap_or(defaults.output_dir),
125 refresh_interval: project_config
126 .refresh_interval
127 .or(file_config.refresh_interval)
128 .unwrap_or(defaults.refresh_interval),
129 output_format: project_config
130 .output_format
131 .or(file_config.output_format)
132 .unwrap_or(defaults.output_format),
133 source: project_config
134 .source
135 .or(file_config.source)
136 .unwrap_or(defaults.source),
137 telemetry: project_config
138 .telemetry
139 .or(file_config.telemetry)
140 .unwrap_or(defaults.telemetry),
141 feedback: project_config
142 .feedback
143 .or(file_config.feedback)
144 .unwrap_or(defaults.feedback),
145 telemetry_url: project_config
146 .telemetry_url
147 .or(file_config.telemetry_url)
148 .unwrap_or(defaults.telemetry_url),
149 }
150}
151
152fn find_project_chub_dir() -> Option<PathBuf> {
154 let start = std::env::current_dir().ok()?;
155 let mut current = start.as_path();
156 loop {
157 let candidate = current.join(".chub");
158 if candidate.is_dir() {
159 let home_chub = chub_dir();
161 if candidate != home_chub {
162 return Some(candidate);
163 }
164 }
165 current = current.parent()?;
166 }
167}