diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..2cc7b98 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,15 @@ +{ + "format_on_save": "on", + "lsp": { + "rust-analyzer": { + "initialization_options": { + "check": { + "command": "clippy", + }, + "rustfmt": { + "extraArgs": ["+nightly"], + }, + }, + }, + }, +} diff --git a/Cargo.lock b/Cargo.lock index 0084c8d..b57ff0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ dependencies = [ "accesskit_consumer", "atspi-common", "serde", - "zvariant 5.10.0", + "zvariant 5.11.0", ] [[package]] @@ -72,7 +72,7 @@ dependencies = [ "serde", "tokio", "tokio-stream", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -109,13 +109,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ - "cfg-if", "cipher", - "cpufeatures", + "cpubits", + "cpufeatures 0.3.0", ] [[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" @@ -367,7 +361,7 @@ dependencies = [ "serde_repr", "tokio", "url", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -387,7 +381,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -573,11 +567,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus 5.14.0", + "zbus 5.15.0", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", ] [[package]] @@ -588,7 +582,7 @@ checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" dependencies = [ "atspi-common", "serde", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -605,9 +599,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "av-scenechange" @@ -645,9 +639,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] @@ -727,6 +721,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "block2" version = "0.5.1" @@ -778,6 +782,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -800,15 +813,15 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "by_address" @@ -900,9 +913,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -959,11 +972,11 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common", + "crypto-common 0.2.2", "inout", ] @@ -979,7 +992,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 +1001,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", @@ -997,13 +1008,10 @@ 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", -] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "cocoa" @@ -1211,6 +1219,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1301,7 +1315,6 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=160b086#160b086abe03cd34a8a375d7fbe47b24308d1f38" dependencies = [ "bitflags 2.11.1", "cosmic-protocols", @@ -1314,7 +1327,6 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1329,13 +1341,12 @@ dependencies = [ "tokio", "tracing", "xdg", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] name = "cosmic-config-derive" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "quote", "syn", @@ -1343,10 +1354,11 @@ dependencies = [ [[package]] name = "cosmic-files" -version = "1.0.11" +version = "1.0.13" dependencies = [ "anyhow", "atomic_float", + "bstr", "bzip2", "compio", "cosmic-client-toolkit", @@ -1368,7 +1380,7 @@ dependencies = [ "jiff-icu", "jxl-oxide", "libc", - "libcosmic", + "libcosmic-yoda", "log", "lzma-rust2", "md-5", @@ -1410,7 +1422,7 @@ dependencies = [ [[package]] name = "cosmic-files-applet" -version = "1.0.11" +version = "1.0.13" dependencies = [ "cosmic-files", "log", @@ -1420,7 +1432,6 @@ dependencies = [ [[package]] name = "cosmic-freedesktop-icons" version = "0.4.0" -source = "git+https://github.com/pop-os/freedesktop-icons#9c562fe3ecf03241a46a60c0078cd6ea10bd75ce" dependencies = [ "bstr", "btoi", @@ -1445,7 +1456,6 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=160b086#160b086abe03cd34a8a375d7fbe47b24308d1f38" dependencies = [ "bitflags 2.11.1", "wayland-backend", @@ -1459,7 +1469,6 @@ dependencies = [ [[package]] name = "cosmic-settings-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#716da6d6af0b252e2f78aba2ad72ee19ae0241e0" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1471,16 +1480,14 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon" -version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +version = "0.1.1-yoda.1" dependencies = [ - "zbus 5.14.0", + "zbus 5.15.0", ] [[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", @@ -1498,12 +1505,12 @@ 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", @@ -1517,6 +1524,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1526,6 +1539,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1578,7 +1600,6 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryoglyph" version = "0.1.0" -source = "git+https://github.com/pop-os/glyphon.git?tag=cosmic-0.14#c49de15bce4d8254ac136d1be9911960cc85ce12" dependencies = [ "cosmic-text", "etagere", @@ -1597,6 +1618,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "css-color" version = "0.2.8" @@ -1616,10 +1646,13 @@ dependencies = [ ] [[package]] -name = "ctor-lite" -version = "0.1.2" +name = "ctutils" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] [[package]] name = "cursor-icon" @@ -1747,9 +1780,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "zeroize", ] [[package]] @@ -1850,7 +1895,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", @@ -1877,46 +1921,6 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] 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" @@ -1926,9 +1930,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "endi" @@ -2089,23 +2093,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -2127,13 +2117,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -2503,16 +2492,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link 0.2.1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -2786,9 +2765,9 @@ dependencies = [ [[package]] name = "grid" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e2d4c0a8296178d8802098410ca05d86b17a10bb5ab559b3fb404c1f948220" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" [[package]] name = "guillotiere" @@ -2851,9 +2830,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -2887,11 +2866,20 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", ] [[package]] @@ -2988,7 +2976,6 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "dnd", "iced_accessibility", @@ -3009,7 +2996,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 +3004,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", @@ -3042,7 +3027,6 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "iced_core", "iced_futures", @@ -3052,7 +3036,6 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "futures", "iced_core", @@ -3066,7 +3049,6 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bitflags 2.11.1", "bytemuck", @@ -3087,7 +3069,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 +3077,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,7 +3088,6 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#c423ad1bfc25057922406c687f2ddc75ead5ab67" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3123,7 +3102,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 +3118,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 +3135,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,7 +3162,6 @@ 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", @@ -3642,9 +3614,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3692,9 +3664,9 @@ dependencies = [ [[package]] name = "image-extras" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d29ba92ef6970a2685cc758b455d190842b8b9e96c865ffd31cdb9954b7548" +checksum = "60d02eb2c9ccbbab470538fce34c7bc3be7b4e59268e65a3171367b296cdb842" dependencies = [ "image", ] @@ -3717,9 +3689,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "indexmap" @@ -3739,7 +3711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3775,11 +3747,11 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -3814,9 +3786,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -3882,9 +3854,9 @@ checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "6835eea34fb6321b9b3aa7b685c2b433948c09447e389dc017fdf687d5d11e65" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3908,9 +3880,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "3c22e04db9c58f5136eb1757f3d5c49a7b187f49e52185228cbd2f5acdfcc08c" dependencies = [ "proc-macro2", "quote", @@ -4002,9 +3974,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -4249,11 +4221,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -4298,20 +4270,19 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libbz2-rs-sys" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[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", @@ -4358,7 +4329,7 @@ dependencies = [ "tracing", "unicode-segmentation", "url", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -4396,7 +4367,7 @@ dependencies = [ "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.7.5", ] [[package]] @@ -4420,12 +4391,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" @@ -4468,9 +4433,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "loom" @@ -4513,9 +4478,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f" +checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31" dependencies = [ "lyon_path", "num-traits", @@ -4555,11 +4520,11 @@ dependencies = [ [[package]] name = "lzma-rust2" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20" dependencies = [ - "sha2", + "sha2 0.11.0", ] [[package]] @@ -4609,7 +4574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -4663,7 +4628,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", ] @@ -4805,9 +4769,9 @@ dependencies = [ [[package]] name = "no_std_io2" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" dependencies = [ "memchr", ] @@ -4870,16 +4834,16 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.15.0" +version = "4.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8146c105ae33d744e2d645f684d063b01176a99daf5986556266777b428816" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" dependencies = [ "futures-lite", "log", "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -4912,9 +4876,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -5212,9 +5176,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.3" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "is-wsl", "libc", @@ -5229,9 +5193,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" dependencies = [ "libc", "libredox", @@ -5384,11 +5348,11 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest", + "digest 0.11.3", "hmac", ] @@ -5493,18 +5457,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -5734,18 +5698,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn", @@ -5798,16 +5762,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", - "serde", ] [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", + "serde", ] [[package]] @@ -6016,9 +5980,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ "bitflags 2.11.1", ] @@ -6213,7 +6177,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -6390,9 +6354,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap 2.14.0", "itoa", @@ -6424,11 +6388,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -6443,9 +6408,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -6460,8 +6425,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6471,8 +6447,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6542,9 +6529,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" @@ -6610,7 +6597,6 @@ dependencies = [ [[package]] name = "smithay-clipboard" version = "0.8.0" -source = "git+https://github.com/pop-os/smithay-clipboard?tag=sctk-0.20#859b02c88f45c554049a67c6ddeec1692ce0e20b" dependencies = [ "libc", "raw-window-handle", @@ -6641,14 +6627,11 @@ dependencies = [ [[package]] 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]] @@ -6713,12 +6694,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "svg_fmt" version = "0.4.5" @@ -6759,9 +6734,9 @@ dependencies = [ [[package]] name = "synchrony" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c174d82fd56da8214ec095cfe4568e59e5ccb49d060e70c2f98e3ba352b23e45" +checksum = "416090a4d8f6358526df5f9f65dfe28750b8b7bfd1fd8a5620f483fc4a75722c" dependencies = [ "futures-util", "loom", @@ -6814,9 +6789,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -6871,9 +6846,9 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" dependencies = [ "env_logger", "test-log-macros", @@ -6881,16 +6856,26 @@ dependencies = [ ] [[package]] -name = "test-log-macros" -version = "0.2.19" +name = "test-log-core" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn", + "test-log-core", +] + [[package]] name = "thin-cell" version = "0.1.2" @@ -7038,19 +7023,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" @@ -7079,9 +7051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -7137,7 +7109,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.1", + "winnow", ] [[package]] @@ -7158,7 +7130,7 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime", "toml_parser", - "winnow 1.0.1", + "winnow", ] [[package]] @@ -7167,7 +7139,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow", ] [[package]] @@ -7287,9 +7259,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" @@ -7534,11 +7506,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -7547,14 +7519,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -7565,9 +7537,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -7575,9 +7547,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7585,9 +7557,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -7598,9 +7570,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -7774,7 +7746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.39.2", + "quick-xml 0.39.4", "quote", ] @@ -7805,9 +7777,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -8020,12 +7992,10 @@ 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", "clipboard_wayland", - "clipboard_x11", "dnd", "mime 0.1.0", "raw-window-handle", @@ -8540,7 +8510,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" 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", "cfg_aliases", @@ -8560,13 +8529,11 @@ dependencies = [ "winit-wayland", "winit-web", "winit-win32", - "winit-x11", ] [[package]] name = "winit-android" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "android-activity", "bitflags 2.11.1", @@ -8581,7 +8548,6 @@ dependencies = [ [[package]] name = "winit-appkit" 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", "block2 0.6.2", @@ -8603,7 +8569,6 @@ dependencies = [ [[package]] name = "winit-common" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "memmap2 0.9.10", "objc2 0.6.4", @@ -8611,14 +8576,12 @@ dependencies = [ "smol_str", "tracing", "winit-core", - "x11-dl", "xkbcommon-dl", ] [[package]] name = "winit-core" 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", "cursor-icon", @@ -8632,14 +8595,13 @@ dependencies = [ [[package]] name = "winit-orbital" 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", "dpi", "libredox", "orbclient", "raw-window-handle", - "redox_syscall 0.7.4", + "redox_syscall 0.7.5", "smol_str", "tracing", "winit-core", @@ -8648,7 +8610,6 @@ dependencies = [ [[package]] name = "winit-uikit" 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", "block2 0.6.2", @@ -8668,7 +8629,6 @@ dependencies = [ [[package]] name = "winit-wayland" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "ahash", "bitflags 2.11.1", @@ -8694,7 +8654,6 @@ dependencies = [ [[package]] name = "winit-web" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "atomic-waker", "bitflags 2.11.1", @@ -8716,7 +8675,6 @@ dependencies = [ [[package]] name = "winit-win32" 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", "cursor-icon", @@ -8729,43 +8687,11 @@ 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" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -8779,6 +8705,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -8873,39 +8805,6 @@ 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]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "xattr" version = "1.6.1" @@ -9019,9 +8918,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xml-rs" @@ -9112,7 +9011,7 @@ dependencies = [ "rand 0.8.6", "serde", "serde_repr", - "sha1", + "sha1 0.10.6", "static_assertions", "tracing", "uds_windows", @@ -9125,9 +9024,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-executor", @@ -9153,10 +9052,10 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", - "zbus_macros 5.14.0", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "winnow", + "zbus_macros 5.15.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", ] [[package]] @@ -9166,7 +9065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", - "zvariant 5.10.0", + "zvariant 5.11.0", ] [[package]] @@ -9180,7 +9079,7 @@ dependencies = [ "syn", "zbus-lockstep", "zbus_xml", - "zvariant 5.10.0", + "zvariant 5.11.0", ] [[package]] @@ -9198,17 +9097,17 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", - "zbus_names 4.3.1", - "zvariant 5.10.0", - "zvariant_utils 3.3.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", + "zvariant_utils 3.3.1", ] [[package]] @@ -9224,25 +9123,25 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", - "zvariant 5.10.0", + "winnow", + "zvariant 5.11.0", ] [[package]] name = "zbus_xml" -version = "5.1.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" +checksum = "a8067892e940ed1727dea64690378601603b31d62dfde019a5335fbb7c0e0ed9" dependencies = [ - "quick-xml 0.38.4", + "quick-xml 0.39.4", "serde", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", ] [[package]] @@ -9273,9 +9172,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -9335,9 +9234,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.5.1" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "aes", "bzip2", @@ -9352,7 +9251,7 @@ dependencies = [ "memchr", "pbkdf2", "ppmd-rust", - "sha1", + "sha1 0.11.0", "time", "typed-path", "zeroize", @@ -9466,17 +9365,17 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 0.7.15", - "zvariant_derive 5.10.0", - "zvariant_utils 3.3.0", + "winnow", + "zvariant_derive 5.11.0", + "zvariant_utils 3.3.1", ] [[package]] @@ -9494,15 +9393,15 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", - "zvariant_utils 3.3.0", + "zvariant_utils 3.3.1", ] [[package]] @@ -9518,13 +9417,17 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", "syn", - "winnow 0.7.15", + "winnow", ] + +[[patch.unused]] +name = "winit-x11" +version = "0.31.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index c1b75ae..9e3fe98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,17 @@ [package] name = "cosmic-files" -version = "1.0.11" +version = "1.0.13" authors = ["Jeremy Soller "] edition = "2024" license = "GPL-3.0-only" -rust-version = "1.90" +rust-version = "1.93" [dependencies] anyhow = "1" jiff = "0.2" jiff-icu = "0.2" -icu = { version = "2.1.1", features = ["compiled_data"] } -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } +icu = { version = "2.2.0", features = ["compiled_data"] } +cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true } cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true } dirs = "6.0.0" gio = { version = "0.21", optional = true } @@ -24,7 +24,7 @@ log = "0.4" mime_guess = "2" notify-debouncer-full = "0.7" notify-rust = { version = "4", optional = true } -open = "5.3.3" +open = "5.3.4" paste = "1.0" regex = "1" rustc-hash = "2.1" @@ -36,15 +36,15 @@ tokio = { version = "1", features = ["process", "sync"] } trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" } url = "2.5" walkdir = "2.5.0" -wayland-client = { version = "0.31.13", optional = true } +wayland-client = { version = "0.31.14", optional = true } xdg = { version = "3.0", optional = true } xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" } # Compression bzip2 = { version = "0.6", optional = true } #TODO: replace with pure Rust crate flate2 = "1.1" -tar = "0.4.44" +tar = "0.4.45" lzma-rust2 = { version = "0.16", optional = true } -ordermap = { version = "1.1.0", features = ["serde"] } +ordermap = { version = "1.2.0", features = ["serde"] } # Internationalization i18n-embed = { version = "0.16", features = [ "fluent-system", @@ -61,10 +61,11 @@ jxl-oxide = { version = "0.12.5", features = ["image"] } num_cpus = "1.17.0" filetime = "0.2" tracing = "0.1.44" -tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } thiserror = "2.0.18" atomic_float = "1.1.0" num_enum = "0.7.6" +bstr = "1.12.1" # Completion-based IO runtime to enable io_uring / IOCP file IO support. [dependencies.compio] @@ -72,8 +73,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 +84,7 @@ features = [ "autosize", "multi-window", "tokio", - "winit", + "wayland", "surface-message", ] @@ -110,15 +112,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 +146,12 @@ 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" } - - -# [patch.'https://github.com/pop-os/smithay-clipboard'] -# smithay-clipboard = { path = "../smithay-clipboard" } +[patch.'https://github.com/pop-os/cosmic-text.git'] +cosmic-text = { path = "../cosmic-text" } [workspace] members = ["cosmic-files-applet"] diff --git a/build.rs b/build.rs index 5edc53b..206e897 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ -use std::{env, fs, path::PathBuf}; +use std::path::PathBuf; +use std::{env, fs}; use xdgen::{App, Context, FluentString}; fn main() { diff --git a/cosmic-files-applet/Cargo.toml b/cosmic-files-applet/Cargo.toml index b20226b..8fcf2bd 100644 --- a/cosmic-files-applet/Cargo.toml +++ b/cosmic-files-applet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-files-applet" -version = "1.0.11" +version = "1.0.13" edition = "2024" [dependencies] diff --git a/debian/changelog b/debian/changelog index bea1aee..e4697d2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +cosmic-files (1.0.13) noble; urgency=medium + + * Epoch 1.0.13 version update + + -- Jeremy Soller Tue, 12 May 2026 09:39:14 -0600 + +cosmic-files (1.0.12) noble; urgency=medium + + * Epoch 1.0.12 version update + + -- Jeremy Soller Tue, 05 May 2026 10:23:57 -0600 + cosmic-files (1.0.11) noble; urgency=medium * Epoch 1.0.11 version update 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/examples/copy.rs b/examples/copy.rs index 6396209..7f37abd 100644 --- a/examples/copy.rs +++ b/examples/copy.rs @@ -1,6 +1,8 @@ -use cosmic_files::operation::recursive::Method; -use cosmic_files::operation::{Controller, ReplaceResult, recursive::Context}; -use std::{error::Error, io, path::PathBuf}; +use cosmic_files::operation::recursive::{Context, Method}; +use cosmic_files::operation::{Controller, ReplaceResult}; +use std::error::Error; +use std::io; +use std::path::PathBuf; #[compio::main] async fn main() -> Result<(), Box> { diff --git a/examples/dialog.rs b/examples/dialog.rs index be93576..df77069 100644 --- a/examples/dialog.rs +++ b/examples/dialog.rs @@ -1,15 +1,12 @@ -use cosmic::{ - Application, Element, - app::{self, Core, Settings, Task}, - executor, - iced::{Subscription, window}, - widget, -}; +use cosmic::app::{self, Core, Settings, Task}; +use cosmic::iced::{Subscription, window}; +use cosmic::{Application, Element, executor, widget}; use cosmic_files::dialog::{ Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind, DialogMessage, DialogResult, DialogSettings, }; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; fn main() -> Result<(), Box> { let log_format = tracing_subscriber::fmt::format() diff --git a/i18n/ar/cosmic_files.ftl b/i18n/ar/cosmic_files.ftl index 867e83c..c4e3292 100644 --- a/i18n/ar/cosmic_files.ftl +++ b/i18n/ar/cosmic_files.ftl @@ -372,3 +372,14 @@ show-recents = مجلد الحديثة في الشريط الجانبي clear-recents-history = امحُ التأريخ الحديث copy-path = انسخ المسار mixed = مختلط +context-action = إجراء السياق +context-action-confirm-title = شغِّل "{ $name }"؟ +context-action-confirm-warning = + سيُشغِّل هذا على { $items } { $items -> + [one] عنصر + [two] عنصرين + [few] عناصر + [many] عنصرًا + *[other] عنصر + }. +run = شغِّل diff --git a/i18n/ca/cosmic_files.ftl b/i18n/ca/cosmic_files.ftl index 010ddb0..48e7033 100644 --- a/i18n/ca/cosmic_files.ftl +++ b/i18n/ca/cosmic_files.ftl @@ -74,7 +74,7 @@ name-no-slashes = El nom no pot contenir barres. ## Open/Save Dialog -cancel = Cancel·la +cancel = Cancel·lar create = Crea open = Obre open-file = Obre el fixer @@ -352,3 +352,4 @@ sort-newest-first = Primer més recents sort-oldest-first = Primer més antics sort-smallest-to-largest = De petit a gran sort-largest-to-smallest = De gran a petit +run = Executa diff --git a/i18n/cs/cosmic_files.ftl b/i18n/cs/cosmic_files.ftl index 54b9e10..29814ba 100644 --- a/i18n/cs/cosmic_files.ftl +++ b/i18n/cs/cosmic_files.ftl @@ -17,7 +17,7 @@ size = Velikost ## Empty Trash Dialog -empty-trash = Vysypat koš +empty-trash = Vyprázdnit koš empty-trash-warning = Položky v koši budou trvale smazány ## New File/Folder Dialog @@ -195,8 +195,8 @@ deleted = [few] položky *[other] položek } z koše -emptying-trash = Vysypávání koše ({ $progress })... -emptied-trash = Koš vysypán +emptying-trash = Vyprazdňování koše ({ $progress })... +emptied-trash = Koš vyprázdněn restoring = Obnovování { $items } { $items -> [one] položky @@ -403,7 +403,7 @@ sort-largest-to-smallest = Od největšího po nejmenší gallery-preview = Náhled galerie sort = Řazení sort-a-z = A-Z -empty-trash-title = Vysypat koš? +empty-trash-title = Vyprázdnit koš? type-to-search-select = Vybere první shodující se soubor nebo složku pasted-image = Vložený obrázek pasted-text = Vložený text @@ -418,3 +418,12 @@ show-recents = Složka „Nedávné“ v postranním panelu copy-path = Kopírovat cestu clear-recents-history = Vymazat historii „Nedávné“ mixed = Různé +context-action = Kontextová akce +context-action-confirm-title = Spustit „{ $name }“? +context-action-confirm-warning = + Tato akce se spustí pro { $items } { $items -> + [one] položku + [few] položky + *[other] položek + }. +run = Spustit diff --git a/i18n/de/cosmic_files.ftl b/i18n/de/cosmic_files.ftl index 98d2f1b..993af2d 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -429,3 +429,11 @@ remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen move-to = Verschieben nach... copy-path = Pfad kopieren mixed = Gemischt +context-action = Kontextaktion +context-action-confirm-title = „{ $name }“ ausführen? +context-action-confirm-warning = + Dies wird ausgeführt auf { $items } { $items -> + [one] Element + *[other] Elementen + }. +run = Ausführen diff --git a/i18n/el/cosmic_files.ftl b/i18n/el/cosmic_files.ftl index b28272c..7471630 100644 --- a/i18n/el/cosmic_files.ftl +++ b/i18n/el/cosmic_files.ftl @@ -1,15 +1,329 @@ empty-folder = Άδειος φάκελος no-results = Δεν βρέθηκαν αποτελέσματα -trash = Κάδος Ανακύκλωσης +trash = Απορρίμματα recents = Πρόσφατα -cosmic-files = COSMIC Αρχεία -empty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά αντικείμενα) +cosmic-files = Αρχεία COSMIC +empty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά στοιχεία) filesystem = Σύστημα αρχείων home = Προσωπικός φάκελος -networks = Δίκτυο -comment = Αρχεία για το COSMIC περιβάλλον -keywords = Φάκελος;Διαχειριστής; +networks = Δίκτυα +comment = Διαχείριση αρχείων για το περιβάλλον επιφάνειας εργασίας COSMIC +keywords = Αρχείο;Φάκελος;Διαχείριση;Folder;Manager; rename = Μετονομασία... close-tab = Κλείσιμο καρτέλας -light = Φωτεινό -dark = Σκοτεινό +light = Ανοιχτόχρωμο +dark = Σκουρόχρωμο +connect = Σύνδεση +dismiss = Απόρριψη μηνύματος +copy_noun = Αντιγραφή +open-file = Άνοιγμα αρχείου +save = Αποθήκευση +password = Κωδικός πρόσβασης +remove = Αφαίρεση +create = Δημιουργία +pause = Παύση +quit = Έξοδος +calculating = Υπολογισμός... +keep = Διατήρηση +edit = Επεξεργασία +connecting = Σύνδεση... +copy = Αντιγραφή +theme = Θέμα +appearance = Εμφάνιση +name = Όνομα +resume = Συνέχιση +username = Όνομα χρήστη +delete = Διαγραφή +repository = Αποθετήριο +support = Υποστήριξη +eject = Εξαγωγή +group = Ομάδα +skip = Παράλειψη +paste = Επικόλληση +menu-settings = Ρυθμίσεις... +view = Προβολή +undo = Αναίρεση +details = Λεπτομέρειες +sort-a-z = Α-Ω +extract-here = Αποσυμπίεση +cancel = Ακύρωση +sort-z-a = Ω-Α +open = Άνοιγμα +history = Ιστορικό +domain = Τομέας +sort = Ταξινόμηση +settings = Ρυθμίσεις +pending = Σε εκκρεμότητα +open-folder = Άνοιγμα φακέλου +replace = Αντικατάσταση +cut = Αποκοπή +file = Αρχείο +today = Σήμερα +compress = Συμπίεση... +size = Μέγεθος +related-apps = Σχετικές εφαρμογές +zoom-in = Μεγέθυνση +select-all = Επιλογή όλων +icon-size-and-spacing = Μέγεθος και απόσταση εικονιδίων +new-window = Νέο παράθυρο +zoom-out = Σμίκρυνση +default-size = Προεπιλεγμένο μέγεθος +create-archive = Δημιουργία συμπιεσμένου αρχείου +other-apps = Άλλες εφαρμογές +rename-folder = Μετονομασία φακέλου +folder-name = Όνομα φακέλου +connect-anonymously = Ανώνυμη σύνδεση +replace-with = Αντικατάσταση με +mounted-drives = Προσαρτημένες μονάδες +desktop-view-options = Επιλογές προβολής επιφάνειας εργασίας... +show-on-desktop = Εμφάνιση στην επιφάνεια εργασίας +trash-folder-icon = Εικονίδιο φακέλου απορριμμάτων +open-with = Άνοιγμα με +keep-both = Διατήρηση αμφότερων +icon-size = Μέγεθος εικονιδίων +open-with-title = Πώς θέλετε να ανοίξετε το «{ $name }»; +extract-password-required = Απαιτείται κωδικός πρόσβασης +rename-file = Μετονομασία αρχείου +file-name = Όνομα αρχείου +save-file = Αποθήκευση αρχείου +name-hidden = Τα ονόματα που ξεκινούν με «.» θα αποκρύπτονται +folder-already-exists = Υπάρχει ήδη ένας φάκελος με αυτό το όνομα +empty-trash = Άδειασμα απορριμμάτων +permanently-delete-question = Οριστική διαγραφή; +copy-to-button-label = Αντιγραφή +move-to-button-label = Μετακίνηση +run = Εκτέλεση +copy-to-title = Επιλογή προορισμού αντιγραφής +sort-newest-first = Πρώτα τα νεότερα +default-app = { $name } (προεπιλογή) +renamed = Έγινε μετονομασία από «{ $from }» σε «{ $to }» +read-execute = Ανάγνωση και εκτέλεση +deleted = + Έγινε διαγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { trash } +item-modified = Ημερομηνία τροποποίησης: { $modified } +list-view = Προβολή λίστας +reload-folder = Επαναφόρτωση φακέλου +favorite-path-error = Σφάλμα ανοίγματος καταλόγου +progress = { $percent }% +remove-from-sidebar = Αφαίρεση από την πλαϊνή στήλη +restoring = + Ανάκτηση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { trash } ({ $progress })... +network-drive-error = Αδυναμία πρόσβασης σε μονάδα δικτύου +gallery-preview = Προεπισκόπηση συλλογής +sort-smallest-to-largest = Από τα μικρότερα στα μεγαλύτερα +removing-from-recents = + Αφαίρεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { recents } +type-to-search-enter-path = Εισέρχεται στη διαδρομή προς τον κατάλογο ή το αρχείο +emptying-trash = Άδειασμα φακέλου «{ trash }» ({ $progress })... +trashed-on = Ημερομηνία διαγραφής +compressing = + Συμπίεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στο αρχείο «{ $to }» ({ $progress })... +move-to-trash = Μετακίνηση στα απορρίμματα +menu-about = Σχετικά με τα Αρχεία COSMIC... +setting-executable-and-launching = Ορισμός του «{ $name }» ως εκτελέσιμου και εκκίνηση +open-multiple-files = Άνοιγμα πολλαπλών αρχείων +menu-open-with = Άνοιγμα με... +extracted = + Έγινε αποσυμπίεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από το αρχείο «{ $from }» στον φάκελο «{ $to }» +create-new-folder = Δημιουργία νέου φακέλου +original-file = Πρωτότυπο αρχείο +read-write-execute = Ανάγνωση, εγγραφή και εκτέλεση +set-permissions = Έγινε ορισμός των δικαιωμάτων για το «{ $name }» σε: { $mode } +sort-by-size = Ταξινόμηση κατά μέγεθος +item-size = Μέγεθος: { $size } +permanently-deleting = + Οριστική διαγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } +read-write = Ανάγνωση και εγγραφή +none = Κανένα +items = Στοιχεία: { $items } +type = Τύπος: { $mime } +compressed = + Έγινε συμπίεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στο αρχείο «{ $to }» +replace-warning = Θέλετε να το αντικαταστήσετε με αυτό που αποθηκεύετε; Αυτό θα οδηγήσει στην αντικατάσταση του περιεχομένου του. +new-file = Νέο αρχείο... +open-in-terminal = Άνοιγμα σε τερματικό +open-multiple-folders = Άνοιγμα πολλαπλών φακέλων +remember-password = Απομνημόνευση κωδικού πρόσβασης +show-details = Εμφάνιση λεπτομερειών +grid-spacing = Απόσταση πλέγματος +extract-to = Αποσυμπίεση σε... +add-network-drive = Προσθήκη μονάδας δικτύου +copying = + Αντιγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στον φάκελο «{ $to }» ({ $progress })... +sort-oldest-first = Πρώτα τα παλαιότερα +create-new-file = Δημιουργία νέου αρχείου +sort-by-trashed = Ταξινόμηση κατά ημερομηνία διαγραφής +replace-warning-operation = Θέλετε να το αντικαταστήσετε; Αυτό θα οδηγήσει στην αντικατάσταση του περιεχομένου του. +try-again = Δοκιμή ξανά +copied = + Έγινε αντιγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στον φάκελο «{ $to }» +other = Άλλο +open-in-new-window = Άνοιγμα σε νέο παράθυρο +sort-by-modified = Ταξινόμηση κατά ημερομηνία τροποποίησης +list-directories-first = Παράθεση των καταλόγων πρώτα +read-only = Μόνο ανάγνωση +browse-store = Περιήγηση στο { $store } +enter-server-address = Εισαγάγετε τη διεύθυνση διακομιστή +remove-from-recents = Αφαίρεση από τα πρόσφατα +apply-to-all = Εφαρμογή σε όλα +moving = + Μετακίνηση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στον φάκελο «{ $to }» ({ $progress })... +change-wallpaper = Αλλαγή ταπετσαρίας... +network-drive-description = + Οι διευθύνσεις διακομιστών αποτελούνται από ένα πρόθεμα πρωτοκόλλου και μια διεύθυνση. + Παραδείγματα: ssh://192.168.0.1, ftp://[2001:db8::1] +deleting = + Διαγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { trash } ({ $progress })... +single-click = Μονό κλικ για άνοιγμα +setting-permissions = Ορισμός των δικαιωμάτων για το «{ $name }» σε: { $mode } +owner = Κάτοχος +creating = Δημιουργία του «{ $name }» στον φάκελο «{ $parent }» +execute-only = Μόνο εκτέλεση +open-item-location = Άνοιγμα τοποθεσίας στοιχείου +set-executable-and-launched = Έγινε ορισμός του «{ $name }» ως εκτελέσιμου και εκκινήθηκε +mount-error = Αδυναμία πρόσβασης στη μονάδα +grid-view = Προβολή πλέγματος +set-and-launch = Ορισμός και εκκίνηση +removed-from-recents = + Έγινε αφαίρεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { recents } +add-to-sidebar = Προσθήκη στην πλαϊνή στήλη +item-created = Ημερομηνία δημιουργίας: { $created } +network-drive-schemes = + Διαθέσιμα πρωτόκολλα,Πρόθεμα + AppleTalk,afp:// + File Transfer Protocol,ftp:// ή ftps:// + Network File System,nfs:// + Server Message Block,smb:// + SSH File Transfer Protocol,sftp:// ή ssh:// + WebDAV,dav:// ή davs:// +set-executable-and-launch = Ορισμός ως εκτελέσιμο και εκκίνηση +restored = + Έγινε ανάκτηση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τα { trash } +type-to-search-recursive = Κάνει αναζήτηση στον τρέχοντα φάκελο και όλους τους υποφακέλους +progress-paused = { $percent }%, σε παύση +cancelled = Ακυρωμένες +new-folder = Νέος φάκελος... +match-desktop = Συμφωνία με την επιφάνεια εργασίας +operations-running-finished = + Εκτέλεση { $running } { $running -> + [one] διεργασίας + *[other] διεργασιών + } ({ $percent }%), { $finished } ολοκληρωμένες... +sort-by-name = Ταξινόμηση κατά όνομα +edit-history = Ιστορικό επεξεργασιών +show-hidden-files = Εμφάνιση κρυφών αρχείων +progress-failed = { $percent }%, απέτυχε +item-accessed = Ημερομηνία πρόσβασης: { $accessed } +extract-to-title = Αποσυμπίεση σε φάκελο +extracting = + Αποσυμπίεση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από το αρχείο «{ $from }» στον φάκελο «{ $to }» ({ $progress })... +permanently-deleted = + Έγινε οριστική διαγραφή { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } +complete = Ολοκληρωμένες +write-execute = Εγγραφή και εκτέλεση +desktop-folder-content = Περιεχόμενο φακέλου επιφάνειας εργασίας +renaming = Μετονομασία από «{ $from }» σε «{ $to }» +set-executable-and-launch-description = Θέλετε να ορίσετε το «{ $name }» ως εκτελέσιμο και να το εκκινήσετε; +no-history = Δεν υπάρχουν στοιχεία στο ιστορικό. +emptied-trash = Έγινε άδειασμα του φακέλου «{ trash }» +sort-largest-to-smallest = Από τα μεγαλύτερα στα μικρότερα +restore-from-trash = Ανάκτηση από τα απορρίμματα +moved = + Έγινε μετακίνηση { $items } { $items -> + [one] στοιχείου + *[other] στοιχείων + } από τον φάκελο «{ $from }» στον φάκελο «{ $to }» +progress-cancelled = { $percent }%, ακυρώθηκε +open-in-new-tab = Άνοιγμα σε νέα καρτέλα +unknown-folder = άγνωστος φάκελος +created = Έγινε δημιουργία του «{ $name }» στον φάκελο «{ $parent }» +delete-permanently = Οριστική διαγραφή +write-only = Μόνο εγγραφή +display-settings = Ρυθμίσεις οθόνης... +new-tab = Νέα καρτέλα +failed = Αποτυχημένες +modified = Ημερομηνία τροποποίησης +desktop-appearance = Εμφάνιση επιφάνειας εργασίας... +file-already-exists = Υπάρχει ήδη ένα αρχείο με αυτό το όνομα +permanently-delete-warning = Θα διαγραφούν οριστικά τα εξής: { $target }. Δεν είναι δυνατή η αναίρεση αυτής της ενέργειας. +favorite-path-error-description = + Αδυναμία ανοίγματος του «{ $path }» + Το «{ $path }» ενδέχεται να μην υπάρχει ή να μην έχετε το δικαίωμα να το ανοίξετε + + Θέλετε να το αφαιρέσετε από την πλαϊνή στήλη; +empty-trash-warning = Τα στοιχεία του φακέλου «Απορρίμματα» θα διαγραφούν οριστικά +empty-trash-title = Άδειασμα απορριμμάτων; +type-to-search = Πληκτρολόγηση για αναζήτηση +notification-in-progress = Βρίσκονται σε εξέλιξη διεργασίες αρχείων +name-no-slashes = Το όνομα δεν μπορεί να περιέχει καθέτους +replace-title = Το «{ $filename }» υπάρχει ήδη σε αυτήν την τοποθεσία +name-invalid = Το όνομα δεν μπορεί να είναι «{ $filename }» +operations-running = + Εκτέλεση { $running } { $running -> + [one] διεργασίας + *[other] διεργασιών + } ({ $percent }%)... +context-action-confirm-title = Εκτέλεση του «{ $name }»; +pasted-image = Επικολλημένη εικόνα +pasted-text = Επικολλημένο κείμενο +pasted-video = Επικολλημένο βίντεο +show-recents = Φάκελος «Πρόσφατα» στην πλαϊνή στήλη +move-to = Μετακίνηση σε... +copy-path = Αντιγραφή διαδρομής +move-to-title = Επιλογή προορισμού μετακίνησης +selected-items = { $items } επιλεγμένα στοιχεία +context-action = Ενέργεια περιβάλλοντος +copy-to = Αντιγραφή σε... +mixed = Μικτό +type-to-search-select = Επιλέγει την πρώτη αντιστοιχία αρχείου ή φακέλου +clear-recents-history = Απαλοιφή ιστορικού πρόσφατων +context-action-confirm-warning = + Θα εκτελεστεί σε { $items } { $items -> + [one] στοιχείο + *[other] στοιχεία + }. diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 69a76c6..489ff7a 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 @@ -116,6 +117,7 @@ permanently-delete-warning = {$target} will be permanently deleted. This action ## Rename Dialog rename-file = Rename file rename-folder = Rename folder +rename-confirm = Rename ## Replace Dialog replace = Replace @@ -138,6 +140,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/fi/cosmic_files.ftl b/i18n/fi/cosmic_files.ftl index f22b7b4..f1c3c58 100644 --- a/i18n/fi/cosmic_files.ftl +++ b/i18n/fi/cosmic_files.ftl @@ -405,3 +405,12 @@ removed-from-recents = [one] kohde *[other] kohdetta } viimeaikaisista +mixed = Sekoitettu +context-action = Kontekstitoiminto +context-action-confirm-title = Suoritetaanko "{ $name }"? +context-action-confirm-warning = + Tämä suorittaa { $items } { $items -> + [one] kohteen + *[other] kohdetta + }. +run = Suorita diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index a461a9f..9f7941a 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 @@ -437,3 +443,11 @@ show-recents = Dossier Récents dans la barre latérale copy-path = Copier le chemin clear-recents-history = Effacer l'historique des Récents mixed = Mixte +context-action-confirm-title = Exécuter "{ $name }" ? +context-action-confirm-warning = + Cela exécutera sur { $items } { $items -> + [one] élément + *[other] éléments + }. +run = Exécuter +context-action = Action contextuelle diff --git a/i18n/ga/cosmic_files.ftl b/i18n/ga/cosmic_files.ftl index 5a9aa9a..04d546f 100644 --- a/i18n/ga/cosmic_files.ftl +++ b/i18n/ga/cosmic_files.ftl @@ -434,3 +434,11 @@ show-recents = Fillteán le déanaí sa bharra taoibh clear-recents-history = Glan stair na n-earraí le déanaí copy-path = Cóipeáil an chosán mixed = Measctha +context-action = Gníomh comhthéacsúil +context-action-confirm-title = Rith "{ $name }"? +context-action-confirm-warning = + Rithfidh sé seo ar { $items } { $items -> + [one] mhír + *[other] míreanna + }. +run = Rith diff --git a/i18n/hu/cosmic_files.ftl b/i18n/hu/cosmic_files.ftl index 6a87c44..2cf31e6 100644 --- a/i18n/hu/cosmic_files.ftl +++ b/i18n/hu/cosmic_files.ftl @@ -437,3 +437,11 @@ show-recents = Legutóbbiak mappa megjelenítése az oldalsávban copy-path = Útvonal másolása clear-recents-history = Legutóbbiak előzményének törlése mixed = Vegyes +context-action = Helyi művelet +context-action-confirm-title = Futtatod ezt: „{ $name }”? +context-action-confirm-warning = + Ez a művelet { $items } { $items -> + [one] elemen + *[other] elemen + } fog lefutni. +run = Futtatás diff --git a/i18n/id/cosmic_files.ftl b/i18n/id/cosmic_files.ftl index 2f1f21b..aefff98 100644 --- a/i18n/id/cosmic_files.ftl +++ b/i18n/id/cosmic_files.ftl @@ -319,3 +319,11 @@ show-recents = Map terbaru di bilah sisi clear-recents-history = Bersihkan riwayat Terbaru copy-path = Salin jalur mixed = Bercampur +context-action = Tindakan konteks +context-action-confirm-title = Jalankan "{ $name }"? +run = Jalankan +context-action-confirm-warning = + Ini akan dijalankan pada { $items } { $items -> + [one] item + *[other] item + }. diff --git a/i18n/kk/cosmic_files.ftl b/i18n/kk/cosmic_files.ftl index 3b97025..8aabf01 100644 --- a/i18n/kk/cosmic_files.ftl +++ b/i18n/kk/cosmic_files.ftl @@ -319,3 +319,11 @@ show-recents = Бүйір панеліндегі «Жуырдағы құжатт clear-recents-history = Жуырдағылар тарихын өшіру copy-path = Орналасқан жолын көшіру mixed = Аралас +context-action = Контекст әрекеті +context-action-confirm-title = "{ $name }" орындау керек пе? +context-action-confirm-warning = + Бұл { $items } орындалады { $items -> + [one] нәрсеге + *[other] нәрсеге + }. +run = Орындау diff --git a/i18n/ko/cosmic_files.ftl b/i18n/ko/cosmic_files.ftl index 3a9f2f6..5683c39 100644 --- a/i18n/ko/cosmic_files.ftl +++ b/i18n/ko/cosmic_files.ftl @@ -277,3 +277,4 @@ copy-to-button-label = 복사 move-to-button-label = 이동 clear-recents-history = 최근 기록 비우기 copy-path = 복사 경로 +move-to-title = 이동 위치 선택 diff --git a/i18n/lo/cosmic_files.ftl b/i18n/lo/cosmic_files.ftl new file mode 100644 index 0000000..e69de29 diff --git a/i18n/lt/cosmic_files.ftl b/i18n/lt/cosmic_files.ftl index acceb7c..2a8e36f 100644 --- a/i18n/lt/cosmic_files.ftl +++ b/i18n/lt/cosmic_files.ftl @@ -1,5 +1,5 @@ progress = { $percent }% -cosmic-files = Cosmic Files +cosmic-files = COSMIC Failai empty-folder = Tuščias aplankas empty-folder-hidden = Tuščias aplankas (turi paslėptų failų) no-results = Rezultatų nėra @@ -76,7 +76,7 @@ delete = Ištrinti permanently-delete-warning = { $target } bus ištrintas visam laikui. Šis veiksmas yra negrįžtamas. rename-file = Pervadinti failą rename-folder = Pervadinti aplanką -replace = Pakeisti +replace = Keisti replace-title = „{ $filename }“ jau egzistuoja šioje vietoje replace-warning = Ar norite pakeisti tai su tuo, kas yra įrašoma? Keičiant bus pakeistas turinys. replace-warning-operation = Ar norite pakeisti tai? Pakeičiant bus keičiamas turinys. @@ -244,7 +244,7 @@ item-created = Sukurtas: { $created } item-modified = Modifikuota: { $modified } item-accessed = Paskutinė prieiga: { $accessed } calculating = Skaičiuojama... -settings = Nustatymai +settings = Nuostatos single-click = Vieno paspaudimo atidarymas appearance = Išvaizda match-desktop = Pagal darbalaukio temą @@ -280,18 +280,18 @@ quit = Išeiti edit = Redaguoti cut = Iškirpti copy = Kopijuoti -paste = Įklijuoti -select-all = Pažymėti viską -zoom-in = Priartinti +paste = Įdėti +select-all = Žymėti viską +zoom-in = Artinti default-size = Numatytas dydis -zoom-out = Nutolinti +zoom-out = Tolinti view = Rodymas grid-view = Tinklelio išdėstymas list-view = Sąrašo išdėstymas show-hidden-files = Rodyti paslėptus failus list-directories-first = Pirmiau pateikti aplankus gallery-preview = Galerijos peržiūra -menu-settings = Nustatymai... +menu-settings = Nuostatos... menu-about = Apie COSMIC Files... sort = Rikiuoti sort-a-z = A-Ž @@ -302,7 +302,7 @@ sort-smallest-to-largest = Nuo mažiausio iki didžiausio sort-largest-to-smallest = Nuo didžiausio iki mažiausio dark = Tamsus light = Šviesus -comment = COSMIC desktop failų tvarkyklė +comment = COSMIC aplinkos failų tvarkyklė keywords = Aplankas;Tvarkyklė; copy-to-title = Pasirinkti kopijavimo vietą copy-to-button-label = Kopijuoti @@ -317,3 +317,4 @@ clear-recents-history = Išvalyti Neseniai naudotų istoriją copy-to = Kopijuoti į... move-to = Perkeltiį į... copy-path = Kopijuoti kelią +theme = Stilius diff --git a/i18n/pl/cosmic_files.ftl b/i18n/pl/cosmic_files.ftl index b0bc7a8..9af6027 100644 --- a/i18n/pl/cosmic_files.ftl +++ b/i18n/pl/cosmic_files.ftl @@ -438,3 +438,11 @@ show-recents = Ostatnie katalogi w panelu bocznym clear-recents-history = Wyczyść bierzącą historię copy-path = Skopiuj ścieżkę mixed = Mieszane +context-action = Akcja sytuacyjna +context-action-confirm-title = Uruchomić "{ $name }"? +context-action-confirm-warning = + Zostanie uruchomionych { $items } { $items -> + [one] element + *[other] elementów + }. +run = Uruchom diff --git a/i18n/pt-BR/cosmic_files.ftl b/i18n/pt-BR/cosmic_files.ftl index 86ebd88..6d33279 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -437,3 +437,11 @@ show-recents = Pasta de recentes na barra lateral clear-recents-history = Limpar histórico de recentes copy-path = Copiar caminho mixed = Misto +context-action = Ação de contexto +context-action-confirm-title = Executar "{ $name }"? +context-action-confirm-warning = + Isso será executado em { $items } { $items -> + [one] item + *[other] itens + }. +run = Executar diff --git a/i18n/pt/cosmic_files.ftl b/i18n/pt/cosmic_files.ftl index 33921f1..211b094 100644 --- a/i18n/pt/cosmic_files.ftl +++ b/i18n/pt/cosmic_files.ftl @@ -352,3 +352,5 @@ sort-newest-first = Mais recentes primeiro sort-oldest-first = Mais antigos primeiro sort-smallest-to-largest = Do menor para o maior sort-largest-to-smallest = Do maior para o menor +context-action-confirm-title = Executar "{ $name }"? +run = Executar diff --git a/i18n/ru/cosmic_files.ftl b/i18n/ru/cosmic_files.ftl index e9673f1..f8e76c1 100644 --- a/i18n/ru/cosmic_files.ftl +++ b/i18n/ru/cosmic_files.ftl @@ -381,3 +381,11 @@ show-recents = «Недавние документы» в бок. панели clear-recents-history = Очистить историю недавних copy-path = Копировать путь mixed = Смешанные +context-action = Контекстная команда +context-action-confirm-title = Выполнить «{ $name }»? +context-action-confirm-warning = + Команда затронет { $items } { $items -> + [one] элемент + *[other] элем. + }. +run = Выполнить diff --git a/i18n/sr/cosmic_files.ftl b/i18n/sr/cosmic_files.ftl index e69de29..b71afe5 100644 --- a/i18n/sr/cosmic_files.ftl +++ b/i18n/sr/cosmic_files.ftl @@ -0,0 +1,329 @@ +open-file = Отвори датотеку +quit = Изађи +cancel = Откажи +open = Отвори +run = Покрени +connect = Повежи +save = Сачувај +password = Лозинка +remove = Уклони +appearance = Изглед +username = Корисничко име +light = Светла +dark = Тамна +settings = Подешавања +replace = Замени +size = Величина +sort-newest-first = Најновије прво +default-app = { $name } (подразумевано) +renamed = Преименована „{ $from }“ у „{ $to }“ +read-execute = Читање и извршавање +deleted = + Обрисано је { $items } { $items -> + [one] ставка + *[other] ставки + } из { trash } +item-modified = Измењено: { $modified } +dismiss = Одбаци поруку +list-view = Преглед у виду списака +reload-folder = Поново учитај фасциклу +copy_noun = Копија +favorite-path-error = Грешка при отварању директоријума +progress = { $percent }% +remove-from-sidebar = Уклони из бочне траке +related-apps = Повезани програми +restoring = + Враћање { $items } { $items -> + [one] ставке + *[other] ставки + } из { trash } ({ $progress })... +network-drive-error = Немогуће приступити мрежном уређају +gallery-preview = Преглед галерије +sort-smallest-to-largest = Од најмање до највеће +zoom-in = Увећајте приказ +select-all = Означи све +icon-size-and-spacing = Величина и размак иконица +removing-from-recents = + Уклањање { $items } { $items -> + [one] ставке + *[other] ставки + } из { recents } +cosmic-files = Космик датотеке +type-to-search-enter-path = Уноси путању до директоријума или датотеке +trash = Смеће +emptying-trash = Пражњење { trash } ({ $progress })... +trashed-on = Премештено у смеће +new-window = Нови прозор +zoom-out = Умањите приказ +compressing = + Сажимање { $items } { $items -> + [one] ставке + *[other] ставки + } из „{ $from }“ у „{ $to }“ ({ $progress })... +move-to-trash = Премести у смеће +menu-about = О програму Космик Датотеке... +setting-executable-and-launching = Подешавање „{ $name }“ као извршне датотеке и покретање +open-multiple-files = Отвори више датотека +default-size = Подразумевана величина +menu-open-with = Отвори програмом... +extracted = + Извлачено { $items } { $items -> + [one] ставка + *[other] ставки + } из „{ $from }“ у „{ $to }“ +create-new-folder = Направи нову фасциклу +original-file = Изворна датотека +create = Направи +create-archive = Направи архиву +read-write-execute = Читање, уписивање и извршавање +other-apps = Остали програми +set-permissions = Овлашћења за „{ $name }“ су постављена на { $mode } +pause = Паузирај +calculating = Израчунавам… +sort-by-size = Поређај по величини +rename = Преименуј... +empty-folder-hidden = Празна фасцикла (има сакривених ставки) +keep = Задржи +item-size = Величина: { $size } +permanently-deleting = + Трајно брисање { $items } { $items -> + [one] ставке + *[other] ставки + } +edit = Уреди +connecting = Повезујем се... +read-write = Читање и уписивање +copy = Умножи +none = Ништа +items = Ставки: { $items } +no-results = Нису пронађени резултати +theme = Тема +type = Врста: { $mime } +compressed = + Сажето { $items } { $items -> + [one] ставка + *[other] ставки + } из „{ $from }“ у „{ $to }“ +replace-warning = Да ли желите да га замените оним који снимате? Замена ће преписати његов садржај. +rename-folder = Преименуј фасциклу +new-file = Нова датотека... +close-tab = Затвори језичак +name = Назив +open-in-terminal = Отвори у терминалу +resume = Настави +open-multiple-folders = Отвори више фасцикла +remember-password = Запамти лозинку +show-details = Прикажи детаље +grid-spacing = Размак мреже +extract-to = Распакуј у... +add-network-drive = Додај мрежни уређај +copying = + Умножавање { $items } { $items -> + [one] ставке + *[other] ставки + } из „{ $from }“ у „{ $to }“ ({ $progress })... +delete = Обриши +sort-oldest-first = Најстарије прво +repository = Ризница +create-new-file = Направи нову датотеку +sort-by-trashed = Поређај по времену брисања +replace-warning-operation = Да ли желите да га замените? Замена ће преписати његов садржај. +support = Подршка +try-again = Покушај поново +eject = Избаци +copied = + Умножено { $items } { $items -> + [one] ставке + *[other] ставки + } из „{ $from }“ у „{ $to }“ +other = Друго +open-in-new-window = Отвори у новом прозору +empty-folder = Празна фасцикла +sort-by-modified = Поређај по датуму измене +list-directories-first = Прикажи директоријуме прво +read-only = Само за читање +folder-name = Назив фасцикле +browse-store = Разгледај { $store } +enter-server-address = Унесите адресу сервера +remove-from-recents = Уклони из недавних +connect-anonymously = Повежи се анонимно +group = Група +apply-to-all = Примени на све +skip = Прескочи +paste = Залепи +menu-settings = Подешавања... +moving = + Премештање { $items } { $items -> + [one] датотеке + *[other] датотека + } из „{ $from }“ у „{ $to }“ ({ $progress })... +replace-with = Замени са +recents = Недавно +change-wallpaper = Промени позадину... +network-drive-description = + Адресе сервера укључују префикс протокола и адресу. + Примери: ssh://192.168.0.1, ftp://[2001:db8::1] +deleting = + Брише се { $items } { $items -> + [one] ставка + *[other] ставки + } из { trash } ({ $progress })... +single-click = Један клик за отварање +view = Преглед +undo = Опозови +setting-permissions = Подешавање овлашћења за „{ $name }“ на { $mode } +owner = Власник +creating = Правим „{ $name }“ у „{ $parent }“ +execute-only = Само извршавање +open-item-location = Отвори локацију ставке +details = Детаљи +set-executable-and-launched = Постављено „{ $name }“ као извршну датотеку и покренуто +mounted-drives = Прикључени дискови +sort-a-z = А-Ш +mount-error = Немогуће приступити уређају +extract-here = Извуци +grid-view = Преглед у виду мреже +filesystem = Систем датотека +set-and-launch = Подеси и покрени +removed-from-recents = + Уклоњено { $items } { $items -> + [one] ставке + *[other] ставки + } из { recents } +add-to-sidebar = Додај у страничник +item-created = Направљено: { $created } +network-drive-schemes = + Доступни протоколи,Префикс + ЕплТок,afp:// + Протокол за пренос датотека,ftp:// или ftps:// + Мрежни систем датотека,nfs:// + Серверски блок порука,smb:// + SSH протокол за пренос датотека,sftp:// или ssh:// + ВебДАВ,dav:// или davs:// +home = Лична +set-executable-and-launch = Постави као извршну и покрени +restored = + Враћено { $items } { $items -> + [one] ставка + *[other] ставки + } из { trash } +sort-z-a = Ш-А +type-to-search-recursive = Претражује тренутну фасциклу и све подфасцикле +history = Историјат +progress-paused = { $percent }%, паузирано +desktop-view-options = Могућности приказа радне површине... +show-on-desktop = Прикажи на радној површини +cancelled = Отказано +new-folder = Нова фасцикла... +match-desktop = Прати радну површину +domain = Домен +operations-running-finished = + { $running } { $running -> + [one] радња покренута + *[other] радње покренуте + } ({ $percent }%), { $finished } завршено... +sort-by-name = Поређај по називу +edit-history = Историјат уређивања +sort = Поређај +show-hidden-files = Прикажи скривене датотеке +progress-failed = { $percent }%, није успело +trash-folder-icon = Иконица фасцикле Смеће +item-accessed = Приступљено: { $accessed } +extract-to-title = Распакуј у фасциклу +open-with = Отвори помоћу +keep-both = Задржи оба +icon-size = Величина иконице +open-with-title = Како желите да отворите „{ $name }“? +extracting = + Извлачење { $items } { $items -> + [one] ставке + *[other] ставки + } из „{ $from }“ у „{ $to }“ ({ $progress })... +permanently-deleted = + Трајно обрисано { $items } { $items -> + [one] ставке + *[other] ставки + } +complete = Завршено +write-execute = Писање и извршавање +extract-password-required = Потребна је лозинка +pending = У току +desktop-folder-content = Садржај фасцикле радне површине +renaming = Преименовање „{ $from }“ у „{ $to }“ +set-executable-and-launch-description = Да ли желите да поставите „{ $name }“ као извршну и покренете је? +no-history = Нема ставки у историјату. +open-folder = Отвори фасциклу +emptied-trash = Опражњено { trash } +rename-file = Преименуј датотеку +sort-largest-to-smallest = Од највеће до најмање +restore-from-trash = Врати из смећа +cut = Исеци +moved = + Премештено { $items } { $items -> + [one] датотеке + *[other] датотека + } из „{ $from }“ у „{ $to }“ +progress-cancelled = { $percent }%, отказано +open-in-new-tab = Отвори у новом језичку +unknown-folder = непозната фасцикла +file = Датотека +file-name = Назив датотеке +save-file = Сачувај датотеку +created = Направљено „{ $name }“ у „{ $parent }“ +delete-permanently = Трајно обриши +networks = Мреже +write-only = Само писање +today = Данас +display-settings = Подешавања екрана... +new-tab = Нови језичак +failed = Неуспешно +modified = Измењено +desktop-appearance = Изглед радне површине... +file-already-exists = Датотека са овим називом већ постоји +name-hidden = Називи који почињу тачком „.“ ће бити сакривени +folder-already-exists = Фасцикла са овим називом већ постоји +permanently-delete-warning = { $target } ће бити трајно обрисано. Ова радња се не може поништити. +favorite-path-error-description = + Не можемо да отворимо „{ $path }“ + „{ $path }“ можда не постоји или немате дозволу за његово отварање + + Желите ли да га уклоните из бочне површи? +empty-trash-warning = Ставке у смећу биће трајно обрисане +empty-trash = Испразни смеће +empty-trash-title = Испразнити смеће? +type-to-search = Куцајте за претрагу +notification-in-progress = Радње са датотекама су у току +name-no-slashes = Назив не може садржати косе црте +permanently-delete-question = Трајно обриши? +replace-title = „{ $filename }“ већ постоји на овој локацији +name-invalid = Назив не може бити „{ $filename }“ +operations-running = + { $running } { $running -> + [one] радња покренута + *[other] радње покренуте + } ({ $percent }%)... +comment = Управник датотека за Космик радну површину +keywords = Folder;Manager;Фасцикла;Управник;fascikla;upravnik; +copy-to-title = Изабери одредиште умножавања +copy-to-button-label = Умножи +move-to-title = Изабери одредиште премештања +move-to-button-label = Помери +context-action = Контекстна радња +context-action-confirm-title = Покрени „{ $name }“? +context-action-confirm-warning = + Ово ће се извршити на { $items } { $items -> + [one] ставку + *[other] ставки + }. +selected-items = Изабраних { $items } ставки +mixed = Помешано +pasted-image = Убачена слика +pasted-text = Убачен текст +pasted-video = Убачен видео +show-recents = Недавна фасцикла у бочној површи +type-to-search-select = Обира прву подударајућу датотеку или фасциклу +clear-recents-history = Очисти историју недавних +compress = Сажми... +copy-to = Умножи у... +move-to = Помери у... +copy-path = Умножи путању diff --git a/i18n/sv/cosmic_files.ftl b/i18n/sv/cosmic_files.ftl index cbd28dd..974c7dc 100644 --- a/i18n/sv/cosmic_files.ftl +++ b/i18n/sv/cosmic_files.ftl @@ -410,3 +410,11 @@ move-to = Flytta till... show-recents = Mapp för senast använda filer i sidofältet clear-recents-history = Töm historik för Senaste copy-path = Kopiera sökväg +context-action = Kontextåtgärd +context-action-confirm-title = Kör "{ $name }"? +context-action-confirm-warning = + Detta kommer att köras på { $items } { $items -> + [one] objekt + *[other] objekt + }. +run = Kör diff --git a/i18n/uk/cosmic_files.ftl b/i18n/uk/cosmic_files.ftl index 611c2b1..7299a7d 100644 --- a/i18n/uk/cosmic_files.ftl +++ b/i18n/uk/cosmic_files.ftl @@ -5,7 +5,7 @@ filesystem = Файлова система home = Домівка trash = Смітник recents = Нещодавні -undo = Відмінити +undo = Скасувати # List view name = Назва modified = Змінено @@ -86,6 +86,7 @@ copying = copied = Скопійовано { $items } { $items -> [one] елемент + [few] елементи *[other] елеменів } з «{ $from }» в «{ $to }» emptying-trash = Спорожнення { trash } ({ $progress })... @@ -98,7 +99,8 @@ moving = moved = Переміщено { $items } { $items -> [one] елемент - *[other] елементи + [few] елементи + *[other] елементів } з «{ $from }» в «{ $to }» renaming = Перейменування «{ $from }» на «{ $to }» renamed = Перейменовано «{ $from }» на «{ $to }» @@ -110,7 +112,8 @@ restoring = restored = Відновлено { $items } { $items -> [one] елемент - *[other] елементи + [few] елементи + *[other] елементів } з { trash } unknown-folder = невідома тека @@ -223,7 +226,7 @@ permanently-delete-question = Остаточно видалити? delete = Видалити permanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати. set-executable-and-launch = Зробити виконуваним і запустити -set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його? +set-executable-and-launch-description = Бажаєте зробити «{ $name }» виконуваним і запустити його? set-and-launch = Зробити і запустити open-with = Відкрити за допомогою owner = Власник @@ -245,7 +248,7 @@ favorite-path-error-description = Вилучити з бічної панелі? keep = Залишити add-network-drive = Додати мережевий диск -connect = З'єднати +connect = З’єднати connect-anonymously = З'єднатись анонімно connecting = З'єднання… domain = Домен @@ -274,12 +277,13 @@ compressing = Стиснення { $items } { $items -> [one] елемента *[other] елементів - } з "{ $from }" до "{ $to }" ({ $progress })... + } з «{ $from }» до «{ $to }» ({ $progress })... compressed = Стиснуто { $items } { $items -> [one] елемент - *[other] елементи - } з "{ $from }" до "{ $to }" + [few] елементи + *[other] елементів + } з «{ $from }» до «{ $to }» deleting = Видалення { $items } { $items -> [one] елемента @@ -288,6 +292,7 @@ deleting = deleted = Видалено { $items } { $items -> [one] елемент + [few] елементи *[other] елементи } з { trash } extracting = @@ -298,7 +303,8 @@ extracting = extracted = Видобуто { $items } { $items -> [one] елемент - *[other] елементи + [few] елементи + *[other] елементів } з «{ $from }» в «{ $to }» setting-executable-and-launching = Надання «{ $name }» прав на виконання та запуск set-executable-and-launched = «{ $name }» надано права на виконання і відкрито @@ -343,7 +349,8 @@ permanently-deleting = permanently-deleted = Остаточно вилучено { $items } { $items -> [one] елемент - *[other] елементи + [few] елементи + *[other] елементів } removing-from-recents = Вилучення { $items } { $items -> @@ -353,13 +360,14 @@ removing-from-recents = removed-from-recents = Вилучено { $items } { $items -> [one] елемент - *[other] елементи + [few] елементи + *[other] елементів } з { recents } empty-trash-title = Спорожити смітник? type-to-search-select = Вибирає перший відповідний файл або папку -pasted-image = Вставлене Зображення -pasted-text = Вставлений Текст -pasted-video = Вставлене Видиво +pasted-image = Вставлене зображення +pasted-text = Вставлений текст +pasted-video = Вставлене відео copy-to-button-label = Копіювати move-to-button-label = Перемістити copy-to = Копіювати до… @@ -371,3 +379,11 @@ keywords = Тека;Папка;Провідник;Менеджер;Катало show-recents = Тека «Нещодавні» на бічній панелі copy-path = Копіювати шлях clear-recents-history = Очистити нещодавні +context-action-confirm-title = Запустити «{ $name }»? +run = Виконати +context-action-confirm-warning = + Запуститься для { $items } { $items -> + [one] елемента + *[other] елементів + }. +context-action = Контекстна дія diff --git a/i18n/zh-CN/cosmic_files.ftl b/i18n/zh-CN/cosmic_files.ftl index 11441d1..abc8fda 100644 --- a/i18n/zh-CN/cosmic_files.ftl +++ b/i18n/zh-CN/cosmic_files.ftl @@ -167,7 +167,7 @@ read-write-execute = 读取、写入和执行 ## Favorite Path Error Dialog -favorite-path-error = 打开路径时出错 +favorite-path-error = 打开目录时出错 favorite-path-error-description = 无法打开 "{ $path }" 。 "{ $path }" 可能不存在或您没有权限打开它。 @@ -348,7 +348,7 @@ light = 亮色模式 type-to-search = 输入即可搜索 type-to-search-recursive = 搜索当前文件夹及其所有子文件夹 -type-to-search-enter-path = 输入文件夹或文件路径 +type-to-search-enter-path = 输入文件夹或文件目录 # Context menu add-to-sidebar = 加入侧边栏 compress = 压缩… @@ -437,3 +437,11 @@ clear-recents-history = 清除最近访问历史 copy-path = 复制文件路径 show-recents = 侧边栏中的最近访问 mixed = 混合 +context-action-confirm-title = 运行 “{ $name }” ? +run = 运行 +context-action-confirm-warning = + 该行动将会在 { $items } { $items -> + [one] 项目 + *[other] 项目 + } 上运行。 +context-action = 环境行动 diff --git a/i18n/zh-TW/cosmic_files.ftl b/i18n/zh-TW/cosmic_files.ftl index 2bf8e5b..b356db3 100644 --- a/i18n/zh-TW/cosmic_files.ftl +++ b/i18n/zh-TW/cosmic_files.ftl @@ -42,7 +42,7 @@ name-no-slashes = 名稱不能包含斜線 ## Open/Save Dialog cancel = 取消 -create = 創作 +create = 建立 open = 開啟 open-file = 開啟檔案 open-folder = 開啟資料夾 @@ -358,7 +358,7 @@ calculating = 計算中... single-click = 點按以開啟 type-to-search = 輸入進行搜尋 type-to-search-recursive = 搜尋目前資料夾及全部子資料夾 -type-to-search-enter-path = 輸入目錄或檔案的路徑 +type-to-search-enter-path = 輸入目錄或檔案的目錄 delete-permanently = 永久刪除 eject = 彈出 remove-from-recents = 從最近項目中移除 @@ -384,3 +384,11 @@ set-executable-and-launched = 設定「{ $name }」為可以執行並已經啟 setting-permissions = 設定「{ $name }」的權限至 { $mode } set-permissions = 設定「{ $name }」的權限至 { $mode } mixed = 混合 +context-action = 環境行動 +context-action-confirm-title = 執行「{ $name }」嗎? +context-action-confirm-warning = + 該行動將會在 { $items } { $items -> + [one] 項目 + *[other] 項目 + } 上執行。 +run = 執行 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..fb5449a --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.93.0" +components = ["clippy", "rustfmt"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c1578aa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Module" diff --git a/src/app.rs b/src/app.rs index feca9c5..0fc82ae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,23 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use cosmic::app::{self, Core, Task, context_drawer}; +use cosmic::cosmic_config::{self, ConfigSet}; +use cosmic::iced::clipboard::dnd::DndAction; +use cosmic::iced::core::SmolStr; +use cosmic::iced::core::widget::operation::focusable::unfocus; +use cosmic::iced::futures::{self, SinkExt}; +use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers}; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::platform_specific::shell::wayland::commands::overlap_notify::overlap_notify; +use cosmic::iced::runtime::{clipboard, task}; +use cosmic::iced::widget::button::focus; +use cosmic::iced::widget::scrollable; +use cosmic::iced::widget::scrollable::AbsoluteOffset; +use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId}; +use cosmic::iced::{ + self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream, +}; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::{ Limits, Point, @@ -14,43 +29,20 @@ use cosmic::iced::{ Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface, }, }; -use cosmic::{ - Application, ApplicationExt, Element, - app::{self, Core, Task, context_drawer}, - cosmic_config::{self, ConfigSet}, - cosmic_theme, executor, - iced::core::widget::operation::focusable::unfocus, - iced::runtime::{clipboard, task}, - iced::widget::{button::focus, scrollable::AbsoluteOffset}, - iced::{ - self, Alignment, Event, Length, Rectangle, Size, Subscription, - clipboard::dnd::DndAction, - core::SmolStr, - event, - futures::{self, SinkExt}, - keyboard::{Event as KeyEvent, Key, Modifiers}, - mouse, stream, - widget::scrollable, - window::{self, Event as WindowEvent, Id as WindowId}, - }, - style, surface, theme, - widget::{ - self, - about::About, - dnd_destination::DragId, - icon, - menu::{action::MenuAction, key_bind::KeyBind}, - segmented_button::{self, Entity, ReorderEvent}, - space, - }, -}; +use cosmic::widget::about::About; +use cosmic::widget::dnd_destination::DragId; +use cosmic::widget::menu::action::MenuAction; +use cosmic::widget::menu::key_bind::KeyBind; +use cosmic::widget::segmented_button::{self, Entity, ReorderEvent}; +use cosmic::widget::{self, icon, settings, space}; +use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme}; use mime_guess::Mime; -use notify_debouncer_full::{ - DebouncedEvent, Debouncer, RecommendedCache, new_debouncer, - notify::{self, RecommendedWatcher}, -}; +use notify_debouncer_full::notify::{self, RecommendedWatcher}; +use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; 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 +53,7 @@ use std::{ path::{Path, PathBuf}, pin::Pin, process, - sync::{Arc, LazyLock, Mutex}, + sync::{Arc, LazyLock}, time::{self, Duration, Instant}, }; use tokio::sync::mpsc; @@ -69,37 +61,33 @@ use trash::TrashItem; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use wayland_client::{Proxy, protocol::wl_output::WlOutput}; -use crate::{ - FxOrderMap, - clipboard::{ - ClipboardCache, ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage, - ClipboardPasteText, ClipboardPasteVideo, - }, - config::{ - AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, - TimeConfig, TypeToSearch, - }, - context_action, - dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}, - fl, home_dir, - key_bind::key_binds, - localize::LANGUAGE_SORTER, - menu, - mime_app::{self, MimeApp, MimeAppCache}, - mime_icon, - mounter::{MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage}, - operation::{ - Controller, Operation, OperationError, OperationErrorType, OperationSelection, - ReplaceResult, copy_unique_path, - }, - spawn_detached::spawn_detached, - tab::{ - self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, - SearchLocation, Tab, - }, - trash::{Trash, TrashExt}, - zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, +use crate::clipboard::{ + ClipboardCache, ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage, + ClipboardPasteText, ClipboardPasteVideo, }; +use crate::config::{ + AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, + TimeConfig, ToolbarAction, TypeToSearch, default_toolbar, +}; +use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}; +use crate::key_bind::key_binds; +use crate::localize::LANGUAGE_SORTER; +use crate::mime_app::{MimeApp, MimeAppCache}; +use crate::mounter::{ + MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, +}; +use crate::operation::{ + Controller, Operation, OperationError, OperationErrorType, OperationSelection, ReplaceResult, + copy_unique_path, +}; +use crate::spawn_detached::spawn_detached; +use crate::tab::{ + self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, + SearchLocation, Tab, +}; +use crate::trash::{Trash, TrashExt}; +use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default}; +use crate::{FxOrderMap, context_action, fl, home_dir, menu, mime_icon}; static PERMANENT_DELETE_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("permanent-delete-button")); @@ -145,6 +133,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, @@ -305,7 +392,7 @@ impl MenuAction for Action { } #[derive(Clone, Debug)] -pub struct PreviewItem(pub tab::Item); +pub struct PreviewItem(pub Box); impl PartialEq for PreviewItem { fn eq(&self, other: &Self) -> bool { @@ -407,6 +494,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 +535,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), @@ -460,7 +564,7 @@ pub enum Message { TabRescan( Entity, Location, - Option, + Option>, Vec, Option>, ), @@ -577,6 +681,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]>, @@ -591,8 +698,8 @@ pub enum DialogPage { dir: bool, }, Replace { - from: tab::Item, - to: tab::Item, + from: Box, + to: Box, multiple: bool, apply_to_all: bool, conflict_count: usize, @@ -734,6 +841,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, @@ -901,32 +1013,41 @@ impl App { #[cfg(feature = "desktop")] fn launch_desktop_entries(paths: &[impl AsRef]) { + use crate::mime_app; use cosmic::desktop::fde::DesktopEntry; + let locales = cosmic::desktop::fde::get_languages_from_env(); 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(commands) => { - let cwd_opt = entry.desktop_entry("Path"); + Some(exec) => { + match mime_app::exec_to_command( + exec, + entry.name(&locales).as_deref().unwrap_or_default(), + Some(path), + &[] as &[&str; 0], + ) { + Some(commands) => { + let cwd_opt = entry.desktop_entry("Path"); - for mut command in commands { - if let Some(cwd) = cwd_opt { - command.current_dir(cwd); - } + for mut command in commands { + if let Some(cwd) = cwd_opt { + command.current_dir(cwd); + } - if let Err(err) = spawn_detached(&mut command) { - log::warn!("failed to execute {}: {}", path.display(), err); + if let Err(err) = spawn_detached(&mut command) { + log::warn!("failed to execute {}: {}", path.display(), err); + } } } + None => { + log::warn!( + "failed to parse {}: invalid Desktop Entry/Exec", + path.display() + ); + } } - None => { - log::warn!( - "failed to parse {}: invalid Desktop Entry/Exec", - path.display() - ); - } - }, + } None => { log::warn!( "failed to parse {}: missing Desktop Entry/Exec", @@ -1081,7 +1202,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 +1252,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; @@ -1401,7 +1510,7 @@ impl App { // Manually rescan any trash tabs after any operation is completed commands.push(self.rescan_trash()); - return Task::batch(commands); + Task::batch(commands) } fn handle_operation_errors(&mut self, errors: Vec<(u64, OperationError)>) -> Task { @@ -1446,7 +1555,7 @@ impl App { } // Manually rescan any trash tabs after any operation is completed tasks.push(self.rescan_trash()); - return Task::batch(tasks); + Task::batch(tasks) } fn remove_window(&mut self, id: &window::Id) { @@ -1513,12 +1622,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 +1803,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 +1817,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| { @@ -1771,6 +1933,8 @@ impl App { if let Some(path) = favorite.path_opt() { let name = if matches!(favorite, Favorite::Home) { fl!("home") + } else if let Favorite::Network { name, .. } = favorite { + name.clone() } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { file_name.to_string() } else { @@ -1946,7 +2110,7 @@ impl App { fn network_drive(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let mut table = widget::column::with_capacity(8); for (i, line) in fl!("network-drive-schemes").lines().enumerate() { let mut row = widget::row::with_capacity(2); @@ -1977,25 +2141,23 @@ impl App { fn desktop_view_options(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_m, space_l, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let config = self.config.desktop; - let mut column = widget::column::with_capacity(2); - - let mut section = widget::settings::section().title(fl!("show-on-desktop")); - section = section.add( - widget::settings::item::builder(fl!("desktop-folder-content")).toggler( - config.show_content, - move |show_content| { - Message::DesktopConfig(DesktopConfig { - show_content, - ..config - }) - }, - ), - ); - section = section.add( - widget::settings::item::builder(fl!("mounted-drives")).toggler( + let show_on_desktop = settings::section() + .title(fl!("show-on-desktop")) + .add( + settings::item::builder(fl!("desktop-folder-content")).toggler( + config.show_content, + move |show_content| { + Message::DesktopConfig(DesktopConfig { + show_content, + ..config + }) + }, + ), + ) + .add(settings::item::builder(fl!("mounted-drives")).toggler( config.show_mounted_drives, move |show_mounted_drives| { Message::DesktopConfig(DesktopConfig { @@ -2003,10 +2165,8 @@ impl App { ..config }) }, - ), - ); - section = section.add( - widget::settings::item::builder(fl!("trash-folder-icon")).toggler( + )) + .add(settings::item::builder(fl!("trash-folder-icon")).toggler( config.show_trash, move |show_trash| { Message::DesktopConfig(DesktopConfig { @@ -2014,50 +2174,49 @@ impl App { ..config }) }, - ), - ); - column = column.push(section); + )); - let mut section = widget::settings::section().title(fl!("icon-size-and-spacing")); let icon_size = config.icon_size; - section = section.add( - widget::settings::item::builder(fl!("icon-size")) - .description(format!("{icon_size}%")) - .control( - widget::slider(50..=500, icon_size.get(), move |new_value| { - Message::DesktopConfig(DesktopConfig { - icon_size: NonZeroU16::new(new_value).unwrap_or(icon_size), - ..config - }) - }) - .step(25u16), - ), - ); - let grid_spacing = config.grid_spacing; - section = section.add( - widget::settings::item::builder(fl!("grid-spacing")) - .description(format!("{grid_spacing}%")) - .control( - widget::slider(50..=500, grid_spacing.get(), move |new_value| { - Message::DesktopConfig(DesktopConfig { - grid_spacing: NonZeroU16::new(new_value).unwrap_or(grid_spacing), - ..config + let icon_size_and_spacing = settings::section() + .title(fl!("icon-size-and-spacing")) + .add( + settings::item::builder(fl!("icon-size")) + .description(format!("{icon_size}%")) + .control( + widget::slider(50..=500, icon_size.get(), move |new_value| { + Message::DesktopConfig(DesktopConfig { + icon_size: NonZeroU16::new(new_value).unwrap_or(icon_size), + ..config + }) }) - }) - .step(25u16), - ), - ); - column = column.push(section); + .step(25u16), + ), + ) + .add( + settings::item::builder(fl!("grid-spacing")) + .description(format!("{grid_spacing}%")) + .control( + widget::slider(50..=500, grid_spacing.get(), move |new_value| { + Message::DesktopConfig(DesktopConfig { + grid_spacing: NonZeroU16::new(new_value).unwrap_or(grid_spacing), + ..config + }) + }) + .step(25u16), + ), + ); - column + widget::column::with_capacity(2) .padding([0, space_l, space_l, space_l]) .spacing(space_m) + .push(show_on_desktop) + .push(icon_size_and_spacing) .into() } fn edit_history(&self) -> Element<'_, Message> { - let cosmic_theme::Spacing { space_m, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_m, .. } = theme::spacing(); let mut children = Vec::new(); @@ -2149,7 +2308,7 @@ impl App { kind: &'a PreviewKind, context_drawer: bool, ) -> Element<'a, tab::Message> { - let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_l, .. } = theme::spacing(); let mut children = Vec::with_capacity(1); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); @@ -2219,8 +2378,8 @@ impl App { let tab_config = self.config.tab; // TODO: Should dialog be updated here too? - widget::settings::view_column(vec![ - widget::settings::section() + settings::view_column(vec![ + settings::section() .title(fl!("appearance")) .add({ let app_theme_selected = match self.config.app_theme { @@ -2228,7 +2387,7 @@ impl App { AppTheme::Light => 2, AppTheme::System => 0, }; - widget::settings::item::builder(fl!("theme")).control(widget::dropdown( + settings::item::builder(fl!("theme")).control(widget::dropdown( &self.app_themes, Some(app_theme_selected), move |index| { @@ -2241,31 +2400,32 @@ impl App { )) }) .into(), - widget::settings::section() + settings::section() .title(fl!("type-to-search")) - .add(widget::radio( - widget::text::body(fl!("type-to-search-recursive")), - TypeToSearch::Recursive, - Some(self.config.type_to_search), - Message::SetTypeToSearch, - )) - .add(widget::radio( - widget::text::body(fl!("type-to-search-enter-path")), - TypeToSearch::EnterPath, - Some(self.config.type_to_search), - Message::SetTypeToSearch, - )) - .add(widget::radio( - widget::text::body(fl!("type-to-search-select")), + .add( + settings::item::builder(fl!("type-to-search-recursive")).radio( + TypeToSearch::Recursive, + Some(self.config.type_to_search), + Message::SetTypeToSearch, + ), + ) + .add( + settings::item::builder(fl!("type-to-search-enter-path")).radio( + TypeToSearch::EnterPath, + Some(self.config.type_to_search), + Message::SetTypeToSearch, + ), + ) + .add(settings::item::builder(fl!("type-to-search-select")).radio( TypeToSearch::SelectByPrefix, Some(self.config.type_to_search), Message::SetTypeToSearch, )) .into(), - widget::settings::section() + settings::section() .title(fl!("other")) .add({ - widget::settings::item::builder(fl!("single-click")).toggler( + settings::item::builder(fl!("single-click")).toggler( tab_config.single_click, move |single_click| { Message::TabConfig(TabConfig { @@ -2276,14 +2436,138 @@ impl App { ) }) .add({ - widget::settings::item::builder(fl!("show-recents")) + settings::item::builder(fl!("show-recents")) .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 +2739,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, @@ -2961,10 +3246,12 @@ impl Application for App { Message::Config(config) => { if config != self.config { log::info!("update config"); - // Show details is preserved for existing instances + // Show details and military time are preserved for existing instances let show_details = self.config.show_details; + let military_time = self.config.tab.military_time; self.config = config; self.config.show_details = show_details; + self.config.tab.military_time = military_time; return self.update_config(); } } @@ -3232,6 +3519,7 @@ impl Application for App { path, mime, selected, + set_default, .. } => { let available_apps = self.get_apps_for_mime(&mime); @@ -3250,6 +3538,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!( @@ -3369,7 +3662,7 @@ impl Application for App { if self.core.main_window_id() == Some(window_id) || in_surface_ids { let entity = self.tab_model.active(); for (key_bind, action) in &self.key_binds { - if key_bind.matches(modifiers, &key) { + if key_bind.matches(modifiers, &key, None) { return self.update(action.message(Some(entity))); } } @@ -3872,6 +4165,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 +4177,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 +4660,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(); @@ -4588,6 +4969,14 @@ impl Application for App { tab::Command::DropFiles(to, from) => { commands.push(self.update(Message::PasteContents(to, from))); } + tab::Command::ClearRecents => { + match recently_used_xbel::clear_recently_used() { + Ok(()) => {} + Err(err) => { + log::warn!("failed to clear recents history: {}", err); + } + } + } tab::Command::EmptyTrash => { return self.push_dialog( DialogPage::EmptyTrash, @@ -5110,6 +5499,7 @@ impl Application for App { .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), + set_default: false, }, None, ); @@ -5219,7 +5609,7 @@ impl Application for App { Ok(item) => { self.context_page = ContextPage::Preview( None, - PreviewKind::Custom(PreviewItem(item)), + PreviewKind::Custom(PreviewItem(Box::new(item))), ); self.set_show_context(true); } @@ -5526,7 +5916,7 @@ impl Application for App { let cosmic_theme::Spacing { space_xxs, space_s, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let dialog = match dialog_page { DialogPage::Compress { @@ -5665,7 +6055,7 @@ impl Application for App { } DialogPage::FailedOperations(ids) => { let errors: Vec = ids - .into_iter() + .iter() .filter_map(|id| match self.failed_operations.get(id) { Some((operation, _, err)) => Some(format!("{operation:#?}\n{err}")), _ => None, @@ -5953,7 +6343,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 +6428,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( @@ -6144,7 +6548,7 @@ impl Application for App { dialog .primary_action( - widget::button::suggested(fl!("rename")) + widget::button::suggested(fl!("rename-confirm")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( @@ -6288,7 +6692,7 @@ impl Application for App { let cosmic_theme::Spacing { space_xs, space_s, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let mut title = String::new(); let mut total_progress = 0.0; @@ -6444,8 +6848,11 @@ impl Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let cosmic_theme::Spacing { - space_xxs, space_s, .. - } = theme::active().cosmic().spacing; + space_xxs, + space_xs, + space_s, + .. + } = theme::spacing(); let mut tab_column = widget::column::with_capacity(4); @@ -6488,6 +6895,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 @@ -7037,21 +7474,17 @@ impl Application for App { // Ideally, tests would use the cap-std crate which limits path traversal. #[cfg(test)] pub(crate) mod test_utils { - use std::{ - cmp::Ordering, - fs::File, - io::{self, Write}, - iter, - path::Path, - }; + use std::cmp::Ordering; + use std::fs::File; + use std::io::{self, Write}; + use std::iter; + use std::path::Path; use log::{debug, trace}; use tempfile::{TempDir, tempdir}; - use crate::{ - config::{IconSizes, TabConfig, ThumbCfg}, - tab::Item, - }; + use crate::config::{IconSizes, TabConfig, ThumbCfg}; + use crate::tab::Item; use super::*; diff --git a/src/archive.rs b/src/archive.rs index ba11cbb..cff0c1e 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,16 +1,14 @@ -use crate::{ - mime_icon::mime_for_path, - operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}, -}; +use crate::mime_icon::mime_for_path; +use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}; use cosmic::iced::futures; -use jiff::{Zoned, civil::DateTime, tz::TimeZone}; -use std::{ - collections::HashSet, - fs, - io::{self, Read, Write}, - path::{Path, PathBuf}, - time::SystemTime, -}; +use jiff::Zoned; +use jiff::civil::DateTime; +use jiff::tz::TimeZone; +use std::collections::HashSet; +use std::fs; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; use zip::result::ZipError; pub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[ @@ -112,7 +110,8 @@ fn zip_extract>( password: Option<&str>, controller: Controller, ) -> zip::result::ZipResult<()> { - use std::{ffi::OsString, fs}; + use std::ffi::OsString; + use std::fs; use zip::result::ZipError; fn make_writable_dir_all>( diff --git a/src/channel.rs b/src/channel.rs index e91bc35..2a295f9 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,13 +1,9 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 -use std::{ - collections::VecDeque, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, -}; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; /// Create a channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque. pub fn channel() -> (Sender, Receiver) { diff --git a/src/clipboard.rs b/src/clipboard.rs index 5892071..7d06be5 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -2,12 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; -use std::{ - borrow::Cow, - error::Error, - path::{Path, PathBuf}, - str, -}; +use std::borrow::Cow; +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::str; use url::Url; #[derive(Clone, Copy, Debug)] @@ -132,9 +130,11 @@ 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..8f493c0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,20 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{any::TypeId, num::NonZeroU16, path::PathBuf}; +use std::any::TypeId; +use std::num::NonZeroU16; +use std::path::PathBuf; -use cosmic::{ - Application, - cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}, - iced::Subscription, - theme, -}; +use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry; +use cosmic::cosmic_config::{self, CosmicConfigEntry}; +use cosmic::iced::Subscription; +use cosmic::{Application, theme}; use serde::{Deserialize, Serialize}; -use crate::{ - FxOrderMap, - app::App, - tab::{HeadingOptions, Location, View}, -}; +use crate::FxOrderMap; +use crate::app::App; +use crate::tab::{HeadingOptions, Location, View}; pub use crate::context_action::{ContextActionPreset, ContextActionSelection}; @@ -172,6 +170,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 +239,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/context_action.rs b/src/context_action.rs index 86f3bc6..34dfb6e 100644 --- a/src/context_action.rs +++ b/src/context_action.rs @@ -4,7 +4,8 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::{mime_app, spawn_detached::spawn_detached}; +use crate::mime_app; +use crate::spawn_detached::spawn_detached; #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub enum ContextActionSelection { @@ -46,7 +47,7 @@ impl ContextActionPreset { } for step in &self.steps { - let Some(commands) = mime_app::exec_to_command(step, paths) else { + let Some(commands) = mime_app::exec_to_command(step, &self.name, None, paths) else { log::warn!( "failed to parse context action {:?}: invalid Exec {:?}", self.name, diff --git a/src/dialog.rs b/src/dialog.rs index cac5907..69a1e7a 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,58 +1,46 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use cosmic::{ - Application, ApplicationExt, Element, - app::{Core, Task, context_drawer, cosmic::Cosmic}, - cosmic_config, cosmic_theme, executor, - iced::core::widget::operation, - iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper}, - iced::widget::scrollable::AbsoluteOffset, - iced::{ - self, Alignment, Event, Length, Size, Subscription, - core::SmolStr, - event, - futures::{self, SinkExt}, - keyboard::{Event as KeyEvent, Key, Modifiers, key::Named}, - mouse, stream, - widget::scrollable, - window, - }, - theme, - widget::{ - self, Operation, - menu::{Action as MenuAction, KeyBind, key_bind::Modifier}, - segmented_button, - }, +use cosmic::app::cosmic::Cosmic; +use cosmic::app::{Core, Task, context_drawer}; +use cosmic::iced::core::SmolStr; +use cosmic::iced::core::widget::operation; +use cosmic::iced::futures::{self, SinkExt}; +use cosmic::iced::keyboard::key::Named; +use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers}; +use cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper}; +use cosmic::iced::widget::scrollable; +use cosmic::iced::widget::scrollable::AbsoluteOffset; +use cosmic::iced::{ + self, Alignment, Event, Length, Size, Subscription, event, mouse, stream, window, }; +use cosmic::widget::menu::key_bind::Modifier; +use cosmic::widget::menu::{Action as MenuAction, KeyBind}; +use cosmic::widget::{self, Operation, segmented_button}; +use cosmic::{Application, ApplicationExt, Element, cosmic_config, cosmic_theme, executor, theme}; use mime_guess::{Mime, mime}; -use notify_debouncer_full::{ - DebouncedEvent, Debouncer, RecommendedCache, new_debouncer, - notify::{self, RecommendedWatcher}, -}; +use notify_debouncer_full::notify::{self, RecommendedWatcher}; +use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; use recently_used_xbel::update_recently_used; use rustc_hash::{FxHashMap, FxHashSet}; -use std::{ - any::TypeId, - collections::{HashMap, VecDeque}, - env, fmt, fs, - path::PathBuf, - time::{self, Instant}, -}; +use std::any::TypeId; +use std::collections::{HashMap, VecDeque}; +use std::path::PathBuf; +use std::time::{self, Instant}; +use std::{env, fmt, fs}; -use crate::{ - app::{ - Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID, - }, - config::{Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch}, - fl, home_dir, - key_bind::key_binds, - localize::LANGUAGE_SORTER, - menu, - mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}, - tab::{self, ItemMetadata, Location, SearchLocation, Tab}, - zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, +use crate::app::{ + Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID, }; +use crate::config::{ + Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch, +}; +use crate::key_bind::key_binds; +use crate::localize::LANGUAGE_SORTER; +use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}; +use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab}; +use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default}; +use crate::{fl, home_dir, menu}; #[derive(Clone, Debug)] pub struct DialogMessage(cosmic::Action); @@ -487,7 +475,7 @@ enum Message { TabMessage(tab::Message), TabRescan( Location, - Option, + Option>, Vec, Option>, ), @@ -587,7 +575,7 @@ impl App { space_s, space_l, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let is_condensed = self.core().is_condensed(); let mut col = widget::column::with_capacity(2).spacing(space_xxs); @@ -744,11 +732,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 @@ -837,9 +831,10 @@ impl App { fn update_config(&mut self) -> Task { self.core.window.show_context = self.flags.config.dialog.show_details; - self.tab.config = self.flags.config.dialog_tab(); + let config = self.flags.config.dialog_tab(); + self.tab.config.view = config.view; self.update_nav_model(); - self.update(Message::TabMessage(tab::Message::Config(self.tab.config))) + self.update(Message::TabMessage(tab::Message::Config(config))) } fn with_dialog_config(&mut self, f: F) -> Task { @@ -902,6 +897,8 @@ impl App { if let Some(path) = favorite.path_opt() { let name = if matches!(favorite, Favorite::Home) { fl!("home") + } else if let Favorite::Network { name, .. } = favorite { + name.clone() } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { file_name.to_string() } else { @@ -1125,7 +1122,7 @@ impl Application for App { } fn dialog(&self) -> Option> { - let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); //TODO: should gallery view just be a dialog? if self.tab.gallery { @@ -1403,7 +1400,10 @@ impl Application for App { Message::Config(config) => { if config != self.flags.config { log::info!("update config"); + // Don't overwrite military time + let military_time = self.flags.config.tab.military_time; self.flags.config = config; + self.flags.config.tab.military_time = military_time; return self.update_config(); } } @@ -1458,14 +1458,14 @@ impl Application for App { } Message::Key(modifiers, key, text) => { for (key_bind, action) in &self.key_binds { - if key_bind.matches(modifiers, &key) { + if key_bind.matches(modifiers, &key, None) { return self.update(Message::from(action.message())); } } // Check key binds from accept label if let Some(key_bind) = &self.accept_label.key_bind_opt - && key_bind.matches(modifiers, &key) + && key_bind.matches(modifiers, &key, None) { return self.update(if self.flags.kind.save() { Message::Save(false) @@ -2017,7 +2017,7 @@ impl Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Message> { - let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); let mut col = widget::column::with_capacity(2); diff --git a/src/key_bind.rs b/src/key_bind.rs index 57a9dbf..f47d403 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -1,11 +1,10 @@ -use cosmic::{ - iced::core::keyboard::key::Named, - iced::keyboard::Key, - widget::menu::key_bind::{KeyBind, Modifier}, -}; +use cosmic::iced::core::keyboard::key::Named; +use cosmic::iced::keyboard::Key; +use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; use std::collections::HashMap; -use crate::{app::Action, tab}; +use crate::app::Action; +use crate::tab; //TODO: load from config pub fn key_binds(mode: &tab::Mode) -> HashMap { diff --git a/src/large_image.rs b/src/large_image.rs index 8686a97..1d78bc5 100644 --- a/src/large_image.rs +++ b/src/large_image.rs @@ -1,9 +1,7 @@ use cosmic::widget; use image::ImageReader; -use std::{ - collections::{HashMap, HashSet}, - path::{Path, PathBuf}, -}; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; /// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes) pub const RGBA_BYTES_PER_PIXEL: u64 = 4; diff --git a/src/lib.rs b/src/lib.rs index 3e89861..44b87f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,16 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use cosmic::{app::Settings, iced::Limits}; -use std::{env, fs, path::PathBuf, process}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use cosmic::app::Settings; +use cosmic::iced::Limits; +use std::path::PathBuf; +use std::{env, fs, process}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; -use crate::{ - app::{App, Flags}, - config::{Config, State}, - tab::Location, -}; +use crate::app::{App, Flags}; +use crate::config::{Config, State}; +use crate::tab::Location; pub mod app; mod archive; @@ -37,6 +38,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/load_image.rs b/src/load_image.rs index d54fe7f..88ceb1b 100644 --- a/src/load_image.rs +++ b/src/load_image.rs @@ -1,11 +1,10 @@ use cosmic::iced::{core as iced_core, widget as iced_widget}; use iced_core::event::Event; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; -use iced_core::renderer; use iced_core::widget::{Operation, Tree}; -use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +use iced_core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, + renderer, +}; pub fn loaded_image<'a, Message: 'static, Theme>( handle: ::Handle, diff --git a/src/localize.rs b/src/localize.rs index f561a42..b569807 100644 --- a/src/localize.rs +++ b/src/localize.rs @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only -use i18n_embed::{ - DefaultLocalizer, LanguageLoader, Localizer, - fluent::{FluentLanguageLoader, fluent_language_loader}, -}; -use icu::collator::{ - Collator, CollatorBorrowed, CollatorPreferences, options::CollatorOptions, - preferences::CollationNumericOrdering, -}; +use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; +use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; +use icu::collator::options::CollatorOptions; +use icu::collator::preferences::CollationNumericOrdering; +use icu::collator::{Collator, CollatorBorrowed, CollatorPreferences}; use icu::locale::Locale; use rust_embed::RustEmbed; use std::sync::LazyLock; diff --git a/src/menu.rs b/src/menu.rs index 89e4a02..d167f41 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,30 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-only -use cosmic::{ - Element, - app::Core, - iced::{ - Alignment, Background, Border, Length, advanced::widget::text::Style as TextStyle, - keyboard::Modifiers, - }, - theme, - widget::{ - self, Row, button, column, container, divider, - menu::{self, ItemHeight, ItemWidth, MenuBar, key_bind::KeyBind}, - responsive_menu_bar, space, text, - }, +use cosmic::app::Core; +use cosmic::iced::advanced::widget::text::Style as TextStyle; +use cosmic::iced::keyboard::Modifiers; +use cosmic::iced::{Alignment, Background, Border, Length}; +use cosmic::widget::menu::key_bind::KeyBind; +use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar}; +use cosmic::widget::{ + self, Row, button, column, container, divider, responsive_menu_bar, space, text, }; +use cosmic::{Element, theme}; +#[cfg(feature = "desktop")] use i18n_embed::LanguageLoader; use mime_guess::Mime; -use std::{collections::HashMap, sync::LazyLock}; +use std::collections::HashMap; +use std::sync::LazyLock; -use crate::{ - app::{Action, Message}, - config::{Config, ContextActionPreset}, - fl, - tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab}, - trash::{Trash, TrashExt}, -}; +use crate::app::{Action, Message}; +use crate::config::{Config, ContextActionPreset}; +use crate::fl; +use crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab}; +use crate::trash::{Trash, TrashExt}; static MENU_ID: LazyLock = LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); @@ -38,7 +34,7 @@ macro_rules! menu_button { .height(Length::Fixed(24.0)) .align_y(Alignment::Center) ) - .padding([theme::active().cosmic().spacing.space_xxs, 16]) + .padding([theme::spacing().space_xxs, 16]) .width(Length::Fill) .class(theme::Button::MenuItem) ); @@ -143,12 +139,11 @@ pub fn context_menu<'a>( Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => { selected_trash_only = true } - Some(Location::Path(path)) => { + Some(Location::Path(path)) if selected == 1 - && path.extension().and_then(|s| s.to_str()) == Some("desktop") - { - selected_desktop_entry = Some(&**path); - } + && path.extension().and_then(|s| s.to_str()) == Some("desktop") => + { + selected_desktop_entry = Some(&**path); } _ => (), } @@ -196,11 +191,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(), )); } @@ -579,7 +574,7 @@ pub fn dialog_menu( ]) .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) - .spacing(theme::active().cosmic().spacing.space_xxxs.into()) + .spacing(theme::spacing().space_xxxs.into()) .into() } @@ -634,7 +629,7 @@ pub fn menu_bar<'a>( responsive_menu_bar() .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) - .spacing(theme::active().cosmic().spacing.space_xxxs.into()) + .spacing(theme::spacing().space_xxxs.into()) .into_element( core, key_binds, diff --git a/src/mime_app.rs b/src/mime_app.rs index 4a55cdf..7535556 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -1,155 +1,123 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use bstr::{BString, ByteSlice, ByteVec}; #[cfg(feature = "desktop")] 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, + os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process, - time::Instant, }; -// Supported exec key field codes -const EXEC_HANDLERS: [&str; 4] = ["%f", "%F", "%u", "%U"]; -// Deprecated field codes. The spec advises to ignore these handlers. -const DEPRECATED_HANDLERS: [&str; 6] = ["%d", "%D", "%n", "%N", "%v", "%m"]; - pub fn exec_to_command( exec: &str, + entry_name: &str, + entry_path: Option<&Path>, path_opt: &[impl AsRef], ) -> Option> { - let args_vec = shlex::split(exec)?; - let program = args_vec.first()?; - // Skip program to make indexing easier - let args_vec = &args_vec[1..]; + let arguments = shlex::split(exec)?; - // Base Command instance(s) - // 1. We may need to launch multiple of the same process. - // 2. Each of those processes will need to be passed args from exec. - // 3. Each of those args may appear in any order. - // 4. Arg order should be preserved. - // - // So, we'll go through exec in two passes. The first pass handles paths (%f etc) and args up - // to the field code followed by the second which passes extra, non-% args to each processes. - // - // While it'd be marginally faster to process everything in one pass, that's problematic: - // 1. path_opt may need to be cloned because it may be moved on each iteration (borrowck - // doesn't know we'll only use it once) - // 2. We have to keep track of which modifier (%f etc) we've used/seen already - // 3. We have to keep track of which processes received non-modifier args which gets messy fast - // 4. `exec` is likely small so looping over it twice is not a big deal - let field_code_pos = args_vec + if arguments.is_empty() { + tracing::error!("command does not contain any arguments"); + return None; + } + + let mut commands = Vec::new(); + + let paths = path_opt .iter() - .position(|arg| EXEC_HANDLERS.contains(&arg.as_str())); - let args_handler = field_code_pos.and_then(|i| args_vec.get(i)); - // msrv - // .inspect(|handler| log::trace!("Found paths handler: {handler} for exec: {exec}")); - // Number of args before the field code. - // This won't be an off by one err below because take is not zero indexed. - let field_code_pos = field_code_pos.unwrap_or_default(); - let mut processes = match args_handler.map(String::as_str) { - Some("%f") => { - let mut processes = Vec::with_capacity(path_opt.len()); + .map(AsRef::as_ref) + .map(Some) + // Add a single `None` if no path was given. + .chain(std::iter::repeat_n( + None, + if path_opt.is_empty() { 1 } else { 0 }, + )); - for path in path_opt.iter().map(AsRef::as_ref) { - // TODO: %f and %F need to handle non-file URLs (see spec) - if from_file_or_dir(path).is_none() { - log::warn!("Desktop file expects a file path instead of a URL: {path:?}"); + for path in paths { + let mut batch_process = false; + let mut args = Vec::with_capacity(arguments.len()); + let mut field_code_used = false; + + for argument in arguments.iter().skip(1) { + let mut new_argument = BString::new(Vec::with_capacity(argument.capacity())); + let mut chars = argument.chars(); + while let Some(char) = chars.next() { + // https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html + if char == '%' { + match chars.next() { + Some('%') => new_argument.push_char(char), + Some('c') => new_argument.push_str(entry_name), + Some('k') => { + if let Some(path) = entry_path { + new_argument.push_str(path.as_os_str().as_bytes()); + } + } + + // %f and %u behave the same in a file manager. + Some('f' | 'u') => { + if let Some(path) = path + && !field_code_used + { + // TODO: files on remote file systems should be copied to a temporary local file. + batch_process = true; + field_code_used = true; + new_argument.push_str(path.as_bytes()); + } + } + + // %F and %U behave the same in a file manager. + Some('F') | Some('U') => { + if !field_code_used && new_argument.is_empty() { + field_code_used = true; + for path in path_opt.iter().map(AsRef::as_ref) { + args.push(BString::new(path.as_bytes().to_owned())); + } + } + } + + _ => (), + } + } else { + new_argument.push_char(char); } - - // Passing multiple paths to %f should open an instance per path - let mut process = process::Command::new(program); - process.args( - args_vec - .iter() - .map(AsRef::as_ref) - .take(field_code_pos) - .chain(std::iter::once(path)), - ); - processes.push(process); } - processes - } - Some("%F") => { - // TODO: %f and %F need to handle non-file URLs (see spec) - for invalid in path_opt - .iter() - .map(AsRef::as_ref) - .filter(|&path| from_file_or_dir(path).is_none()) - { - log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}"); + if !new_argument.is_empty() { + args.push(new_argument); } - - // Launch one instance with all args - let mut process = process::Command::new(program); - process.args( - args_vec - .iter() - .map(OsStr::new) - .take(field_code_pos) - .chain(path_opt.iter().map(AsRef::as_ref)), - ); - - vec![process] } - Some("%u") => path_opt - .iter() - .map(|path| { - let mut process = process::Command::new(program); - process.args( - args_vec - .iter() - .map(OsStr::new) - .take(field_code_pos) - .chain(std::iter::once(path.as_ref())), - ); - process - }) - .collect(), - Some("%U") => { - let mut process = process::Command::new(program); - process.args( - args_vec - .iter() - .map(OsStr::new) - .take(field_code_pos) - .chain(path_opt.iter().map(AsRef::as_ref)), - ); - vec![process] - } - Some(invalid) => unreachable!("All valid variants were checked; got: {invalid}"), - None => vec![process::Command::new(program)], - }; - // Pass 2: Add remaining arguments that are not % to each process - for arg in args_vec.iter().skip(field_code_pos) { - match arg.as_str() { - // Consume path field codes or fail on codes we don't handle yet - field_code if arg.starts_with('%') => { - if !EXEC_HANDLERS.contains(&field_code) - && !DEPRECATED_HANDLERS.contains(&field_code) - { - log::warn!("unsupported Exec code {field_code:?} in {exec:?}"); + let mut command = process::Command::new(&arguments[0]); + + for arg in args { + match arg.to_os_str() { + Ok(arg) => { + command.arg(arg); + } + Err(_) => { + tracing::error!("invalid string encoding in command"); return None; } } - arg => { - for process in &mut processes { - process.arg(arg); - } - } + } + + commands.push(command); + + if !batch_process { + break; } } #[cfg(debug_assertions)] - for command in &processes { + for command in &commands { log::debug!( "Parsed program {} with args: {:?}", command.get_program().to_string_lossy(), @@ -157,13 +125,7 @@ pub fn exec_to_command( ); } - Some(processes) -} - -fn from_file_or_dir(path: impl AsRef) -> Option { - url::Url::from_file_path(&path) - .ok() - .or_else(|| url::Url::from_directory_path(&path).ok()) + Some(commands) } #[derive(Clone, Debug)] @@ -179,7 +141,12 @@ pub struct MimeApp { impl MimeApp { //TODO: move to libcosmic, support multiple files pub fn command>(&self, path_opt: &[O]) -> Option> { - exec_to_command(self.exec.as_deref()?, path_opt) + exec_to_command( + self.exec.as_deref()?, + &self.name, + self.path.as_deref(), + path_opt, + ) } } @@ -399,9 +366,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); @@ -475,11 +445,43 @@ impl Default for MimeAppCache { mod tests { use super::exec_to_command; + #[test] + fn keys_within_words() { + let exec = "/usr/bin/foo --option=%f"; + let paths = ["file1"]; + let commands = exec_to_command(exec, "keys_within_words", None, &paths) + .expect("Should parse valid exec"); + + assert_eq!(1, commands.len()); + let command = commands.first().unwrap(); + + assert_eq!("/usr/bin/foo", command.get_program().to_str().unwrap()); + assert_eq!( + "--option=file1", + command.get_args().next().unwrap().to_str().unwrap() + ); + } + + #[test] + fn no_path_f_field_code() { + let exec = "/usr/bin/foo %f"; + let paths: [&str; 0] = []; + let commands = exec_to_command(exec, "no_path_f_field_code", None, &paths) + .expect("Should parse valid exec"); + + assert_eq!(1, commands.len()); + let command = commands.first().unwrap(); + + assert_eq!("/usr/bin/foo", command.get_program().to_str().unwrap()); + assert_eq!(0, command.get_args().len()); + } + #[test] fn one_path_f_field_code() { let exec = "/usr/bin/foo %f"; let paths = ["file1"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "one_path_f_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -494,31 +496,40 @@ mod tests { #[test] #[allow(non_snake_case)] fn one_path_F_field_code() { - let exec = "/usr/bin/bar %F"; - let paths = ["cat"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let exec = "/usr/bin/cosmic-term -w %F"; + let paths = ["/home/user"]; + let commands = exec_to_command(exec, "one_path_F_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); + let mut args = command.get_args(); - assert_eq!("/usr/bin/bar", command.get_program().to_str().unwrap()); - assert_eq!("cat", command.get_args().next().unwrap().to_str().unwrap()); + assert_eq!( + "/usr/bin/cosmic-term", + command.get_program().to_str().unwrap() + ); + assert_eq!("-w", args.next().unwrap().to_str().unwrap()); + assert_eq!(paths[0], args.next().unwrap().to_str().unwrap()); } #[test] fn one_path_u_field_code() { - let exec = "/usr/bin/foobar %u"; - let paths = ["/home/josh/krumpli"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let exec = "/usr/bin/cosmic-term -w %u"; + let paths = ["/home/user"]; + let commands = exec_to_command(exec, "one_path_u_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); + let mut args = command.get_args(); - assert_eq!("/usr/bin/foobar", command.get_program().to_str().unwrap()); assert_eq!( - *paths.first().unwrap(), - command.get_args().next().unwrap().to_str().unwrap() + "/usr/bin/cosmic-term", + command.get_program().to_str().unwrap() ); + assert_eq!("-w", args.next().unwrap().to_str().unwrap()); + assert_eq!(paths[0], args.next().unwrap().to_str().unwrap()); } #[test] @@ -526,7 +537,8 @@ mod tests { fn one_path_U_field_code() { let exec = "/usr/bin/rmrfbye %U"; let paths = ["/"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "one_path_U_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -542,7 +554,8 @@ mod tests { "/usr/share/games/psp/miku.iso", "/usr/share/games/psp/eternia.iso", ]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "mult_path_f_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(paths.len(), commands.len()); for (command, path) in commands.into_iter().zip(paths.iter()) { @@ -562,7 +575,8 @@ mod tests { "/usr/share/games/doom2/hr.wad", "/usr/share/games/doom2/hrmus.wad", ]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "mult_path_F_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -584,7 +598,8 @@ mod tests { "https://redox-os.org/", "https://system76.com/", ]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "mult_path_u_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(paths.len(), commands.len()); for (command, path) in commands.into_iter().zip(paths.iter()) { @@ -607,7 +622,8 @@ mod tests { "frieren01.mkv", "rtmp://example.org/this/video/doesnt/exist.avi", ]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "mult_path_U_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -635,7 +651,8 @@ mod tests { "@@u", ]; let paths = ["file1.rs", "file2.rs"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "flatpak_style_exec", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -658,7 +675,8 @@ mod tests { "file:///usr/share/games/roguelike/mods/mod1", "file:///usr/share/games/roguelike/mods/mod2", ]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "multiple_field_codes", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); @@ -691,7 +709,8 @@ mod tests { ]; let paths = ["rust_game_dev.pdf", "superhero_ferris.epub"]; let args_trailing = ["@@"]; - let commands = exec_to_command(exec, &paths).expect("Should parse valid exec"); + let commands = exec_to_command(exec, "sandwiched_field_code", None, &paths) + .expect("Should parse valid exec"); assert_eq!(1, commands.len()); let command = commands.first().unwrap(); diff --git a/src/mime_icon.rs b/src/mime_icon.rs index 8d8d2e5..64f007f 100644 --- a/src/mime_icon.rs +++ b/src/mime_icon.rs @@ -3,11 +3,9 @@ use cosmic::widget::icon; use mime_guess::Mime; use rustc_hash::FxHashMap; -use std::{ - fs, - path::Path, - sync::{LazyLock, Mutex}, -}; +use std::fs; +use std::path::Path; +use std::sync::{LazyLock, Mutex}; pub const FALLBACK_MIME_ICON: &str = "text-x-generic"; @@ -47,7 +45,7 @@ impl MimeIconCache { return None; } let icon_name = icon_names.remove(0); - let mut named = icon::from_name(icon_name).size(key.size); + let mut named = icon::from_name(icon_name).prefer_svg(true).size(key.size); if !icon_names.is_empty() { let fallback_names = icon_names.into_iter().map(std::borrow::Cow::from).collect(); @@ -114,7 +112,10 @@ pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle { let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); match mime_icon_cache.get(MimeIconKey { mime, size }) { Some(handle) => handle, - None => icon::from_name(FALLBACK_MIME_ICON).size(size).handle(), + None => icon::from_name(FALLBACK_MIME_ICON) + .prefer_svg(true) + .size(size) + .handle(), } } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index d6294b6..7c28668 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -1,18 +1,20 @@ -use cosmic::{ - Task, - iced::{Subscription, futures::SinkExt, stream}, - widget, -}; -use gio::{glib, prelude::*}; -use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc}; +use cosmic::iced::futures::SinkExt; +use cosmic::iced::{Subscription, stream}; +use cosmic::{Task, widget}; +use gio::glib; +use gio::prelude::*; +use std::any::TypeId; +use std::cell::Cell; +use std::future::pending; +use std::hash::Hash; +use std::path::PathBuf; +use std::sync::Arc; use tokio::sync::mpsc; use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; -use crate::{ - config::IconSizes, - err_str, - tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location}, -}; +use crate::config::IconSizes; +use crate::err_str; +use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location}; const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri"; diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index b97f32a..a5ee75a 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -1,13 +1,13 @@ -use cosmic::{Task, iced::Subscription, widget}; -use std::{ - collections::BTreeMap, - fmt, - path::PathBuf, - sync::{Arc, LazyLock}, -}; +use cosmic::iced::Subscription; +use cosmic::{Task, widget}; +use std::collections::BTreeMap; +use std::fmt; +use std::path::PathBuf; +use std::sync::{Arc, LazyLock}; use tokio::sync::mpsc; -use crate::{config::IconSizes, tab}; +use crate::config::IconSizes; +use crate::tab; #[cfg(feature = "gvfs")] mod gvfs; @@ -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/mouse_area.rs b/src/mouse_area.rs index a57b36a..73bd710 100644 --- a/src/mouse_area.rs +++ b/src/mouse_area.rs @@ -3,21 +3,17 @@ use std::time::Instant; use crate::tab::DOUBLE_CLICK_DURATION; -use cosmic::{ - Element, Renderer, Theme, - iced::core::{ - Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, - border::Border, - event::Event, - layout, - mouse::{self, click}, - overlay, - renderer::{self, Quad, Renderer as _}, - touch, - widget::{Operation, Tree, tree}, - }, - widget::Id, +use cosmic::iced::core::border::Border; +use cosmic::iced::core::event::Event; +use cosmic::iced::core::mouse::{self, click}; +use cosmic::iced::core::renderer::{self, Quad, Renderer as _}; +use cosmic::iced::core::widget::{Operation, Tree, tree}; +use cosmic::iced::core::{ + Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, + overlay, touch, }; +use cosmic::widget::Id; +use cosmic::{Element, Renderer, Theme}; /// Emit messages on mouse events. #[allow(missing_debug_implementations)] diff --git a/src/operation/mod.rs b/src/operation/mod.rs index b4a66e9..c490655 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -1,20 +1,15 @@ -use crate::{ - app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID}, - archive, - config::IconSizes, - fl, - spawn_detached::spawn_detached, - tab, -}; -use cosmic::iced::futures::{self, SinkExt, StreamExt, channel::mpsc::Sender, stream}; -use std::{ - borrow::Cow, - fmt::Formatter, - fs, - io::{self, Read, Write}, - path::{Path, PathBuf}, - sync::Arc, -}; +use crate::app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID}; +use crate::config::IconSizes; +use crate::spawn_detached::spawn_detached; +use crate::{archive, fl, tab}; +use cosmic::iced::futures::channel::mpsc::Sender; +use cosmic::iced::futures::{self, SinkExt, StreamExt, stream}; +use std::borrow::Cow; +use std::fmt::Formatter; +use std::fs; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; use tokio::sync::{Mutex as TokioMutex, mpsc}; use walkdir::WalkDir; use zip::AesMode::Aes256; @@ -39,7 +34,7 @@ async fn handle_replace( conflict_count: usize, ) -> ReplaceResult { let item_from = match tab::item_from_path(file_from, IconSizes::default()) { - Ok(ok) => ok, + Ok(ok) => Box::new(ok), Err(err) => { log::warn!("{err}"); return ReplaceResult::Cancel; @@ -47,7 +42,7 @@ async fn handle_replace( }; let item_to = match tab::item_from_path(file_to, IconSizes::default()) { - Ok(ok) => ok, + Ok(ok) => Box::new(ok), Err(err) => { log::warn!("{err}"); return ReplaceResult::Cancel; @@ -1243,28 +1238,23 @@ fn wrap_compio_spawn_error(err: Box) -> OperationError #[cfg(test)] mod tests { - use std::{ - fs::{self, File}, - io, - path::PathBuf, - }; + use std::fs::{self, File}; + use std::io; + use std::path::PathBuf; - use cosmic::iced::futures::{StreamExt, channel::mpsc, future}; + use cosmic::iced::futures::channel::mpsc; + use cosmic::iced::futures::{StreamExt, future}; use log::debug; use test_log::test; use tokio::sync; use super::{Controller, Operation, OperationError, OperationSelection, ReplaceResult}; - use crate::{ - app::{ - DialogPage, Message, - test_utils::{ - NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs, - filter_files, simple_fs, - }, - }, - fl, + use crate::app::test_utils::{ + NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs, filter_files, + simple_fs, }; + use crate::app::{DialogPage, Message}; + use crate::fl; /// Simple wrapper around `[Operation::Copy]` pub async fn operation_copy( diff --git a/src/operation/reader.rs b/src/operation/reader.rs index 75088df..9f45441 100644 --- a/src/operation/reader.rs +++ b/src/operation/reader.rs @@ -1,4 +1,5 @@ -use std::{fs, io, path::Path}; +use std::path::Path; +use std::{fs, io}; use crate::operation::OperationError; diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index 9a33ebd..0f09bd6 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -6,15 +6,21 @@ use crate::operation::{OperationError, sync_to_disk}; use anyhow::Context as AnyhowContext; use compio::BufResult; use compio::buf::{IntoInner, IoBuf}; -use compio::driver::{ToSharedFd, op::AsyncifyFd}; +use compio::driver::ToSharedFd; +use compio::driver::op::AsyncifyFd; use compio::io::{AsyncReadAt, AsyncWriteAt}; use cosmic::iced::futures; +#[cfg(feature = "gvfs")] use futures::{FutureExt, StreamExt}; +use std::cell::Cell; +use std::error::Error; +use std::fs; use std::future::Future; +use std::ops::ControlFlow; +use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; use std::time::Instant; -use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf}; use walkdir::WalkDir; #[cfg(feature = "gvfs")] @@ -473,12 +479,12 @@ impl Op { progress.total_bytes = metadata.as_ref().map(|m| m.len()); (ctx.on_progress)(self, &progress); - if let Some(metadata) = metadata.as_ref() { - if let Err(why) = to_file.set_permissions(metadata.permissions()).await { - // This error is not propagated upwards as some filesystems do not support setting permissions - if !matches!(why.kind(), std::io::ErrorKind::Unsupported) { - tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),); - } + if let Some(metadata) = metadata.as_ref() + && let Err(why) = to_file.set_permissions(metadata.permissions()).await + { + // This error is not propagated upwards as some filesystems do not support setting permissions + if !matches!(why.kind(), std::io::ErrorKind::Unsupported) { + tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),); } } diff --git a/src/tab.rs b/src/tab.rs index 94eacdd..d7339f8 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1,91 +1,72 @@ #[cfg(feature = "desktop")] use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env}; -use cosmic::{ - Apply, Element, cosmic_theme, font, - iced::core::{mouse::ScrollDelta, widget::tree}, - iced::{ - Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector, - advanced::{ - graphics, - text::{self, Paragraph}, - }, - alignment::Vertical, - clipboard::dnd::DndAction, - futures::{self, SinkExt}, - keyboard::Modifiers, - padding, stream, - widget::{ - rule, - scrollable::{self, AbsoluteOffset, Viewport}, - stack, - }, - window, - }, - theme, - widget::{ - self, DndDestination, DndSource, Id, RcElementWrapper, Widget, - menu::{action::MenuAction, key_bind::KeyBind}, - space, - }, +use cosmic::iced::advanced::graphics; +use cosmic::iced::advanced::text::{self, Paragraph}; +use cosmic::iced::alignment::Vertical; +use cosmic::iced::clipboard::dnd::DndAction; +use cosmic::iced::core::mouse::ScrollDelta; +use cosmic::iced::core::widget::tree; +use cosmic::iced::futures::{self, SinkExt}; +use cosmic::iced::keyboard::Modifiers; +use cosmic::iced::widget::scrollable::{self, AbsoluteOffset, Viewport}; +use cosmic::iced::widget::{rule, stack}; +use cosmic::iced::{ + Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector, + padding, stream, window, }; +use cosmic::widget::menu::action::MenuAction; +use cosmic::widget::menu::key_bind::KeyBind; +use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space}; +use cosmic::{Apply, Element, cosmic_theme, font, theme}; +#[cfg(feature = "desktop")] use i18n_embed::LanguageLoader; -use icu::{ - datetime::{ - DateTimeFormatter, DateTimeFormatterPreferences, fieldsets, input::DateTime, - options::TimePrecision, - }, - locale::preferences::extensions::unicode::keywords::HourCycle, -}; +use icu::datetime::input::DateTime; +use icu::datetime::options::TimePrecision; +use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets}; +use icu::locale::preferences::extensions::unicode::keywords::HourCycle; use image::{DynamicImage, ImageReader}; use jiff_icu::ConvertFrom; use mime_guess::{Mime, mime}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::cell::Cell; +use std::cmp::{Ordering, Reverse}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::error::Error; +use std::fmt::{self, Display}; +use std::fs::{self, File, Metadata}; +use std::hash::Hash; +use std::io::{BufRead, BufReader, Read}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use std::{ - borrow::Cow, - cell::Cell, - cmp::{Ordering, Reverse}, - collections::{BTreeMap, BTreeSet, HashMap}, - error::Error, - fmt::{self, Display}, - fs::{self, File, Metadata}, - hash::Hash, - io::{BufRead, BufReader}, - path::{self, Path, PathBuf}, - sync::{Arc, LazyLock, RwLock, atomic}, - time::{Duration, Instant, SystemTime}, -}; +use std::path::{self, Path, PathBuf}; +use std::sync::{Arc, LazyLock, RwLock, atomic}; +use std::time::{Duration, Instant, SystemTime}; use tempfile::NamedTempFile; use tokio::sync::mpsc; use trash::{TrashItem, TrashItemMetadata, TrashItemSize}; use walkdir::WalkDir; -use crate::{ - FxOrderMap, - app::{Action, PreviewItem, PreviewKind}, - clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, - config::{ - ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, - ThumbCfg, - }, - dialog::DialogKind, - fl, - large_image::{ - LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker, - should_use_tiling, - }, - localize::{LANGUAGE_SORTER, LOCALE}, - menu, mime_app, - mime_icon::{mime_for_path, mime_icon}, - mounter::MOUNTERS, - mouse_area, - operation::{Controller, OperationError}, - thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize}, - thumbnailer::thumbnailer, - trash::{Trash, TrashExt}, +use crate::app::{Action, PreviewItem, PreviewKind}; +use crate::clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}; +use crate::config::{ + ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, + ThumbCfg, }; +use crate::dialog::DialogKind; +use crate::large_image::{ + LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker, + should_use_tiling, +}; +use crate::localize::{LANGUAGE_SORTER, LOCALE}; +use crate::mime_icon::{mime_for_path, mime_icon}; +use crate::mounter::MOUNTERS; +use crate::operation::{Controller, OperationError}; +use crate::thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize}; +use crate::thumbnailer::thumbnailer; +use crate::trash::{Trash, TrashExt}; +use crate::{FxOrderMap, fl, menu, mime_app, mouse_area}; pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); pub const HOVER_DURATION: Duration = Duration::from_millis(1600); @@ -95,6 +76,11 @@ const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20); const MAX_SEARCH_RESULTS: usize = 200; //TODO: configurable thumbnail size? const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32); +/// Maximum bytes of text to pass to the editor for preview; caps shaping work to avoid blocking. +/// Files larger than this get a truncated preview (first N bytes only). +const TEXT_PREVIEW_MAX_BYTES: usize = 256 * 1024; // 256 KiB +/// Maximum file size (bytes) to attempt text preview; files larger than this are skipped entirely. +const TEXT_PREVIEW_MAX_FILE_BYTES: u64 = 8 * 1000 * 1000; // 8 MiB // Thumbnail generation semaphore - limits parallel thumbnail workers // Uses 4 workers for balanced throughput and memory usage @@ -289,6 +275,7 @@ fn button_style( pub fn folder_icon(path: &PathBuf, icon_size: u16) -> widget::icon::Handle { widget::icon::from_name(SPECIAL_DIRS.get(path).map_or("folder", |x| *x)) + .prefer_svg(true) .size(icon_size) .handle() } @@ -302,6 +289,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 +565,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 +584,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 } @@ -601,7 +608,10 @@ fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle { if icon_path.is_absolute() && icon_path.exists() { widget::icon::from_path(icon_path.to_path_buf()) } else { - widget::icon::from_name(icon).size(size).handle() + widget::icon::from_name(icon) + .prefer_svg(true) + .size(size) + .handle() } } @@ -665,9 +675,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 +694,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 +762,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 +813,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 +831,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 +959,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")] { @@ -1558,7 +1562,7 @@ impl Location { } } - pub fn scan(&self, sizes: IconSizes) -> (Option, Vec) { + pub fn scan(&self, sizes: IconSizes) -> (Option>, Vec) { let items = match self { Self::Desktop(path, display, desktop_config) => { scan_desktop(path, display, *desktop_config, sizes) @@ -1574,7 +1578,7 @@ impl Location { }; let parent_item_opt = match self.path_opt() { Some(path) => match item_from_path(path, sizes) { - Ok(item) => Some(item), + Ok(item) => Some(Box::new(item)), Err(err) => { log::warn!("failed to get item for {}: {}", path.display(), err); None @@ -1683,6 +1687,7 @@ pub enum Command { ContextMenu(Option, Option), Delete(Vec), DropFiles(PathBuf, ClipboardPaste), + ClearRecents, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(cosmic::desktop::DesktopEntryData, usize), @@ -1722,6 +1727,7 @@ pub enum Message { EditLocationSubmit, EditLocationTab, OpenInNewTab(PathBuf), + ClearRecents, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(Option, usize), @@ -2084,17 +2090,37 @@ impl ItemThumbnail { log::warn!("failed to read {}: {}", path.display(), err); } } - } else if mime.type_() == mime::TEXT && check_size("text", 8 * 1000 * 1000) { - /*TODO: fix performance issues, widget::text_editor::Content::with_text forces all text to shape, which blocks rendering - match fs::read_to_string(&path) { - Ok(data) => { - return ItemThumbnail::Text(widget::text_editor::Content::with_text(&data)); - } - Err(err) => { - log::warn!("failed to read {}: {}", path.display(), err); + } else if mime.type_() == mime::TEXT && check_size("text", TEXT_PREVIEW_MAX_FILE_BYTES) { + tried_supported_file = true; + if size > 0 { + // Reuse size from metadata above; cap allocation and read + let read_cap = (size.min(TEXT_PREVIEW_MAX_BYTES as u64)) as usize; + let mut buf = vec![0u8; read_cap]; + match File::open(path).and_then(|f| { + let n = Read::read(&mut f.take(read_cap as u64), &mut buf)?; + buf.truncate(n); + Ok(()) + }) { + Ok(()) => { + let text = match std::str::from_utf8(&buf) { + Ok(s) => s.to_string(), + Err(e) => { + // Use only the valid UTF-8 prefix (slice is guaranteed valid by valid_up_to()) + std::str::from_utf8(&buf[..e.valid_up_to()]) + .unwrap_or("") + .to_string() + } + }; + if !text.is_empty() { + return Self::Text(widget::text_editor::Content::with_text(&text)); + } + } + Err(err) => { + log::warn!("failed to read {}: {}", path.display(), err); + } } } - */ + // size == 0: empty file or unknown size; skip read and allocation } // If we weren't able to create a thumbnail, but we should have @@ -2278,7 +2304,7 @@ impl Item { } fn preview(&self) -> Element<'_, Message> { - let spacing = cosmic::theme::active().cosmic().spacing; + let spacing = cosmic::theme::spacing(); // This loads the image only if thumbnailing worked let icon = widget::icon::icon(self.icon_handle_grid.clone()) .content_fit(ContentFit::Contain) @@ -2339,7 +2365,7 @@ impl Item { space_xxxs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let mut column = widget::column::with_capacity(4).spacing(space_m); @@ -2517,7 +2543,7 @@ impl Item { } pub fn replace_view(&self, heading: String, military_time: bool) -> Element<'_, Message> { - let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing(); let mut row = widget::row::with_capacity(2).spacing(space_xxxs); row = row.push(self.preview()); @@ -2657,7 +2683,7 @@ pub struct Tab { pub sort_name: HeadingOptions, pub sort_direction: bool, pub gallery: bool, - pub(crate) parent_item_opt: Option, + pub(crate) parent_item_opt: Option>, pub(crate) items_opt: Option>, pub dnd_hovered: Option<(Location, Instant)>, pub(crate) scrollable_id: widget::Id, @@ -2991,10 +3017,10 @@ impl Tab { return None; }; let search_items = after - .into_iter() + .iter_mut() .enumerate() .map(|(i, item)| (i + start, item)) - .chain(until.into_iter().enumerate()); + .chain(until.iter_mut().enumerate()); if forward { Self::select_first_prefix_match(prefix_lower, search_items) @@ -3011,7 +3037,7 @@ impl Tab { items: impl Iterator, ) -> Option { for (i, item) in items { - if item.name.to_lowercase().starts_with(&prefix) { + if item.name.to_lowercase().starts_with(prefix) { item.selected = true; return Some(i); } @@ -3607,7 +3633,7 @@ impl Tab { match item_from_path(&path, IconSizes::default()) { Ok(item) => { commands.push(Command::Preview(PreviewKind::Custom( - PreviewItem(item), + PreviewItem(Box::new(item)), ))); } Err(err) => { @@ -3697,6 +3723,9 @@ impl Tab { Message::OpenInNewTab(path) => { commands.push(Command::OpenInNewTab(path)); } + Message::ClearRecents => { + commands.push(Command::ClearRecents); + } Message::EmptyTrash => { commands.push(Command::EmptyTrash); } @@ -4332,7 +4361,7 @@ impl Tab { Message::ShiftPermissions(path_mode_opt, shift, bits) => match path_mode_opt { Some((path, mode)) => commands.push(Command::SetPermissions( path, - set_mode_part(mode, shift, bits.try_into().unwrap()), + set_mode_part(mode, shift, bits), )), // Shift permissions on all selected items None => { @@ -4343,13 +4372,9 @@ impl Tab { #[cfg(unix)] if let (Some(path), Some(mode)) = ( item.path_opt(), - item.file_metadata() - .and_then(|metadata| Some(metadata.mode())), + item.file_metadata().map(|metadata| metadata.mode()), ) { - permissions.push(( - path.clone(), - set_mode_part(mode, shift, bits.try_into().unwrap()), - )); + permissions.push((path.clone(), set_mode_part(mode, shift, bits))); } } commands.push(Command::SetMultiplePermissions(permissions)); @@ -4765,7 +4790,7 @@ impl Tab { space_xs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); //TODO: display error messages when image not found? let mut name_opt = None; @@ -4963,7 +4988,7 @@ impl Tab { space_s, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let size = self.size_opt.get().unwrap_or(Size::new(0.0, 0.0)); @@ -5298,7 +5323,7 @@ impl Tab { } pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> { - let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; + let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); mouse_area::MouseArea::new(widget::column::with_children([widget::container( match self.mode { @@ -5338,7 +5363,7 @@ impl Tab { space_xxs, space_xxxs, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let TabConfig { show_hidden, @@ -5448,8 +5473,7 @@ impl Tab { widget::button::custom( widget::icon::icon(item.icon_handle_grid.clone()) .content_fit(ContentFit::Contain) - .size(icon_sizes.grid()) - .width(Length::Shrink), + .size(icon_sizes.grid()), ) .padding(space_xxxs) .class(button_style( @@ -5684,7 +5708,7 @@ impl Tab { ) { let cosmic_theme::Spacing { space_s, space_xxs, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let TabConfig { show_hidden, @@ -6086,7 +6110,7 @@ impl Tab { space_xxs, space_xs, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let location_view_opt = if matches!(self.mode, Mode::Desktop) { None @@ -6216,6 +6240,24 @@ impl Tab { ); } } + Location::Recents | Location::Search(SearchLocation::Recents, ..) => { + if let Some(items) = self.items_opt() + && !items.is_empty() + { + tab_column = tab_column.push( + widget::layer_container(widget::row::with_children([ + widget::space::horizontal().into(), + widget::button::standard(fl!("clear-recents-history")) + .on_press(Message::ClearRecents) + .into(), + ])) + .padding([space_xxs, space_xs]) + .layer(cosmic_theme::Layer::Primary) + .apply(widget::container) + .padding([0, 0, 7, 0]), + ); + } + } Location::Network(uri, _display_name, _path) if uri == "network:///" => { tab_column = tab_column.push( widget::layer_container(widget::row::with_children([ @@ -6282,7 +6324,7 @@ impl Tab { space_xxxs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let mut column = widget::column::with_capacity(4).spacing(space_m); @@ -6415,33 +6457,31 @@ impl Tab { let mut settings = Vec::new(); // Only allow modifying open-with if all mime types are the same - if mime_types.len() == 1 { - if let Some(mime) = mime_types - .get(0) + if mime_types.len() == 1 + && let Some(mime) = mime_types + .first() .and_then(|(mime, _)| mime.parse::().ok()) - { - if let Some(mime_app_cache) = mime_app_cache_opt { - let mime_apps = mime_app_cache.get(&mime); - if !mime_apps.is_empty() { - let mime_closure = mime.clone(); - settings.push( - widget::settings::item::builder(fl!("open-with")).control( - Element::from( - widget::dropdown( - mime_apps, - mime_apps.iter().position(|x| x.is_default), - move |index| (index, mime_closure.clone()), - ) - .icons(Cow::Borrowed(mime_app_cache.icons(&mime))), - ) - .map(|(index, mime)| { - let mime_app = &mime_apps[index]; - Message::SetOpenWith(mime, mime_app.id.clone()) - }), - ), - ); - } - } + && let Some(mime_app_cache) = mime_app_cache_opt + { + let mime_apps = mime_app_cache.get(&mime); + if !mime_apps.is_empty() { + let mime_closure = mime.clone(); + settings.push( + widget::settings::item::builder(fl!("open-with")).control( + Element::from( + widget::dropdown( + mime_apps, + mime_apps.iter().position(|x| x.is_default), + move |index| (index, mime_closure.clone()), + ) + .icons(Cow::Borrowed(mime_app_cache.icons(&mime))), + ) + .map(|(index, mime)| { + let mime_app = &mime_apps[index]; + Message::SetOpenWith(mime, mime_app.id.clone()) + }), + ), + ); } } @@ -7046,21 +7086,25 @@ fn text_editor_class( #[cfg(test)] mod tests { - use std::{fs, io, path::PathBuf}; + use std::path::PathBuf; + use std::{fs, io}; - use cosmic::{iced::mouse::ScrollDelta, iced::runtime::keyboard::Modifiers, widget}; + use cosmic::iced::mouse::ScrollDelta; + use cosmic::iced::runtime::keyboard::Modifiers; + use cosmic::widget; use log::{debug, trace}; + use mime_guess::mime; use tempfile::TempDir; use test_log::test; - use super::{Location, Message, Tab, respond_to_scroll_direction, scan_path}; - use crate::{ - app::test_utils::{ - NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, assert_eq_tab_path, empty_fs, - eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new, - }, - config::{IconSizes, TabConfig, ThumbCfg}, + use super::{ + ItemMetadata, ItemThumbnail, Location, Message, Tab, respond_to_scroll_direction, scan_path, }; + use crate::app::test_utils::{ + NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, assert_eq_tab_path, empty_fs, + eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new, + }; + use crate::config::{IconSizes, TabConfig, ThumbCfg}; // Boilerplate for tab tests. Checks if simulated clicks selected items. fn tab_selects_item( @@ -7473,4 +7517,86 @@ mod tests { } } } + + #[test] + fn item_thumbnail_text_preview_small_utf8_returns_text() -> io::Result<()> { + let dir = TempDir::new()?; + let path = dir.path().join("preview.txt"); + fs::write(&path, "Hello, world!")?; + let metadata = fs::metadata(&path)?; + let item_metadata = ItemMetadata::Path { + metadata, + children_opt: None, + }; + let thumb = ItemThumbnail::new( + &path, + item_metadata, + mime::TEXT_PLAIN, + 128, + 100 * 1024 * 1024, + 1, + 8, + ); + assert!( + matches!(thumb, ItemThumbnail::Text(_)), + "small text file should produce Text thumbnail" + ); + Ok(()) + } + + #[test] + fn item_thumbnail_text_preview_empty_file_returns_not_image() -> io::Result<()> { + let dir = TempDir::new()?; + let path = dir.path().join("empty.txt"); + fs::File::create(&path)?; + let metadata = fs::metadata(&path)?; + let item_metadata = ItemMetadata::Path { + metadata, + children_opt: None, + }; + let thumb = ItemThumbnail::new( + &path, + item_metadata, + mime::TEXT_PLAIN, + 128, + 100 * 1024 * 1024, + 1, + 8, + ); + assert!( + matches!(thumb, ItemThumbnail::NotImage), + "empty text file should produce NotImage (no read)" + ); + Ok(()) + } + + #[test] + fn item_thumbnail_text_preview_invalid_utf8_uses_valid_prefix() -> io::Result<()> { + let dir = TempDir::new()?; + let path = dir.path().join("invalid_utf8.txt"); + // Valid UTF-8 "ab" then invalid byte sequence then "c" + fs::write(&path, b"ab\xff\xfe\xfdc")?; + let metadata = fs::metadata(&path)?; + let item_metadata = ItemMetadata::Path { + metadata, + children_opt: None, + }; + let thumb = ItemThumbnail::new( + &path, + item_metadata, + mime::TEXT_PLAIN, + 128, + 100 * 1024 * 1024, + 1, + 8, + ); + match &thumb { + ItemThumbnail::Text(content) => { + // Text editor content may add a trailing newline + assert_eq!(content.text().trim_end(), "ab"); + } + _ => panic!("expected Text thumbnail with valid prefix only, got {:?}", thumb), + } + Ok(()) + } } diff --git a/src/thumbnail_cacher.rs b/src/thumbnail_cacher.rs index e4d616e..e42d343 100644 --- a/src/thumbnail_cacher.rs +++ b/src/thumbnail_cacher.rs @@ -1,16 +1,14 @@ use image::DynamicImage; use md5::{Digest, Md5}; use rustc_hash::FxHashMap; +use std::error::Error; +use std::fs::{self, File}; +use std::io::{self, BufReader, BufWriter}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use std::{ - error::Error, - fs::{self, File}, - io::{self, BufReader, BufWriter}, - path::{Path, PathBuf}, - sync::LazyLock, - time::UNIX_EPOCH, -}; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; +use std::time::UNIX_EPOCH; use tempfile::NamedTempFile; use url::Url; 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..618e77c 100644 --- a/src/trash.rs +++ b/src/trash.rs @@ -1,11 +1,10 @@ use cosmic::widget; use regex::Regex; -use std::{collections::HashSet, path::PathBuf}; +use std::collections::HashSet; +use std::path::PathBuf; -use crate::{ - config::IconSizes, - tab::{Item, SearchItem}, -}; +use crate::config::IconSizes; +use crate::tab::{Item, SearchItem}; pub trait TrashExt { fn is_empty() -> bool { @@ -27,7 +26,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() { @@ -81,7 +80,8 @@ impl TrashExt for Trash { } fn scan(sizes: IconSizes) -> Vec { - use crate::{localize::LANGUAGE_SORTER, tab::item_from_trash_entry}; + use crate::localize::LANGUAGE_SORTER; + use crate::tab::item_from_trash_entry; use std::cmp::Ordering; let entries = match trash::os_limited::list() { @@ -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); + } +} diff --git a/src/zoom.rs b/src/zoom.rs index 600d0b5..89ae5b8 100644 --- a/src/zoom.rs +++ b/src/zoom.rs @@ -1,6 +1,7 @@ use std::num::NonZeroU16; -use crate::{config::IconSizes, tab::View}; +use crate::config::IconSizes; +use crate::tab::View; static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap(); static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();