Workspace-Liste laden und speichern #11

Merged
RDietermann merged 3 commits from feat/workspace-liste into main 2026-08-28 07:58:32 +00:00
4 changed files with 157 additions and 0 deletions
Showing only changes of commit 37a7bab30f - Show all commits

23
Cargo.lock generated
View File

@@ -2329,6 +2329,9 @@ dependencies = [
"dioxus",
"dioxus-code",
"dioxus-code-editor",
"dirs",
"ron",
"serde",
"strum",
]
@@ -5886,6 +5889,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ron"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3"
dependencies = [
"bitflags 2.13.1",
"once_cell",
"serde",
"serde_derive",
"typeid",
"unicode-ident",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -7012,6 +7029,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.20.1"

View File

@@ -10,6 +10,9 @@ edition = "2021"
dioxus = { version = "0.7.1", features = ["router"] }
dioxus-code = "0.1.2"
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
dirs = "6.0.0"
ron = "0.12.2"
serde = { version = "1.0.229", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
[features]

View File

@@ -2,6 +2,7 @@ mod components;
mod route;
mod views;
mod workspace;
mod workspace_list;
use dioxus::prelude::*;

130
src/workspace_list.rs Normal file
View File

@@ -0,0 +1,130 @@
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<PathBuf>,
}
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<PathBuf> {
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<String> {
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());
}
}