From 8eb0e3325bfd0d52d29e907b043060e816e9e2b3 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Thu, 27 Aug 2026 15:28:28 +0200 Subject: [PATCH] docs: Rechercheergebnisse zu Ordner-Dialog und Konfigurationsablage uebernehmen Beide Recherchen lagen bisher nur auf lokalen Wegwerf-Branches. Die Kurzfassung steht an den Tickets #2 und #3, die Langfassungen sind mit Quellenangaben und einer Trennung zwischen Belegtem und Vermutetem aber das, worauf man zurueckkommt, wenn spaeter jemand fragt, warum unter Windows Local statt Roaming gewaehlt wurde. Damit sind research/ordner-dialog und research/config-ablageort ueberfluessig und koennen geloescht werden. --- docs/research/konfigurations-ablageort.md | 565 ++++++++++++++++++++++ docs/research/nativer-ordner-dialog.md | 536 ++++++++++++++++++++ 2 files changed, 1101 insertions(+) create mode 100644 docs/research/konfigurations-ablageort.md create mode 100644 docs/research/nativer-ordner-dialog.md 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): /