Skip to main content

quasar_cli/
config.rs

1use {
2    crate::error::CliError,
3    serde::{Deserialize, Serialize},
4    std::path::{Path, PathBuf},
5};
6
7// ---------------------------------------------------------------------------
8// Project config (Quasar.toml)
9// ---------------------------------------------------------------------------
10
11#[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// ---------------------------------------------------------------------------
96// Global config (~/.quasar/config.toml) — saved preferences across projects
97// ---------------------------------------------------------------------------
98
99#[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    /// Show the animated banner on `quasar init` (default: true)
117    #[serde(default = "default_true")]
118    pub animation: bool,
119    /// Use colored output (default: true)
120    #[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        // Simulates the init flow: default config → save with animation: false
207        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}