use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExtensionStatus {
Disabled,
Enabled,
#[default]
Auto,
}
impl ExtensionStatus {
#[allow(dead_code)]
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled | Self::Auto)
}
}
#[derive(Debug, Default)]
pub struct ExtensionConfig {
extensions: HashMap<String, ExtensionStatus>,
}
impl ExtensionConfig {
pub fn get(&self, name: &str) -> ExtensionStatus {
if let Some(w) = self.extensions.get("*") {
return *w;
}
self.extensions
.get(name)
.copied()
.unwrap_or(ExtensionStatus::Disabled)
}
#[allow(dead_code)]
pub fn allow_all() -> Self {
let mut m = HashMap::new();
m.insert("*".to_string(), ExtensionStatus::Auto);
Self { extensions: m }
}
pub fn set_project_status(name: &str, status: ExtensionStatus) -> Result<()> {
use std::fs;
use std::path::Path;
use serde_yaml::{Value as YamlValue, Mapping as YamlMapping};
let project_config_path = Path::new(".oo").join("config.yaml");
let mut root = if project_config_path.exists() {
let data = fs::read_to_string(&project_config_path)?;
serde_yaml::from_str::<YamlValue>(&data).unwrap_or(YamlValue::Mapping(YamlMapping::new()))
} else {
YamlValue::Mapping(YamlMapping::new())
};
if let YamlValue::Mapping(ref mut map) = root {
let key = YamlValue::String("extensions".to_string());
match map.get_mut(&key) {
Some(YamlValue::Mapping(ext_map)) => {
if let ExtensionStatus::Disabled = status {
ext_map.remove(YamlValue::String(name.to_string()));
} else {
ext_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
}
if ext_map.is_empty() {
map.remove(&key);
}
}
Some(_) => {
let mut new_map = YamlMapping::new();
if let ExtensionStatus::Disabled = status {} else {
new_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
map.insert(key, YamlValue::Mapping(new_map));
}
}
None => {
if let ExtensionStatus::Disabled = status {} else {
let mut new_map = YamlMapping::new();
new_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
map.insert(key, YamlValue::Mapping(new_map));
}
}
}
}
if let Some(parent) = project_config_path.parent() {
fs::create_dir_all(parent)?;
}
let yaml = serde_yaml::to_string(&root)?;
fs::write(&project_config_path, yaml)?;
let legacy_path = Path::new(".oo").join("extension.yaml");
let mut map: HashMap<String, String> = if legacy_path.exists() {
let data = fs::read_to_string(&legacy_path)?;
serde_saphyr::from_str(&data).unwrap_or_default()
} else {
HashMap::new()
};
if let ExtensionStatus::Disabled = status {
map.remove(name);
} else {
map.insert(name.to_string(), status_to_str(status).to_string());
}
if map.is_empty() {
if legacy_path.exists() {
fs::remove_file(&legacy_path)?;
}
} else {
if let Some(parent) = legacy_path.parent() {
fs::create_dir_all(parent)?;
}
let yaml = serde_saphyr::to_string(&map)?;
fs::write(&legacy_path, yaml)?;
}
Ok(())
}
pub fn set_global_status(name: &str, status: ExtensionStatus) -> Result<()> {
use std::fs;
use serde_yaml::{Value as YamlValue, Mapping as YamlMapping};
let global_config_dir = directories::ProjectDirs::from("com", "cyloncore", "oo")
.ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
.config_dir()
.to_path_buf();
let global_config_path = global_config_dir.join("config.yaml");
let mut root = if global_config_path.exists() {
let data = fs::read_to_string(&global_config_path)?;
serde_yaml::from_str::<YamlValue>(&data).unwrap_or(YamlValue::Mapping(YamlMapping::new()))
} else {
YamlValue::Mapping(YamlMapping::new())
};
if let YamlValue::Mapping(ref mut map) = root {
let key = YamlValue::String("extensions".to_string());
match map.get_mut(&key) {
Some(YamlValue::Mapping(ext_map)) => {
if let ExtensionStatus::Disabled = status {
ext_map.remove(YamlValue::String(name.to_string()));
} else {
ext_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
}
if ext_map.is_empty() {
map.remove(&key);
}
}
Some(_) => {
let mut new_map = YamlMapping::new();
if let ExtensionStatus::Disabled = status {} else {
new_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
map.insert(key, YamlValue::Mapping(new_map));
}
}
None => {
if let ExtensionStatus::Disabled = status {} else {
let mut new_map = YamlMapping::new();
new_map.insert(
YamlValue::String(name.to_string()),
YamlValue::String(status_to_str(status).to_string()),
);
map.insert(key, YamlValue::Mapping(new_map));
}
}
}
}
if let Some(parent) = global_config_path.parent() {
fs::create_dir_all(parent)?;
}
let yaml = serde_yaml::to_string(&root)?;
fs::write(&global_config_path, yaml)?;
let legacy_global = global_config_dir.join("extension.yaml");
let mut map: HashMap<String, String> = if legacy_global.exists() {
let data = fs::read_to_string(&legacy_global)?;
serde_saphyr::from_str(&data).unwrap_or_default()
} else {
HashMap::new()
};
if let ExtensionStatus::Disabled = status {
map.remove(name);
} else {
map.insert(name.to_string(), status_to_str(status).to_string());
}
if map.is_empty() {
if legacy_global.exists() {
fs::remove_file(&legacy_global)?;
}
} else {
if let Some(parent) = legacy_global.parent() {
fs::create_dir_all(parent)?;
}
let yaml = serde_saphyr::to_string(&map)?;
fs::write(&legacy_global, yaml)?;
}
Ok(())
}
}
fn status_to_str(status: ExtensionStatus) -> &'static str {
match status {
ExtensionStatus::Enabled => "enabled",
ExtensionStatus::Auto => "auto",
ExtensionStatus::Disabled => "disabled",
}
}
pub fn load() -> Result<ExtensionConfig> {
let global_config_dir = directories::ProjectDirs::from("com", "cyloncore", "oo")
.ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
.config_dir()
.to_path_buf();
let global_config_path = global_config_dir.join("config.yaml");
let project_config_path = Path::new(".oo").join("config.yaml");
let mut config = ExtensionConfig::default();
fn merge_from_value(config: &mut ExtensionConfig, value: &serde_yaml::Value) -> Result<()> {
if let serde_yaml::Value::Mapping(mapping) = value {
for (k, v) in mapping.iter() {
if let serde_yaml::Value::String(key) = k {
if key == "extensions_dir" || key == "auto_load" {
continue;
}
let status = match v {
serde_yaml::Value::String(s) => match s.as_str() {
"enabled" => ExtensionStatus::Enabled,
"disabled" => ExtensionStatus::Disabled,
"auto" => ExtensionStatus::Auto,
other => {
log::warn!("Unknown extension status '{}', treating as disabled", other);
ExtensionStatus::Disabled
}
},
serde_yaml::Value::Null => ExtensionStatus::Disabled,
_ => {
log::warn!("Invalid extension status for '{}', treating as disabled", key);
ExtensionStatus::Disabled
}
};
config.extensions.insert(key.clone(), status);
}
}
}
Ok(())
}
if let Ok(value) = serde_yaml::from_str::<serde_yaml::Value>(include_str!("../../data/default_settings.yaml"))
&& let serde_yaml::Value::Mapping(map) = value
&& let Some(ext_val) = map.get(serde_yaml::Value::String("extensions".to_string())) {
merge_from_value(&mut config, ext_val)?;
}
if global_config_path.exists() {
let data = std::fs::read_to_string(&global_config_path)?;
if let Ok(value) = serde_yaml::from_str::<serde_yaml::Value>(&data)
&& let serde_yaml::Value::Mapping(map) = value
&& let Some(ext_val) = map.get(serde_yaml::Value::String("extensions".to_string())) {
merge_from_value(&mut config, ext_val)?;
}
}
if project_config_path.exists() {
let data = std::fs::read_to_string(&project_config_path)?;
if let Ok(value) = serde_yaml::from_str::<serde_yaml::Value>(&data)
&& let serde_yaml::Value::Mapping(map) = value
&& let Some(ext_val) = map.get(serde_yaml::Value::String("extensions".to_string())) {
merge_from_value(&mut config, ext_val)?;
}
}
let legacy_global = global_config_dir.join("extension.yaml");
if legacy_global.exists() {
let data = std::fs::read_to_string(&legacy_global)?;
merge_yaml(&mut config, &data)?;
log::debug!("Loaded legacy global extension config from {:?}", legacy_global);
}
let legacy_project = Path::new(".oo").join("extension.yaml");
if legacy_project.exists() {
let data = std::fs::read_to_string(&legacy_project)?;
merge_yaml(&mut config, &data)?;
log::debug!("Loaded legacy project extension config from {:?}", legacy_project);
}
Ok(config)
}
fn merge_yaml(config: &mut ExtensionConfig, data: &str) -> Result<()> {
use saphyr::LoadableYamlNode;
let nodes = saphyr::Yaml::load_from_str(data)?;
if let Some(node) = nodes.first() {
merge_node(config, "", node)?;
}
Ok(())
}
fn merge_node(config: &mut ExtensionConfig, prefix: &str, node: &saphyr::Yaml) -> Result<()> {
use saphyr::{Scalar, Yaml};
match node {
Yaml::Mapping(mapping) => {
for (k, v) in mapping.iter() {
let key = k.as_str().ok_or_else(|| anyhow!("Expected string key"))?;
let full_key = if prefix.is_empty() {
key.to_string()
} else {
format!("{}.{}", prefix, key)
};
merge_node(config, &full_key, v)?;
}
Ok(())
}
Yaml::Value(value) => {
let status = match value {
Scalar::String(s) => {
let s_str: &str = s;
match s_str {
"enabled" => ExtensionStatus::Enabled,
"disabled" => ExtensionStatus::Disabled,
"auto" => ExtensionStatus::Auto,
other => {
log::warn!("Unknown extension status '{}', treating as disabled", other);
ExtensionStatus::Disabled
}
}
}
Scalar::Null => ExtensionStatus::Disabled,
_ => {
log::warn!("Invalid extension status type, treating as disabled");
ExtensionStatus::Disabled
}
};
config.extensions.insert(prefix.to_string(), status);
Ok(())
}
_ => Ok(()),
}
}