Files
Sprimo/crates/sprimo-config/src/lib.rs

236 lines
5.8 KiB
Rust

use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("no supported configuration directory for this platform")]
MissingProjectDir,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid config file: {0}")]
Parse(#[from] toml::de::Error),
#[error("could not encode config: {0}")]
Encode(#[from] toml::ser::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
pub window: WindowConfig,
pub animation: AnimationConfig,
pub sprite: SpriteConfig,
pub api: ApiConfig,
pub logging: LoggingConfig,
pub controls: ControlsConfig,
pub frontend: FrontendConfig,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
window: WindowConfig::default(),
animation: AnimationConfig::default(),
sprite: SpriteConfig::default(),
api: ApiConfig::default(),
logging: LoggingConfig::default(),
controls: ControlsConfig::default(),
frontend: FrontendConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WindowConfig {
pub x: f32,
pub y: f32,
pub monitor_id: Option<String>,
pub scale: f32,
pub always_on_top: bool,
pub click_through: bool,
pub visible: bool,
}
impl Default for WindowConfig {
fn default() -> Self {
Self {
x: 200.0,
y: 200.0,
monitor_id: None,
scale: 1.0,
always_on_top: true,
click_through: false,
visible: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AnimationConfig {
pub fps: u16,
pub idle_timeout_ms: u64,
}
impl Default for AnimationConfig {
fn default() -> Self {
Self {
fps: 30,
idle_timeout_ms: 3_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SpriteConfig {
pub selected_pack: String,
pub sprite_packs_dir: String,
}
impl Default for SpriteConfig {
fn default() -> Self {
Self {
selected_pack: "default".to_string(),
sprite_packs_dir: "sprite-packs".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ApiConfig {
pub port: u16,
pub auth_token: String,
}
impl Default for ApiConfig {
fn default() -> Self {
Self {
port: 32_145,
auth_token: Uuid::new_v4().to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub level: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ControlsConfig {
pub hotkey_enabled: bool,
pub recovery_hotkey: String,
}
impl Default for ControlsConfig {
fn default() -> Self {
Self {
hotkey_enabled: true,
recovery_hotkey: "Ctrl+Alt+P".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBackend {
Bevy,
Tauri,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FrontendConfig {
pub backend: FrontendBackend,
pub debug_overlay_visible: bool,
}
impl Default for FrontendConfig {
fn default() -> Self {
Self {
backend: FrontendBackend::Bevy,
debug_overlay_visible: false,
}
}
}
#[must_use]
pub fn config_path(app_name: &str) -> Result<PathBuf, ConfigError> {
let dirs =
ProjectDirs::from("", "", app_name).ok_or(ConfigError::MissingProjectDir)?;
Ok(dirs.config_dir().join("config.toml"))
}
pub fn load_or_create(app_name: &str) -> Result<(PathBuf, AppConfig), ConfigError> {
let path = config_path(app_name)?;
load_or_create_at(&path)
}
pub fn load_or_create_at(path: &Path) -> Result<(PathBuf, AppConfig), ConfigError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
if !path.exists() {
let cfg = AppConfig::default();
save(path, &cfg)?;
return Ok((path.to_path_buf(), cfg));
}
let raw = fs::read_to_string(path)?;
let cfg = toml::from_str::<AppConfig>(&raw)?;
Ok((path.to_path_buf(), cfg))
}
pub fn save(path: &Path, config: &AppConfig) -> Result<(), ConfigError> {
let encoded = toml::to_string_pretty(config)?;
fs::write(path, encoded)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{load_or_create_at, save, AppConfig};
use tempfile::TempDir;
#[test]
fn bootstrap_writes_default_config() {
let temp = TempDir::new().expect("tempdir");
let path = temp.path().join("config.toml");
let (_, config) = load_or_create_at(&path).expect("load or create");
assert_eq!(config.api.port, 32_145);
}
#[test]
fn save_roundtrip() {
let temp = TempDir::new().expect("tempdir");
let path = temp.path().join("config.toml");
let mut config = AppConfig::default();
config.window.x = 42.0;
config.frontend.backend = super::FrontendBackend::Tauri;
config.frontend.debug_overlay_visible = true;
save(&path, &config).expect("save");
let (_, loaded) = load_or_create_at(&path).expect("reload");
assert!((loaded.window.x - 42.0).abs() < f32::EPSILON);
assert_eq!(loaded.frontend.backend, super::FrontendBackend::Tauri);
assert!(loaded.frontend.debug_overlay_visible);
}
}