Merge refactor/qbz-app-developer-settings: developer settings store
This commit is contained in:
3 files changed
+220
-122
No files matched your search
@@ -0,0 +1,213 @@
|
||||
//! Developer settings persistence.
|
||||
//!
|
||||
//! This module stores portable developer-mode toggles only. Tauri command
|
||||
//! wrappers and restart messaging stay outside `qbz-app`.
|
||||
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeveloperSettings {
|
||||
pub force_dmabuf: bool,
|
||||
}
|
||||
|
||||
impl Default for DeveloperSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
force_dmabuf: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeveloperSettingsStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl DeveloperSettingsStore {
|
||||
/// Lightweight read-only open for startup before host-managed state exists.
|
||||
/// Opens existing DB without creating tables or running migrations.
|
||||
pub fn new_readonly() -> Result<Self, String> {
|
||||
let db_path = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz")
|
||||
.join("developer_settings.db");
|
||||
Self::new_readonly_at_path(&db_path)
|
||||
}
|
||||
|
||||
pub fn new_readonly_at_path(db_path: &Path) -> Result<Self, String> {
|
||||
if !db_path.exists() {
|
||||
return Err("Developer settings DB does not exist yet".to_string());
|
||||
}
|
||||
|
||||
let conn = Connection::open_with_flags(
|
||||
db_path,
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to open developer settings database (readonly): {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open developer settings database: {}", e))?;
|
||||
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to enable WAL for developer settings database: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS developer_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
force_dmabuf INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT OR IGNORE INTO developer_settings (id, force_dmabuf) VALUES (1, 0);",
|
||||
)
|
||||
.map_err(|e| format!("Failed to create developer settings table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "developer_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "developer_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<DeveloperSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT force_dmabuf FROM developer_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(DeveloperSettings {
|
||||
force_dmabuf: row.get::<_, i64>(0)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Failed to get developer settings: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_force_dmabuf(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE developer_settings SET force_dmabuf = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set force_dmabuf: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for host state management.
|
||||
pub struct DeveloperSettingsState {
|
||||
pub store: Arc<Mutex<Option<DeveloperSettingsStore>>>,
|
||||
}
|
||||
|
||||
impl DeveloperSettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(Some(DeveloperSettingsStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DeveloperSettingsState {
|
||||
fn default() -> Self {
|
||||
Self::new_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn unique_test_dir(name: &str) -> std::path::PathBuf {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("qbz-app-{name}-{}-{nonce}", std::process::id()))
|
||||
}
|
||||
|
||||
fn fresh_store(name: &str) -> (std::path::PathBuf, DeveloperSettingsStore) {
|
||||
let dir = unique_test_dir(name);
|
||||
let store = DeveloperSettingsStore::new_at(&dir).expect("open store in temp dir");
|
||||
(dir, store)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn developer_settings_default_values_are_stable() {
|
||||
let settings = DeveloperSettings::default();
|
||||
|
||||
assert!(!settings.force_dmabuf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn developer_settings_store_returns_defaults() {
|
||||
let (dir, store) = fresh_store("developer-default");
|
||||
|
||||
let settings = store.get_settings().expect("get settings");
|
||||
|
||||
assert!(!settings.force_dmabuf);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn developer_settings_persist_force_dmabuf() {
|
||||
let dir = unique_test_dir("developer-force-dmabuf");
|
||||
{
|
||||
let store = DeveloperSettingsStore::new_at(&dir).expect("open store");
|
||||
store.set_force_dmabuf(true).expect("set force dmabuf");
|
||||
}
|
||||
|
||||
let reopened = DeveloperSettingsStore::new_at(&dir).expect("reopen store");
|
||||
let settings = reopened.get_settings().expect("get settings");
|
||||
|
||||
assert!(settings.force_dmabuf);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn developer_settings_readonly_opens_existing_db() {
|
||||
let dir = unique_test_dir("developer-readonly");
|
||||
let db_path = dir.join("developer_settings.db");
|
||||
{
|
||||
let store = DeveloperSettingsStore::new_at(&dir).expect("open store");
|
||||
store.set_force_dmabuf(true).expect("set force dmabuf");
|
||||
}
|
||||
|
||||
let readonly = DeveloperSettingsStore::new_readonly_at_path(&db_path)
|
||||
.expect("open existing store read-only");
|
||||
let settings = readonly.get_settings().expect("get settings");
|
||||
|
||||
assert!(settings.force_dmabuf);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod developer;
|
||||
pub mod download;
|
||||
pub mod favorites;
|
||||
pub mod graphics;
|
||||
|
||||
@@ -1,127 +1,11 @@
|
||||
//! Developer settings persistence
|
||||
//! Tauri adapter for developer settings.
|
||||
//!
|
||||
//! Stores developer-mode toggles (e.g. force DMA-BUF override for NVIDIA+Wayland).
|
||||
//! These settings are device-level (not per-user) and persist across sessions.
|
||||
//! The portable settings store lives in `qbz-app`; this module keeps the
|
||||
//! existing Tauri command surface unchanged.
|
||||
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeveloperSettings {
|
||||
pub force_dmabuf: bool,
|
||||
}
|
||||
|
||||
impl Default for DeveloperSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
force_dmabuf: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeveloperSettingsStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl DeveloperSettingsStore {
|
||||
/// Lightweight read-only open for startup (before Tauri state).
|
||||
pub fn new_readonly() -> Result<Self, String> {
|
||||
let db_path = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz")
|
||||
.join("developer_settings.db");
|
||||
|
||||
if !db_path.exists() {
|
||||
return Err("Developer settings DB does not exist yet".to_string());
|
||||
}
|
||||
|
||||
let conn = Connection::open_with_flags(
|
||||
&db_path,
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
)
|
||||
.map_err(|e| format!("Failed to open developer settings database (readonly): {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("developer_settings.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open developer settings database: {}", e))?;
|
||||
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to enable WAL for developer settings database: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS developer_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
force_dmabuf INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT OR IGNORE INTO developer_settings (id, force_dmabuf) VALUES (1, 0);",
|
||||
)
|
||||
.map_err(|e| format!("Failed to create developer settings table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<DeveloperSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT force_dmabuf FROM developer_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(DeveloperSettings {
|
||||
force_dmabuf: row.get::<_, i64>(0)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Failed to get developer settings: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_force_dmabuf(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE developer_settings SET force_dmabuf = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set force_dmabuf: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for Tauri state management
|
||||
pub struct DeveloperSettingsState {
|
||||
pub store: Arc<Mutex<Option<DeveloperSettingsStore>>>,
|
||||
}
|
||||
|
||||
impl DeveloperSettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(Some(DeveloperSettingsStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
pub use qbz_app::settings::developer::{
|
||||
DeveloperSettings, DeveloperSettingsState, DeveloperSettingsStore,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_developer_settings(
|
||||
|
||||
Reference in new issue
Block a user