use serde::{Deserialize, Serialize}; use std::fs; use std::io; use std::path::PathBuf; /// Verzeichnisname der App. Überall kleingeschrieben — unter Linux ist das /// Konvention, unter Windows in `%LOCALAPPDATA%` unauffällig. Eine Crate nur /// für diese Normalisierung (`directories`) wäre der teurere Weg gewesen. const APP_DIR: &str = "dionotes"; const FILE_NAME: &str = "workspaces.ron"; /// Die anwendungsweite Liste bekannter Workspaces. /// /// Sie enthält nur Pfade: Der Anzeigename ist der Ordnername und wäre als /// eigenes Feld nach dem ersten Umbenennen falsch. Auch keinen Zeitstempel — /// die **Reihenfolge ist die Sortierung**, zuletzt geöffnet steht vorn. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct WorkspaceList { workspaces: Vec, } impl WorkspaceList { pub fn entries(&self) -> &[PathBuf] { &self.workspaces } /// Hebt einen Workspace an die Spitze — beim Öffnen wie beim Hinzufügen. /// Weil vorher gefiltert wird, kann derselbe Pfad nicht doppelt vorkommen. pub fn promote(&mut self, path: PathBuf) { self.workspaces.retain(|known| known != &path); self.workspaces.insert(0, path); } /// Wo die Liste liegt. /// /// Ein Ausdruck für alle Plattformen: `state_dir()` liefert nur unter Linux /// `Some` — dort führt die XDG-Spec „recently used files" ausdrücklich als /// State, gehört also nach `$XDG_STATE_HOME` und nicht nach `~/.config`. /// Windows und macOS fallen auf das lokale Konfigurationsverzeichnis durch; /// unter Windows bewusst `%LOCALAPPDATA%` statt Roaming, weil absolute /// Workspace-Pfade maschinenspezifisch sind. pub fn path() -> Option { let base = dirs::state_dir().or_else(dirs::config_local_dir)?; Some(base.join(APP_DIR).join(FILE_NAME)) } /// Lesen. Eine fehlende oder unlesbare Datei ergibt eine leere Liste statt /// eines Fehlers: Sie enthält nichts Unwiederbringliches — der Nutzer wählt /// seine Ordner einmal neu, seine Notizen sind nicht betroffen. Deshalb /// braucht die Datei auch keine Schema-Version. pub fn load() -> Self { Self::path() .and_then(|path| fs::read_to_string(path).ok()) .map(|text| Self::parse(&text)) .unwrap_or_default() } pub fn save(&self) -> io::Result<()> { let path = Self::path().ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "kein Konfigurationsverzeichnis") })?; // Das Basisverzeichnis existiert garantiert, das der App nicht. if let Some(dir) = path.parent() { fs::create_dir_all(dir)?; } fs::write(path, self.to_ron()?) } fn parse(text: &str) -> Self { ron::from_str(text).unwrap_or_default() } fn to_ron(&self) -> io::Result { ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) } } #[cfg(test)] mod tests { use super::WorkspaceList; use std::path::PathBuf; fn list_of(paths: &[&str]) -> WorkspaceList { let mut list = WorkspaceList::default(); // rückwärts, damit die Reihenfolge am Ende der Eingabe entspricht for p in paths.iter().rev() { list.promote(PathBuf::from(p)); } list } #[test] fn promote_puts_new_entries_first() { let mut list = list_of(&["a", "b"]); list.promote(PathBuf::from("c")); assert_eq!(list.entries(), [PathBuf::from("c"), PathBuf::from("a"), PathBuf::from("b")]); } #[test] fn promote_moves_known_entries_without_duplicating() { let mut list = list_of(&["a", "b", "c"]); list.promote(PathBuf::from("c")); assert_eq!(list.entries(), [PathBuf::from("c"), PathBuf::from("a"), PathBuf::from("b")]); } #[test] fn survives_a_ron_roundtrip_with_windows_paths() { let list = list_of(&[r"C:\Users\marvin\Notizen", r"D:\Projekte\Test"]); let text = list.to_ron().expect("serialisierbar"); assert_eq!(WorkspaceList::parse(&text), list); } #[test] fn path_lands_in_the_app_directory() { let path = WorkspaceList::path().expect("Konfigurationsverzeichnis vorhanden"); assert!( path.ends_with("dionotes/workspaces.ron"), "unerwarteter Ablageort: {}", path.display() ); println!("Ablageort auf dieser Plattform: {}", path.display()); } #[test] fn unreadable_content_yields_an_empty_list() { assert_eq!(WorkspaceList::parse("kein RON, nur Text"), WorkspaceList::default()); assert_eq!(WorkspaceList::parse(""), WorkspaceList::default()); } }