use iced::Theme;
use redb::{Database, ReadableDatabase, TableDefinition};
use std::path::PathBuf;
macro_rules! map_storage_err {
($expr:expr, $ctx:literal) => {
$expr.map_err(|e| format!("{}: {}", $ctx, e))?
};
}
const THEME_KEY: &[u8] = b"theme";
const TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("preferences");
macro_rules! theme_variants {
( $( $variant:ident ),+ $(,)? ) => {
fn theme_to_string(theme: &Theme) -> String {
match theme {
$( Theme::$variant => stringify!($variant).to_string(), )+
_ => "Dark".to_string(),
}
}
fn theme_from_string(s: &str) -> Option<Theme> {
match s {
$( stringify!($variant) => Some(Theme::$variant), )+
_ => None,
}
}
pub fn all_themes() -> Vec<Theme> {
vec![ $( Theme::$variant, )+ ]
}
};
}
theme_variants!(
Dark,
Light,
Dracula,
Nord,
SolarizedLight,
SolarizedDark,
GruvboxLight,
GruvboxDark,
CatppuccinLatte,
CatppuccinFrappe,
CatppuccinMacchiato,
CatppuccinMocha,
TokyoNight,
TokyoNightStorm,
TokyoNightLight,
KanagawaWave,
KanagawaDragon,
KanagawaLotus,
Moonfly,
Nightfly,
Oxocarbon,
);
pub struct Storage {
db: Database,
}
impl std::fmt::Debug for Storage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Storage")
.field("db", &"<redb::Database>")
.finish()
}
}
impl Storage {
pub fn new() -> Result<Self, String> {
let db_path = Self::get_db_path()?;
let db =
Database::create(db_path).map_err(|e| format!("Failed to open database: {}", e))?;
Ok(Self { db })
}
fn get_db_path() -> Result<PathBuf, String> {
let mut path =
dirs::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
path.push("flashkraft");
std::fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
path.push("preferences.db");
Ok(path)
}
pub fn load_theme(&self) -> Option<Theme> {
let read_txn = self.db.begin_read().ok()?;
let table = read_txn.open_table(TABLE).ok()?;
let value = table.get(THEME_KEY).ok()??;
let theme_name = String::from_utf8(value.value().to_vec()).ok()?;
theme_from_string(&theme_name)
}
pub fn save_theme(&self, theme: &Theme) -> Result<(), String> {
let theme_name = theme_to_string(theme);
let write_txn = map_storage_err!(self.db.begin_write(), "Failed to save theme");
{
let mut table = map_storage_err!(write_txn.open_table(TABLE), "Failed to save theme");
map_storage_err!(
table.insert(THEME_KEY, theme_name.as_bytes()),
"Failed to save theme"
);
}
map_storage_err!(write_txn.commit(), "Failed to save theme");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_to_string() {
assert_eq!(theme_to_string(&Theme::Dark), "Dark");
assert_eq!(theme_to_string(&Theme::Light), "Light");
assert_eq!(theme_to_string(&Theme::Dracula), "Dracula");
}
#[test]
fn test_theme_from_string() {
assert!(matches!(theme_from_string("Dark"), Some(Theme::Dark)));
assert!(matches!(theme_from_string("Light"), Some(Theme::Light)));
assert!(theme_from_string("Invalid").is_none());
}
#[test]
fn test_roundtrip() {
let themes = vec![
Theme::Dark,
Theme::Light,
Theme::Dracula,
Theme::Nord,
Theme::CatppuccinMocha,
];
for theme in themes {
let name = theme_to_string(&theme);
let restored = theme_from_string(&name);
assert!(restored.is_some());
}
}
#[test]
fn test_all_themes_count() {
let themes = all_themes();
assert_eq!(themes.len(), 21);
}
#[test]
fn test_all_themes_roundtrip() {
for theme in all_themes() {
let name = theme_to_string(&theme);
let restored = theme_from_string(&name).expect("roundtrip should succeed");
assert_eq!(
theme_to_string(&restored),
name,
"roundtrip failed for {}",
name
);
}
}
}