This commit is contained in:
Generated
+8
@@ -2305,6 +2305,14 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rabby-runtime"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rabby-core",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["rabby-core", "src-tauri"]
|
members = ["rabby-core", "rabby-runtime", "src-tauri"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "rabby-runtime"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rabby-core = { path = "../rabby-core" }
|
||||||
|
serde_json = "1"
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
use rabby_core::AppConfig;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub trait ConfigStore {
|
||||||
|
fn load(&self) -> Result<AppConfig, ConfigError>;
|
||||||
|
fn save(&self, config: &AppConfig) -> Result<(), ConfigError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Json(serde_json::Error),
|
||||||
|
Invalid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Io(err) => write!(f, "config I/O error: {err}"),
|
||||||
|
Self::Json(err) => write!(f, "config JSON error: {err}"),
|
||||||
|
Self::Invalid(message) => write!(f, "invalid config: {message}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ConfigError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::Io(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for ConfigError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Self::Json(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MemoryConfigStore {
|
||||||
|
config: Mutex<AppConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryConfigStore {
|
||||||
|
pub fn new(config: AppConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config: Mutex::new(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigStore for MemoryConfigStore {
|
||||||
|
fn load(&self) -> Result<AppConfig, ConfigError> {
|
||||||
|
self.config
|
||||||
|
.lock()
|
||||||
|
.map(|config| config.clone())
|
||||||
|
.map_err(|_| ConfigError::Invalid("memory config lock poisoned".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, config: &AppConfig) -> Result<(), ConfigError> {
|
||||||
|
config.validate().map_err(ConfigError::Invalid)?;
|
||||||
|
*self
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| ConfigError::Invalid("memory config lock poisoned".to_string()))? =
|
||||||
|
config.clone();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct JsonFileConfigStore {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonFileConfigStore {
|
||||||
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self { path: path.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigStore for JsonFileConfigStore {
|
||||||
|
fn load(&self) -> Result<AppConfig, ConfigError> {
|
||||||
|
let raw = std::fs::read_to_string(&self.path)?;
|
||||||
|
let config: AppConfig = serde_json::from_str(&raw)?;
|
||||||
|
config.validate().map_err(ConfigError::Invalid)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, config: &AppConfig) -> Result<(), ConfigError> {
|
||||||
|
config.validate().map_err(ConfigError::Invalid)?;
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let raw = serde_json::to_string_pretty(config)?;
|
||||||
|
std::fs::write(&self.path, raw)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ConfigPathResolver {
|
||||||
|
executable_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigPathResolver {
|
||||||
|
pub fn new(executable_path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
executable_path: executable_path.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(&self) -> Result<PathBuf, ConfigError> {
|
||||||
|
let exe_dir = self
|
||||||
|
.executable_path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| ConfigError::Invalid("executable path has no parent".to_string()))?;
|
||||||
|
let portable_data = exe_dir.join("data");
|
||||||
|
if portable_data.is_dir() {
|
||||||
|
return Ok(portable_data.join("config.json"));
|
||||||
|
}
|
||||||
|
Ok(exe_dir.join("rabby-config.json"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rabby_core::AppConfig;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn temp_path(name: &str) -> PathBuf {
|
||||||
|
let unique = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("rabby-{name}-{unique}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_store_round_trips_config() {
|
||||||
|
let store = MemoryConfigStore::new(AppConfig::default_linux());
|
||||||
|
let mut config = store.load().unwrap();
|
||||||
|
config.theme_id = "solarized-dark".to_string();
|
||||||
|
|
||||||
|
store.save(&config).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(store.load().unwrap().theme_id, "solarized-dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_file_store_writes_and_reads_config() {
|
||||||
|
let dir = temp_path("json-store");
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("config.json");
|
||||||
|
let store = JsonFileConfigStore::new(path.clone());
|
||||||
|
let mut config = AppConfig::default_linux();
|
||||||
|
config.theme_id = "tabby-compatible".to_string();
|
||||||
|
|
||||||
|
store.save(&config).unwrap();
|
||||||
|
|
||||||
|
let raw = std::fs::read_to_string(&path).unwrap();
|
||||||
|
assert!(raw.contains("tabby-compatible"));
|
||||||
|
assert_eq!(store.load().unwrap(), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn portable_mode_prefers_data_dir_beside_executable() {
|
||||||
|
let dir = temp_path("portable");
|
||||||
|
let exe = dir.join("rabby");
|
||||||
|
let data = dir.join("data");
|
||||||
|
std::fs::create_dir_all(&data).unwrap();
|
||||||
|
|
||||||
|
let resolved = ConfigPathResolver::new(exe).resolve().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved, data.join("config.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod config_store;
|
||||||
Reference in New Issue
Block a user