Files
ostiary/src/config.rs
2026-05-29 05:42:38 +07:00

158 lines
4.7 KiB
Rust

use serde::{Deserialize, Serialize};
use anyhow::Result;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Endpoint {
pub name: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub private_key: Option<String>,
}
impl Endpoint {
/// Creates a new endpoint with a freshly generated Ed25519 key pair.
pub fn new(name: String, url: String) -> Self {
Self {
name,
url,
private_key: Some(crate::identity::Identity::generate().to_b64()),
}
}
pub fn identity(&self) -> Option<crate::identity::Identity> {
self.private_key.as_deref()
.and_then(|k| crate::identity::Identity::from_b64(k).ok())
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
// Legacy field — present only in old configs; migrated to endpoints on load.
#[serde(default, skip_serializing)]
server_url: Option<String>,
#[serde(default)]
pub endpoints: Vec<Endpoint>,
#[serde(default)]
pub active_endpoint: String,
pub timeout_sec: u64,
#[serde(default = "default_theme")]
pub theme: Theme,
#[serde(default)]
pub update_api: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Theme {
#[serde(default = "default_selected_bg")]
pub selected_bg: String,
}
fn default_selected_bg() -> String {
"blue".to_string()
}
fn default_theme() -> Theme {
Theme { selected_bg: default_selected_bg() }
}
pub fn config_path() -> PathBuf {
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config")
});
base.join(env!("CARGO_PKG_NAME")).join("config.toml")
}
impl Config {
pub fn exists() -> bool {
config_path().exists()
}
pub fn active_url(&self) -> &str {
&self.active_endpoint
}
/// Returns the identity for the currently active endpoint, if any.
pub fn active_identity(&self) -> Option<crate::identity::Identity> {
self.endpoints.iter()
.find(|ep| ep.url == self.active_endpoint)
.and_then(|ep| ep.identity())
}
pub fn load() -> Result<Self> {
let path = config_path();
let content = fs::read_to_string(&path)?;
let mut cfg: Config = toml::from_str(&content)?;
// Migrate old single server_url format to endpoints list.
if cfg.endpoints.is_empty() {
if let Some(url) = cfg.server_url.take() {
cfg.active_endpoint = url.clone();
cfg.endpoints.push(Endpoint { name: "Default".to_string(), url, private_key: None });
}
}
if cfg.active_endpoint.is_empty() {
if let Some(ep) = cfg.endpoints.first() {
cfg.active_endpoint = ep.url.clone();
}
}
Ok(cfg)
}
pub fn create_with_endpoint(url: &str, name: &str) -> Result<Self> {
let path = config_path();
let config = Config {
server_url: None,
endpoints: vec![Endpoint::new(name.to_string(), url.to_string())],
active_endpoint: url.to_string(),
timeout_sec: 10,
theme: default_theme(),
update_api: Some("https://git.vainend.com/api/v1/repos/admin/ostiary".to_string()),
};
fs::create_dir_all(path.parent().unwrap())?;
fs::write(&path, config.to_toml())?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let path = config_path();
fs::create_dir_all(path.parent().unwrap())?;
fs::write(&path, self.to_toml())?;
Ok(())
}
fn to_toml(&self) -> String {
let mut s = format!(
"active_endpoint = \"{}\"\ntimeout_sec = {}\n",
escape_toml(&self.active_endpoint),
self.timeout_sec,
);
if let Some(api) = &self.update_api {
s.push_str(&format!("update_api = \"{}\"\n", escape_toml(api)));
}
s.push_str(&format!(
"\n[theme]\nselected_bg = \"{}\"\n",
escape_toml(&self.theme.selected_bg),
));
for ep in &self.endpoints {
s.push_str(&format!(
"\n[[endpoints]]\nname = \"{}\"\nurl = \"{}\"\n",
escape_toml(&ep.name),
escape_toml(&ep.url),
));
if let Some(key) = &ep.private_key {
s.push_str(&format!("private_key = \"{}\"\n", escape_toml(key)));
}
}
s
}
}
fn escape_toml(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}