Skip to main content

flashkraft_gui/core/
storage.rs

1//! Storage Module - Theme Persistence
2//!
3//! This module handles persistent storage of user preferences using sled,
4//! an embedded database. Currently stores theme selection.
5
6use iced::Theme;
7use sled::Db;
8use std::path::PathBuf;
9
10/// Key for storing the theme preference
11const THEME_KEY: &[u8] = b"theme";
12
13/// Storage manager for application preferences
14#[derive(Debug)]
15pub struct Storage {
16    db: Db,
17}
18
19impl Storage {
20    /// Create a new storage instance
21    ///
22    /// Opens or creates the sled database in the user's config directory
23    pub fn new() -> Result<Self, String> {
24        let db_path = Self::get_db_path()?;
25        let db = sled::open(db_path).map_err(|e| format!("Failed to open database: {}", e))?;
26        Ok(Self { db })
27    }
28
29    /// Get the database path
30    ///
31    /// Uses the appropriate config directory based on the OS
32    fn get_db_path() -> Result<PathBuf, String> {
33        let mut path =
34            dirs::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
35        path.push("flashkraft");
36        std::fs::create_dir_all(&path)
37            .map_err(|e| format!("Failed to create config directory: {}", e))?;
38        path.push("preferences.db");
39        Ok(path)
40    }
41
42    /// Load the saved theme
43    ///
44    /// Returns the saved theme or None if no theme is saved
45    pub fn load_theme(&self) -> Option<Theme> {
46        let value = self.db.get(THEME_KEY).ok()??;
47        let theme_name = String::from_utf8(value.to_vec()).ok()?;
48        Self::theme_from_string(&theme_name)
49    }
50
51    /// Save the current theme
52    ///
53    /// Persists the theme selection to disk
54    pub fn save_theme(&self, theme: &Theme) -> Result<(), String> {
55        let theme_name = Self::theme_to_string(theme);
56        self.db
57            .insert(THEME_KEY, theme_name.as_bytes())
58            .map_err(|e| format!("Failed to save theme: {}", e))?;
59        self.db
60            .flush()
61            .map_err(|e| format!("Failed to flush database: {}", e))?;
62        Ok(())
63    }
64
65    /// Convert a Theme to a string for storage
66    fn theme_to_string(theme: &Theme) -> String {
67        match theme {
68            Theme::Dark => "Dark",
69            Theme::Light => "Light",
70            Theme::Dracula => "Dracula",
71            Theme::Nord => "Nord",
72            Theme::SolarizedLight => "SolarizedLight",
73            Theme::SolarizedDark => "SolarizedDark",
74            Theme::GruvboxLight => "GruvboxLight",
75            Theme::GruvboxDark => "GruvboxDark",
76            Theme::CatppuccinLatte => "CatppuccinLatte",
77            Theme::CatppuccinFrappe => "CatppuccinFrappe",
78            Theme::CatppuccinMacchiato => "CatppuccinMacchiato",
79            Theme::CatppuccinMocha => "CatppuccinMocha",
80            Theme::TokyoNight => "TokyoNight",
81            Theme::TokyoNightStorm => "TokyoNightStorm",
82            Theme::TokyoNightLight => "TokyoNightLight",
83            Theme::KanagawaWave => "KanagawaWave",
84            Theme::KanagawaDragon => "KanagawaDragon",
85            Theme::KanagawaLotus => "KanagawaLotus",
86            Theme::Moonfly => "Moonfly",
87            Theme::Nightfly => "Nightfly",
88            Theme::Oxocarbon => "Oxocarbon",
89            _ => "Dark",
90        }
91        .to_string()
92    }
93
94    /// Convert a string to a Theme
95    fn theme_from_string(s: &str) -> Option<Theme> {
96        match s {
97            "Dark" => Some(Theme::Dark),
98            "Light" => Some(Theme::Light),
99            "Dracula" => Some(Theme::Dracula),
100            "Nord" => Some(Theme::Nord),
101            "SolarizedLight" => Some(Theme::SolarizedLight),
102            "SolarizedDark" => Some(Theme::SolarizedDark),
103            "GruvboxLight" => Some(Theme::GruvboxLight),
104            "GruvboxDark" => Some(Theme::GruvboxDark),
105            "CatppuccinLatte" => Some(Theme::CatppuccinLatte),
106            "CatppuccinFrappe" => Some(Theme::CatppuccinFrappe),
107            "CatppuccinMacchiato" => Some(Theme::CatppuccinMacchiato),
108            "CatppuccinMocha" => Some(Theme::CatppuccinMocha),
109            "TokyoNight" => Some(Theme::TokyoNight),
110            "TokyoNightStorm" => Some(Theme::TokyoNightStorm),
111            "TokyoNightLight" => Some(Theme::TokyoNightLight),
112            "KanagawaWave" => Some(Theme::KanagawaWave),
113            "KanagawaDragon" => Some(Theme::KanagawaDragon),
114            "KanagawaLotus" => Some(Theme::KanagawaLotus),
115            "Moonfly" => Some(Theme::Moonfly),
116            "Nightfly" => Some(Theme::Nightfly),
117            "Oxocarbon" => Some(Theme::Oxocarbon),
118            _ => None,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_theme_to_string() {
129        assert_eq!(Storage::theme_to_string(&Theme::Dark), "Dark");
130        assert_eq!(Storage::theme_to_string(&Theme::Light), "Light");
131        assert_eq!(Storage::theme_to_string(&Theme::Dracula), "Dracula");
132    }
133
134    #[test]
135    fn test_theme_from_string() {
136        assert!(matches!(
137            Storage::theme_from_string("Dark"),
138            Some(Theme::Dark)
139        ));
140        assert!(matches!(
141            Storage::theme_from_string("Light"),
142            Some(Theme::Light)
143        ));
144        assert!(Storage::theme_from_string("Invalid").is_none());
145    }
146
147    #[test]
148    fn test_roundtrip() {
149        let themes = vec![
150            Theme::Dark,
151            Theme::Light,
152            Theme::Dracula,
153            Theme::Nord,
154            Theme::CatppuccinMocha,
155        ];
156
157        for theme in themes {
158            let name = Storage::theme_to_string(&theme);
159            let restored = Storage::theme_from_string(&name);
160            assert!(restored.is_some());
161        }
162    }
163}