158 lines
4.7 KiB
Rust
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('"', "\\\"")
|
|
}
|