Workspace-Liste laden und speichern #11
23
Cargo.lock
generated
23
Cargo.lock
generated
@@ -2329,6 +2329,9 @@ dependencies = [
|
|||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-code",
|
"dioxus-code",
|
||||||
"dioxus-code-editor",
|
"dioxus-code-editor",
|
||||||
|
"dirs",
|
||||||
|
"ron",
|
||||||
|
"serde",
|
||||||
"strum",
|
"strum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5886,6 +5889,20 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -7012,6 +7029,12 @@ dependencies = [
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typeid"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ edition = "2021"
|
|||||||
dioxus = { version = "0.7.1", features = ["router"] }
|
dioxus = { version = "0.7.1", features = ["router"] }
|
||||||
dioxus-code = "0.1.2"
|
dioxus-code = "0.1.2"
|
||||||
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
|
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"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod components;
|
|||||||
mod route;
|
mod route;
|
||||||
mod views;
|
mod views;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
mod workspace_list;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
|||||||
130
src/workspace_list.rs
Normal file
130
src/workspace_list.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user