1use {
2 crate::error::CliError,
3 serde::{Deserialize, Serialize},
4 std::path::{Path, PathBuf},
5};
6
7#[derive(Debug, Deserialize)]
12pub struct QuasarConfig {
13 pub project: ProjectConfig,
14 pub toolchain: ToolchainConfig,
15 pub testing: TestingConfig,
16}
17
18#[derive(Debug, Deserialize)]
19pub struct ProjectConfig {
20 pub name: String,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct ToolchainConfig {
25 #[serde(rename = "type")]
26 pub toolchain_type: String,
27}
28
29#[derive(Debug, Deserialize)]
30pub struct TestingConfig {
31 pub framework: String,
32}
33
34impl QuasarConfig {
35 pub fn load() -> Result<Self, CliError> {
36 Self::load_from(Path::new("Quasar.toml"))
37 }
38
39 pub fn load_from(path: &Path) -> Result<Self, CliError> {
40 if !path.exists() {
41 use crate::style;
42 eprintln!(
43 "\n {}",
44 style::fail(&format!("{} not found.", path.display()))
45 );
46 eprintln!();
47 eprintln!(" Are you in a Quasar project directory?");
48 eprintln!(
49 " Run {} to create a new project.",
50 style::bold("quasar init")
51 );
52 eprintln!();
53 std::process::exit(1);
54 }
55 let contents = std::fs::read_to_string(path).map_err(|e| {
56 eprintln!(
57 "\n {}",
58 crate::style::fail(&format!("Failed to read {}: {e}", path.display()))
59 );
60 e
61 })?;
62 let config: QuasarConfig = toml::from_str(&contents).map_err(|e| {
63 eprintln!(
64 "\n {}",
65 crate::style::fail(&format!("Invalid {}: {e}", path.display()))
66 );
67 e
68 })?;
69 Ok(config)
70 }
71
72 pub fn is_solana_toolchain(&self) -> bool {
73 self.toolchain.toolchain_type == "solana"
74 }
75
76 pub fn module_name(&self) -> String {
77 self.project.name.replace('-', "_")
78 }
79
80 pub fn has_typescript_tests(&self) -> bool {
81 matches!(
82 self.testing.framework.as_str(),
83 "quasarsvm-web3js" | "quasarsvm-kit"
84 )
85 }
86
87 pub fn has_rust_tests(&self) -> bool {
88 matches!(
89 self.testing.framework.as_str(),
90 "mollusk" | "quasarsvm-rust"
91 )
92 }
93}
94
95#[derive(Debug, Deserialize, Serialize, Default)]
100pub struct GlobalConfig {
101 #[serde(default)]
102 pub defaults: GlobalDefaults,
103 #[serde(default)]
104 pub ui: UiConfig,
105}
106
107#[derive(Debug, Deserialize, Serialize, Default)]
108pub struct GlobalDefaults {
109 pub toolchain: Option<String>,
110 pub framework: Option<String>,
111 pub template: Option<String>,
112}
113
114#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
115pub struct UiConfig {
116 #[serde(default = "default_true")]
118 pub animation: bool,
119 #[serde(default = "default_true")]
121 pub color: bool,
122}
123
124fn default_true() -> bool {
125 true
126}
127
128impl Default for UiConfig {
129 fn default() -> Self {
130 Self {
131 animation: true,
132 color: true,
133 }
134 }
135}
136
137impl GlobalConfig {
138 pub fn path() -> PathBuf {
139 dirs::home_dir()
140 .unwrap_or_else(|| PathBuf::from("."))
141 .join(".quasar")
142 .join("config.toml")
143 }
144
145 pub fn load() -> Self {
146 let path = Self::path();
147 if path.exists() {
148 let contents = std::fs::read_to_string(&path).unwrap_or_default();
149 toml::from_str(&contents).unwrap_or_default()
150 } else {
151 Self::default()
152 }
153 }
154
155 pub fn save(&self) -> Result<(), CliError> {
156 let path = Self::path();
157 if let Some(parent) = path.parent() {
158 std::fs::create_dir_all(parent)?;
159 }
160 let toml_str = toml::to_string_pretty(self)?;
161 std::fs::write(path, toml_str)?;
162 Ok(())
163 }
164
165 pub fn load_from_str(s: &str) -> Self {
166 toml::from_str(s).unwrap_or_default()
167 }
168
169 pub fn to_toml(&self) -> String {
170 toml::to_string_pretty(self).unwrap_or_default()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn default_config_has_animation_enabled() {
180 let config = GlobalConfig::default();
181 assert!(config.ui.animation);
182 }
183
184 #[test]
185 fn animation_disabled_survives_roundtrip() {
186 let config = GlobalConfig {
187 ui: UiConfig {
188 animation: false,
189 ..UiConfig::default()
190 },
191 ..GlobalConfig::default()
192 };
193 let toml_str = config.to_toml();
194 let loaded = GlobalConfig::load_from_str(&toml_str);
195 assert!(!loaded.ui.animation);
196 }
197
198 #[test]
199 fn empty_config_defaults_animation_true() {
200 let loaded = GlobalConfig::load_from_str("");
201 assert!(loaded.ui.animation);
202 }
203
204 #[test]
205 fn saved_config_disables_animation() {
206 let globals = GlobalConfig::default();
208 assert!(globals.ui.animation);
209
210 let saved = GlobalConfig {
211 defaults: GlobalDefaults {
212 toolchain: Some("solana".into()),
213 framework: Some("quasarsvm-rust".into()),
214 template: Some("minimal".into()),
215 },
216 ui: UiConfig {
217 animation: false,
218 ..globals.ui
219 },
220 };
221 let toml_str = saved.to_toml();
222 let reloaded = GlobalConfig::load_from_str(&toml_str);
223 assert!(!reloaded.ui.animation);
224 assert_eq!(reloaded.defaults.toolchain.as_deref(), Some("solana"));
225 }
226}