diff --git a/docs/research/konfigurations-ablageort.md b/docs/research/konfigurations-ablageort.md new file mode 100644 index 0000000..e1785c6 --- /dev/null +++ b/docs/research/konfigurations-ablageort.md @@ -0,0 +1,565 @@ +# Ablageort für die anwendungsweite Konfiguration + +**Gitea-Issue:** #3 (Part of #1) · **Stand der Recherche:** 2026-08-27 +**Umfeld:** DioNotes, Rust (edition 2021), Dioxus 0.7.1 mit Default-Feature `desktop`. +Entwicklung unter Windows 11; **Linux wird als Zielplattform mitgedacht**, macOS nachrangig. + +Alle Aussagen sind als **[belegt]** (Primärquelle, URL angegeben) oder **[Vermutung]** +(Schlussfolgerung, Erfahrungswert, nicht durch eine Quelle abgedeckt) markiert. + +--- + +## 1. Kurzantwort + +Für die Liste zuletzt benutzter Workspaces: + +**Pfad unter Windows (ausgeschrieben):** + +``` +C:\Users\\AppData\Local\DioNotes\config\recent-workspaces.json +``` + +**Pfad unter Linux (ausgeschrieben):** + +``` +/home//.local/state/dionotes/recent-workspaces.json +``` + +Nein, das ist kein Tippfehler und keine Inkonsistenz: Linux hat für „zuletzt benutzt"-Listen +mit `$XDG_STATE_HOME` einen *eigenen* Ort, den Windows und macOS nicht kennen. Die XDG-Spec +nennt „recently used files" wörtlich als Beispiel. Begründung und Beleg in Abschnitt 4.2 — +ein einziger Rust-Ausdruck deckt beide Fälle ab. + +**Rust-Aufruf, der ihn liefert** (Crate `directories` 6.0.0): + +```rust +use directories::ProjectDirs; +use std::fs; +use std::path::PathBuf; + +fn recent_workspaces_file() -> std::io::Result { + let dirs = ProjectDirs::from("", "", "DioNotes").ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "Home-Verzeichnis des Benutzers nicht ermittelbar", + ) + })?; + + // state_dir() existiert nur unter Linux (XDG_STATE_HOME) — genau dort gehört + // eine "zuletzt benutzt"-Liste hin. Windows/macOS liefern None, dort ist + // config_local_dir() der richtige Ort. Siehe Abschnitt 3 und 4. + let dir = dirs.state_dir().unwrap_or_else(|| dirs.config_local_dir()); + + fs::create_dir_all(dir)?; // muss die App selbst tun, siehe Abschnitt 5 + Ok(dir.join("recent-workspaces.json")) +} +``` + +Derselbe Aufruf liefert plattformübergreifend: + +| Plattform | `state_dir().unwrap_or_else(\|\| config_local_dir())` | Herkunft | +|---|---|---| +| Windows | `C:\Users\\AppData\Local\DioNotes\config` | `config_local_dir()` (`state_dir()` ist `None`) | +| Linux | `$XDG_STATE_HOME/dionotes` bzw. `~/.local/state/dionotes` | `state_dir()` | +| macOS | `~/Library/Application Support/DioNotes` | `config_local_dir()` (`state_dir()` ist `None`) | + +Zum Vergleich, falls doch eine *echte* Einstellung (Theme, Schriftgröße) dazukommt — die gehört +nach `config_local_dir()` bzw. `config_dir()`: + +| Plattform | `config_local_dir()` | `config_dir()` | +|---|---|---| +| Windows | `…\AppData\Local\DioNotes\config` | `…\AppData\Roaming\DioNotes\config` | +| Linux | `~/.config/dionotes` | `~/.config/dionotes` *(identisch)* | +| macOS | `~/Library/Application Support/DioNotes` | `~/Library/Application Support/DioNotes` *(identisch)* | + +**[belegt]** — empirisch verifiziert: Ein Wegwerf-Crate mit `directories = "6"` unter Windows 11 +ausgeführt liefert für `ProjectDirs::from("", "", "DioNotes")`: + +``` +project_path = "DioNotes" +config_dir = "C:\Users\marvin.dietermann\AppData\Roaming\DioNotes\config" +config_local_dir = "C:\Users\marvin.dietermann\AppData\Local\DioNotes\config" +data_local_dir = "C:\Users\marvin.dietermann\AppData\Local\DioNotes\data" +cache_dir = "C:\Users\marvin.dietermann\AppData\Local\DioNotes\cache" +state_dir = None +chosen (state_dir OR config_local_dir) = "C:\Users\marvin.dietermann\AppData\Local\DioNotes\config" +``` + +Die Linux-/macOS-Werte stammen aus dem Quelltext von `directories` 6.0.0 (nicht auf den +Plattformen selbst ausgeführt) — siehe Abschnitt 4. + +--- + +## 2. Welche Crate? (`dirs` vs. `directories`) + +### Die beiden Kandidaten + +Beide stammen vom selben Autor, teilen sich die Low-Level-Schicht `dirs-sys` und sind seit +**2025-01-12 gemeinsam auf Version 6.0.0**. **[belegt]** — crates.io-API, + und : + +| Crate | Version | Release-Datum | Downloads gesamt | Downloads (90 Tage) | +|---|---|---|---|---| +| `dirs` | 6.0.0 | 2025-01-12 | ~307,9 Mio. | ~72,4 Mio. | +| `directories` | 6.0.0 | 2025-01-12 | ~67,1 Mio. | ~11,9 Mio. | +| `dirs-sys` | 0.5.0 | 2025-01-11 | ~319,0 Mio. | ~76,5 Mio. | + +Beide sind damit **der übliche Weg** im Rust-Ökosystem; alles andere ist eine Nische. + +**Arbeitsteilung** **[belegt]** — README von `dirs`: +> „If your requirements are more complex, e.g. computing cache, config, etc. paths for specific +> applications or projects, consider using `directories` instead." +> +> (README, gespiegelt unter +> ) + +- `dirs` = flache Funktionen, liefern nur das *Basisverzeichnis* (`dirs::config_dir()` → + `%APPDATA%`). Den App-Namen hängt man selbst an. +- `directories` = `ProjectDirs`, rechnet den anwendungsspezifischen Unterpfad plattformgerecht + aus (Windows: `Organisation\App\config`, Linux: kleingeschrieben, macOS: Bundle-ID-Form). + +### Wartungszustand — die wichtigste Einschränkung + +**[belegt]** Die GitHub-Repos sind **archiviert**. `github.com/dirs-dev/directories-rs` zeigt: +„This repository was archived by the owner on Feb 18, 2025. It is now read-only." und verweist +auf . +Quellen: , GitHub-API `repos/dirs-dev/directories-rs` +(`archived: true`). + +**[belegt]** Die Codeberg-Repos sind **nicht** archiviert und sind der aktive Ort. +Codeberg-API `repos/dirs/directories-rs`: `archived=false`, `updated_at=2026-03-22`. +Die Commit-Historie zeigt seit dem Release 6.0.0 aber **nur noch Metadaten-Commits**: + +``` +2026-03-20 Update README.md +2025-08-26 Update README to reflect migration from GitHub to Codeberg +2025-04-29 Update Cargo.toml to reflect migration from GitHub to Codeberg +2025-01-12 Release 6.0.0 +``` + +Quelle: (identisches Bild bei +`dirs/dirs-rs`). + +**Interpretation [Vermutung]:** Die Crates sind nicht verwaist, aber funktional +fertig — seit über 1,5 Jahren keine Code-Änderung. Für „gib mir einen Pfad" ist das kein +Problem; die Known-Folder-/XDG-Konventionen ändern sich nicht. Ein Risiko entstünde erst, wenn +`windows-sys` o. ä. eine Breaking-Change-Welle auslöst. + +### Aktiv entwickelte Alternative + +`etcetera` 0.11.0, veröffentlicht 2025-10-28, MSRV 1.87. **[belegt]** — crates.io-API, +, Repo . +Bietet explizit wählbare „Strategien" (native vs. XDG-überall). +**[Vermutung]:** Für DioNotes kein Vorteil — die Extra-Flexibilität wird nicht gebraucht, +und die Verbreitung ist geringer. + +### Empfehlung + +**`directories` 6.0.0.** Begründung: + +1. Es nimmt einem die plattformspezifische Namensform ab (Windows `DioNotes`, Linux `dionotes`, + macOS `DioNotes` als Bundle-ID-Fragment) — genau die Sorte Detail, die man sonst falsch macht. +2. **[belegt]** `dirs` 6.0.0 ist **bereits transitiv im Dependency-Graph** des Projekts: + `Cargo.lock` listet `dirs 6.0.0` und `dirs-sys 0.5.0`, hereingezogen über `wry` und + `tray-icon` (beide über `dioxus-desktop`). Quelle: `Cargo.lock` im Repo, Einträge + `name = "dirs"` (Z. 2915) sowie die Dependency-Listen von `tray-icon` und `wry`. + `directories` hat als einzige Nicht-Dev-Dependency `dirs-sys ^0.5.0` + () — das Hinzufügen kostet + also genau **eine** zusätzlich kompilierte Crate. +3. **[belegt]** Das offizielle Dioxus-SDK verwendet für seinen Storage selbst `directories`: + ```rust + directories::BaseDirs::new().unwrap().data_local_dir().to_path_buf() + ``` + + +4. **[belegt]** Es liefert `state_dir()`, das unter Linux auf `$XDG_STATE_HOME` zeigt und auf + Windows/macOS `None` ist — siehe Abschnitt 4.2. `dirs` hat unter Windows zwar auch ein + `state_dir()` (das dort `None` liefert), aber ohne `ProjectDirs` muss man den App-Namen und + dessen plattformspezifische Schreibweise wieder selbst zusammenbauen. + +Mit `dirs::config_local_dir()` käme man unter Windows genauso weit — dann aber ohne +`config`-Unterordner, ohne den XDG-State-Pfad und **ohne die Linux-Kleinschreibung**: Man +erzeugte `~/.config/DioNotes` statt `~/.config/dionotes`. Da Linux ausdrücklich mitgedacht werden +soll, fällt `dirs` allein damit aus. + +--- + +## 3. Welcher Pfad ist der richtige — und unter Windows: Roaming oder Local? + +### Was die Crate zurückgibt + +**[belegt]** — Quelltext `directories` 6.0.0, `src/win.rs` (aus dem lokalen +Cargo-Registry-Checkout, entspricht ): + +```rust +let config_dir = app_data_roaming.join("config"); +let config_local_dir = app_data_local.join("config"); +let data_dir = app_data_roaming.join("data"); +``` + +Und `dirs` 6.0.0, `src/win.rs`: + +```rust +pub fn cache_dir() -> Option { data_local_dir() } +pub fn config_dir() -> Option { dirs_sys::known_folder_roaming_app_data() } +pub fn config_local_dir() -> Option { dirs_sys::known_folder_local_app_data() } +pub fn data_dir() -> Option { dirs_sys::known_folder_roaming_app_data() } +pub fn data_local_dir() -> Option { dirs_sys::known_folder_local_app_data() } +pub fn preference_dir() -> Option { dirs_sys::known_folder_local_app_data() } +``` + +> ⚠️ **Fallstrick:** Die README-Tabelle von `dirs` führt `preference_dir` unter Windows als +> `{FOLDERID_RoamingAppData}`. Der Quelltext und die Laufzeitausgabe sagen +> **LocalAppData**. **[belegt]** durch beides (Quelltext oben; Testlauf lieferte +> `dirs::preference_dir() = Some("C:\Users\...\AppData\Local")`). Im Zweifel gilt der Code. +> `preference_dir` ist ohnehin nur auf macOS ein eigener Ort und dort *nicht* zu benutzen +> (siehe unten). + +Nicht der richtige Pfad ist also: +- `cache_dir()` — löschbar, darf jederzeit verschwinden. +- `data_dir()` / `data_local_dir()` — für Nutzdaten der App, nicht für Einstellungen. +- `preference_dir()` — auf macOS `~/Library/Preferences`, und dort darf man laut Apple gar + nicht selbst hineinschreiben (siehe Abschnitt 4). + +Richtig ist `config_dir()` **oder** `config_local_dir()`. + +### Roaming vs. Local — die Windows-Konvention + +**[belegt]** Beide Known Folders, Default-Pfade laut Microsoft +(): + +| KNOWNFOLDERID | GUID | Display Name | Default Path | CSIDL | +|---|---|---|---|---| +| `FOLDERID_RoamingAppData` | `{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}` | Roaming | `%APPDATA%` (`%USERPROFILE%\AppData\Roaming`) | `CSIDL_APPDATA` | +| `FOLDERID_LocalAppData` | `{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}` | Local | `%LOCALAPPDATA%` (`%USERPROFILE%\AppData\Local`) | `CSIDL_LOCAL_APPDATA` | + +**[belegt]** Die Bedeutung, wörtlich aus dem Microsoft-Deployment-Guide +(): + +> „Windows uses the Local and LocalLow folders for application data that does not roam with the +> user. **Usually this data is either machine specific or too large to roam.**" + +> „Windows uses the Roaming folder for application specific data, such as custom dictionaries, +> **which are machine independent and should roam with the user profile.**" + +**[belegt]** Dieselbe Unterscheidung, aktuell gepflegt, in der .NET-API-Doku +(, `ms.date` 2025-07, +zuletzt aktualisiert 2026-05): + +> `ApplicationData` — „The directory that serves as a common repository for application-specific +> data **for the current roaming user.** A roaming user works on more than one computer on a +> network. A roaming user's profile is kept on a server on the network and is loaded onto a +> system when the user logs on." +> +> `LocalApplicationData` — „The directory that serves as a common repository for +> application-specific data that is used by the current, **non-roaming** user." + +**[belegt]** Und die alte CSIDL-Doku +(): + +> `CSIDL_LOCAL_APPDATA` — „The file system directory that serves as a data repository for local +> **(nonroaming)** applications." + +### Das Entscheidungskriterium — und die Anwendung auf DioNotes + +Die Konvention fragt nicht „Konfiguration oder Daten?", sondern **„maschinenunabhängig oder +maschinenspezifisch?"**: + +- **maschinenunabhängig** (Theme, Schriftgröße, Tastaturkürzel, benutzerdefiniertes Wörterbuch) + → `%APPDATA%` (Roaming) → `config_dir()` +- **maschinenspezifisch oder groß** (absolute Dateipfade, Fensterpositionen, Fenstergrößen, + Caches, Indizes) → `%LOCALAPPDATA%` (Local) → `config_local_dir()` + +**Die Liste zuletzt benutzter Workspaces besteht aus absoluten Dateisystempfaden.** Ein +`D:\Notizen\Privat` existiert auf dem Laptop im Homeoffice schlicht nicht. Sie ist damit +per Definition maschinenspezifisch und gehört nach **`%LOCALAPPDATA%`**. +Das ist eine direkte Anwendung des oben zitierten Microsoft-Satzes — **[belegt]** in der +Prämisse, **[Vermutung]** nur insofern, als Microsoft DioNotes nicht namentlich erwähnt. + +**Empfehlung [Vermutung, gut begründet]:** Vorerst *alles* nach `config_local_dir()` legen — +also `%LOCALAPPDATA%\DioNotes\config\`. Gründe: + +1. Der einzige heute existierende Konfigurationsinhalt (die Workspace-Liste) ist eindeutig + maschinenspezifisch. +2. Roaming hat reale Fehlermodi, die man sich ohne Not einhandelt: Der Roaming-Ordner wird bei + Domänen-Profilen erst beim Ab-/Anmelden synchronisiert, wodurch bei paralleler Nutzung + zweier Rechner die letzte Abmeldung gewinnt und Änderungen der anderen Sitzung überschreibt. + (Mechanik des Roaming User Profile: ) +3. Zwei Verzeichnisse für eine Handvoll Einstellungen sind Overhead. Ein zweiter Ort + (`config_dir()` → `%APPDATA%\DioNotes\config\`) lohnt sich erst, wenn eine echte + maschinenunabhängige Einstellung existiert, die roamen *soll*. + +**Wichtig für die Portabilität:** Diese Roaming-/Local-Entscheidung ist **rein Windows-spezifisch +und kostet auf den anderen Plattformen nichts**. **[belegt]** — Quelltext `directories` 6.0.0: +unter Linux ist `config_local_dir = config_dir.clone()` (`src/lin.rs`), unter macOS ebenso +(`src/mac.rs`). `config_local_dir()` zu wählen verbaut also unter Linux/macOS keine Option; +es ist dort schlicht derselbe Pfad. + +--- + +## 4. Linux und macOS + +> Ursprünglich als Nebenfrage angelegt — Linux hat sich beim Nachrecherchieren aber als der +> Fall herausgestellt, der die Empfehlung tatsächlich verändert. Siehe 4.2. + +### 4.1 Wohin `ProjectDirs` zeigt + +**[belegt]** Quelltext `directories` 6.0.0: + +`src/lin.rs`: +```rust +pub fn project_dirs_from(_qualifier: &str, _organization: &str, application: &str) -> Option { + ProjectDirs::from_path(PathBuf::from(&trim_and_lowercase_then_replace_spaces(application, ""))) +} +// config_dir = $XDG_CONFIG_HOME (sonst $HOME/.config) + project_path +// config_local_dir = config_dir.clone() ← unter Linux identisch! +``` + +`src/mac.rs`: +```rust +pub fn project_dirs_from(qualifier: &str, organization: &str, application: &str) -> Option { + let organization = organization.replace(" ", "-"); + let application = application.replace(" ", "-"); + let mut parts = vec![qualifier, &organization, &application]; parts.retain(|e| !e.is_empty()); + let bundle_id = parts.join("."); + ProjectDirs::from_path(PathBuf::from(bundle_id)) +} +// config_dir = $HOME/Library/Application Support + project_path +// preference_dir = $HOME/Library/Preferences + project_path +``` + +Daraus folgt für `ProjectDirs::from("", "", "DioNotes")`: +- **Linux:** `~/.config/dionotes` — `config_dir()` und `config_local_dir()` sind dort *dasselbe*. + Die Roaming-Frage stellt sich unter Linux nicht. +- **macOS:** `~/Library/Application Support/DioNotes` (leerer Qualifier/Organisation werden aus + der Bundle-ID herausgefiltert). + +**[belegt]** Die Linux-Zuordnung deckt sich mit der XDG Base Directory Specification +(): +> „`$XDG_CONFIG_HOME` defines the base directory relative to which user-specific configuration +> files should be stored. If `$XDG_CONFIG_HOME` is either not set or empty, a default equal to +> `$HOME`/.config should be used." + +**[belegt]** Für macOS ist `Application Support` — nicht `Preferences` — korrekt. Apple, File +System Programming Guide +(): + +> `Library/Application Support` — „Use this directory to store all app data files except those +> associated with the user's documents. For example, you might use this directory to store +> app-created data files, **configuration files**, templates … All content in this directory +> should be placed in a custom subdirectory whose name is that of your app's bundle identifier +> or your company." +> +> `Library/Preferences` — „This directory contains app-specific preference files. **You should +> not create files in this directory yourself.** Instead, use the `NSUserDefaults` class or +> CFPreferences API…" + +Das ist der Grund, `preference_dir()` **nicht** zu benutzen: Es zeigt auf macOS genau in den +Ordner, in den man laut Apple nicht selbst schreiben soll. + +### 4.2 Linux kennt einen dritten Ort — und der ist für unseren Fall der richtige + +XDG hat neben `config` und `data` noch `$XDG_STATE_HOME`. **[belegt]** — XDG Base Directory +Specification **Version 0.8** (08. Mai 2021), : + +> „`$XDG_STATE_HOME` defines the base directory relative to which user-specific state files +> should be stored. If `$XDG_STATE_HOME` is either not set or empty, a default equal to +> `$HOME`/.local/state should be used." + +Und zum Inhalt, wörtlich: + +> „The `$XDG_STATE_HOME` contains state data that should persist between (application) restarts, +> but that is not important or portable enough to the user that it should be stored in +> `$XDG_DATA_HOME`. It may contain: +> - actions history (logs, history, **recently used files**, …) +> - current state of the application that can be reused on a restart (view, layout, +> **open files**, undo history, …)" + +Das ist keine Auslegung — die Spezifikation nennt „recently used files" und „open files" +**namentlich**. Eine Liste zuletzt benutzter Workspaces ist unter Linux genau das: State, +nicht Config. Sie gehört nach `~/.local/state/dionotes`, nicht nach `~/.config/dionotes`. + +**[belegt]** `directories` 6.0.0 bildet das ab, und zwar nur dort, wo es existiert: + +| Plattform | Quelltext | `ProjectDirs::state_dir()` | +|---|---|---| +| Linux | `src/lin.rs` Z. 73/84: `$XDG_STATE_HOME` bzw. `$HOME/.local/state`, dann `.join(project_path)`, `state_dir: Some(state_dir)` | `Some("~/.local/state/dionotes")` | +| Windows | `src/win.rs` Z. 89: `state_dir: None` | `None` | +| macOS | `src/mac.rs` Z. 83: `state_dir: None` | `None` | + +Empirisch auf Windows 11 bestätigt: `state_dir() = None`, und +`state_dir().unwrap_or_else(|| config_local_dir())` liefert +`C:\Users\…\AppData\Local\DioNotes\config`. + +**Konsequenz für den Aufruf [Vermutung, gut begründet]:** Ein einziger Ausdruck ist auf allen drei +Plattformen richtig, weil der Fallback genau dann greift, wenn das Betriebssystem den Begriff +„State" gar nicht kennt: + +```rust +let dir = dirs.state_dir().unwrap_or_else(|| dirs.config_local_dir()); +``` + +Das ist bewusst *kein* `#[cfg(target_os = ...)]` — die Fallunterscheidung steckt bereits im +`Option`, das `directories` zurückgibt. + +### 4.3 Weitere Linux-Punkte + +**[belegt] Kleinschreibung wird automatisch erledigt.** `src/lin.rs` reicht den Anwendungsnamen +durch `trim_and_lowercase_then_replace_spaces` — aus `"DioNotes"` wird `dionotes`. Genau das ist +unter Linux üblich; mit `dirs` (statt `directories`) müsste man das von Hand tun und würde sonst +ein unschönes `~/.config/DioNotes` erzeugen. Das ist das stärkste einzelne Argument für +`directories` statt `dirs`. + +**[belegt] Flatpak/Snap funktionieren ohne Sonderbehandlung.** `directories` liest die +XDG-Variablen zur Laufzeit aus (`env::var_os("XDG_CONFIG_HOME")`, `…("XDG_STATE_HOME")`, +jeweils gefiltert durch `dirs_sys::is_absolute_path`, `src/lin.rs` Z. 12–19 bzw. 66–73). Eine +Sandbox, die diese Variablen umbiegt (Flatpak setzt sie auf `~/.var/app//…`), wird damit +automatisch respektiert. +**[Vermutung]:** Das eigentliche Sandbox-Problem läge woanders — die in der Liste gespeicherten +absoluten Workspace-Pfade zeigen ggf. aus der Sandbox heraus. Das ist aber eine Frage des +Workspace-Zugriffs, nicht des Ablageorts der Liste. + +**[belegt] Der `HOME`-Fallback ist robust.** Ist `$XDG_STATE_HOME` nicht gesetzt oder kein +absoluter Pfad, greift `$HOME/.local/state`. `ProjectDirs::from(...)` liefert nur dann `None`, +wenn sich gar kein Home-Verzeichnis ermitteln lässt — der Fehlerfall ist also derselbe wie unter +Windows und muss ohnehin behandelt werden. + +--- + +## 5. Muss die App das Verzeichnis anlegen? + +**Ja, den anwendungsspezifischen Teil.** + +**[belegt]** README von `directories` und `dirs`, wörtlich identisch: +> „This library does not create directories or check for their existence. The library only +> provides information on what the path to a certain directory **should** be." +> +> · + +**[belegt]** Empirisch auf dem Entwicklungsrechner: Der Testlauf zeigte +`"C:\Users\...\AppData\Local\DioNotes" exists = false` und +`"C:\Users\...\AppData\Roaming\DioNotes" exists = false` — `ProjectDirs` liefert also einen Pfad, +den es nicht gibt. + +**[belegt]** Der Fall ist im Ökosystem üblich: Das Dioxus-SDK ruft direkt nach dem Ermitteln des +Pfads `std::fs::create_dir_all(path).unwrap()` auf +(). + +**[belegt]** Die Basis-Ordner `%APPDATA%` / `%LOCALAPPDATA%` selbst existieren dagegen: Sie sind +registrierte Known Folders vom Typ `PERUSER` und werden mit dem Benutzerprofil angelegt +(). Anzulegen ist also nur +der Teil `DioNotes\config`. + +**[belegt]** Unter Linux erwartet auch die XDG-Spec, dass die App das tut: +> „When attempting to write a file, if the destination directory is non-existent an attempt +> should be made to create it with permission `0700`." + +Praktisch: `std::fs::create_dir_all(dir)?` beim Start bzw. vor dem ersten Schreiben. +`create_dir_all` ist idempotent und liefert `Ok(())`, wenn das Verzeichnis schon existiert. + +--- + +## 6. Gibt es in Dioxus 0.7 eine eigene Abstraktion dafür? + +**Nein.** Vier unabhängige Belege: + +1. **[belegt]** `dioxus_desktop::Config` hat genau zwei Pfad-Methoden — `with_resource_directory` + und `with_data_directory` — und keine davon liefert ein OS-Konfigurationsverzeichnis. + + +2. **[belegt] `with_data_directory` ist eine Falle.** Es klingt richtig, ist aber etwas anderes. + Quelltext, Tag `v0.7.1` + (): + ```rust + /// set the directory where data will be stored in release mode. + /// + /// > Note: This **must** be set when bundling on Windows. + pub fn with_data_directory(mut self, path: impl Into) -> Self { + self.data_dir = Some(path.into()); + self + } + ``` + Verwendet wird `data_dir` an genau einer Stelle + (, Z. 262): + ```rust + let mut web_context = WebContext::new(cfg.data_dir.clone()); + ``` + Und `wry::WebContext` ist das *Browser-Profil* der WebView: + > „A context that is shared between multiple WebViews. A browser would have a context for all + > the normal tabs and a different context for all the private/incognito tabs." + > `data_directory`: „Whether the WebView window should have a custom user data path. This is + > useful in Windows when a bundled application can't have the webview data inside Program Files." + > + > + + Das ist der Ort für WebView2-Cookies/LocalStorage/Cache, **nicht** für die Konfiguration der + Anwendung. + +3. **[belegt]** Die Item-Liste von `dioxus` 0.7.1 () + enthält nichts Passendes. `dioxus-cli-config` betrifft Build-/Serve-Parameter der CLI, + `SyncStorage`/`UnsyncStorage` sind Signal-Backends, kein Festplattenspeicher. + Zwei unabhängige Doku-Korpora (`dioxuslabs.com/learn/0.7` als llms-full sowie die Docsite) + liefern auf die Frage nach einem Konfigurationsverzeichnis keinen Treffer. + Auch die repo-eigene `AGENTS.md` (Dioxus-0.7-API-Referenz) enthält dazu nichts. + +4. **[belegt]** Das offizielle **Dioxus-SDK** greift für seinen `use_persistent`-Storage selbst zu + `directories` — wäre in Dioxus etwas eingebaut, hätte es das benutzt. + + +**Zu `dioxus-sdk` / `dioxus-storage` als Alternative [Vermutung]:** Es wäre ebenfalls eine +Extra-Dependency (aktuell auf Dioxus 0.6 ausgerichtet, `dioxus-sdk = "0.6"`), legt alles nach +`BaseDirs::data_local_dir()` — also `%LOCALAPPDATA%` **ohne** App-Unterordner — und bringt einen +kompletten Hook-Apparat mit. Für eine einzelne JSON-Datei ist das mehr Maschinerie als Nutzen. + +--- + +## 7. Offene Punkte / bewusst nicht geklärt + +- **[nicht geprüft]** Verhalten unter Windows, wenn DioNotes als MSIX/Store-App verpackt würde + (dann greift Filesystem-Virtualisierung und `%LOCALAPPDATA%` wird umgeleitet). Für + `dx bundle` als klassische Desktop-App ist das irrelevant. +- **[nicht geprüft]** Die Linux-/macOS-Pfade wurden aus dem Quelltext von `directories` 6.0.0 + abgeleitet und sind dort eindeutig, aber nicht auf den Plattformen selbst ausgeführt. Sobald + ein Linux-Build existiert, sollte man den ermittelten Pfad einmal gegenprüfen. +- **[nicht geprüft]** Verhalten unter Flatpak/Snap in der Praxis. Der Mechanismus ist klar + (XDG-Variablen werden zur Laufzeit gelesen und damit von der Sandbox respektiert), aber nicht + in einer echten Sandbox getestet. Ungeklärt ist dort separat, ob die gespeicherten absoluten + Workspace-Pfade aus der Sandbox heraus überhaupt erreichbar sind — eigenes Thema. +- **[offen, Folgeentscheidung]** Ob `state_dir()` auch für spätere echte Einstellungen benutzt + wird oder ob dafür ein zweiter Ort (`config_local_dir()`) aufgemacht wird. Unter Linux wären + das zwei getrennte Verzeichnisse (`~/.local/state/dionotes` und `~/.config/dionotes`), unter + Windows/macOS fielen sie zusammen. Solange es nur die Workspace-Liste gibt, stellt sich die + Frage nicht. +- **[offen, Folgeentscheidung]** Dateiformat und Schema der Workspace-Liste (JSON? TOML? + Migrationsstrategie bei Formatänderung?) — das ist eine eigene Frage und nicht Teil dieses + Tickets. + +--- + +## 8. Quellenverzeichnis + +**Microsoft (Primärquellen für die Windows-Konvention)** +- KNOWNFOLDERID: +- CSIDL: +- `Environment.SpecialFolder`: +- Managing Roaming User Data Deployment Guide: +- Deploy roaming user profiles: + +**Plattform-Konventionen Linux / macOS** +- XDG Base Directory Specification, Version 0.8 (08.05.2021) — `$XDG_CONFIG_HOME`, + `$XDG_STATE_HOME`, Anlegen mit `0700`: +- Apple, File System Programming Guide: + +**Crates** +- `directories` (docs): +- `directories` (Quelltext, aktiv): +- `dirs` (Quelltext, aktiv): +- Archivierte GitHub-Spiegel: , +- crates.io-Metadaten: , , + +**Dioxus** +- `dioxus_desktop::Config`: +- `packages/desktop/src/config.rs` @ v0.7.1: +- `packages/desktop/src/webview.rs` @ v0.7.1: +- `wry::WebContext`: +- Dioxus SDK Storage: diff --git a/docs/research/nativer-ordner-dialog.md b/docs/research/nativer-ordner-dialog.md new file mode 100644 index 0000000..1fb1408 --- /dev/null +++ b/docs/research/nativer-ordner-dialog.md @@ -0,0 +1,536 @@ +# Nativer Ordner-Dialog unter Dioxus 0.7 Desktop + +> Research zu Gitea-Issue #2. Stand: 2026-08-27. +> Recherchiert gegen Primärquellen (docs.rs, crates.io-API, Quellcode auf GitHub, offizielle Dioxus-Doku). +> **Nichts davon wurde kompiliert** — im Repo wurde bewusst keine Dependency hinzugefügt. Alles unter +> „Belegt" ist aus Signaturen/Quellcode/Manifesten abgeleitet, nicht aus einem laufenden Build. + +## Kurzantwort + +Die zugrundeliegende Bibliothek ist in beiden Fällen **`rfd` („Rusty File Dialogs"), `0.17.2`** — +aber es gibt **zwei Wege**, und der bequemere war zunächst übersehen worden: + +**Weg A — ohne neue Dependency (offiziell dokumentiert).** +`input { r#type: "file", directory: true, onchange: … }` und der Pfad kommt aus +`evt.files()[0].path() -> PathBuf`. `dioxus-desktop` ruft dahinter intern +`rfd::AsyncFileDialog::pick_folder()` auf. Kostet **null** zusätzliche Abhängigkeiten. +Nachteil: braucht ein DOM-Element und einen echten Nutzer-Klick darauf — nicht aus beliebigem +Rust-Code heraus auslösbar. + +**Weg B — `rfd` direkt als eigene Dependency.** +`rfd::AsyncFileDialog::new().pick_folder().await` in einem `onclick`-Handler. Der einzige Weg für +einen Dialog, den man **programmatisch** startet. Kostet praktisch nichts, weil `rfd 0.17.2` über +`dioxus-desktop` ohnehin schon im Dependency-Graph steht (siehe Abschnitt 2). + +Abbruch liefert in beiden Fällen `None`. Unter Windows läuft beides **ohne Extra-Abhängigkeit und +ohne Feature-Flag** — nur Linux bringt zusätzliche Anforderungen mit. + +> **Korrektur zur ersten Fassung dieses Dokuments:** Dort stand „Dioxus selbst bringt keine +> Datei-/Ordner-Dialog-API mit". **Das war falsch** — der `directory`-Attributweg (Weg A) ist in der +> offiziellen 0.7-Doku beschrieben. Richtig bleibt: `dioxus-desktop` **re-exportiert** `rfd` nicht, +> und `dioxus-sdk` hat kein Dialog-Modul. + +--- + +## 1. Welche Crate? + +### Belegt + +- `rfd` ist eine plattformübergreifende Bibliothek für **native** Öffnen-/Speichern-Dialoge und + bietet „both asynchronous and synchronous APIs". Unterstützte Plattformen laut Crate-Doku: + Windows, macOS, Linux & BSDs (GTK3 oder XDG Desktop Portal), WASM32 (nur async). + → +- Aktuelle Version: **0.17.2**, veröffentlicht 2026-01-12, ~27 Mio. Downloads gesamt. + → +- Repository: +- „PickFolder" ist in der offiziellen Feature-Matrix der Crate für **Linux ✔ / Windows ✔ / macOS ✔** + ausgewiesen (nur Wasm32 ✖). + → + +### Was es *nicht* gibt + +- **Das offizielle `dioxus-sdk` hat kein Dialog-Modul.** Aktuell `dioxus-sdk 0.7.0` (2025-11-06), + Repo . Die vollständige Feature-Liste der Crate lautet: + `geolocation`, `notification`, `storage`, `sync`, `time`, `util`, `window` — kein `file`, + kein `dialog`, kein `fs`. + → +- **`dioxus-desktop` re-exportiert `rfd` nicht.** Die Crate *benutzt* `rfd` intern (siehe Weg A in + Abschnitt 3a), aber `mod file_upload;` ist privat und die Re-Export-Liste in `lib.rs` enthält nur + `tao`, `wry`, `muda`, `trayicon`, `launch`, `AssetRequest`, `Config`, `DesktopContext`, + `DesktopService`, `WryEventHandler`, `hooks::*`, Shortcut-Typen und `wry::RequestAsyncResponder`. + Ein `dioxus_desktop::rfd` gibt es also **nicht** — wer `rfd` direkt aufrufen will, muss es selbst + in die `Cargo.toml` schreiben. + → + +### Korrigiert gegenüber der ersten Fassung + +Ein reiner Webview-/HTML-Weg **existiert doch** und ist offiziell dokumentiert — er ist nur kein +„Webview-eigener" Dialog, sondern eine Dioxus-Eigenheit, die auf den nativen `rfd`-Dialog abgebildet +wird. Siehe Abschnitt 3a. + +--- + +## 2. Kompatibilität — und: `rfd` ist bereits im Graph + +### Belegt: die tatsächlich aufgelöste Version ist 0.7.10, nicht 0.7.1 + +`Cargo.toml` deklariert `dioxus = "0.7.1"`, das ist ein Caret-Requirement. Aufgelöst wird in +`Cargo.lock` tatsächlich: + +| Crate | Version in `Cargo.lock` | +|---|---| +| `dioxus` | **0.7.10** | +| `dioxus-desktop` | **0.7.10** | +| `rfd` | **0.17.2** | +| `tao` | 0.34.8 | +| `wry` | 0.53.5 | +| `raw-window-handle` | 0.5.2 **und** 0.6.2 | + +→ `Cargo.lock` im Repo-Root (Zeilen 2337, 2493, 5847, 6521, 5716/5722). + +**Das ist der wichtigste praktische Befund:** `dioxus-desktop 0.7.10` hat `rfd` als **nicht-optionale** +Dependency mit `req = "^0.17.2"`, `default-features = false`, `features = ["xdg-portal"]`, unter +`cfg(any(windows, macos, linux, dragonfly, freebsd, netbsd, openbsd))`. +→ + +Ein `rfd = "0.17"` in unserer `Cargo.toml` fügt dem Graph also **keinen neuen Crate hinzu** und +kostet **keinen zusätzlichen Compile-Unit** — es macht nur den bereits gebauten Knoten sichtbar. +Cargo vereinigt die Features additiv; unsere Default-Features von `rfd` sind ohnehin `xdg-portal`, +also genau das, was `dioxus-desktop` schon setzt. + +*(Historische Notiz: `dioxus-desktop 0.7.1` hatte noch `rfd ^0.15.3`. Der Sprung auf 0.17 kam +zwischen 0.7.1 und 0.7.10. Für uns zählt 0.7.10.)* + +### Belegt: keine Versionskonflikte + +- `dioxus-desktop 0.7.1` zieht **`tao ^0.34.0`** und **`wry ^0.53.5`**. + → +- `rfd 0.17.2` hat **keinerlei Abhängigkeit auf Dioxus, wry oder tao**. Die einzige gemeinsame + Berührungsfläche ist `raw-window-handle ^0.6` (unbedingte Dependency von `rfd`). + → +- `tao 0.34.x` implementiert `HasWindowHandle` und `HasDisplayHandle` aus `raw-window-handle 0.6` + für `tao::window::Window`, hinter dem Feature `rwh_06`. + → +- `rwh_06` ist ein **Default-Feature** von `tao` → , + und `dioxus-desktop` schaltet Default-Features nicht ab (`tao = { workspace = true, features = ["rwh_05"] }` + — additiv, kein `default-features = false`). + → + +**Fazit:** Es gibt keinen Versionskonflikt. `rfd` und `dioxus-desktop` teilen sich `raw-window-handle 0.6` +und passen semver-kompatibel zusammen. + +### Vermutung (nicht verifiziert) + +- Daraus folgt *rechnerisch*, dass `AsyncFileDialog::set_parent(&window)` mit dem Dioxus-Fenster + typprüfen sollte: `use_window()` liefert `DesktopContext = Rc` + (, + ), `DesktopService` + implementiert `Deref` + (), und + `set_parent` nimmt `&W where W: HasWindowHandle + HasDisplayHandle + ?Sized` + (). + **Das ist nicht kompiliert worden.** Für einen ersten Wurf ist `set_parent` optional — ohne + Parent erscheint der Dialog als eigenständiges Fenster statt als besitzer-modaler Dialog. + +--- + +## 3a. Weg A — der offizielle Weg ohne neue Dependency + +### Belegt + +Die offizielle Dioxus-0.7-Doku beschreibt Ordner-Auswahl über ein `directory`-Attribut am +File-Input: + +> „Dioxus will map this attribute to browser specific attributes, because there is no standardized +> way to allow a directory to be selected." + +```rust +input { + type: "file", + // Select a folder by setting the directory attribute + directory: true, + onchange: move |evt| { + for file in evt.files() { + println!("{}", file.name()); + } + } +} +``` +→ + +Auf dem **Desktop** ist das kein Browser-Dialog, sondern feuert intern exakt den nativen +`rfd`-Ordner-Dialog: + +```rust +// packages/desktop/src/file_upload.rs +if self.directory { + if self.multiple { + dialog.pick_folders().await + } else { + dialog.pick_folder().await + } +} + +pub struct DesktopFileData(pub(crate) PathBuf); + +fn path(&self) -> PathBuf { self.0.clone() } +``` +→ + +Und der **absolute Pfad** ist öffentlich abgreifbar — `FileData::path()` ist Teil der +`dioxus-html`-API: + +```rust +pub fn path(&self) -> PathBuf +``` +→ + +### Snippet für Weg A + +```rust +use dioxus::prelude::*; +use std::path::PathBuf; + +#[component] +fn WorkspaceOrdnerWahlOhneDependency() -> Element { + let mut ordner = use_signal(|| None::); + + let label = ordner() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "Noch kein Ordner gewählt".to_string()); + + rsx! { + input { + r#type: "file", + directory: true, + onchange: move |evt| { + // Bei Abbruch ist die Liste leer -> kein Event-File + if let Some(file) = evt.files().first() { + ordner.set(Some(file.path())); + } + }, + } + p { "{label}" } + } +} +``` + +### Grenzen von Weg A + +- Braucht ein **DOM-Element** und einen echten Nutzer-Klick darauf. Ein Ordner-Dialog, den man aus + Rust heraus programmatisch öffnet (z. B. aus einem Menüeintrag, einem Shortcut oder nach einem + anderen Ereignis), ist damit **nicht** möglich. +- Das Styling ist ein `` — für einen hübschen Button muss man das Element + verstecken und per Label/JS triggern. +- **Vermutung, nicht verifiziert:** wie sich `evt.files()` bei Abbruch genau verhält (leere Liste + vs. gar kein `onchange`-Event). Das obige `if let Some(...)` ist gegen beides robust. + +--- + +## 3b. Blockierend oder async? — Für Weg B im `onclick` zwingend **async** + +### Belegt: die Signaturen + +`rfd` hat zwei parallele Typen. Beide bauen per Builder, unterscheiden sich nur im Rückgabetyp: + +```rust +// Synchron — rfd::FileDialog +pub fn pick_folder(self) -> Option; +pub fn pick_folders(self) -> Option>; + +// Asynchron — rfd::AsyncFileDialog +pub fn pick_folder(self) -> impl Future>; +pub fn pick_folders(self) -> impl Future>>; +``` + +→ , + + +### Belegt: warum die synchrone Variante in Dioxus schadet + +- Die synchrone Windows-Implementierung fährt die modale COM-Dialogschleife **auf dem aufrufenden + Thread**: + + ```rust + fn pick_folder(self) -> Option { + fn run(opt: FileDialog) -> Result { + init_com(|| { + let dialog = IDialog::build_pick_folder(&opt)?; + dialog.show()?; // blockiert hier bis der Nutzer fertig ist + dialog.get_result() + })? + } + run(self).ok() + } + ``` + → + +- Dioxus-Event-Handler und via `spawn` gestartete Tasks laufen auf **demselben Thread** wie die + Event-Loop: `pub fn spawn(fut: impl Future + 'static) -> Task` — **kein `Send`-Bound**, + d. h. die Futures können den Thread gar nicht wechseln. + → + + Ein `FileDialog::pick_folder()` direkt im `onclick` blockiert also die tao-Event-Loop → Fenster + friert ein, solange der Dialog offen ist. + +- Die Crate-Doku selbst empfiehlt, Dialoge auf dem Main-Thread zu *starten* + („It is also recommended to spawn dialogs on your main thread") — genau das tut man mit der + async-Variante aus einem Dioxus-Handler heraus. + → + +### Belegt: warum die async-Variante unter Windows *nicht* blockiert + +Die Windows-async-Variante lagert den modalen Dialog auf einen eigenen OS-Thread aus und liefert das +Ergebnis über einen Channel zurück: + +```rust +// async_thread(..) — Kern der Implementierung +std::thread::spawn(move || { + tx.send(f()).ok(); +}); + +// pick_folder_async +async_thread(move || Self::pick_folder(self).map(FileHandle::wrap)) +``` +→ + +Die Dioxus-Event-Loop läuft weiter; das `.await` gibt den Main-Thread frei, bis der Channel liefert. +**Kein Tokio-/async-std-Feature nötig** — `rfd` hat keine Runtime-Features, die Implementierung nutzt +`std::thread` plus Channel. → +(keine Runtime-Dependency; nur `log`, `raw-window-handle` unbedingt) + +Nebenbedingung aus dem offiziellen `rfd`-Beispiel: der Dialog soll **auf dem Main-Thread gestartet** +(`pick_*()` aufgerufen) und darf anderswo awaited werden — „Spawn dialog on main thread / Await +somewhere else". → +Im Dioxus-Fall passiert **beides** auf dem Main-Thread (`spawn` hat kein `Send`-Bound), die Bedingung +ist damit erfüllt. + +--- + +## 4. Was kommt bei Abbruch zurück? + +### Belegt + +**`None`.** Sowohl `FileDialog::pick_folder() -> Option` als auch +`AsyncFileDialog::pick_folder() -> impl Future>` liefern bei Abbruch +`None`. Es gibt keinen `Result`-Typ und keine Fehlerunterscheidung „abgebrochen vs. fehlgeschlagen" — +ein technisch fehlgeschlagener Dialog ist von einem abgebrochenen nicht unterscheidbar (die +Windows-Impl macht am Ende `run(self).ok()`, verwirft den Fehler also). +→ , +, + + +Aus dem `FileHandle` kommt man so an den Pfad: + +```rust +pub fn path(&self) -> &Path; // existiert nicht unter WASM32 +pub fn file_name(&self) -> String; +// zusätzlich: impl From for PathBuf +``` +→ + +--- + +## 5. Plattformen und Feature-Flags + +### Belegt: Windows — out of the box, ja + +Einzige Windows-Dependency von `rfd 0.17.2`: + +| Target | Crate | Version | Features | +|---|---|---|---| +| `cfg(target_os = "windows")` | `windows-sys` | `^0.61` | `Win32_Foundation`, `Win32_System_Com`, `Win32_UI_Shell_Common`, `Win32_UI_Shell`, `Win32_UI_WindowsAndMessaging` | + +→ + +`windows-sys` ist reines Rust-Binding auf die Systembibliotheken — **keine C-Header, kein vcpkg, +kein Build-Tool, kein Feature-Flag**. Zusammen mit der Implementierung über `IFileOpenDialog`/COM +(siehe `init_com` / `IDialog::build_pick_folder` oben) heißt das: +**`rfd = "0.17"` in die `Cargo.toml`, fertig.** + +Optional unter Windows: das Feature `common-controls-v6` — betrifft ausschließlich +**Message-Dialoge** (`TaskDialogIndirect` aus ComCtl32.dll v6, für eigene Button-Texte) und braucht +zusätzlich ein Application-Manifest. **Für Ordner-Dialoge irrelevant.** +→ + +### Belegt: macOS + +Reine Rust/objc2-Dependencies (`objc2`, `objc2-app-kit`, `objc2-foundation`, `block2`, `dispatch2`), +keine Feature-Flags nötig. Einschränkung aus der Crate-Doku: + +> „macOS async dialogs require an `NSApplication` instance, so the dialog is only truly async when +> opened in windowed environment like `winit` or `SDL2`. Otherwise, it will fallback to sync dialog." + +Eine Dioxus-Desktop-App **ist** eine windowed App (tao/`NSApplication`), die Bedingung ist also +erfüllt. → , + + +### Belegt: Linux — hier steckt der einzige echte Stolperstein + +Zwei Backends, per Cargo-Feature wählbar: + +- **`xdg-portal` (Default)** — XDG Desktop Portal über D-Bus. Braucht zur **Laufzeit** `libdbus` + (fällt sonst auf `zenity` zurück) **und** ein installiertes Portal-Backend (GTK, GNOME oder KDE). + Das wlroots-Portal-Backend implementiert die benötigte D-Bus-API **nicht**. +- **`gtk3`** — `default-features = false, features = ["gtk3"]`. Braucht die GTK3-**Entwicklungs**- + Header schon zur **Build**-Zeit (`libgtk-3-dev` / `gtk3-devel` / `pacman -S gtk3`). + +→ + +**Also: nein, „derselbe Aufruf überall" gilt für den *Rust-Code*, aber nicht für das *Packaging*.** +Der Aufruf im Code ist auf allen drei Plattformen identisch; Linux-Builds brauchen zusätzlich eine +Aussage darüber, welches Backend gewählt wird und was beim Nutzer installiert sein muss. +Für DioNotes auf Windows 11 ist das derzeit kein Thema. + +--- + +## 6. Lauffähiges Beispiel-Snippet für Weg B (Dioxus 0.7 API) + +`Cargo.toml` — **nicht angewendet**, gehört in ein Implementierungs-Ticket: + +```toml +[dependencies] +rfd = "0.17" # bereits transitiv via dioxus-desktop 0.7.10 im Lock-File +``` + +Komponente: + +```rust +use dioxus::prelude::*; +use std::path::PathBuf; + +#[component] +fn WorkspaceOrdnerWahl() -> Element { + let mut ordner = use_signal(|| None::); + + // Anzeige-Text vorberechnen, damit im rsx! kein Ausdruck nötig ist + let label = ordner() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "Noch kein Ordner gewählt".to_string()); + + rsx! { + button { + // Async-Closures in Event-Handlern werden von Dioxus automatisch gespawnt. + onclick: move |_| async move { + let auswahl = rfd::AsyncFileDialog::new() + .set_title("Workspace-Ordner wählen") + .pick_folder() + .await; + + match auswahl { + // FileHandle::path() -> &Path + Some(handle) => ordner.set(Some(handle.path().to_path_buf())), + // Nutzer hat abgebrochen -> None; Zustand bewusst unverändert lassen + None => {} + } + }, + "Ordner wählen…" + } + p { "{label}" } + } +} +``` + +Belege für die Dioxus-Seite des Snippets: + +- Async-Closure direkt als Event-Handler: „Async closures passed to event handlers are automatically + spawned" — offizielles Beispiel in der 0.7-Doku. + → (Abschnitt „Use async closures in event handlers") +- Alternative, falls die Async-Closure irgendwo Ärger macht — `spawn` explizit: + ```rust + onclick: move |_| { + spawn(async move { + if let Some(h) = rfd::AsyncFileDialog::new().pick_folder().await { + ordner.set(Some(h.path().to_path_buf())); + } + }); + }, + ``` + → , + (Abschnitt „Spawn background tasks in event handlers") +- `use_signal` / `.set()` statt `use_state`: entspricht der 0.7-API in `AGENTS.md`. + +### Optionale Erweiterung (Vermutung, nicht kompiliert) + +Damit der Dialog besitzer-modal am App-Fenster hängt: + +```rust +let window = use_window(); // DesktopContext = Rc, Deref -> tao::Window +// ... +rfd::AsyncFileDialog::new() + .set_parent(&*window) // &DesktopService --deref--> &tao::window::Window + .set_title("Workspace-Ordner wählen") + .pick_folder() + .await +``` + +Typtheoretisch geht das auf (siehe Abschnitt 2), **aber ungetestet**. Zwei offene Punkte: + +1. Ob die Deref-Kette bis `tao::window::Window` vom Compiler an `set_parent`s Bound + `W: HasWindowHandle + HasDisplayHandle + ?Sized` angepasst wird, oder ob ein expliziter + `&*(*window)` / Zwischenschritt nötig ist. +2. Windows führt den async-Dialog auf einem **anderen** Thread aus (Abschnitt 3). Ein Parent-HWND + über Thread-Grenzen hinweg ist unter Win32 grundsätzlich zulässig, das Modalitäts-Verhalten + (Deaktivierung des Owner-Fensters) sollte man aber praktisch prüfen. + +Empfehlung: **erst ohne `set_parent` implementieren**, das ist die belegte Variante. + +--- + +## 7. Empfehlung für DioNotes + +Für „Ordner beim Anlegen eines Workspace wählen" ist die Auswahl eine reine UX-Frage: + +- Wenn der Dialog aus einem **sichtbaren Button/Input im Formular** kommt → **Weg A**, + null neue Dependencies, offiziell dokumentiert. +- Wenn der Dialog **programmatisch** aufgehen soll (Menü, Shortcut, Folgeschritt eines Wizards) + → **Weg B** mit `rfd = "0.17"`. Kostet dank des schon vorhandenen Lock-Eintrags praktisch nichts. + +Beide landen unter Windows im selben nativen `IFileOpenDialog`. + +## 8. Zusammenfassung der offenen Punkte + +| Punkt | Status | +|---|---| +| Crate + Version | **belegt** — `rfd 0.17.2` | +| `rfd` bereits transitiv im Lock-File (via dioxus-desktop 0.7.10) | **belegt** (`Cargo.lock` + crates.io) | +| Projekt löst `dioxus 0.7.1` real auf **0.7.10** auf | **belegt** (`Cargo.lock`) | +| Weg A: `directory: true` + `FileData::path()` | **belegt** (offizielle Doku + docs.rs + Quellcode) | +| Weg A nutzt intern `rfd::…::pick_folder()` | **belegt** (`packages/desktop/src/file_upload.rs`) | +| Kein Konflikt mit tao 0.34 / wry 0.53 | **belegt** (Manifeste) | +| async statt sync in `onclick` (Weg B) | **belegt** (Signaturen + Quellcode + `spawn`-Signatur ohne `Send`) | +| Abbruch → `None` | **belegt** | +| Windows out of the box, kein Feature-Flag | **belegt** (nur `windows-sys ^0.61`) | +| Linux braucht Portal-Backend bzw. `gtk3`-Header | **belegt** | +| `dioxus-sdk` hat kein Dialog-Modul | **belegt** (Feature-Liste 0.7.0) | +| `dioxus_desktop::rfd` existiert nicht (kein Re-Export) | **belegt** (`lib.rs`) | +| `set_parent` mit dem Dioxus-Fenster | **Vermutung** — plausibel, nicht kompiliert | +| `evt.files()`-Verhalten bei Abbruch in Weg A | **Vermutung** — Snippet ist gegen beide Varianten robust | +| Verhalten bei Netzlaufwerken / UNC-Pfaden | **nicht recherchiert** | +| Verhalten unter `dx serve` / Hot-Reload | **nicht recherchiert** | + +## Quellenliste + +- rfd Crate-Doku (Quellcode-Ansicht der `//!`-Doku): +- `rfd::FileDialog`: +- `rfd::AsyncFileDialog`: +- `rfd::FileHandle`: +- rfd Windows-Backend (Quellcode): +- rfd Crate-Metadaten: +- rfd async-Beispiel: +- dioxus-sdk Crate-Metadaten (Feature-Liste): +- rfd 0.17.2 Dependencies: +- dioxus-desktop 0.7.1 Dependencies: +- dioxus-desktop **0.7.10** Dependencies (die real aufgelöste Version): +- dioxus-desktop `file_upload.rs` (interner rfd-Aufruf) @ v0.7.10: +- dioxus-desktop `lib.rs` (Re-Export-Liste) @ v0.7.1: +- `dioxus_html::FileData` (`path()`): +- Dioxus 0.7 Doku, `directory`-Attribut: +- dioxus-desktop `Cargo.toml` @ v0.7.1: +- `DesktopService`: +- `DesktopContext`: +- `use_window`: +- `dioxus::prelude::spawn`: +- tao 0.34.4 `Window` (rwh-Impls): +- tao 0.34.4 Feature-Liste: +- Dioxus 0.7 Doku (Async, Desktop): /