diff --git a/Cargo.lock b/Cargo.lock index 0084c8d..e574618 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]] @@ -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,7 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "objc", "objc-foundation", @@ -989,7 +983,7 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "dnd", "mime 0.1.0", @@ -999,7 +993,7 @@ dependencies = [ [[package]] name = "clipboard_x11" version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "thiserror 1.0.69", "x11rb", @@ -1314,14 +1308,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 +1325,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 +1386,7 @@ dependencies = [ "jiff-icu", "jxl-oxide", "libc", - "libcosmic", + "libcosmic-yoda", "log", "lzma-rust2", "md-5", @@ -1461,7 +1479,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", @@ -1479,8 +1497,8 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.18.2" -source = "git+https://github.com/pop-os/cosmic-text.git#4d74f795cc771fdcc7ea0f9cacba63fcf036fad6" +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", @@ -1498,16 +1516,16 @@ dependencies = [ "unicode-linebreak", "unicode-script", "unicode-segmentation", + "unicode-width", ] [[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", @@ -1615,12 +1633,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" @@ -1801,7 +1813,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1850,7 +1862,7 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "bitflags 2.11.1", "mime 0.1.0", @@ -1879,45 +1891,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" @@ -2011,7 +1984,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]] @@ -2489,7 +2462,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", + "windows-link 0.1.3", "windows-result 0.4.1", ] @@ -2618,7 +2591,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2988,13 +2961,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 +2981,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 +2989,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 +3009,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 +3053,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 +3089,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 +3097,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 +3108,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 +3122,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,9 +3138,7 @@ 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", "bytemuck", "cosmic-client-toolkit", @@ -3159,19 +3155,16 @@ 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]] name = "iced_widget" version = "0.14.2" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3189,13 +3182,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", @@ -3892,7 +3884,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4234,7 +4226,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]] @@ -4309,15 +4301,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.2" 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 +4321,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", @@ -4420,12 +4411,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" @@ -4663,7 +4648,7 @@ dependencies = [ [[package]] name = "mime" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "smithay-clipboard", ] @@ -4897,7 +4882,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]] @@ -5274,7 +5259,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]] @@ -6261,7 +6246,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6635,7 +6620,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]] @@ -6643,12 +6628,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", @@ -6658,14 +6641,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]] @@ -6857,7 +6838,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7038,19 +7019,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" @@ -7299,7 +7267,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8008,7 +7976,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]] @@ -8020,7 +7988,7 @@ 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" +source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85" dependencies = [ "clipboard-win", "clipboard_macos", @@ -8560,7 +8528,6 @@ dependencies = [ "winit-wayland", "winit-web", "winit-win32", - "winit-x11", ] [[package]] @@ -8611,7 +8578,6 @@ dependencies = [ "smol_str", "tracing", "winit-core", - "x11-dl", "xkbcommon-dl", ] @@ -8729,29 +8695,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" @@ -8873,31 +8816,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]] diff --git a/Cargo.toml b/Cargo.toml index c1b75ae..a55104f 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,20 +145,22 @@ 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" } +# 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. -# [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" } +# 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'] +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/smithay-clipboard'] -# smithay-clipboard = { path = "../smithay-clipboard" } +[patch.'https://github.com/pop-os/cosmic-text.git'] +cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503" } [workspace] members = ["cosmic-files-applet"] 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/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 69a76c6..8e4f54e 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 @@ -138,6 +139,11 @@ open-with = Open with 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 none = None diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index a461a9f..214781e 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 @@ -130,6 +131,11 @@ open-with = Ouvrir avec 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 feca9c5..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}, @@ -61,7 +63,7 @@ use std::{ path::{Path, PathBuf}, pin::Pin, process, - sync::{Arc, LazyLock, Mutex}, + sync::{Arc, LazyLock}, time::{self, Duration, Instant}, }; use tokio::sync::mpsc; @@ -77,7 +79,7 @@ use crate::{ }, config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, - TimeConfig, TypeToSearch, + TimeConfig, ToolbarAction, TypeToSearch, default_toolbar, }, context_action, dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}, @@ -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::{ @@ -145,6 +147,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, @@ -407,6 +508,7 @@ pub enum Message { OpenWithBrowse, OpenWithDialog(Option), OpenWithSelection(usize), + OpenWithToggleDefault(bool), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Overlap(window::Id, OverlapNotifyEvent), Paste(Option), @@ -447,6 +549,22 @@ pub enum Message { SearchInput(String), SetShowDetails(bool), SetShowRecents(bool), + /// Yoda phase 3 — toolbar editing messages. + 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, + /// 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), @@ -577,6 +695,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]>, @@ -734,6 +855,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, @@ -906,7 +1032,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"); @@ -1081,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.), @@ -1131,30 +1257,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; @@ -1513,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 @@ -1688,6 +1808,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 @@ -1701,6 +1822,52 @@ 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. + /// + /// 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); + self.toolbar_model + .insert() + .icon(widget::icon::from_name(icon_name).size(16).icon()) + .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| { @@ -2280,10 +2447,134 @@ impl App { .toggler(self.config.show_recents, Message::SetShowRecents) }) .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, 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 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 { + let last = enabled.len() - 1; + for (pos, a) in enabled.iter().copied().enumerate() { + section = section.add(row_enabled(a, pos, last)); + } + } + + 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(); @@ -2455,6 +2746,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, @@ -3232,6 +3524,7 @@ impl Application for App { path, mime, selected, + set_default, .. } => { let available_apps = self.get_apps_for_mime(&mime); @@ -3250,6 +3543,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 +4170,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 +4182,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) @@ -4359,6 +4665,86 @@ impl Application for App { config_set!(show_recents, show_recents); return self.update_config(); } + 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::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(); + } + 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(); @@ -5110,6 +5496,7 @@ impl Application for App { .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), + set_default: false, }, None, ); @@ -5953,7 +6340,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 +6425,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( @@ -6444,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_s, .. + space_xxs, + space_xs, + space_s, + .. } = theme::active().cosmic().spacing; let mut tab_column = widget::column::with_capacity(4); @@ -6488,6 +6892,36 @@ impl Application for App { ); } + // 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 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_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::Shrink) + .padding([space_xxs, space_s]), + ); + } + let entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data::(entity) { let tab_view = tab 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/config.rs b/src/config.rs index 0ce9c22..6d89367 100644 --- a/src/config.rs +++ b/src/config.rs @@ -172,6 +172,11 @@ pub struct Config { pub show_details: bool, pub show_recents: bool, pub tab: TabConfig, + /// 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, } @@ -236,11 +241,97 @@ impl Default for Config { show_details: false, show_recents: true, tab: TabConfig::default(), + toolbar: default_toolbar(), type_to_search: TypeToSearch::Recursive, } } } +/// 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 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)] #[serde(default)] pub struct DesktopConfig { 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/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 4a55cdf..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 @@ -399,9 +398,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); 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 94eacdd..ec2260a 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::{ @@ -302,6 +303,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() @@ -558,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 } @@ -577,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 } @@ -665,9 +686,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 +705,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); @@ -759,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); @@ -807,7 +824,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 +842,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); @@ -958,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")] { 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)] 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); + } +}