Add native file dialogs for workspace and export paths

A 📂 button beside each path field opens a native picker: a folder
chooser for the workspace and an .odt save dialog for the export (which
appends the extension if omitted). Uses rfd with the XDG Desktop Portal
backend, so there is no GTK build dependency and no new shared library
at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
landon
2026-07-26 14:17:24 -05:00
parent 7008463ebf
commit 7271e5e2ef
5 changed files with 339 additions and 42 deletions
Generated
+271 -37
View File
@@ -35,7 +35,7 @@ dependencies = [
"atspi-common",
"serde",
"thiserror 1.0.69",
"zvariant",
"zvariant 4.2.0",
]
[[package]]
@@ -77,7 +77,7 @@ dependencies = [
"futures-lite",
"futures-util",
"serde",
"zbus",
"zbus 4.4.0",
]
[[package]]
@@ -214,6 +214,28 @@ dependencies = [
"libloading",
]
[[package]]
name = "ashpd"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.5",
"raw-window-handle",
"serde",
"serde_repr",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus 5.18.0",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -292,6 +314,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
@@ -382,11 +415,11 @@ dependencies = [
"enumflags2",
"serde",
"static_assertions",
"zbus",
"zbus 4.4.0",
"zbus-lockstep",
"zbus-lockstep-macros",
"zbus_names",
"zvariant",
"zbus_names 3.0.0",
"zvariant 4.2.0",
]
[[package]]
@@ -398,7 +431,7 @@ dependencies = [
"atspi-common",
"atspi-proxies",
"futures-lite",
"zbus",
"zbus 4.4.0",
]
[[package]]
@@ -409,8 +442,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496"
dependencies = [
"atspi-common",
"serde",
"zbus",
"zvariant",
"zbus 4.4.0",
"zvariant 4.2.0",
]
[[package]]
@@ -470,6 +503,15 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "blocking"
version = "1.6.2"
@@ -825,6 +867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.13.1",
"block2 0.6.2",
"libc",
"objc2 0.6.4",
]
@@ -1157,6 +1201,15 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.33"
@@ -1796,6 +1849,7 @@ dependencies = [
"eframe",
"egui",
"pulldown-cmark",
"rfd",
"serde",
"serde_json",
"zip",
@@ -2001,7 +2055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
"objc2-core-data",
@@ -2017,6 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.13.1",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
@@ -2030,7 +2085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -2042,7 +2097,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -2054,7 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -2089,7 +2144,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
@@ -2101,7 +2156,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation 0.2.2",
@@ -2120,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"dispatch",
"libc",
"objc2 0.5.2",
@@ -2154,7 +2209,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -2167,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -2179,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
@@ -2202,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
@@ -2222,7 +2277,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -2234,7 +2289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -2398,6 +2453,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -2504,8 +2565,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
@@ -2515,7 +2586,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
@@ -2527,6 +2608,15 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -2577,6 +2667,30 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rfd"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
dependencies = [
"ashpd",
"block2 0.6.2",
"dispatch2",
"js-sys",
"log",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -3168,14 +3282,32 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
dependencies = [
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
@@ -3901,7 +4033,7 @@ dependencies = [
"android-activity",
"atomic-waker",
"bitflags 2.13.1",
"block2",
"block2 0.5.1",
"bytemuck",
"calloop 0.13.0",
"cfg_aliases 0.2.2",
@@ -4084,7 +4216,7 @@ dependencies = [
"hex",
"nix",
"ordered-stream",
"rand",
"rand 0.8.7",
"serde",
"serde_repr",
"sha1",
@@ -4093,9 +4225,44 @@ dependencies = [
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
"zbus_macros 4.4.0",
"zbus_names 3.0.0",
"zvariant 4.2.0",
]
[[package]]
name = "zbus"
version = "5.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix 1.1.4",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros 5.18.0",
"zbus_names 4.3.4",
"zvariant 5.13.1",
]
[[package]]
@@ -4105,7 +4272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d"
dependencies = [
"zbus_xml",
"zvariant",
"zvariant 4.2.0",
]
[[package]]
@@ -4119,7 +4286,7 @@ dependencies = [
"syn 2.0.119",
"zbus-lockstep",
"zbus_xml",
"zvariant",
"zvariant 4.2.0",
]
[[package]]
@@ -4132,7 +4299,22 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"zvariant_utils",
"zvariant_utils 2.1.0",
]
[[package]]
name = "zbus_macros"
version = "5.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
"zbus_names 4.3.4",
"zvariant 5.13.1",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -4143,7 +4325,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
"zvariant",
"zvariant 4.2.0",
]
[[package]]
name = "zbus_names"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
"zvariant 5.13.1",
]
[[package]]
@@ -4155,8 +4348,8 @@ dependencies = [
"quick-xml 0.30.0",
"serde",
"static_assertions",
"zbus_names",
"zvariant",
"zbus_names 3.0.0",
"zvariant 4.2.0",
]
[[package]]
@@ -4278,7 +4471,22 @@ dependencies = [
"enumflags2",
"serde",
"static_assertions",
"zvariant_derive",
"zvariant_derive 4.2.0",
]
[[package]]
name = "zvariant"
version = "5.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow",
"zvariant_derive 5.13.1",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -4291,7 +4499,20 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"zvariant_utils",
"zvariant_utils 2.1.0",
]
[[package]]
name = "zvariant_derive"
version = "5.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
"zvariant_utils 3.5.0",
]
[[package]]
@@ -4304,3 +4525,16 @@ dependencies = [
"quote",
"syn 2.0.119",
]
[[package]]
name = "zvariant_utils"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.119",
"winnow",
]
+2
View File
@@ -12,6 +12,8 @@ serde_json = "1"
pulldown-cmark = { version = "0.12", default-features = false }
zip = { version = "2", default-features = false, features = ["deflate"] }
dirs = "5"
# Native open/save dialogs via the XDG Desktop Portal (no GTK build dependency).
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
[profile.release]
opt-level = 2
+9
View File
@@ -44,6 +44,14 @@ sudo apt install -y \
libx11-6 libx11-xcb1 libxcursor1 libxi6 libxrandr2 libxinerama1 libxrender1
```
The **📂 Browse** buttons open native file dialogs through the XDG Desktop
Portal. On GNOME this is already installed, but if the dialogs don't appear,
ensure the portal and a file-chooser backend are present:
```sh
sudo apt install -y xdg-desktop-portal xdg-desktop-portal-gtk
```
---
## 2. Get the source onto the tablet
@@ -188,6 +196,7 @@ rm -f ~/.local/share/applications/md-manuscript.desktop
|---|---|
| `error while loading shared libraries: libGL.so.1` (or similar) | Install the runtime libraries in step 1b. |
| Window fails with *"neither WAYLAND_DISPLAY nor DISPLAY is set"* | You're in a text-only session/SSH; run it from the tablet's graphical desktop. |
| **📂 Browse** buttons do nothing / no dialog appears | Install `xdg-desktop-portal` and `xdg-desktop-portal-gtk` (step 1b); you can still type paths by hand. |
| Build error mentioning `rustc … is not supported` | `rustup update`, then `cargo build --release` again. |
| `cargo: command not found` | Run `. "$HOME/.cargo/env"` or open a new terminal after installing rustup. |
| UI text far too small/large | Adjust GNOME *Displays → Scale*, or set `WINIT_X11_SCALE_FACTOR` (X11). |
+5 -3
View File
@@ -51,8 +51,9 @@ Terminal=false
## Use
1. Set the **Workspace** path in the top bar (default: `~/Manuscript`) and press
**Open**. The directory is created if it does not exist.
1. Set the **Workspace** path in the top bar (default: `~/Manuscript`) — type it,
or click **📂** to pick a folder in a native file browser — and press **Open**.
The directory is created if it does not exist.
2. Press **Init git** once to make the workspace a git repository. To sync with
another machine, add a remote yourself:
```sh
@@ -69,7 +70,8 @@ Terminal=false
blank falls back to a `# Title:` line, then the file's first level-1 heading
(`# ...`), then the file name. Titles are saved to `titles.json` and sync with
git.
5. Set the **Export** path and press **Export ODT** to produce the concatenated
5. Set the **Export** path (type it, or click **📂** to choose the `.odt` file in a
native save dialog) and press **Export ODT** to produce the concatenated
document. Each file becomes a chapter headed by its title, optionally followed
by a caption from its `# Slug:` line.
+52 -2
View File
@@ -340,6 +340,50 @@ impl App {
}
}
/// Open a native folder picker to choose the workspace directory.
fn browse_workspace(&mut self) {
let start = PathBuf::from(self.workspace_input.trim());
let mut dialog = rfd::FileDialog::new().set_title("Choose workspace folder");
if start.is_dir() {
dialog = dialog.set_directory(&start);
}
if let Some(path) = dialog.pick_folder() {
self.save_current();
self.workspace_input = path.display().to_string();
self.config.workspace = path;
self.config.save();
self.open_workspace();
}
}
/// Open a native save dialog to choose the export `.odt` path.
fn browse_export(&mut self) {
let current = PathBuf::from(self.export_input.trim());
let mut dialog = rfd::FileDialog::new()
.set_title("Choose export file")
.add_filter("OpenDocument Text", &["odt"]);
if let Some(parent) = current.parent().filter(|p| p.is_dir()) {
dialog = dialog.set_directory(parent);
}
let name = current
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("manuscript.odt");
if let Some(mut path) = dialog.set_file_name(name).save_file() {
// Ensure the chosen path ends in .odt even if the user omitted it.
let has_odt = path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("odt"));
if !has_odt {
path.set_extension("odt");
}
self.export_input = path.display().to_string();
self.config.export_path = path;
self.config.save();
}
}
fn reorder(&mut self, from: usize, mut to: usize) {
if from >= self.files.len() || from == to {
return;
@@ -370,8 +414,11 @@ impl App {
ui.label("Workspace:");
let resp = ui.add(
egui::TextEdit::singleline(&mut self.workspace_input)
.desired_width(360.0),
.desired_width(300.0),
);
if ui.button("📂").on_hover_text("Browse for workspace folder").clicked() {
self.browse_workspace();
}
if ui.button("Open").clicked()
|| (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
{
@@ -396,8 +443,11 @@ impl App {
ui.horizontal(|ui| {
ui.label("Export:");
ui.add(
egui::TextEdit::singleline(&mut self.export_input).desired_width(360.0),
egui::TextEdit::singleline(&mut self.export_input).desired_width(300.0),
);
if ui.button("📂").on_hover_text("Browse for export .odt file").clicked() {
self.browse_export();
}
if ui.button("Export ODT").clicked() {
self.export_odt();
}