From 9bcfe7a1f58580c713e21703e5c2fb2acd397cda Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Wed, 22 Apr 2026 15:13:55 +0200 Subject: [PATCH 01/21] Cargo.toml: patch libcosmic via local path for dev builds Activates the [patch.'https://github.com/pop-os/libcosmic'] override pointing at ../libcosmic, enabling local development against a patched libcosmic checkout (e.g. to pick up WindowControlsPosition / macOS-style window controls). This branch is intentionally dev-local: do NOT merge upstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1b75ae..8f81194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,13 +147,12 @@ tokio = { version = "1", features = ["rt", "macros"] } # [patch.'https://github.com/pop-os/cosmic-text'] # cosmic-text = { path = "../cosmic-text" } -# [patch.'https://github.com/pop-os/libcosmic'] -# libcosmic = { path = "../libcosmic" } -# cosmic-config = { path = "../libcosmic/cosmic-config" } -# cosmic-theme = { path = "../libcosmic/cosmic-theme" } -# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" } -# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" } -# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" } +[patch.'https://github.com/pop-os/libcosmic'] +libcosmic = { path = "../libcosmic" } +cosmic-config = { path = "../libcosmic/cosmic-config" } +cosmic-theme = { path = "../libcosmic/cosmic-theme" } +iced_futures = { path = "../libcosmic/iced/futures" } +iced_winit = { path = "../libcosmic/iced/winit" } # [patch.'https://github.com/pop-os/smithay-clipboard'] From 04abd13d936d02607e4b50ebcc7b912a43f5807e Mon Sep 17 00:00:00 2001 From: leyoda Date: Thu, 23 Apr 2026 15:38:04 +0200 Subject: [PATCH 02/21] yoda: depend on libcosmic-yoda (path) instead of upstream libcosmic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewire cosmic-files (lib + file manager) onto the yoda fork of libcosmic. - [dependencies.libcosmic] removed, replaced by [dependencies.libcosmic-yoda] pointing at ../libcosmic (local path; the leyoda/libcosmic-yoda clone) - Features: winit dropped, wayland added explicitly in the default set - Feature refs "libcosmic/xxx" rewritten to "libcosmic-yoda/xxx" - [patch] block removed — transitive libcosmic refs no longer exist cosmic-files lib and the file manager binary build clean against libcosmic-yoda 0.1.0-yoda (3 warnings, all pre-existing unused-var in search code). --- Cargo.lock | 118 ++++++++++++++++++++++++++++++++++++----------------- Cargo.toml | 31 +++++--------- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0084c8d..f307246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,14 +1314,13 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "atomicwrites", - "cosmic-config-derive", + "cosmic-config-derive 1.0.0", "cosmic-settings-daemon", "dirs 6.0.0", "futures-util", - "iced_futures", + "iced_futures 0.14.0", "known-folders", "notify", "ron 0.12.1", @@ -1332,10 +1331,35 @@ dependencies = [ "zbus 5.14.0", ] +[[package]] +name = "cosmic-config" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#dad5f1e2731dbdccb3044f136a81f18dfead9de4" +dependencies = [ + "atomicwrites", + "cosmic-config-derive 1.0.0 (git+https://github.com/pop-os/libcosmic)", + "dirs 6.0.0", + "iced_futures 0.14.0 (git+https://github.com/pop-os/libcosmic)", + "known-folders", + "notify", + "ron 0.12.1", + "serde", + "tracing", + "xdg", +] + [[package]] name = "cosmic-config-derive" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cosmic-config-derive" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#dad5f1e2731dbdccb3044f136a81f18dfead9de4" dependencies = [ "quote", "syn", @@ -1368,7 +1392,7 @@ dependencies = [ "jiff-icu", "jxl-oxide", "libc", - "libcosmic", + "libcosmic-yoda", "log", "lzma-rust2", "md-5", @@ -1461,7 +1485,7 @@ name = "cosmic-settings-config" version = "0.1.0" source = "git+https://github.com/pop-os/cosmic-settings-daemon#716da6d6af0b252e2f78aba2ad72ee19ae0241e0" dependencies = [ - "cosmic-config", + "cosmic-config 1.0.0 (git+https://github.com/pop-os/libcosmic)", "ron 0.11.0", "serde", "serde_with", @@ -1503,11 +1527,10 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "almost", "configparser", - "cosmic-config", + "cosmic-config 1.0.0", "csscolorparser", "dirs 6.0.0", "palette", @@ -2988,13 +3011,12 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "dnd", "iced_accessibility", - "iced_core", + "iced_core 0.14.0", "iced_debug", - "iced_futures", + "iced_futures 0.14.0", "iced_program", "iced_renderer", "iced_runtime", @@ -3009,7 +3031,6 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "accesskit", "accesskit_winit", @@ -3018,7 +3039,6 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bitflags 2.11.1", "bytes", @@ -3039,23 +3059,43 @@ dependencies = [ "window_clipboard", ] +[[package]] +name = "iced_core" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#dad5f1e2731dbdccb3044f136a81f18dfead9de4" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "dnd", + "glam", + "lilt", + "log", + "mime 0.1.0", + "num-traits", + "palette", + "raw-window-handle", + "rustc-hash 2.1.2", + "smol_str", + "thiserror 2.0.18", + "web-time", + "window_clipboard", +] + [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ - "iced_core", - "iced_futures", + "iced_core 0.14.0", + "iced_futures 0.14.0", "log", ] [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "futures", - "iced_core", + "iced_core 0.14.0", "log", "rustc-hash 2.1.2", "tokio", @@ -3063,17 +3103,29 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "iced_futures" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#dad5f1e2731dbdccb3044f136a81f18dfead9de4" +dependencies = [ + "futures", + "iced_core 0.14.0 (git+https://github.com/pop-os/libcosmic)", + "log", + "rustc-hash 2.1.2", + "wasm-bindgen-futures", + "wasmtimer", +] + [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bitflags 2.11.1", "bytemuck", "cosmic-text", "half", - "iced_core", - "iced_futures", + "iced_core 0.14.0", + "iced_futures 0.14.0", "image", "kamadak-exif", "log", @@ -3087,7 +3139,6 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "iced_graphics", "iced_runtime", @@ -3096,7 +3147,6 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3108,13 +3158,12 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bytes", "cosmic-client-toolkit", "dnd", - "iced_core", - "iced_futures", + "iced_core 0.14.0", + "iced_futures 0.14.0", "raw-window-handle", "thiserror 2.0.18", "window_clipboard", @@ -3123,7 +3172,6 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bytemuck", "cosmic-text", @@ -3140,7 +3188,6 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "as-raw-xcb-connection", "bitflags 2.11.1", @@ -3171,7 +3218,6 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.2" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3189,13 +3235,12 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "cosmic-client-toolkit", "cursor-icon", "dnd", "iced_debug", - "iced_futures", + "iced_futures 0.14.0", "iced_graphics", "iced_program", "iced_runtime", @@ -4309,15 +4354,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] -name = "libcosmic" -version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" +name = "libcosmic-yoda" +version = "0.1.0-yoda" dependencies = [ "apply", "ashpd 0.12.3", "auto_enums", "cosmic-client-toolkit", - "cosmic-config", + "cosmic-config 1.0.0", "cosmic-freedesktop-icons", "cosmic-settings-config", "cosmic-settings-daemon", @@ -4330,8 +4374,8 @@ dependencies = [ "i18n-embed", "i18n-embed-fl", "iced", - "iced_core", - "iced_futures", + "iced_core 0.14.0", + "iced_futures 0.14.0", "iced_renderer", "iced_runtime", "iced_tiny_skia", diff --git a/Cargo.toml b/Cargo.toml index 8f81194..aa69d13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,9 @@ version = "0.18" default-features = false features = ["fs", "io", "macros", "polling", "runtime"] -[dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic.git" +# Yoda fork — depend on libcosmic-yoda directly by path (no git/no patch). +[dependencies.libcosmic-yoda] +path = "../libcosmic" default-features = false #TODO: a11y feature crashes features = [ @@ -82,7 +83,7 @@ features = [ "autosize", "multi-window", "tokio", - "winit", + "wayland", "surface-message", ] @@ -110,15 +111,15 @@ default = [ "wayland", "wgpu", ] -dbus-config = ["libcosmic/dbus-config"] -desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"] +dbus-config = ["libcosmic-yoda/dbus-config"] +desktop = ["libcosmic-yoda/desktop", "dep:cosmic-mime-apps", "dep:xdg"] desktop-applet = [] gvfs = ["dep:gio", "dep:glib"] io-uring = ["compio/io-uring"] jemalloc = ["dep:tikv-jemallocator"] notify = ["dep:notify-rust"] -wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"] -wgpu = ["libcosmic/wgpu"] +wayland = ["libcosmic-yoda/wayland", "dep:cctk", "dep:wayland-client"] +wgpu = ["libcosmic-yoda/wgpu"] [profile.dev] opt-level = 1 @@ -144,19 +145,9 @@ fastrand = "2" test-log = "0.2" tokio = { version = "1", features = ["rt", "macros"] } -# [patch.'https://github.com/pop-os/cosmic-text'] -# cosmic-text = { path = "../cosmic-text" } - -[patch.'https://github.com/pop-os/libcosmic'] -libcosmic = { path = "../libcosmic" } -cosmic-config = { path = "../libcosmic/cosmic-config" } -cosmic-theme = { path = "../libcosmic/cosmic-theme" } -iced_futures = { path = "../libcosmic/iced/futures" } -iced_winit = { path = "../libcosmic/iced/winit" } - - -# [patch.'https://github.com/pop-os/smithay-clipboard'] -# smithay-clipboard = { path = "../smithay-clipboard" } +# Yoda fork — libcosmic dep is now a direct path dep (libcosmic-yoda above), +# no [patch] block needed anymore. Keeping the block below would be a no-op +# since nothing in the dep graph still asks for pop-os/libcosmic.git. [workspace] members = ["cosmic-files-applet"] From 02adcc3cf66424e88b1e724e2b137a6604e1b5f7 Mon Sep 17 00:00:00 2001 From: leyoda Date: Thu, 23 Apr 2026 18:46:16 +0200 Subject: [PATCH 03/21] lockfile: libcosmic-yoda 0.1.0-yoda -> 0.1.0-yoda.2 Picks up the yoda-v2 libcosmic changes (color_picker Theme ref + context_menu/menu winit ungate). Binary rebuilt and installed. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f307246..28e26da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4355,7 +4355,7 @@ checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libcosmic-yoda" -version = "0.1.0-yoda" +version = "0.1.0-yoda.2" dependencies = [ "apply", "ashpd 0.12.3", From a025fd6380cbd907e7abf999ffde6003e9e00436 Mon Sep 17 00:00:00 2001 From: leyoda Date: Thu, 23 Apr 2026 19:17:26 +0200 Subject: [PATCH 04/21] yoda: prefer cosmic-yoterm over upstream cosmic-term in terminal fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mime_app::MimeAppCache::terminal() hardcoded "com.system76.CosmicTerm" as the only non-xdg-default fallback. On a yoda stack the relevant terminal is our fork cosmic-yoterm (desktop id com.aditua.CosmicYoterm), so we add it first in preference_order. xdg-mime default still wins when set — this just covers the case where it isn't. Fixes "Open in terminal" launching Konsole (or first random terminal in apps list) instead of cosmic-yoterm when xdg-mime default is unset or points to something else. --- src/mime_app.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mime_app.rs b/src/mime_app.rs index 4a55cdf..b8985ce 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -399,9 +399,12 @@ impl MimeAppCache { // The current approach works but might not adhere to the spec (yet) // Look for and return preferred terminals - //TODO: fallback order beyond cosmic-term? - - let mut preference_order = vec!["com.system76.CosmicTerm".to_string()]; + // Yoda: cosmic-yoterm (our fork) wins over upstream cosmic-term if both + // are installed — useful when xdg-mime default is not set. + let mut preference_order = vec![ + "com.aditua.CosmicYoterm".to_string(), + "com.system76.CosmicTerm".to_string(), + ]; if let Some(id) = self.get_default_terminal() { preference_order.insert(0, id); From e8d62ae43d0eb6b21f0c35910106f7713fcd8a5f Mon Sep 17 00:00:00 2001 From: leyoda Date: Thu, 23 Apr 2026 20:18:21 +0200 Subject: [PATCH 05/21] yoda: add "Always use this app" toggle to OpenWith dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Open with...' dialog let you pick an app but never remembered your choice — you'd see the same dialog again next time. The infrastructure was already there (MimeAppCache::set_default writes to mimeapps.list), just never wired to the UI. Adds a toggler below the app list labelled 'Always use this app for this file type' (EN) / 'Toujours utiliser cette application pour ce type de fichier' (FR). When enabled, after spawning the selected app, the default handler for the file's mime type is persisted via self.mime_app_cache.set_default(mime, app.id). Implementation: - DialogPage::OpenWith gains a set_default: bool field (defaulted false) - Message::OpenWithToggleDefault(bool) + handler mutates the dialog state - DialogComplete handler for OpenWith calls set_default after a clean spawn when the flag is set - Dialog rendering adds a .control(widget::row) with label + toggler, between the scrollable list and the action buttons - i18n strings added: en/fr open-with-set-default --- i18n/en/cosmic_files.ftl | 1 + i18n/fr/cosmic_files.ftl | 1 + src/app.rs | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 69a76c6..9fed322 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -96,6 +96,7 @@ save-file = Save file ## Open With Dialog open-with-title = How do you want to open "{$name}"? +open-with-set-default = Always use this app for this file type browse-store = Browse {$store} other-apps = Other applications related-apps = Related applications diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index a461a9f..e0fa6f9 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -92,6 +92,7 @@ save-file = Enregistrer fichier ## Open With Dialog open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ? +open-with-set-default = Toujours utiliser cette application pour ce type de fichier browse-store = Parcourir { $store } ## Permanently delete Dialog diff --git a/src/app.rs b/src/app.rs index feca9c5..2224ab5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -407,6 +407,7 @@ pub enum Message { OpenWithBrowse, OpenWithDialog(Option), OpenWithSelection(usize), + OpenWithToggleDefault(bool), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Overlap(window::Id, OverlapNotifyEvent), Paste(Option), @@ -577,6 +578,9 @@ pub enum DialogPage { mime: mime_guess::Mime, selected: usize, store_opt: Option, + /// When true, the chosen app is written to mimeapps.list as the + /// default handler for `mime` after it spawns. + set_default: bool, }, PermanentlyDelete { paths: Box<[PathBuf]>, @@ -3232,6 +3236,7 @@ impl Application for App { path, mime, selected, + set_default, .. } => { let available_apps = self.get_apps_for_mime(&mime); @@ -3250,6 +3255,11 @@ impl Application for App { None, ); } + // Yoda: persist as default if the user asked for it. + if set_default { + self.mime_app_cache + .set_default(mime.clone(), app.id.clone()); + } } Err(err) => { log::warn!( @@ -3872,6 +3882,7 @@ impl Application for App { .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), + set_default: false, }, Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), ); @@ -3883,6 +3894,13 @@ impl Application for App { *selected = index; } } + Message::OpenWithToggleDefault(enabled) => { + if let Some(DialogPage::OpenWith { set_default, .. }) = + self.dialog_pages.front_mut() + { + *set_default = enabled; + } + } Message::Paste(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) @@ -5110,6 +5128,7 @@ impl Application for App { .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), + set_default: false, }, None, ); @@ -5953,7 +5972,7 @@ impl Application for App { mime, selected, store_opt, - .. + set_default, } => { let name = match path.file_name() { Some(file_name) => file_name.to_str(), @@ -6038,7 +6057,21 @@ impl Application for App { } else { Length::Shrink } - })); + })) + // Yoda: let the user make this choice stick. A plain row + // instead of settings::item::builder because the latter + // returns a section Item, not an Element usable in .control(). + .control( + widget::row::with_children([ + widget::text::body(fl!("open-with-set-default")).into(), + widget::space::horizontal().into(), + widget::toggler(*set_default) + .on_toggle(Message::OpenWithToggleDefault) + .into(), + ]) + .spacing(space_s) + .align_y(Alignment::Center), + ); if let Some(app) = store_opt { dialog = dialog.tertiary_action( From 8fb2b15c6839cb31e1aae6d55d2adbb1798e490e Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 07:09:48 +0200 Subject: [PATCH 06/21] yoda wayland-v5: redirect window_clipboard + cosmic-text to local forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Propagates the [patch] blocks added in cosmic-yoterm v5 to keep the whole yoda app family on a single Wayland-only stack. Without these, iced_winit fails to select a window_clipboard version because our fork exposes a `wayland` feature that upstream doesn't. - window_clipboard → /home/lionel/Devels/window_clipboard (x11 gated behind opt-in feature) - cosmic-text → /home/lionel/Devels/cosmic-text (EAW terminal_cells + upstream PR#503 applied) --- Cargo.lock | 128 ++--------------------------------------------------- Cargo.toml | 10 +++++ 2 files changed, 14 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28e26da..5d2823c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,12 +328,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - [[package]] name = "as-slice" version = "0.2.1" @@ -979,7 +973,6 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "objc", "objc-foundation", @@ -989,7 +982,6 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "dnd", "mime 0.1.0", @@ -999,7 +991,6 @@ dependencies = [ [[package]] name = "clipboard_x11" version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "thiserror 1.0.69", "x11rb", @@ -1638,12 +1629,6 @@ dependencies = [ "uncased", ] -[[package]] -name = "ctor-lite" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" - [[package]] name = "cursor-icon" version = "1.2.0" @@ -1873,7 +1858,6 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "bitflags 2.11.1", "mime 0.1.0", @@ -1902,45 +1886,6 @@ name = "dpi" version = "0.1.2" source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" -[[package]] -name = "drm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" -dependencies = [ - "bitflags 2.11.1", - "bytemuck", - "drm-ffi", - "drm-fourcc", - "rustix 0.38.44", -] - -[[package]] -name = "drm-ffi" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" -dependencies = [ - "drm-sys", - "rustix 0.38.44", -] - -[[package]] -name = "drm-fourcc" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" - -[[package]] -name = "drm-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" -dependencies = [ - "libc", - "linux-raw-sys 0.6.5", -] - [[package]] name = "dyn-clone" version = "1.0.20" @@ -3189,7 +3134,6 @@ dependencies = [ name = "iced_wgpu" version = "0.14.0" dependencies = [ - "as-raw-xcb-connection", "bitflags 2.11.1", "bytemuck", "cosmic-client-toolkit", @@ -3206,13 +3150,11 @@ dependencies = [ "rustc-hash 2.1.2", "rustix 0.38.44", "thiserror 2.0.18", - "tiny-xlib", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-sys", "wgpu", - "x11rb", ] [[package]] @@ -4464,12 +4406,6 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "linux-raw-sys" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4707,7 +4643,6 @@ dependencies = [ [[package]] name = "mime" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "smithay-clipboard", ] @@ -6687,12 +6622,10 @@ name = "softbuffer" version = "0.4.1" source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#a3f77e251e7422803f693df6e3fc313c010c4dcb" dependencies = [ - "as-raw-xcb-connection", "bytemuck", "cfg_aliases", "cocoa", "core-graphics", - "drm", "fastrand", "foreign-types", "js-sys", @@ -6702,14 +6635,12 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.5.18", "rustix 0.38.44", - "tiny-xlib", "wasm-bindgen", "wayland-backend", "wayland-client", "wayland-sys", "web-sys", "windows-sys 0.52.0", - "x11rb", ] [[package]] @@ -7082,19 +7013,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny-xlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" -dependencies = [ - "as-raw-xcb-connection", - "ctor-lite", - "libloading", - "pkg-config", - "tracing", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -8064,7 +7982,6 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "clipboard-win", "clipboard_macos", @@ -8604,7 +8521,6 @@ dependencies = [ "winit-wayland", "winit-web", "winit-win32", - "winit-x11", ] [[package]] @@ -8655,7 +8571,6 @@ dependencies = [ "smol_str", "tracing", "winit-core", - "x11-dl", "xkbcommon-dl", ] @@ -8773,29 +8688,6 @@ dependencies = [ "winit-core", ] -[[package]] -name = "winit-x11" -version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" -dependencies = [ - "bitflags 2.11.1", - "bytemuck", - "calloop", - "cursor-icon", - "dpi", - "libc", - "percent-encoding", - "raw-window-handle", - "rustix 1.1.4", - "smol_str", - "tracing", - "winit-common", - "winit-core", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - [[package]] name = "winnow" version = "0.7.15" @@ -8917,31 +8809,15 @@ dependencies = [ "either", ] -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - [[package]] name = "x11rb" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ - "as-raw-xcb-connection", "gethostname", - "libc", - "libloading", - "once_cell", "rustix 1.1.4", "x11rb-protocol", - "xcursor", ] [[package]] @@ -9572,3 +9448,7 @@ dependencies = [ "syn", "winnow 0.7.15", ] + +[[patch.unused]] +name = "cosmic-text" +version = "0.19.0" diff --git a/Cargo.toml b/Cargo.toml index aa69d13..18c9010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,5 +149,15 @@ tokio = { version = "1", features = ["rt", "macros"] } # no [patch] block needed anymore. Keeping the block below would be a no-op # since nothing in the dep graph still asks for pop-os/libcosmic.git. +# Yoda wayland cut v5: redirect window_clipboard + cosmic-text to our local +# forks (x11 gated behind opt-in feature + EAW/PR#503 respectively). +[patch.'https://github.com/pop-os/window_clipboard.git'] +window_clipboard = { path = "/home/lionel/Devels/window_clipboard" } +dnd = { path = "/home/lionel/Devels/window_clipboard/dnd" } +mime = { path = "/home/lionel/Devels/window_clipboard/mime" } + +[patch.'https://github.com/pop-os/cosmic-text'] +cosmic-text = { path = "/home/lionel/Devels/cosmic-text" } + [workspace] members = ["cosmic-files-applet"] From 0595296609082d7b596b7e6a8a315b3581a6ead3 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 07:38:17 +0200 Subject: [PATCH 07/21] yoda: Dolphin-style quick actions toolbar under the headerbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a full-width row of 6 icon buttons between the tab bar and the tab view: New folder · Rename · Delete | Cut · Copy · Paste. Paste is disabled when the clipboard is empty (existing self.clipboard_has_content check). A vertical divider separates file-ops (3 first) from clipboard ops (3 last). Implementation reuses Action::message(entity_opt = None) so keybinding and toolbar dispatch share exactly the same code path — no duplication. Icons are freedesktop *-symbolic names so they inherit the COSMIC theme's symbolic color. Tooltips use the existing fl!() strings (new-folder / rename / delete / cut / copy / paste, EN + FR). Customization (pick which buttons show up) is deferred to a follow-up commit — this first pass is fixed at the minimal-6 set per the user's spec. --- src/app.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/app.rs b/src/app.rs index 2224ab5..0110fc6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6521,6 +6521,48 @@ impl Application for App { ); } + // Yoda: Dolphin-style quick actions toolbar under the headerbar. + // Minimal 6-button set: New folder · Rename · Delete · Cut · Copy · Paste. + // Actions dispatch through Action::message so the keybinding path and + // toolbar path share the same code. + { + let clipboard_has = self.clipboard_has_content(); + let tb_btn = + |icon_name: &'static str, label: String, msg: Message, enabled: bool| { + let btn = widget::button::icon( + widget::icon::from_name(icon_name).size(16), + ); + let btn = if enabled { btn.on_press(msg) } else { btn }; + widget::tooltip( + btn, + widget::text::body(label), + widget::tooltip::Position::Bottom, + ) + }; + let toolbar = widget::row::with_children(vec![ + tb_btn("folder-new-symbolic", fl!("new-folder"), + Action::NewFolder.message(None), true).into(), + tb_btn("edit-rename-symbolic", fl!("rename"), + Action::Rename.message(None), true).into(), + tb_btn("edit-delete-symbolic", fl!("delete"), + Action::Delete.message(None), true).into(), + widget::divider::vertical::light().height(16).into(), + tb_btn("edit-cut-symbolic", fl!("cut"), + Action::Cut.message(None), true).into(), + tb_btn("edit-copy-symbolic", fl!("copy"), + Action::Copy.message(None), true).into(), + tb_btn("edit-paste-symbolic", fl!("paste"), + Action::Paste.message(None), clipboard_has).into(), + ]) + .spacing(space_xxs) + .align_y(Alignment::Center); + tab_column = tab_column.push( + widget::container(toolbar) + .width(Length::Fill) + .padding([space_xxs, space_s]), + ); + } + let entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data::(entity) { let tab_view = tab From 4b6d3451394372ecd98808db0441bfe4a813c1a7 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 07:43:13 +0200 Subject: [PATCH 08/21] yoda: fix missing rename icon in toolbar edit-rename-symbolic isn't part of the COSMIC/Pop/Adwaita/WhiteSur-dark icon sets, so the Rename button rendered empty. Swap to document-edit-symbolic which is present in Adwaita (the standard fallback in the freedesktop-icons resolution chain) and semantically fits edit/rename. --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 0110fc6..ead588b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6542,7 +6542,7 @@ impl Application for App { let toolbar = widget::row::with_children(vec![ tb_btn("folder-new-symbolic", fl!("new-folder"), Action::NewFolder.message(None), true).into(), - tb_btn("edit-rename-symbolic", fl!("rename"), + tb_btn("document-edit-symbolic", fl!("rename"), Action::Rename.message(None), true).into(), tb_btn("edit-delete-symbolic", fl!("delete"), Action::Delete.message(None), true).into(), From 8b51af1632aff9aafd54ed3593b6e904a32a3a27 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 07:48:53 +0200 Subject: [PATCH 09/21] yoda: use pencil-symbolic for the Rename toolbar button document-edit-symbolic isn't in the Cosmic theme (checked Cosmic, Pop, Adwaita, WhiteSur-dark ship lists) so it rendered empty. The Cosmic theme ships pencil-symbolic which matches the edit/rename semantics and is guaranteed to resolve through the current icon theme chain. --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index ead588b..5c6a491 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6542,7 +6542,7 @@ impl Application for App { let toolbar = widget::row::with_children(vec![ tb_btn("folder-new-symbolic", fl!("new-folder"), Action::NewFolder.message(None), true).into(), - tb_btn("document-edit-symbolic", fl!("rename"), + tb_btn("pencil-symbolic", fl!("rename"), Action::Rename.message(None), true).into(), tb_btn("edit-delete-symbolic", fl!("delete"), Action::Delete.message(None), true).into(), From 33a5c8ff99d03c5f84e9174edfd2ff79bf7dd0de Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 07:53:49 +0200 Subject: [PATCH 10/21] =?UTF-8?q?yoda:=20phase=202=20=E2=80=94=20customiza?= =?UTF-8?q?ble=20toolbar=20(settings=20toggles=20per=20button)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 shipped a fixed 6-button toolbar. Phase 2 moves visibility to the config so users pick which buttons appear. Config (config.rs): - new ToolbarItems struct (CosmicConfigEntry) with one bool per button - Config.toolbar: ToolbarItems, default = 'minimal 6' set from phase 1 (new_folder, rename, delete, cut, copy, paste) + 5 extras off (new_file, reload, toggle_show_hidden, open_terminal, location_up) Rendering (view()): - iterate through self.config.toolbar fields in fixed logical order (location → create/edit → clipboard → view toggles) - dividers inserted only between non-empty groups - whole toolbar hidden if every button is off (no empty container) Settings page (settings()): - new 'Toolbar' section with one toggler per button, wired through Message::SetToolbar(ToolbarItems) which persists via config_set! i18n (en + fr): - added 'toolbar' + 'parent-directory' strings - reused existing new-folder / new-file / rename / delete / cut / copy / paste / reload-folder / show-hidden-files / open-in-terminal All actions dispatch through Action::message so keybindings and toolbar share one code path. --- i18n/en/cosmic_files.ftl | 2 + i18n/fr/cosmic_files.ftl | 2 + src/app.rs | 170 ++++++++++++++++++++++++++++++++------- src/config.rs | 41 ++++++++++ 4 files changed, 188 insertions(+), 27 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 9fed322..f0b7264 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -139,6 +139,8 @@ open-with = Open with owner = Owner group = Group other = Other +toolbar = Toolbar +parent-directory = Parent directory mixed = Mixed ### Mode 0 none = None diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index e0fa6f9..c7b0675 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -131,6 +131,8 @@ open-with = Ouvrir avec owner = Propriétaire group = Groupe other = Autre +toolbar = Barre d'outils +parent-directory = Dossier parent ### Mode 0 diff --git a/src/app.rs b/src/app.rs index 5c6a491..cd2058a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,7 +77,7 @@ use crate::{ }, config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, - TimeConfig, TypeToSearch, + TimeConfig, ToolbarItems, TypeToSearch, }, context_action, dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}, @@ -448,6 +448,8 @@ pub enum Message { SearchInput(String), SetShowDetails(bool), SetShowRecents(bool), + /// Yoda: toggle a single toolbar button visibility. + SetToolbar(ToolbarItems), SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), @@ -2284,6 +2286,49 @@ impl App { .toggler(self.config.show_recents, Message::SetShowRecents) }) .into(), + // Yoda: configure which quick-action buttons show in the + // toolbar under the tab bar. Each toggle maps to one button; + // layout order inside the toolbar is fixed (file ops → + // clipboard → view). + { + let tb = self.config.toolbar; + widget::settings::section() + .title(fl!("toolbar")) + .add(widget::settings::item::builder(fl!("new-folder")) + .toggler(tb.new_folder, move |v| + Message::SetToolbar(ToolbarItems { new_folder: v, ..tb }))) + .add(widget::settings::item::builder(fl!("new-file")) + .toggler(tb.new_file, move |v| + Message::SetToolbar(ToolbarItems { new_file: v, ..tb }))) + .add(widget::settings::item::builder(fl!("rename")) + .toggler(tb.rename, move |v| + Message::SetToolbar(ToolbarItems { rename: v, ..tb }))) + .add(widget::settings::item::builder(fl!("delete")) + .toggler(tb.delete, move |v| + Message::SetToolbar(ToolbarItems { delete: v, ..tb }))) + .add(widget::settings::item::builder(fl!("cut")) + .toggler(tb.cut, move |v| + Message::SetToolbar(ToolbarItems { cut: v, ..tb }))) + .add(widget::settings::item::builder(fl!("copy")) + .toggler(tb.copy, move |v| + Message::SetToolbar(ToolbarItems { copy: v, ..tb }))) + .add(widget::settings::item::builder(fl!("paste")) + .toggler(tb.paste, move |v| + Message::SetToolbar(ToolbarItems { paste: v, ..tb }))) + .add(widget::settings::item::builder(fl!("reload-folder")) + .toggler(tb.reload, move |v| + Message::SetToolbar(ToolbarItems { reload: v, ..tb }))) + .add(widget::settings::item::builder(fl!("show-hidden-files")) + .toggler(tb.toggle_show_hidden, move |v| + Message::SetToolbar(ToolbarItems { toggle_show_hidden: v, ..tb }))) + .add(widget::settings::item::builder(fl!("open-in-terminal")) + .toggler(tb.open_terminal, move |v| + Message::SetToolbar(ToolbarItems { open_terminal: v, ..tb }))) + .add(widget::settings::item::builder(fl!("parent-directory")) + .toggler(tb.location_up, move |v| + Message::SetToolbar(ToolbarItems { location_up: v, ..tb }))) + .into() + }, ]) .into() } @@ -4377,6 +4422,10 @@ impl Application for App { config_set!(show_recents, show_recents); return self.update_config(); } + Message::SetToolbar(toolbar) => { + config_set!(toolbar, toolbar); + return self.update_config(); + } Message::SetTypeToSearch(type_to_search) => { config_set!(type_to_search, type_to_search); return self.update_config(); @@ -6522,13 +6571,18 @@ impl Application for App { } // Yoda: Dolphin-style quick actions toolbar under the headerbar. - // Minimal 6-button set: New folder · Rename · Delete · Cut · Copy · Paste. - // Actions dispatch through Action::message so the keybinding path and - // toolbar path share the same code. + // Items are rendered from self.config.toolbar (ToolbarItems). Order + // is fixed (file ops / clipboard / view toggles); visibility per + // item is configurable from the Settings page. + // Dispatch goes through Action::message so keybindings and toolbar + // share exactly the same code path. { let clipboard_has = self.clipboard_has_content(); + let tb = self.config.toolbar; + let mut buttons: Vec> = Vec::new(); + let tb_btn = - |icon_name: &'static str, label: String, msg: Message, enabled: bool| { + |icon_name: &'static str, label: String, msg: Message, enabled: bool| -> Element<_> { let btn = widget::button::icon( widget::icon::from_name(icon_name).size(16), ); @@ -6538,29 +6592,91 @@ impl Application for App { widget::text::body(label), widget::tooltip::Position::Bottom, ) + .into() }; - let toolbar = widget::row::with_children(vec![ - tb_btn("folder-new-symbolic", fl!("new-folder"), - Action::NewFolder.message(None), true).into(), - tb_btn("pencil-symbolic", fl!("rename"), - Action::Rename.message(None), true).into(), - tb_btn("edit-delete-symbolic", fl!("delete"), - Action::Delete.message(None), true).into(), - widget::divider::vertical::light().height(16).into(), - tb_btn("edit-cut-symbolic", fl!("cut"), - Action::Cut.message(None), true).into(), - tb_btn("edit-copy-symbolic", fl!("copy"), - Action::Copy.message(None), true).into(), - tb_btn("edit-paste-symbolic", fl!("paste"), - Action::Paste.message(None), clipboard_has).into(), - ]) - .spacing(space_xxs) - .align_y(Alignment::Center); - tab_column = tab_column.push( - widget::container(toolbar) - .width(Length::Fill) - .padding([space_xxs, space_s]), - ); + let divider = || -> Element<_> { + widget::divider::vertical::light().height(16).into() + }; + + // Group 1: location + let mut added_any = false; + if tb.location_up { + buttons.push(tb_btn("go-up-symbolic", fl!("parent-directory"), + Action::LocationUp.message(None), true)); + added_any = true; + } + if tb.reload { + buttons.push(tb_btn("view-refresh-symbolic", fl!("reload-folder"), + Action::Reload.message(None), true)); + added_any = true; + } + + // Group 2: create / edit + let mut group_started = false; + for (enabled, icon, label, msg) in [ + (tb.new_folder, "folder-new-symbolic", fl!("new-folder"), + Action::NewFolder.message(None)), + (tb.new_file, "document-new-symbolic", fl!("new-file"), + Action::NewFile.message(None)), + (tb.rename, "pencil-symbolic", fl!("rename"), + Action::Rename.message(None)), + (tb.delete, "edit-delete-symbolic", fl!("delete"), + Action::Delete.message(None)), + ] { + if enabled { + if !group_started && added_any { buttons.push(divider()); } + buttons.push(tb_btn(icon, label, msg, true)); + group_started = true; + added_any = true; + } + } + + // Group 3: clipboard + let mut group_started = false; + for (enabled, icon, label, msg, avail) in [ + (tb.cut, "edit-cut-symbolic", fl!("cut"), + Action::Cut.message(None), true), + (tb.copy, "edit-copy-symbolic", fl!("copy"), + Action::Copy.message(None), true), + (tb.paste, "edit-paste-symbolic", fl!("paste"), + Action::Paste.message(None), clipboard_has), + ] { + if enabled { + if !group_started && added_any { buttons.push(divider()); } + buttons.push(tb_btn(icon, label, msg, avail)); + group_started = true; + added_any = true; + } + } + + // Group 4: view toggles + misc + let mut group_started = false; + for (enabled, icon, label, msg) in [ + (tb.toggle_show_hidden, "view-reveal-symbolic", + fl!("show-hidden-files"), + Action::ToggleShowHidden.message(None)), + (tb.open_terminal, "utilities-terminal-symbolic", + fl!("open-in-terminal"), + Action::OpenTerminal.message(None)), + ] { + if enabled { + if !group_started && added_any { buttons.push(divider()); } + buttons.push(tb_btn(icon, label, msg, true)); + group_started = true; + added_any = true; + } + } + + if added_any { + let toolbar = widget::row::with_children(buttons) + .spacing(space_xxs) + .align_y(Alignment::Center); + tab_column = tab_column.push( + widget::container(toolbar) + .width(Length::Fill) + .padding([space_xxs, space_s]), + ); + } } let entity = self.tab_model.active(); diff --git a/src/config.rs b/src/config.rs index 0ce9c22..b773d93 100644 --- a/src/config.rs +++ b/src/config.rs @@ -172,6 +172,10 @@ pub struct Config { pub show_details: bool, pub show_recents: bool, pub tab: TabConfig, + /// Yoda: Dolphin-style quick actions toolbar under the tab bar. + /// Each bool toggles one button; order in the UI is fixed (logical + /// grouping file-ops then clipboard then view toggles). + pub toolbar: ToolbarItems, pub type_to_search: TypeToSearch, } @@ -236,11 +240,48 @@ impl Default for Config { show_details: false, show_recents: true, tab: TabConfig::default(), + toolbar: ToolbarItems::default(), type_to_search: TypeToSearch::Recursive, } } } +/// Yoda: visibility toggles for each quick-action toolbar button. +/// Default = the original "minimal 6" set (new_folder, rename, delete, +/// cut, copy, paste). Other items default to false so users opt in. +#[derive(Clone, Copy, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ToolbarItems { + pub new_folder: bool, + pub new_file: bool, + pub rename: bool, + pub delete: bool, + pub cut: bool, + pub copy: bool, + pub paste: bool, + pub reload: bool, + pub toggle_show_hidden: bool, + pub open_terminal: bool, + pub location_up: bool, +} + +impl Default for ToolbarItems { + fn default() -> Self { + Self { + new_folder: true, + new_file: false, + rename: true, + delete: true, + cut: true, + copy: true, + paste: true, + reload: false, + toggle_show_hidden: false, + open_terminal: false, + location_up: false, + } + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct DesktopConfig { From 1cf17dcde822dc591b916c01ea909de8c2c0ce34 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 08:13:30 +0200 Subject: [PATCH 11/21] =?UTF-8?q?yoda:=20phase=203=20=E2=80=94=20drag-drop?= =?UTF-8?q?=20toolbar=20editor=20in=20Settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the config model from the phase-2 bag-of-bools (ToolbarItems) to an ordered Vec so the user can pick BOTH the set of buttons AND their order in the toolbar. Config (config.rs): - new ToolbarAction enum with 11 variants (LocationUp, Reload, NewFolder, NewFile, Rename, Delete, Cut, Copy, Paste, ToggleShowHidden, OpenTerminal) + to_u8/from_u8 for DnD payload - Config.toolbar: Vec, default = default_toolbar() (NewFolder, Rename, Delete, Cut, Copy, Paste — same 6 as phase 2) Rendering (view()): - iterate self.config.toolbar in order and emit a tooltip'd icon button per entry via the new toolbar_action_ui(action) helper shared with the Settings page. Paste stays disabled when clipboard empty. - No hardcoded groups or auto-dividers anymore — order is 100% user. Settings page (toolbar_settings_section): - two stacked lists: * 'Toolbar': currently-enabled actions in their Vec order. Each row is wrapped in dnd_source (drags a ToolbarActionPayload carrying the enum discriminant) + dnd_destination (accepts drops from other rows, fires Message::ToolbarReorder { src, target } to move src before target in the Vec). A list-drag-handle icon + a minus button (ToolbarRemove) per row. * 'Available': actions not yet enabled, each with a plus button (ToolbarAdd) that pushes to the end of the Vec. - 'Reset to defaults' button at the bottom (ToolbarReset). DnD infra (app.rs top): - TOOLBAR_MIME constant: 'application/x-cosmic-files-toolbar-action' - ToolbarActionPayload(u8) with AsMimeTypes + AllowedMimeTypes + TryFrom<(Vec, String)> impls — single-byte wire format matching the enum discriminant. Messages: - ToolbarAdd(ToolbarAction) — append to toolbar vec if absent - ToolbarRemove(ToolbarAction) - ToolbarReorder { src, target } — remove src, reinsert before target - ToolbarReset — restore default_toolbar() i18n (en + fr): - new keys: toolbar-available, toolbar-empty-hint, toolbar-reset Migration: existing installs with a phase-2 ToolbarItems struct in their config will error at load time (different shape); cosmic_config falls back to Self::default() which gives the phase-2 minimal-6 set — a safe reset rather than a broken partial read. --- i18n/en/cosmic_files.ftl | 3 + i18n/fr/cosmic_files.ftl | 3 + src/app.rs | 412 +++++++++++++++++++++++++-------------- src/config.rs | 120 ++++++++---- 4 files changed, 356 insertions(+), 182 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index f0b7264..8e4f54e 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -140,6 +140,9 @@ owner = Owner group = Group other = Other toolbar = Toolbar +toolbar-available = Available +toolbar-empty-hint = No buttons. Drag or add from below. +toolbar-reset = Reset to defaults parent-directory = Parent directory mixed = Mixed ### Mode 0 diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index c7b0675..214781e 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -132,6 +132,9 @@ owner = Propriétaire group = Groupe other = Autre toolbar = Barre d'outils +toolbar-available = Disponibles +toolbar-empty-hint = Aucun bouton. Glisser-déposer ou ajouter depuis la liste ci-dessous. +toolbar-reset = Rétablir par défaut parent-directory = Dossier parent ### Mode 0 diff --git a/src/app.rs b/src/app.rs index cd2058a..868fad7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,7 +77,7 @@ use crate::{ }, config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, - TimeConfig, ToolbarItems, TypeToSearch, + TimeConfig, ToolbarAction, TypeToSearch, default_toolbar, }, context_action, dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}, @@ -145,6 +145,105 @@ pub struct Flags { pub uris: Vec, } +/// Yoda phase 3: MIME for the DnD payload carried when a user drags a +/// toolbar row in the Settings editor. A single byte = ToolbarAction +/// discriminant (see `ToolbarAction::to_u8`). +const TOOLBAR_MIME: &str = "application/x-cosmic-files-toolbar-action"; + +/// Yoda phase 3: DnD payload wrapping a ToolbarAction discriminant. +#[derive(Clone, Debug)] +pub struct ToolbarActionPayload(pub u8); + +impl cosmic::iced::clipboard::mime::AsMimeTypes for ToolbarActionPayload { + fn available(&self) -> std::borrow::Cow<'static, [String]> { + std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()]) + } + fn as_bytes(&self, mime_type: &str) -> Option> { + if mime_type == TOOLBAR_MIME { + Some(std::borrow::Cow::Owned(vec![self.0])) + } else { + None + } + } +} + +impl cosmic::iced::clipboard::mime::AllowedMimeTypes for ToolbarActionPayload { + fn allowed() -> std::borrow::Cow<'static, [String]> { + std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()]) + } +} + +impl TryFrom<(Vec, String)> for ToolbarActionPayload { + type Error = (); + fn try_from((data, _mime): (Vec, String)) -> Result { + if data.len() == 1 { Ok(Self(data[0])) } else { Err(()) } + } +} + +/// Yoda phase 3 helper: map a ToolbarAction to its button UI (icon name, +/// localized label, app Message). Shared by the toolbar renderer in +/// `view()` and by the Settings page row renderer so the two stay in +/// sync. +fn toolbar_action_ui(a: ToolbarAction) -> (&'static str, String, Message) { + match a { + ToolbarAction::LocationUp => ( + "go-up-symbolic", + fl!("parent-directory"), + Action::LocationUp.message(None), + ), + ToolbarAction::Reload => ( + "view-refresh-symbolic", + fl!("reload-folder"), + Action::Reload.message(None), + ), + ToolbarAction::NewFolder => ( + "folder-new-symbolic", + fl!("new-folder"), + Action::NewFolder.message(None), + ), + ToolbarAction::NewFile => ( + "document-new-symbolic", + fl!("new-file"), + Action::NewFile.message(None), + ), + ToolbarAction::Rename => ( + "pencil-symbolic", + fl!("rename"), + Action::Rename.message(None), + ), + ToolbarAction::Delete => ( + "edit-delete-symbolic", + fl!("delete"), + Action::Delete.message(None), + ), + ToolbarAction::Cut => ( + "edit-cut-symbolic", + fl!("cut"), + Action::Cut.message(None), + ), + ToolbarAction::Copy => ( + "edit-copy-symbolic", + fl!("copy"), + Action::Copy.message(None), + ), + ToolbarAction::Paste => ( + "edit-paste-symbolic", + fl!("paste"), + Action::Paste.message(None), + ), + ToolbarAction::ToggleShowHidden => ( + "view-reveal-symbolic", + fl!("show-hidden-files"), + Action::ToggleShowHidden.message(None), + ), + ToolbarAction::OpenTerminal => ( + "utilities-terminal-symbolic", + fl!("open-in-terminal"), + Action::OpenTerminal.message(None), + ), + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { About, @@ -448,8 +547,11 @@ pub enum Message { SearchInput(String), SetShowDetails(bool), SetShowRecents(bool), - /// Yoda: toggle a single toolbar button visibility. - SetToolbar(ToolbarItems), + /// Yoda phase 3 — toolbar editing messages. + ToolbarAdd(ToolbarAction), + ToolbarRemove(ToolbarAction), + ToolbarReorder { src: ToolbarAction, target: ToolbarAction }, + ToolbarReset, SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), @@ -2286,53 +2388,115 @@ impl App { .toggler(self.config.show_recents, Message::SetShowRecents) }) .into(), - // Yoda: configure which quick-action buttons show in the - // toolbar under the tab bar. Each toggle maps to one button; - // layout order inside the toolbar is fixed (file ops → - // clipboard → view). - { - let tb = self.config.toolbar; - widget::settings::section() - .title(fl!("toolbar")) - .add(widget::settings::item::builder(fl!("new-folder")) - .toggler(tb.new_folder, move |v| - Message::SetToolbar(ToolbarItems { new_folder: v, ..tb }))) - .add(widget::settings::item::builder(fl!("new-file")) - .toggler(tb.new_file, move |v| - Message::SetToolbar(ToolbarItems { new_file: v, ..tb }))) - .add(widget::settings::item::builder(fl!("rename")) - .toggler(tb.rename, move |v| - Message::SetToolbar(ToolbarItems { rename: v, ..tb }))) - .add(widget::settings::item::builder(fl!("delete")) - .toggler(tb.delete, move |v| - Message::SetToolbar(ToolbarItems { delete: v, ..tb }))) - .add(widget::settings::item::builder(fl!("cut")) - .toggler(tb.cut, move |v| - Message::SetToolbar(ToolbarItems { cut: v, ..tb }))) - .add(widget::settings::item::builder(fl!("copy")) - .toggler(tb.copy, move |v| - Message::SetToolbar(ToolbarItems { copy: v, ..tb }))) - .add(widget::settings::item::builder(fl!("paste")) - .toggler(tb.paste, move |v| - Message::SetToolbar(ToolbarItems { paste: v, ..tb }))) - .add(widget::settings::item::builder(fl!("reload-folder")) - .toggler(tb.reload, move |v| - Message::SetToolbar(ToolbarItems { reload: v, ..tb }))) - .add(widget::settings::item::builder(fl!("show-hidden-files")) - .toggler(tb.toggle_show_hidden, move |v| - Message::SetToolbar(ToolbarItems { toggle_show_hidden: v, ..tb }))) - .add(widget::settings::item::builder(fl!("open-in-terminal")) - .toggler(tb.open_terminal, move |v| - Message::SetToolbar(ToolbarItems { open_terminal: v, ..tb }))) - .add(widget::settings::item::builder(fl!("parent-directory")) - .toggler(tb.location_up, move |v| - Message::SetToolbar(ToolbarItems { location_up: v, ..tb }))) - .into() - }, + // Yoda phase 3: toolbar editor. Two stacked lists: + // - top: enabled buttons in their current order (drag to reorder) + // - bottom: available (not-yet-enabled) buttons + // Each row's toggle adds/removes; enabled rows are also + // drag sources + drop targets. + self.toolbar_settings_section(), ]) .into() } + /// Yoda phase 3: build the Toolbar settings section. + fn toolbar_settings_section(&self) -> Element<'_, Message> { + use iced::clipboard::dnd::DndAction; + let enabled = &self.config.toolbar; + let disabled: Vec = ToolbarAction::ALL + .iter() + .copied() + .filter(|a| !enabled.contains(a)) + .collect(); + + let space_xxs = theme::active().cosmic().spacing.space_xxs; + + let drag_icon = |size: u16| -> Element<'static, Message> { + widget::icon::from_name("list-drag-handle-symbolic") + .size(size) + .into() + }; + + let row_enabled = |action: ToolbarAction| -> Element<'_, Message> { + let (icon, label, _msg) = toolbar_action_ui(action); + let row_content: Element<_> = widget::row::with_children(vec![ + drag_icon(14), + widget::icon::from_name(icon).size(16).into(), + widget::text::body(label).width(Length::Fill).into(), + widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14)) + .on_press(Message::ToolbarRemove(action)) + .into(), + ]) + .spacing(space_xxs) + .align_y(Alignment::Center) + .into(); + + let row_container = widget::container(row_content) + .width(Length::Fill) + .padding(space_xxs); + + // Wrap as DnD source (drags itself) + DnD destination (accepts + // drops from other enabled rows; on drop, move the src before + // this row). + let source = widget::dnd_source::(row_container) + .drag_content(move || ToolbarActionPayload(action.to_u8())); + widget::dnd_destination( + source, + vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)], + ) + .data_received_for::(move |payload: Option| { + match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { + Some(src) if src != action => { + Message::ToolbarReorder { src, target: action } + } + // No-op if payload missing / malformed / same row. + _ => Message::ToolbarReorder { src: action, target: action }, + } + }) + .action(DndAction::Move) + .into() + }; + + let row_disabled = |action: ToolbarAction| -> Element<'_, Message> { + let (icon, label, _msg) = toolbar_action_ui(action); + widget::row::with_children(vec![ + widget::icon::from_name(icon).size(16).into(), + widget::text::body(label).width(Length::Fill).into(), + widget::button::icon(widget::icon::from_name("list-add-symbolic").size(14)) + .on_press(Message::ToolbarAdd(action)) + .into(), + ]) + .spacing(space_xxs) + .align_y(Alignment::Center) + .padding(space_xxs) + .into() + }; + + let mut section = widget::settings::section().title(fl!("toolbar")); + if enabled.is_empty() { + section = section + .add(widget::text::body(fl!("toolbar-empty-hint"))); + } else { + for a in enabled.iter().copied() { + section = section.add(row_enabled(a)); + } + } + + let mut col = widget::column::with_capacity(3).spacing(space_xxs); + col = col.push(section); + if !disabled.is_empty() { + let mut avail = widget::settings::section().title(fl!("toolbar-available")); + for a in disabled { + avail = avail.add(row_disabled(a)); + } + col = col.push(avail); + } + col = col.push( + widget::button::standard(fl!("toolbar-reset")) + .on_press(Message::ToolbarReset), + ); + col.into() + } + fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> { let mut results = Vec::new(); @@ -4422,8 +4586,37 @@ impl Application for App { config_set!(show_recents, show_recents); return self.update_config(); } - Message::SetToolbar(toolbar) => { - config_set!(toolbar, toolbar); + Message::ToolbarAdd(action) => { + let mut tb = self.config.toolbar.clone(); + if !tb.contains(&action) { + tb.push(action); + } + config_set!(toolbar, tb); + return self.update_config(); + } + Message::ToolbarRemove(action) => { + let mut tb = self.config.toolbar.clone(); + tb.retain(|a| a != &action); + config_set!(toolbar, tb); + return self.update_config(); + } + Message::ToolbarReorder { src, target } => { + let mut tb = self.config.toolbar.clone(); + if let (Some(src_idx), Some(tgt_idx)) = ( + tb.iter().position(|a| a == &src), + tb.iter().position(|a| a == &target), + ) && src_idx != tgt_idx { + // Pull src out, then insert before the target's new position. + let item = tb.remove(src_idx); + let new_tgt = if src_idx < tgt_idx { tgt_idx - 1 } else { tgt_idx }; + tb.insert(new_tgt, item); + config_set!(toolbar, tb); + return self.update_config(); + } + return Task::none(); + } + Message::ToolbarReset => { + config_set!(toolbar, default_toolbar()); return self.update_config(); } Message::SetTypeToSearch(type_to_search) => { @@ -6570,22 +6763,21 @@ impl Application for App { ); } - // Yoda: Dolphin-style quick actions toolbar under the headerbar. - // Items are rendered from self.config.toolbar (ToolbarItems). Order - // is fixed (file ops / clipboard / view toggles); visibility per - // item is configurable from the Settings page. - // Dispatch goes through Action::message so keybindings and toolbar - // share exactly the same code path. - { + // Yoda phase 3: Dolphin-style quick actions toolbar. Items are + // rendered from self.config.toolbar (Vec) — the user + // picks the set AND the order via drag-drop in Settings. Dispatch + // goes through Action::message so keybinding and toolbar share the + // same code path. + if !self.config.toolbar.is_empty() { let clipboard_has = self.clipboard_has_content(); - let tb = self.config.toolbar; - let mut buttons: Vec> = Vec::new(); - - let tb_btn = - |icon_name: &'static str, label: String, msg: Message, enabled: bool| -> Element<_> { - let btn = widget::button::icon( - widget::icon::from_name(icon_name).size(16), - ); + let buttons: Vec> = self + .config + .toolbar + .iter() + .map(|a| { + let (icon, label, msg) = toolbar_action_ui(*a); + let enabled = !matches!(a, ToolbarAction::Paste) || clipboard_has; + let btn = widget::button::icon(widget::icon::from_name(icon).size(16)); let btn = if enabled { btn.on_press(msg) } else { btn }; widget::tooltip( btn, @@ -6593,90 +6785,16 @@ impl Application for App { widget::tooltip::Position::Bottom, ) .into() - }; - let divider = || -> Element<_> { - widget::divider::vertical::light().height(16).into() - }; - - // Group 1: location - let mut added_any = false; - if tb.location_up { - buttons.push(tb_btn("go-up-symbolic", fl!("parent-directory"), - Action::LocationUp.message(None), true)); - added_any = true; - } - if tb.reload { - buttons.push(tb_btn("view-refresh-symbolic", fl!("reload-folder"), - Action::Reload.message(None), true)); - added_any = true; - } - - // Group 2: create / edit - let mut group_started = false; - for (enabled, icon, label, msg) in [ - (tb.new_folder, "folder-new-symbolic", fl!("new-folder"), - Action::NewFolder.message(None)), - (tb.new_file, "document-new-symbolic", fl!("new-file"), - Action::NewFile.message(None)), - (tb.rename, "pencil-symbolic", fl!("rename"), - Action::Rename.message(None)), - (tb.delete, "edit-delete-symbolic", fl!("delete"), - Action::Delete.message(None)), - ] { - if enabled { - if !group_started && added_any { buttons.push(divider()); } - buttons.push(tb_btn(icon, label, msg, true)); - group_started = true; - added_any = true; - } - } - - // Group 3: clipboard - let mut group_started = false; - for (enabled, icon, label, msg, avail) in [ - (tb.cut, "edit-cut-symbolic", fl!("cut"), - Action::Cut.message(None), true), - (tb.copy, "edit-copy-symbolic", fl!("copy"), - Action::Copy.message(None), true), - (tb.paste, "edit-paste-symbolic", fl!("paste"), - Action::Paste.message(None), clipboard_has), - ] { - if enabled { - if !group_started && added_any { buttons.push(divider()); } - buttons.push(tb_btn(icon, label, msg, avail)); - group_started = true; - added_any = true; - } - } - - // Group 4: view toggles + misc - let mut group_started = false; - for (enabled, icon, label, msg) in [ - (tb.toggle_show_hidden, "view-reveal-symbolic", - fl!("show-hidden-files"), - Action::ToggleShowHidden.message(None)), - (tb.open_terminal, "utilities-terminal-symbolic", - fl!("open-in-terminal"), - Action::OpenTerminal.message(None)), - ] { - if enabled { - if !group_started && added_any { buttons.push(divider()); } - buttons.push(tb_btn(icon, label, msg, true)); - group_started = true; - added_any = true; - } - } - - if added_any { - let toolbar = widget::row::with_children(buttons) - .spacing(space_xxs) - .align_y(Alignment::Center); - tab_column = tab_column.push( - widget::container(toolbar) - .width(Length::Fill) - .padding([space_xxs, space_s]), - ); - } + }) + .collect(); + let toolbar = widget::row::with_children(buttons) + .spacing(space_xxs) + .align_y(Alignment::Center); + tab_column = tab_column.push( + widget::container(toolbar) + .width(Length::Fill) + .padding([space_xxs, space_s]), + ); } let entity = self.tab_model.active(); diff --git a/src/config.rs b/src/config.rs index b773d93..6d89367 100644 --- a/src/config.rs +++ b/src/config.rs @@ -172,10 +172,11 @@ pub struct Config { pub show_details: bool, pub show_recents: bool, pub tab: TabConfig, - /// Yoda: Dolphin-style quick actions toolbar under the tab bar. - /// Each bool toggles one button; order in the UI is fixed (logical - /// grouping file-ops then clipboard then view toggles). - pub toolbar: ToolbarItems, + /// Yoda phase 3: Dolphin-style quick actions toolbar. An ordered list + /// of enabled buttons — position in the vec drives the toolbar order. + /// Reorder in Settings via drag-drop; items not in the vec are + /// hidden. Default = the minimal-6 set from phase 1. + pub toolbar: Vec, pub type_to_search: TypeToSearch, } @@ -240,46 +241,95 @@ impl Default for Config { show_details: false, show_recents: true, tab: TabConfig::default(), - toolbar: ToolbarItems::default(), + toolbar: default_toolbar(), type_to_search: TypeToSearch::Recursive, } } } -/// Yoda: visibility toggles for each quick-action toolbar button. -/// Default = the original "minimal 6" set (new_folder, rename, delete, -/// cut, copy, paste). Other items default to false so users opt in. -#[derive(Clone, Copy, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ToolbarItems { - pub new_folder: bool, - pub new_file: bool, - pub rename: bool, - pub delete: bool, - pub cut: bool, - pub copy: bool, - pub paste: bool, - pub reload: bool, - pub toggle_show_hidden: bool, - pub open_terminal: bool, - pub location_up: bool, +/// Yoda phase 3: ordered enum of quick-action toolbar buttons. +/// The Config stores `Vec` so the user can pick BOTH +/// visibility (just include/exclude the variant) AND order (position in +/// the vec). Drag-drop reorder in the Settings page moves items around. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum ToolbarAction { + LocationUp, + Reload, + NewFolder, + NewFile, + Rename, + Delete, + Cut, + Copy, + Paste, + ToggleShowHidden, + OpenTerminal, } -impl Default for ToolbarItems { - fn default() -> Self { - Self { - new_folder: true, - new_file: false, - rename: true, - delete: true, - cut: true, - copy: true, - paste: true, - reload: false, - toggle_show_hidden: false, - open_terminal: false, - location_up: false, +impl ToolbarAction { + /// Stable list of every supported action. Ordered roughly by logical + /// grouping (location → create/edit → clipboard → view/misc) so that + /// the default enabled set follows a sensible shape and the Settings + /// row for a not-yet-enabled action lands in a predictable spot. + pub const ALL: &'static [Self] = &[ + Self::LocationUp, + Self::Reload, + Self::NewFolder, + Self::NewFile, + Self::Rename, + Self::Delete, + Self::Cut, + Self::Copy, + Self::Paste, + Self::ToggleShowHidden, + Self::OpenTerminal, + ]; + + /// u8 discriminant used to carry the action over a DnD mime payload. + pub const fn to_u8(self) -> u8 { + match self { + Self::LocationUp => 0, + Self::Reload => 1, + Self::NewFolder => 2, + Self::NewFile => 3, + Self::Rename => 4, + Self::Delete => 5, + Self::Cut => 6, + Self::Copy => 7, + Self::Paste => 8, + Self::ToggleShowHidden => 9, + Self::OpenTerminal => 10, } } + + pub const fn from_u8(v: u8) -> Option { + match v { + 0 => Some(Self::LocationUp), + 1 => Some(Self::Reload), + 2 => Some(Self::NewFolder), + 3 => Some(Self::NewFile), + 4 => Some(Self::Rename), + 5 => Some(Self::Delete), + 6 => Some(Self::Cut), + 7 => Some(Self::Copy), + 8 => Some(Self::Paste), + 9 => Some(Self::ToggleShowHidden), + 10 => Some(Self::OpenTerminal), + _ => None, + } + } +} + +/// Default set shown on a fresh install — same "minimal 6" as phase 1/2. +pub fn default_toolbar() -> Vec { + vec![ + ToolbarAction::NewFolder, + ToolbarAction::Rename, + ToolbarAction::Delete, + ToolbarAction::Cut, + ToolbarAction::Copy, + ToolbarAction::Paste, + ] } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] From 11d435770eb69f116334a48bfe0d729c8c685c71 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 08:29:07 +0200 Subject: [PATCH 12/21] =?UTF-8?q?yoda:=20add=20=E2=86=91=E2=86=93=20button?= =?UTF-8?q?s=20next=20to=20drag=20handle=20in=20toolbar=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DnD source+destination wiring from 1cf17dc builds but the drag-drop doesn't fire reliably in practice (suspected: either dnd_source's shell.capture_event() on CursorMoved swallows events the destination needs to see, or intra-window DnD has a Wayland-specific hiccup on the current sctk/cosmic-comp pairing). Adds two plain click-handled buttons per enabled row (↑ go-up-symbolic, ↓ go-down-symbolic, both disabled at list edges) so reorder is functional regardless of DnD state. Backed by new messages ToolbarMoveUp / ToolbarMoveDown that swap adjacent positions in the Vec. The drag handle + DnD source/destination wrapping stays in place — if DnD gets fixed upstream or on a future libcosmic it'll Just Work, and the arrows remain as a keyboard-friendly fallback. --- src/app.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 868fad7..1dbf6e6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -551,6 +551,10 @@ pub enum Message { ToolbarAdd(ToolbarAction), ToolbarRemove(ToolbarAction), ToolbarReorder { src: ToolbarAction, target: ToolbarAction }, + /// Move one step up (toward index 0) inside the enabled toolbar list. + ToolbarMoveUp(ToolbarAction), + /// Move one step down (toward the end) inside the enabled toolbar list. + ToolbarMoveDown(ToolbarAction), ToolbarReset, SetTypeToSearch(TypeToSearch), SystemThemeModeChange, @@ -2416,12 +2420,27 @@ impl App { .into() }; - let row_enabled = |action: ToolbarAction| -> Element<'_, Message> { + let row_enabled = |action: ToolbarAction, pos: usize, last: usize| -> Element<'_, Message> { let (icon, label, _msg) = toolbar_action_ui(action); + let up_btn = widget::button::icon(widget::icon::from_name("go-up-symbolic").size(14)); + let up_btn = if pos > 0 { + up_btn.on_press(Message::ToolbarMoveUp(action)) + } else { + up_btn + }; + let down_btn = widget::button::icon(widget::icon::from_name("go-down-symbolic").size(14)); + let down_btn = if pos < last { + down_btn.on_press(Message::ToolbarMoveDown(action)) + } else { + down_btn + }; + let row_content: Element<_> = widget::row::with_children(vec![ drag_icon(14), widget::icon::from_name(icon).size(16).into(), widget::text::body(label).width(Length::Fill).into(), + up_btn.into(), + down_btn.into(), widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14)) .on_press(Message::ToolbarRemove(action)) .into(), @@ -2476,8 +2495,9 @@ impl App { section = section .add(widget::text::body(fl!("toolbar-empty-hint"))); } else { - for a in enabled.iter().copied() { - section = section.add(row_enabled(a)); + let last = enabled.len() - 1; + for (pos, a) in enabled.iter().copied().enumerate() { + section = section.add(row_enabled(a, pos, last)); } } @@ -4615,6 +4635,28 @@ impl Application for App { } return Task::none(); } + Message::ToolbarMoveUp(action) => { + let mut tb = self.config.toolbar.clone(); + if let Some(i) = tb.iter().position(|a| a == &action) + && i > 0 + { + tb.swap(i, i - 1); + config_set!(toolbar, tb); + return self.update_config(); + } + return Task::none(); + } + Message::ToolbarMoveDown(action) => { + let mut tb = self.config.toolbar.clone(); + if let Some(i) = tb.iter().position(|a| a == &action) + && i + 1 < tb.len() + { + tb.swap(i, i + 1); + config_set!(toolbar, tb); + return self.update_config(); + } + return Task::none(); + } Message::ToolbarReset => { config_set!(toolbar, default_toolbar()); return self.update_config(); From af843d204d3c489d04120150063da3093c1901a4 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 08:37:19 +0200 Subject: [PATCH 13/21] yoda: direct drag-drop reorder on the toolbar itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: going through Settings to reorder is too indirect. Now each toolbar button is wrapped in dnd_source + dnd_destination so the user can grab an icon in the live toolbar and drop it onto another icon to reorder in-place. The underlying icon button keeps firing its on_press for quick clicks (dnd_source only starts a drag past the default 8 px motion threshold), so regular clicks continue to run the associated Action — no mode-switch needed. Settings retains the ↑↓/add/remove UI as a fallback (discoverability + keyboard-friendly) and for the same drag-drop if the user prefers working from the panel. The config model (Vec + ToolbarReorder message) is already shared, so both paths mutate the same state. --- src/app.rs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1dbf6e6..c742244 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6807,25 +6807,44 @@ impl Application for App { // Yoda phase 3: Dolphin-style quick actions toolbar. Items are // rendered from self.config.toolbar (Vec) — the user - // picks the set AND the order via drag-drop in Settings. Dispatch - // goes through Action::message so keybinding and toolbar share the - // same code path. + // picks the set AND the order via direct drag-drop on the toolbar. + // Short click = action (shared Action::message dispatch); drag past + // the default 8px threshold = reorder (ToolbarReorder message). if !self.config.toolbar.is_empty() { + use cosmic::iced::clipboard::dnd::DndAction as DndAct; let clipboard_has = self.clipboard_has_content(); let buttons: Vec> = self .config .toolbar .iter() .map(|a| { - let (icon, label, msg) = toolbar_action_ui(*a); - let enabled = !matches!(a, ToolbarAction::Paste) || clipboard_has; + let action = *a; + let (icon, label, msg) = toolbar_action_ui(action); + let enabled = !matches!(action, ToolbarAction::Paste) || clipboard_has; let btn = widget::button::icon(widget::icon::from_name(icon).size(16)); let btn = if enabled { btn.on_press(msg) } else { btn }; - widget::tooltip( + let tooltip = widget::tooltip( btn, widget::text::body(label), widget::tooltip::Position::Bottom, + ); + let source = widget::dnd_source::(tooltip) + .drag_content(move || ToolbarActionPayload(action.to_u8())); + widget::dnd_destination( + source, + vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)], ) + .data_received_for::( + move |payload: Option| { + match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { + Some(src) if src != action => { + Message::ToolbarReorder { src, target: action } + } + _ => Message::ToolbarReorder { src: action, target: action }, + } + }, + ) + .action(DndAct::Move) .into() }) .collect(); From 94c3e6c5512cad63412af98e9351eb36792a8871 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 11:03:05 +0200 Subject: [PATCH 14/21] yoda: toolbar as segmented_button for working drag reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic dnd_source+dnd_destination pairing didn't reliably fire on intra-window reorders in this setup, while segmented_button's built-in drag (same primitive powering tab_bar, which does work) is proven. Switched the toolbar rendering to segmented_button::horizontal with drag enabled — each segment carries its ToolbarAction as data. App state: - new toolbar_model: segmented_button::Model - rebuild_toolbar_model() mirrors config.toolbar into the model on every update_config (including the initial app.update_config at startup) - sync_toolbar_config_from_model() is the reverse: walk the model's entity order after a reorder, write the new Vec directly via config.set_toolbar (without calling update_config so we don't rebuild the model and wipe the reorder the user just did) Messages: - ToolbarTabActivate(Entity): look up action via model.data(), clear the model's active selection (segmented_button single-select would keep the last click highlighted; we don't want that for action buttons), dispatch the action's message. - ToolbarTabReorder(ReorderEvent): model.reorder then sync. View: - replaces the row-of-dnd-wrapped-icon-buttons with segmented_button::horizontal(&self.toolbar_model) .enable_tab_drag("x-cosmic-files/toolbar-dnd") .on_reorder(...) .on_activate(...) - fixed 36-px square buttons so it still looks toolbar-y rather than stretched pill-segmented-control Kept: Settings panel ↑↓/add/remove UI (no regression). Removed: dnd_source/dnd_destination wrappers from the toolbar (but the ToolbarActionPayload + MIME constant remain in case Settings DnD gets unstuck later). --- src/app.rs | 133 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/src/app.rs b/src/app.rs index c742244..ef2c8e2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -556,6 +556,10 @@ pub enum Message { /// Move one step down (toward the end) inside the enabled toolbar list. ToolbarMoveDown(ToolbarAction), ToolbarReset, + /// Click on a toolbar button (via segmented_button activation). + ToolbarTabActivate(segmented_button::Entity), + /// Drag-reorder inside the toolbar (via segmented_button drag). + ToolbarTabReorder(segmented_button::ReorderEvent), SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), @@ -846,6 +850,11 @@ pub struct App { nav_bar_context_id: segmented_button::Entity, nav_model: segmented_button::SingleSelectModel, tab_model: segmented_button::Model, + /// Yoda phase 3: segmented_button model mirroring config.toolbar so the + /// toolbar row gets free drag-reorder + click activation (same widget + /// that powers the tab bar, its reorder is proven to work in this + /// setup unlike the generic dnd_source/dnd_destination wrappers). + toolbar_model: segmented_button::Model, config_handler: Option, state_handler: Option, config: Config, @@ -1800,6 +1809,7 @@ impl App { fn update_config(&mut self) -> Task { self.update_nav_model(); + self.rebuild_toolbar_model(); // Tabs are collected first to placate the borrowck let tabs: Box<[_]> = self.tab_model.iter().collect(); // Update main conf and each tab with the new config @@ -1813,6 +1823,49 @@ impl App { Task::batch(commands) } + /// Yoda phase 3: rebuild `toolbar_model` so it matches `config.toolbar`. + /// Called on init and on every config update. Each entity carries the + /// associated `ToolbarAction` as data so click/reorder handlers can + /// round-trip Entity → ToolbarAction without maintaining a side map. + fn rebuild_toolbar_model(&mut self) { + self.toolbar_model.clear(); + for action in self.config.toolbar.iter().copied() { + let (icon_name, label, _msg) = toolbar_action_ui(action); + self.toolbar_model + .insert() + .icon(widget::icon::from_name(icon_name).size(16).icon()) + .text(label) + .data::(action); + } + } + + /// Yoda phase 3: after a drag-reorder, sync `config.toolbar` with the + /// new entity order in `toolbar_model`. Inlines what `config_set!` + /// would do (the macro lives inside update()). + fn sync_toolbar_config_from_model(&mut self) -> Task { + let new_toolbar: Vec = self + .toolbar_model + .iter() + .filter_map(|e| self.toolbar_model.data::(e).copied()) + .collect(); + if new_toolbar == self.config.toolbar { + return Task::none(); + } + match self.config_handler.as_ref() { + Some(h) => { + if let Err(err) = self.config.set_toolbar(h, new_toolbar) { + log::warn!("failed to save toolbar order: {err}"); + } + } + None => self.config.toolbar = new_toolbar, + } + // Don't call update_config() — that would rebuild the + // toolbar_model from config and undo the reorder the user just + // made. The model already has the new order; config is just + // catching up for persistence. + Task::none() + } + fn update_desktop(&mut self) -> Task { let needs_reload: Box<[_]> = (self.tab_model.iter()) .filter_map(|entity| { @@ -2688,6 +2741,7 @@ impl Application for App { nav_bar_context_id: segmented_button::Entity::null(), nav_model: segmented_button::ModelBuilder::default().build(), tab_model: segmented_button::ModelBuilder::default().build(), + toolbar_model: segmented_button::ModelBuilder::default().build(), config_handler: flags.config_handler, state_handler: flags.state_handler, config: flags.config, @@ -4661,6 +4715,24 @@ impl Application for App { config_set!(toolbar, default_toolbar()); return self.update_config(); } + Message::ToolbarTabActivate(entity) => { + // Dispatch the stored ToolbarAction's message, then clear + // the "active" selection so the button doesn't stay + // highlighted after a click (we use segmented_button for + // layout/drag but toolbar buttons are action-firing, not + // a mutual-exclusive choice). + let action = self.toolbar_model.data::(entity).copied(); + self.toolbar_model.deactivate(); + if let Some(action) = action { + let (_, _, msg) = toolbar_action_ui(action); + return self.update(msg); + } + return Task::none(); + } + Message::ToolbarTabReorder(event) => { + let _ = self.toolbar_model.reorder(event.dragged, event.target, event.position); + return self.sync_toolbar_config_from_model(); + } Message::SetTypeToSearch(type_to_search) => { config_set!(type_to_search, type_to_search); return self.update_config(); @@ -6805,52 +6877,23 @@ impl Application for App { ); } - // Yoda phase 3: Dolphin-style quick actions toolbar. Items are - // rendered from self.config.toolbar (Vec) — the user - // picks the set AND the order via direct drag-drop on the toolbar. - // Short click = action (shared Action::message dispatch); drag past - // the default 8px threshold = reorder (ToolbarReorder message). + // Yoda phase 3: Dolphin-style quick actions toolbar via + // segmented_button::horizontal — the same widget that powers the + // tab bar, so its built-in drag reorder works reliably (unlike the + // generic dnd_source+dnd_destination pairing we tried earlier). + // Short click = action (ToolbarTabActivate → dispatch the stored + // ToolbarAction's message). Drag past threshold = reorder + // (ToolbarTabReorder → model.reorder + sync to config). if !self.config.toolbar.is_empty() { - use cosmic::iced::clipboard::dnd::DndAction as DndAct; - let clipboard_has = self.clipboard_has_content(); - let buttons: Vec> = self - .config - .toolbar - .iter() - .map(|a| { - let action = *a; - let (icon, label, msg) = toolbar_action_ui(action); - let enabled = !matches!(action, ToolbarAction::Paste) || clipboard_has; - let btn = widget::button::icon(widget::icon::from_name(icon).size(16)); - let btn = if enabled { btn.on_press(msg) } else { btn }; - let tooltip = widget::tooltip( - btn, - widget::text::body(label), - widget::tooltip::Position::Bottom, - ); - let source = widget::dnd_source::(tooltip) - .drag_content(move || ToolbarActionPayload(action.to_u8())); - widget::dnd_destination( - source, - vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)], - ) - .data_received_for::( - move |payload: Option| { - match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { - Some(src) if src != action => { - Message::ToolbarReorder { src, target: action } - } - _ => Message::ToolbarReorder { src: action, target: action }, - } - }, - ) - .action(DndAct::Move) - .into() - }) - .collect(); - let toolbar = widget::row::with_children(buttons) - .spacing(space_xxs) - .align_y(Alignment::Center); + let toolbar = widget::segmented_button::horizontal(&self.toolbar_model) + .button_height(32) + .button_spacing(space_xxs) + .minimum_button_width(36) + .maximum_button_width(36) + .enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd")) + .on_reorder(Message::ToolbarTabReorder) + .tab_drag_threshold(8.) + .on_activate(Message::ToolbarTabActivate); tab_column = tab_column.push( widget::container(toolbar) .width(Length::Fill) From f0538190d98744d9fbac6bbcb40cecf83319ce88 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 11:12:26 +0200 Subject: [PATCH 15/21] yoda: toolbar icon-only + clean visual (Control style, 32px squares) User feedback: segmented visual was noisy; only want icons, no labels. Changes: - rebuild_toolbar_model no longer sets .text() on entities, so the segmented_button widget draws icon-only squares. - toolbar renders with style(SegmentedButton::Control), button_height + min/max_button_width pinned at 32 px (square icon buttons), button_spacing = space_xs for clear separation (was space_xxs which looked conjoined), Alignment::Center. - container width = Length::Shrink so the toolbar only takes the space it needs instead of stretching across the window. --- src/app.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index ef2c8e2..8adb742 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1827,14 +1827,17 @@ impl App { /// Called on init and on every config update. Each entity carries the /// associated `ToolbarAction` as data so click/reorder handlers can /// round-trip Entity → ToolbarAction without maintaining a side map. + /// + /// We insert ONLY the icon (no `.text()`) so the toolbar renders as a + /// clean icon row — user-visible labels stay in Settings/tooltips on + /// other surfaces. fn rebuild_toolbar_model(&mut self) { self.toolbar_model.clear(); for action in self.config.toolbar.iter().copied() { - let (icon_name, label, _msg) = toolbar_action_ui(action); + let (icon_name, _label, _msg) = toolbar_action_ui(action); self.toolbar_model .insert() .icon(widget::icon::from_name(icon_name).size(16).icon()) - .text(label) .data::(action); } } @@ -6833,7 +6836,7 @@ impl Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let cosmic_theme::Spacing { - space_xxs, space_s, .. + space_xxs, space_xs, space_s, .. } = theme::active().cosmic().spacing; let mut tab_column = widget::column::with_capacity(4); @@ -6885,18 +6888,24 @@ impl Application for App { // ToolbarAction's message). Drag past threshold = reorder // (ToolbarTabReorder → model.reorder + sync to config). if !self.config.toolbar.is_empty() { + // Use Control style (no TabBar underline, no bottom border) + // and let each button shrink to its icon. Spacing = space_xs + // keeps the buttons visually separated so it looks like an + // icon toolbar rather than a conjoined segmented control. let toolbar = widget::segmented_button::horizontal(&self.toolbar_model) + .style(theme::SegmentedButton::Control) .button_height(32) - .button_spacing(space_xxs) - .minimum_button_width(36) - .maximum_button_width(36) + .button_spacing(space_xs) + .button_alignment(Alignment::Center) + .minimum_button_width(32) + .maximum_button_width(32) .enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd")) .on_reorder(Message::ToolbarTabReorder) .tab_drag_threshold(8.) .on_activate(Message::ToolbarTabActivate); tab_column = tab_column.push( widget::container(toolbar) - .width(Length::Fill) + .width(Length::Shrink) .padding([space_xxs, space_s]), ); } From 338354c4d047f71abcce35ade1f725f34384c95a Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Tue, 5 May 2026 08:00:08 +0200 Subject: [PATCH 16/21] Improve initial directory listing latency Avoid synchronous child counts during item construction, use extension-based MIME detection for initial scans, and defer expensive MIME icon resolution by using generic file icons for ordinary files. Document the local xdg-desktop-portal FileChooser workaround for COSMIC portal crashes. --- docs/local-performance-and-portal-notes.md | 78 ++++++++++++++++++++++ src/tab.rs | 72 +++++++++++--------- 2 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 docs/local-performance-and-portal-notes.md diff --git a/docs/local-performance-and-portal-notes.md b/docs/local-performance-and-portal-notes.md new file mode 100644 index 0000000..a3dabbe --- /dev/null +++ b/docs/local-performance-and-portal-notes.md @@ -0,0 +1,78 @@ +# Local performance and portal notes + +Date: 2026-05-05 + +This repository carries a local patch aimed at improving initial directory +display latency in large folders, plus a system-level workaround for a COSMIC +portal FileChooser crash observed with Firefox and Chromium. + +## Directory listing latency + +The slow path was the initial construction of `Item` values in `src/tab.rs`. +On a test folder with about 2000 entries, raw filesystem enumeration and stat +calls completed in a few milliseconds, while COSMIC Files took multiple seconds +before showing the directory. + +The local patch keeps initial item construction cheap: + +- directory child counts are no longer computed synchronously in + `item_from_entry` and `item_from_gvfs_info`; +- initial MIME detection uses extension-based `mime_guess`; +- regular files use a generic file icon during the initial scan instead of + resolving the full MIME icon immediately. + +This keeps folders and `.desktop` files special-cased, while avoiding expensive +per-file work for ordinary files during first paint. + +Measured locally during investigation: + +- before the MIME/icon changes: about 3.1 seconds for `~/Téléchargements`; +- after avoiding full MIME icon resolution during scan: below the temporary + 100 ms perf-log threshold for the same folder. + +## File chooser portal workaround + +Firefox and Chromium were failing to open `Save As` on the first attempt because +`xdg-desktop-portal-cosmic` crashed while handling `FileChooser`. + +Logs showed: + +```text +Backend call failed: Remote peer disconnected +xdg-desktop-portal-cosmic ... status=11/SEGV +``` + +The working local system workaround is to remove +`org.freedesktop.impl.portal.FileChooser` from: + +```text +/usr/share/xdg-desktop-portal/portals/cosmic.portal +``` + +so the file chooser falls back to GTK. The resulting local `cosmic.portal` +keeps COSMIC for the other interfaces: + +```ini +[portal] +DBusName=org.freedesktop.impl.portal.desktop.cosmic +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast +UseIn=COSMIC +``` + +Backups created locally: + +```text +/usr/share/xdg-desktop-portal/portals/cosmic.portal.bak-codex-20260505-filechooser +/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505 +/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505-2 +``` + +After editing portal files, restart: + +```sh +systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gtk.service +``` + +Package updates may overwrite `/usr/share/xdg-desktop-portal/portals/cosmic.portal`. +If `Save As` starts needing two attempts again, re-check that `FileChooser` has +not been reintroduced in `cosmic.portal`. diff --git a/src/tab.rs b/src/tab.rs index 94eacdd..641239f 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -302,6 +302,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han .handle() } +fn generic_file_icons( + sizes: IconSizes, +) -> ( + widget::icon::Handle, + widget::icon::Handle, + widget::icon::Handle, +) { + ( + widget::icon::from_name("text-x-generic") + .size(sizes.grid()) + .handle(), + widget::icon::from_name("text-x-generic") + .size(sizes.list()) + .handle(), + widget::icon::from_name("text-x-generic") + .size(sizes.list_condensed()) + .handle(), + ) +} + //TODO: replace with Path::has_trailing_sep when stable fn has_trailing_sep(path: &Path) -> bool { path.as_os_str() @@ -665,9 +685,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS folder_icon(&path, sizes.list_condensed()), ) } else { - // ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive - // @todo - expose this as a config option? - let mime = mime_for_path(&path, None, true); + // Keep the initial directory scan cheap. Opening files still + // recalculates MIME from the real path before launching apps. + let mime = mime_guess::from_path(&path).first_or_octet_stream(); //TODO: clean this up, implement for trash let icon_name_opt = if mime == "application/x-desktop" { @@ -684,28 +704,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS desktop_icon_handle(&icon_name, sizes.list_condensed()), ) } else { + let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = + generic_file_icons(sizes); ( - mime.clone(), - mime_icon(mime.clone(), sizes.grid()), - mime_icon(mime.clone(), sizes.list()), - mime_icon(mime, sizes.list_condensed()), + mime, + icon_handle_grid, + icon_handle_list, + icon_handle_list_condensed, ) } }; - let mut children_opt = None; + let children_opt = None; let mut dir_size = DirSize::NotDirectory; if is_dir && !remote { dir_size = DirSize::Calculating(Controller::default()); - //TODO: calculate children in the background (and make it cancellable?) - match fs::read_dir(&path) { - Ok(entries) => { - children_opt = Some(entries.count()); - } - Err(err) => { - log::warn!("failed to read directory {}: {}", path.display(), err); - } - } } let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop); @@ -807,7 +820,9 @@ pub fn item_from_entry( folder_icon(&path, sizes.list_condensed()), ) } else { - let mime = mime_for_path(&path, Some(&metadata), remote); + // Keep the initial directory scan cheap. Opening files still + // recalculates MIME from the real path before launching apps. + let mime = mime_guess::from_path(&path).first_or_octet_stream(); //TODO: clean this up, implement for trash let icon_name_opt = if mime == "application/x-desktop" { is_desktop = true; @@ -823,28 +838,21 @@ pub fn item_from_entry( desktop_icon_handle(&icon_name, sizes.list_condensed()), ) } else { + let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = + generic_file_icons(sizes); ( - mime.clone(), - mime_icon(mime.clone(), sizes.grid()), - mime_icon(mime.clone(), sizes.list()), - mime_icon(mime, sizes.list_condensed()), + mime, + icon_handle_grid, + icon_handle_list, + icon_handle_list_condensed, ) } }; - let mut children_opt = None; + let children_opt = None; let mut dir_size = DirSize::NotDirectory; if metadata.is_dir() && !remote { dir_size = DirSize::Calculating(Controller::default()); - //TODO: calculate children in the background (and make it cancellable?) - match fs::read_dir(&path) { - Ok(entries) => { - children_opt = Some(entries.count()); - } - Err(err) => { - log::warn!("failed to read directory {}: {}", path.display(), err); - } - } } let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop); From d080bc85afddd5d40b1389315ffe0ca72206c3c4 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Tue, 5 May 2026 08:09:17 +0200 Subject: [PATCH 17/21] Resolve cosmic-files warnings without masking Parse text/uri-list according to the real clipboard format, make unsupported trash search explicit, and update the lockfile so the local cosmic-text patch is actually used instead of reported as unused. --- Cargo.lock | 8 ++------ Cargo.toml | 2 +- src/clipboard.rs | 7 ++++--- src/trash.rs | 12 ++++++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d2823c..b01240f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,8 +1494,7 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.18.2" -source = "git+https://github.com/pop-os/cosmic-text.git#4d74f795cc771fdcc7ea0f9cacba63fcf036fad6" +version = "0.19.0" dependencies = [ "bitflags 2.11.1", "fontdb", @@ -1513,6 +1512,7 @@ dependencies = [ "unicode-linebreak", "unicode-script", "unicode-segmentation", + "unicode-width", ] [[package]] @@ -9448,7 +9448,3 @@ dependencies = [ "syn", "winnow 0.7.15", ] - -[[patch.unused]] -name = "cosmic-text" -version = "0.19.0" diff --git a/Cargo.toml b/Cargo.toml index 18c9010..6ba4d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,7 @@ window_clipboard = { path = "/home/lionel/Devels/window_clipboard" } dnd = { path = "/home/lionel/Devels/window_clipboard/dnd" } mime = { path = "/home/lionel/Devels/window_clipboard/mime" } -[patch.'https://github.com/pop-os/cosmic-text'] +[patch.'https://github.com/pop-os/cosmic-text.git'] cosmic-text = { path = "/home/lionel/Devels/cosmic-text" } [workspace] diff --git a/src/clipboard.rs b/src/clipboard.rs index 5892071..89da7d7 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -132,9 +132,10 @@ impl TryFrom<(Vec, String)> for ClipboardPaste { match mime.as_str() { "text/uri-list" => { let text = str::from_utf8(&data)?; - let lines = text.lines(); - - for line in text.lines() { + for line in text.lines().filter(|line| { + let line = line.trim(); + !line.is_empty() && !line.starts_with('#') + }) { let url = Url::parse(line)?; match url.to_file_path() { Ok(path) => paths.push(path), diff --git a/src/trash.rs b/src/trash.rs index ce37c83..cc39d02 100644 --- a/src/trash.rs +++ b/src/trash.rs @@ -27,7 +27,7 @@ pub trait TrashExt { Vec::new() } - fn scan_search bool + Sync>(callback: F, regex: &Regex) {} + fn scan_search bool + Sync>(callback: F, regex: &Regex); fn icon(icon_size: u16) -> widget::icon::Handle { widget::icon::from_name(if Self::is_empty() { @@ -142,4 +142,12 @@ impl TrashExt for Trash { not(target_os = "android") ) )))] -impl TrashExt for Trash {} +impl TrashExt for Trash { + fn scan_search bool + Sync>(callback: F, regex: &Regex) { + log::warn!( + "searching trash not supported on this platform for pattern {:?}", + regex.as_str() + ); + drop(callback); + } +} From 69c35ab80f9da8fdca633bdc44cfcf0b70894123 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Tue, 5 May 2026 13:47:17 +0200 Subject: [PATCH 18/21] yoda: switch window_clipboard patch to public Forgejo fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the path-local [patch] redirect with a git redirect to https://forge.aditua.com/leyoda/window_clipboard.git (branch yoda-x11-optional). Removes the absolute path dependency on /home/lionel/Devels/window_clipboard so any clone can build. The patch is still needed to consolidate the upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto the same fork iced uses, otherwise cargo would compile two versions of window_clipboard. Leyoda 2026 – GPLv3 --- Cargo.lock | 36 +++++++++++++++++++++--------------- Cargo.toml | 13 ++++++++----- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b01240f..4899293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -266,7 +266,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -973,6 +973,7 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "objc", "objc-foundation", @@ -982,6 +983,7 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "dnd", "mime 0.1.0", @@ -991,6 +993,7 @@ dependencies = [ [[package]] name = "clipboard_x11" version = "0.4.2" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "thiserror 1.0.69", "x11rb", @@ -1809,7 +1812,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1858,6 +1861,7 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "bitflags 2.11.1", "mime 0.1.0", @@ -1979,7 +1983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2457,7 +2461,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", + "windows-link 0.1.3", "windows-result 0.4.1", ] @@ -2586,7 +2590,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3879,7 +3883,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4221,7 +4225,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4643,6 +4647,7 @@ dependencies = [ [[package]] name = "mime" version = "0.1.0" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "smithay-clipboard", ] @@ -4876,7 +4881,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5253,7 +5258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -6240,7 +6245,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6614,7 +6619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6832,7 +6837,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7261,7 +7266,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7970,7 +7975,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -7982,6 +7987,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "clipboard-win", "clipboard_macos", diff --git a/Cargo.toml b/Cargo.toml index 6ba4d74..33bd815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,12 +149,15 @@ tokio = { version = "1", features = ["rt", "macros"] } # no [patch] block needed anymore. Keeping the block below would be a no-op # since nothing in the dep graph still asks for pop-os/libcosmic.git. -# Yoda wayland cut v5: redirect window_clipboard + cosmic-text to our local -# forks (x11 gated behind opt-in feature + EAW/PR#503 respectively). +# Yoda wayland cut: redirect window_clipboard to our public Forgejo fork +# (x11 gated behind opt-in feature) and cosmic-text to a local fork +# (EAW/PR#503). The window_clipboard patch is needed to consolidate the +# upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto +# the same fork iced uses, otherwise cargo compiles two versions. [patch.'https://github.com/pop-os/window_clipboard.git'] -window_clipboard = { path = "/home/lionel/Devels/window_clipboard" } -dnd = { path = "/home/lionel/Devels/window_clipboard/dnd" } -mime = { path = "/home/lionel/Devels/window_clipboard/mime" } +window_clipboard = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" } +dnd = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" } +mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" } [patch.'https://github.com/pop-os/cosmic-text.git'] cosmic-text = { path = "/home/lionel/Devels/cosmic-text" } From 35e115fdb5fef9eb0eeb3027bf3f8d4b44d49370 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Tue, 5 May 2026 15:32:47 +0200 Subject: [PATCH 19/21] yoda: switch cosmic-text patch to public Forgejo fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the path-local [patch] redirect with a git redirect to https://forge.aditua.com/leyoda/cosmic-text.git (branch local/pr-503, which carries upstream PR #503 plus the EAW monospace width fix). No more absolute path dependency on /home/lionel/Devels/cosmic-text. Leyoda 2026 – GPLv3 --- Cargo.lock | 1 + Cargo.toml | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4899293..e574618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1498,6 +1498,7 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.19.0" +source = "git+https://forge.aditua.com/leyoda/cosmic-text.git?branch=local%2Fpr-503#63072bbe29a1657d82cd3deb5db45070404ec7a1" dependencies = [ "bitflags 2.11.1", "fontdb", diff --git a/Cargo.toml b/Cargo.toml index 33bd815..a55104f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,9 +149,9 @@ tokio = { version = "1", features = ["rt", "macros"] } # no [patch] block needed anymore. Keeping the block below would be a no-op # since nothing in the dep graph still asks for pop-os/libcosmic.git. -# Yoda wayland cut: redirect window_clipboard to our public Forgejo fork -# (x11 gated behind opt-in feature) and cosmic-text to a local fork -# (EAW/PR#503). The window_clipboard patch is needed to consolidate the +# Yoda wayland cut: redirect window_clipboard (x11 gated behind opt-in +# feature) and cosmic-text (PR#503: EAW monospace width fix) to our public +# Forgejo forks. The window_clipboard patch is needed to consolidate the # upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto # the same fork iced uses, otherwise cargo compiles two versions. [patch.'https://github.com/pop-os/window_clipboard.git'] @@ -160,7 +160,7 @@ dnd = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = " mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" } [patch.'https://github.com/pop-os/cosmic-text.git'] -cosmic-text = { path = "/home/lionel/Devels/cosmic-text" } +cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503" } [workspace] members = ["cosmic-files-applet"] From 6f3adcd993bc4a0105f6f93186e52635d093e404 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sat, 23 May 2026 20:49:24 +0200 Subject: [PATCH 20/21] chore: clean feature-gated warnings --- src/app.rs | 20 +++++--------------- src/menu.rs | 5 +++-- src/mime_app.rs | 5 ++--- src/tab.rs | 5 +++-- src/thumbnailer.rs | 4 ++-- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8adb742..34d230c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -61,9 +61,11 @@ use std::{ path::{Path, PathBuf}, pin::Pin, process, - sync::{Arc, LazyLock, Mutex}, + sync::{Arc, LazyLock}, time::{self, Duration, Instant}, }; +#[cfg(feature = "notify")] +use std::sync::Mutex; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] @@ -85,7 +87,7 @@ use crate::{ key_bind::key_binds, localize::LANGUAGE_SORTER, menu, - mime_app::{self, MimeApp, MimeAppCache}, + mime_app::{MimeApp, MimeAppCache}, mime_icon, mounter::{MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage}, operation::{ @@ -1027,7 +1029,7 @@ impl App { for path in paths.iter().map(AsRef::as_ref) { match DesktopEntry::from_path::<&str>(path, None) { Ok(entry) => match entry.exec() { - Some(exec) => match mime_app::exec_to_command(exec, &[] as &[&str; 0]) { + Some(exec) => match crate::mime_app::exec_to_command(exec, &[] as &[&str; 0]) { Some(commands) => { let cwd_opt = entry.desktop_entry("Path"); @@ -1252,30 +1254,18 @@ impl App { if tl && !(tr || bl) { *top += min_dim.1; *left += min_dim.0; - - size.height -= min_dim.1; - size.width -= min_dim.0; } if tr && !(tl || br) { *top += min_dim.1; *right += min_dim.0; - - size.height -= min_dim.1; - size.width -= min_dim.0; } if bl && !(br || tl) { *bottom += min_dim.1; *left += min_dim.0; - - size.height -= min_dim.1; - size.width -= min_dim.0; } if br && !(bl || tr) { *bottom += min_dim.1; *right += min_dim.0; - - size.height -= min_dim.1; - size.width -= min_dim.0; } } self.margin = overlaps; diff --git a/src/menu.rs b/src/menu.rs index 89e4a02..9de9af4 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -14,6 +14,7 @@ use cosmic::{ responsive_menu_bar, space, text, }, }; +#[cfg(feature = "desktop")] use i18n_embed::LanguageLoader; use mime_guess::Mime; use std::{collections::HashMap, sync::LazyLock}; @@ -196,11 +197,11 @@ pub fn context_menu<'a>( if !Trash::is_empty() { children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); } - } else if let Some(entry) = selected_desktop_entry { + } else if let Some(_entry) = selected_desktop_entry { children.push(menu_item(fl!("open"), Action::Open).into()); #[cfg(feature = "desktop")] { - children.extend(entry.desktop_actions.into_iter().enumerate().map( + children.extend(_entry.desktop_actions.into_iter().enumerate().map( |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(), )); } diff --git a/src/mime_app.rs b/src/mime_app.rs index b8985ce..a9bf63e 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -6,13 +6,12 @@ use cosmic::desktop; use cosmic::widget; pub use mime_guess::Mime; use rustc_hash::FxHashMap; +#[cfg(feature = "desktop")] +use std::{cmp::Ordering, fs, io, time::Instant}; use std::{ - cmp::Ordering, ffi::OsStr, - fs, io, path::{Path, PathBuf}, process, - time::Instant, }; // Supported exec key field codes diff --git a/src/tab.rs b/src/tab.rs index 641239f..653dabf 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -28,6 +28,7 @@ use cosmic::{ space, }, }; +#[cfg(feature = "desktop")] use i18n_embed::LanguageLoader; use icu::{ datetime::{ @@ -578,7 +579,7 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind { } #[cfg(not(feature = "desktop"))] -fn get_desktop_file_display_name(path: &Path) -> Option { +fn get_desktop_file_display_name(_path: &Path) -> Option { None } @@ -597,7 +598,7 @@ fn get_desktop_file_display_name(path: &Path) -> Option { } #[cfg(not(feature = "desktop"))] -fn get_desktop_file_icon(path: &Path) -> Option { +fn get_desktop_file_icon(_path: &Path) -> Option { None } diff --git a/src/thumbnailer.rs b/src/thumbnailer.rs index f7786ed..8e61447 100644 --- a/src/thumbnailer.rs +++ b/src/thumbnailer.rs @@ -5,12 +5,12 @@ use cosmic::desktop::fde::GenericEntry; use mime_guess::Mime; use rustc_hash::FxHashMap; +#[cfg(feature = "desktop")] +use std::{fs, time::Instant}; use std::{ - fs, path::Path, process, sync::{LazyLock, Mutex}, - time::Instant, }; #[derive(Clone, Debug)] From 57ab1ecbf44f494973fbdeec11358e65ae44dba2 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sun, 24 May 2026 10:27:32 +0200 Subject: [PATCH 21/21] fix: clean files warnings for terminal build --- src/app.rs | 170 +++++++++++++++++++++---------------- src/dialog.rs | 8 +- src/lib.rs | 1 + src/mounter/mod.rs | 5 +- src/operation/recursive.rs | 1 + src/tab.rs | 6 ++ 6 files changed, 114 insertions(+), 77 deletions(-) diff --git a/src/app.rs b/src/app.rs index 34d230c..7bd3884 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,6 +51,8 @@ use notify_debouncer_full::{ }; use rustc_hash::{FxHashMap, FxHashSet}; use slotmap::Key as SlotMapKey; +#[cfg(feature = "notify")] +use std::sync::Mutex; use std::{ any::TypeId, collections::{BTreeMap, BTreeSet, HashMap, VecDeque}, @@ -64,8 +66,6 @@ use std::{ sync::{Arc, LazyLock}, time::{self, Duration, Instant}, }; -#[cfg(feature = "notify")] -use std::sync::Mutex; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] @@ -178,7 +178,11 @@ impl cosmic::iced::clipboard::mime::AllowedMimeTypes for ToolbarActionPayload { impl TryFrom<(Vec, String)> for ToolbarActionPayload { type Error = (); fn try_from((data, _mime): (Vec, String)) -> Result { - if data.len() == 1 { Ok(Self(data[0])) } else { Err(()) } + if data.len() == 1 { + Ok(Self(data[0])) + } else { + Err(()) + } } } @@ -218,11 +222,7 @@ fn toolbar_action_ui(a: ToolbarAction) -> (&'static str, String, Message) { fl!("delete"), Action::Delete.message(None), ), - ToolbarAction::Cut => ( - "edit-cut-symbolic", - fl!("cut"), - Action::Cut.message(None), - ), + ToolbarAction::Cut => ("edit-cut-symbolic", fl!("cut"), Action::Cut.message(None)), ToolbarAction::Copy => ( "edit-copy-symbolic", fl!("copy"), @@ -552,7 +552,10 @@ pub enum Message { /// Yoda phase 3 — toolbar editing messages. ToolbarAdd(ToolbarAction), ToolbarRemove(ToolbarAction), - ToolbarReorder { src: ToolbarAction, target: ToolbarAction }, + ToolbarReorder { + src: ToolbarAction, + target: ToolbarAction, + }, /// Move one step up (toward index 0) inside the enabled toolbar list. ToolbarMoveUp(ToolbarAction), /// Move one step down (toward the end) inside the enabled toolbar list. @@ -1204,7 +1207,7 @@ impl App { .sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height))); for (w_id, overlap) in sorted_overlaps { - let Some((bl, br, tl, tr, mut size)) = self.layer_sizes.get(w_id).map(|s| { + let Some((bl, br, tl, tr, size)) = self.layer_sizes.get(w_id).map(|s| { ( Rectangle::new( Point::new(0., s.height / 2.), @@ -1624,12 +1627,18 @@ impl App { ) -> Task { log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}"); let icon_sizes = self.config.tab.icon_sizes; + #[cfg(feature = "gvfs")] let mounter_items = self.mounter_items.clone(); Task::future(async move { let location2 = location.clone(); match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, mut items)) => { + Ok((parent_item_opt, items)) => { + #[cfg(feature = "gvfs")] + let mut items = items; + #[cfg(not(feature = "gvfs"))] + let items = items; + #[cfg(feature = "gvfs")] { let mounter_paths: Box<[_]> = mounter_items @@ -2466,60 +2475,66 @@ impl App { .into() }; - let row_enabled = |action: ToolbarAction, pos: usize, last: usize| -> Element<'_, Message> { - let (icon, label, _msg) = toolbar_action_ui(action); - let up_btn = widget::button::icon(widget::icon::from_name("go-up-symbolic").size(14)); - let up_btn = if pos > 0 { - up_btn.on_press(Message::ToolbarMoveUp(action)) - } else { - up_btn + let row_enabled = + |action: ToolbarAction, pos: usize, last: usize| -> Element<'_, Message> { + let (icon, label, _msg) = toolbar_action_ui(action); + let up_btn = + widget::button::icon(widget::icon::from_name("go-up-symbolic").size(14)); + let up_btn = if pos > 0 { + up_btn.on_press(Message::ToolbarMoveUp(action)) + } else { + up_btn + }; + let down_btn = + widget::button::icon(widget::icon::from_name("go-down-symbolic").size(14)); + let down_btn = if pos < last { + down_btn.on_press(Message::ToolbarMoveDown(action)) + } else { + down_btn + }; + + let row_content: Element<_> = widget::row::with_children(vec![ + drag_icon(14), + widget::icon::from_name(icon).size(16).into(), + widget::text::body(label).width(Length::Fill).into(), + up_btn.into(), + down_btn.into(), + widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14)) + .on_press(Message::ToolbarRemove(action)) + .into(), + ]) + .spacing(space_xxs) + .align_y(Alignment::Center) + .into(); + + let row_container = widget::container(row_content) + .width(Length::Fill) + .padding(space_xxs); + + // Wrap as DnD source (drags itself) + DnD destination (accepts + // drops from other enabled rows; on drop, move the src before + // this row). + let source = widget::dnd_source::(row_container) + .drag_content(move || ToolbarActionPayload(action.to_u8())); + widget::dnd_destination(source, vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)]) + .data_received_for::( + move |payload: Option| { + match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { + Some(src) if src != action => Message::ToolbarReorder { + src, + target: action, + }, + // No-op if payload missing / malformed / same row. + _ => Message::ToolbarReorder { + src: action, + target: action, + }, + } + }, + ) + .action(DndAction::Move) + .into() }; - let down_btn = widget::button::icon(widget::icon::from_name("go-down-symbolic").size(14)); - let down_btn = if pos < last { - down_btn.on_press(Message::ToolbarMoveDown(action)) - } else { - down_btn - }; - - let row_content: Element<_> = widget::row::with_children(vec![ - drag_icon(14), - widget::icon::from_name(icon).size(16).into(), - widget::text::body(label).width(Length::Fill).into(), - up_btn.into(), - down_btn.into(), - widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14)) - .on_press(Message::ToolbarRemove(action)) - .into(), - ]) - .spacing(space_xxs) - .align_y(Alignment::Center) - .into(); - - let row_container = widget::container(row_content) - .width(Length::Fill) - .padding(space_xxs); - - // Wrap as DnD source (drags itself) + DnD destination (accepts - // drops from other enabled rows; on drop, move the src before - // this row). - let source = widget::dnd_source::(row_container) - .drag_content(move || ToolbarActionPayload(action.to_u8())); - widget::dnd_destination( - source, - vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)], - ) - .data_received_for::(move |payload: Option| { - match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { - Some(src) if src != action => { - Message::ToolbarReorder { src, target: action } - } - // No-op if payload missing / malformed / same row. - _ => Message::ToolbarReorder { src: action, target: action }, - } - }) - .action(DndAction::Move) - .into() - }; let row_disabled = |action: ToolbarAction| -> Element<'_, Message> { let (icon, label, _msg) = toolbar_action_ui(action); @@ -2538,8 +2553,7 @@ impl App { let mut section = widget::settings::section().title(fl!("toolbar")); if enabled.is_empty() { - section = section - .add(widget::text::body(fl!("toolbar-empty-hint"))); + section = section.add(widget::text::body(fl!("toolbar-empty-hint"))); } else { let last = enabled.len() - 1; for (pos, a) in enabled.iter().copied().enumerate() { @@ -2556,10 +2570,8 @@ impl App { } col = col.push(avail); } - col = col.push( - widget::button::standard(fl!("toolbar-reset")) - .on_press(Message::ToolbarReset), - ); + col = col + .push(widget::button::standard(fl!("toolbar-reset")).on_press(Message::ToolbarReset)); col.into() } @@ -4672,10 +4684,15 @@ impl Application for App { if let (Some(src_idx), Some(tgt_idx)) = ( tb.iter().position(|a| a == &src), tb.iter().position(|a| a == &target), - ) && src_idx != tgt_idx { + ) && src_idx != tgt_idx + { // Pull src out, then insert before the target's new position. let item = tb.remove(src_idx); - let new_tgt = if src_idx < tgt_idx { tgt_idx - 1 } else { tgt_idx }; + let new_tgt = if src_idx < tgt_idx { + tgt_idx - 1 + } else { + tgt_idx + }; tb.insert(new_tgt, item); config_set!(toolbar, tb); return self.update_config(); @@ -4723,7 +4740,9 @@ impl Application for App { return Task::none(); } Message::ToolbarTabReorder(event) => { - let _ = self.toolbar_model.reorder(event.dragged, event.target, event.position); + let _ = self + .toolbar_model + .reorder(event.dragged, event.target, event.position); return self.sync_toolbar_config_from_model(); } Message::SetTypeToSearch(type_to_search) => { @@ -6826,7 +6845,10 @@ impl Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let cosmic_theme::Spacing { - space_xxs, space_xs, space_s, .. + space_xxs, + space_xs, + space_s, + .. } = theme::active().cosmic().spacing; let mut tab_column = widget::column::with_capacity(4); diff --git a/src/dialog.rs b/src/dialog.rs index cac5907..6bdeee6 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -744,11 +744,17 @@ impl App { fn rescan_tab(&self, selection_paths: Option>) -> Task { let location = self.tab.location.clone(); let icon_sizes = self.tab.config.icon_sizes; + #[cfg(feature = "gvfs")] let mounter_items = self.mounter_items.clone(); Task::future(async move { let location2 = location.clone(); match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { - Ok((parent_item_opt, mut items)) => { + Ok((parent_item_opt, items)) => { + #[cfg(feature = "gvfs")] + let mut items = items; + #[cfg(not(feature = "gvfs"))] + let items = items; + #[cfg(feature = "gvfs")] { let mounter_paths: Box<[_]> = mounter_items diff --git a/src/lib.rs b/src/lib.rs index 3e89861..a426695 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,7 @@ mod zoom; pub(crate) type FxOrderMap = ordermap::OrderMap; +#[cfg(feature = "gvfs")] pub(crate) fn err_str(err: T) -> String { err.to_string() } diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index b97f32a..dc2ab92 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -75,10 +75,10 @@ impl MounterItem { } } - pub fn icon(&self, symbolic: bool) -> Option { + pub fn icon(&self, _symbolic: bool) -> Option { match self { #[cfg(feature = "gvfs")] - Self::Gvfs(item) => item.icon(symbolic), + Self::Gvfs(item) => item.icon(_symbolic), Self::None => unreachable!(), } } @@ -103,6 +103,7 @@ impl MounterItem { pub type MounterItems = Vec; #[derive(Clone, Debug)] +#[allow(dead_code)] pub enum MounterMessage { Items(MounterItems), MountResult(MounterItem, Result), diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index 9a33ebd..9c9a1c3 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -9,6 +9,7 @@ use compio::buf::{IntoInner, IoBuf}; use compio::driver::{ToSharedFd, op::AsyncifyFd}; use compio::io::{AsyncReadAt, AsyncWriteAt}; use cosmic::iced::futures; +#[cfg(feature = "gvfs")] use futures::{FutureExt, StreamExt}; use std::future::Future; use std::pin::Pin; diff --git a/src/tab.rs b/src/tab.rs index 653dabf..ec2260a 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -773,7 +773,10 @@ pub fn item_from_entry( sizes: IconSizes, ) -> Item { let mut is_desktop = false; + #[cfg(feature = "gvfs")] let mut is_gvfs = false; + #[cfg(not(feature = "gvfs"))] + let is_gvfs = false; let hidden = name.starts_with('.') || hidden_attribute(&metadata); @@ -967,7 +970,10 @@ pub fn item_from_path>(path: P, sizes: IconSizes) -> Result Vec { let mut items = Vec::new(); let mut hidden_files = Box::from([]); + #[cfg(feature = "gvfs")] let mut remote_scannable = false; + #[cfg(not(feature = "gvfs"))] + let remote_scannable = false; #[cfg(feature = "gvfs")] {