From 6e08e0e7bcef25ee41853ac29b10bf3e3c97b593 Mon Sep 17 00:00:00 2001 From: Tom You Date: Thu, 9 Jul 2026 00:18:56 -0500 Subject: [PATCH] feat(app): persist customization settings --- src-tauri/src/main.rs | 59 +++++++++++++++++++++++++++++++++++++++++-- ui/app.js | 26 +++++++++++++++++++ ui/index.html | 13 ++++++++++ ui/styles.css | 5 ++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0a6f483..7ee25b5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,8 @@ use rabby_core::{ - demo_dashboard, wallet_feature_summary, WalletDashboard, WalletFeatureStatus, Workspace, + demo_dashboard, wallet_feature_summary, AppConfig, WalletDashboard, WalletFeatureStatus, + Workspace, }; +use rabby_runtime::config_store::{ConfigStore, JsonFileConfigStore}; use rabby_runtime::local_terminal::{LocalCommand, LocalCommandOutput, LocalTerminal}; use rabby_runtime::wallet_vault::{EncryptedWalletVault, WalletVaultStatus}; use serde::Serialize; @@ -75,6 +77,31 @@ fn resolve_vault_path(path: Option) -> PathBuf { .unwrap_or_else(|| std::env::temp_dir().join("rabby-demo-wallet-vault.json")) } +#[tauri::command] +fn load_app_config(path: Option) -> Result { + let config_path = resolve_config_path(path); + if !config_path.exists() { + return Ok(AppConfig::default_linux()); + } + JsonFileConfigStore::new(config_path) + .load() + .map_err(|err| err.to_string()) +} + +#[tauri::command] +fn save_app_config(config: AppConfig, path: Option) -> Result { + let config_path = resolve_config_path(path); + JsonFileConfigStore::new(config_path) + .save(&config) + .map_err(|err| err.to_string())?; + Ok(config) +} + +fn resolve_config_path(path: Option) -> PathBuf { + path.map(PathBuf::from) + .unwrap_or_else(|| std::env::temp_dir().join("rabby-app-config.json")) +} + fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ @@ -83,7 +110,9 @@ fn main() { wallet_dashboard, save_demo_wallet_vault, load_wallet_vault, - run_local_command + run_local_command, + load_app_config, + save_app_config ]) .run(tauri::generate_context!()) .expect("failed to run Rabby Tauri application"); @@ -114,4 +143,30 @@ mod tests { assert_eq!(output.exit_code, 0); assert_eq!(output.stdout, "tauri-local"); } + + #[test] + fn config_path_has_stable_file_name() { + let path = resolve_config_path(None); + assert_eq!( + path.file_name().and_then(|value| value.to_str()), + Some("rabby-app-config.json") + ); + } + + #[test] + fn save_and_load_app_config_round_trips_custom_theme() { + let unique = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("rabby-tauri-config-{unique}.json")); + let mut config = AppConfig::default_linux(); + config.theme_id = "rabby-light".to_string(); + + save_app_config(config.clone(), Some(path.display().to_string())).unwrap(); + let loaded = load_app_config(Some(path.display().to_string())).unwrap(); + + assert_eq!(loaded.theme_id, "rabby-light"); + assert_eq!(loaded, config); + } } diff --git a/ui/app.js b/ui/app.js index a3445bc..5574acf 100644 --- a/ui/app.js +++ b/ui/app.js @@ -136,6 +136,31 @@ function wireVaultActions() { if (button) button.addEventListener('click', saveDemoVault); } +async function loadAppConfig() { + return invokeOrFallback('load_app_config', { theme_id: 'rabby-light', restore_workspace: true, profiles: [], shortcuts: [] }); +} + +async function saveSettings() { + const theme = document.getElementById('theme-select').value; + const output = document.getElementById('settings-status'); + output.textContent = 'Saving settings…'; + try { + const tauri = window.__TAURI__?.core; + if (!tauri) throw new Error('Tauri bridge unavailable in browser preview'); + const config = await loadAppConfig(); + config.theme_id = theme; + const saved = await tauri.invoke('save_app_config', { config, path: null }); + output.textContent = `Saved ${saved.theme_id}`; + } catch (error) { + output.textContent = String(error); + } +} + +function wireSettingsActions() { + const button = document.getElementById('save-settings'); + if (button) button.addEventListener('click', saveSettings); +} + async function runLocalTerminalCommand() { const command = document.getElementById('terminal-command').value; const output = document.getElementById('terminal-output'); @@ -168,6 +193,7 @@ async function boot() { renderDashboard(dashboard); renderFeatures(features); wireVaultActions(); + wireSettingsActions(); wireTerminalActions(); } diff --git a/ui/index.html b/ui/index.html index f56d675..2c0f4b6 100644 --- a/ui/index.html +++ b/ui/index.html @@ -120,6 +120,19 @@
+
+

Persistent customization

+ +
+ + +
+ Customization persists through the Rust config store. +

Encrypted local state

diff --git a/ui/styles.css b/ui/styles.css index 130da53..35eb59f 100644 --- a/ui/styles.css +++ b/ui/styles.css @@ -312,3 +312,8 @@ h3 { margin-bottom: 0; font-size: 18px; } .terminal-form { margin-top: 18px; display: grid; gap: 12px; } .terminal-form input { width: 100%; border: 1px solid #dfe6f6; border-radius: 14px; padding: 12px 14px; color: #253052; background: #f8faff; font: 14px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; } .terminal-form pre { min-height: 120px; margin: 0; padding: 16px; border-radius: 18px; color: #b9fbcf; background: #12182b; white-space: pre-wrap; overflow: auto; font: 13px/1.5 ui-monospace, SFMono-Regular, Consolas, monospace; } + + +.settings-form { margin-top: 18px; padding-top: 16px; border-top: 1px solid #edf1fb; display: grid; gap: 8px; } +.settings-form label { color: #62708f; font-weight: 800; } +.settings-form select { flex: 1; min-width: 0; border: 1px solid #dfe6f6; border-radius: 14px; padding: 11px 12px; color: #253052; background: #f8faff; }