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 7e4753b..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]] @@ -187,23 +187,21 @@ checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" [[package]] name = "android-activity" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.11.0", + "bitflags 2.11.1", "cc", - "cesu8", "jni", - "jni-sys", "libc", "log", "ndk", "ndk-context", "ndk-sys", "num_enum", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -223,9 +221,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -244,9 +242,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -300,9 +298,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -330,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" @@ -363,13 +355,13 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.2", + "rand 0.9.4", "raw-window-handle", "serde", "serde_repr", "tokio", "url", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -381,7 +373,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_repr", "tokio", @@ -389,7 +381,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -501,9 +493,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -540,6 +532,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" + [[package]] name = "atomicwrites" version = "0.4.2" @@ -569,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]] @@ -584,7 +582,7 @@ checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" dependencies = [ "atspi-common", "serde", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -601,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" @@ -641,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", ] @@ -692,20 +690,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] @@ -723,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" @@ -756,10 +764,11 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ + "bytes", "cfg_aliases", ] @@ -773,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" @@ -795,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" @@ -860,9 +878,9 @@ dependencies = [ [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -874,7 +892,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "polling", "rustix 1.1.4", "slab", @@ -895,9 +913,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -905,12 +923,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfb" version = "0.7.3" @@ -953,7 +965,6 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "pure-rust-locales", "serde", "wasm-bindgen", "windows-link 0.2.1", @@ -961,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", ] @@ -981,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#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" dependencies = [ "objc", "objc-foundation", @@ -991,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#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" dependencies = [ "dnd", "mime 0.1.0", @@ -999,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#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" -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" @@ -1087,9 +1093,9 @@ dependencies = [ [[package]] name = "compio-buf" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8777c3ad31ab42f8a3a4a1bd629b78f688371df9b0f528d94dfbdbe5c945c9" +checksum = "a00d719dbd8c602ab0d25d219cbc6b517008858de7a8d6c51b4dc95aefff4dce" dependencies = [ "arrayvec", "bytes", @@ -1098,9 +1104,9 @@ dependencies = [ [[package]] name = "compio-driver" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8557d3804525f0e97594681f675929931782c9f8783a51c9f86da86b819150" +checksum = "74d42d98dc890ee4db00c1e68a723391711aab6d67085880d716b72830f7c715" dependencies = [ "cfg-if", "cfg_aliases", @@ -1214,10 +1220,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" [[package]] -name = "constant_time_eq" -version = "0.3.1" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation" @@ -1275,20 +1287,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "libc", ] -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "core_affinity" version = "0.8.3" @@ -1309,26 +1312,12 @@ dependencies = [ "libm", ] -[[package]] -name = "cosmic-client-toolkit" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d0e95be#d0e95be25e423cfe523b11111a3666ed7aaf0dc4" -dependencies = [ - "bitflags 2.11.0", - "cosmic-protocols 0.1.0", - "libc", - "smithay-client-toolkit", - "wayland-client", - "wayland-protocols", -] - [[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.0", - "cosmic-protocols 0.2.0", + "bitflags 2.11.1", + "cosmic-protocols", "libc", "smithay-client-toolkit", "wayland-client", @@ -1338,7 +1327,6 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1348,18 +1336,17 @@ dependencies = [ "iced_futures", "known-folders", "notify", - "ron 0.12.0", + "ron 0.12.1", "serde", "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#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "quote", "syn", @@ -1367,16 +1354,16 @@ dependencies = [ [[package]] name = "cosmic-files" -version = "1.0.8" +version = "1.0.13" dependencies = [ "anyhow", + "atomic_float", + "bstr", "bzip2", - "chrono", "compio", - "cosmic-client-toolkit 0.1.0", + "cosmic-client-toolkit", "cosmic-mime-apps", "dirs 6.0.0", - "env_logger", "fastrand", "filetime", "flate2", @@ -1389,9 +1376,11 @@ dependencies = [ "icu", "ignore", "image", + "jiff", + "jiff-icu", "jxl-oxide", "libc", - "libcosmic", + "libcosmic-yoda", "log", "lzma-rust2", "md-5", @@ -1399,6 +1388,7 @@ dependencies = [ "notify-debouncer-full", "notify-rust", "num_cpus", + "num_enum", "open", "ordermap", "paste", @@ -1407,15 +1397,18 @@ dependencies = [ "recently-used-xbel", "regex", "rust-embed", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "shlex", "slotmap", "tar", "tempfile", "test-log", + "thiserror 2.0.18", "tikv-jemallocator", "tokio", + "tracing", + "tracing-subscriber", "trash", "url", "uzers", @@ -1429,7 +1422,7 @@ dependencies = [ [[package]] name = "cosmic-files-applet" -version = "1.0.8" +version = "1.0.13" dependencies = [ "cosmic-files", "log", @@ -1439,7 +1432,6 @@ dependencies = [ [[package]] name = "cosmic-freedesktop-icons" version = "0.4.0" -source = "git+https://github.com/pop-os/freedesktop-icons#7a61a704f6d1ec41f71cbe766e3cc484858523fa" dependencies = [ "bstr", "btoi", @@ -1461,26 +1453,11 @@ dependencies = [ "xdg", ] -[[package]] -name = "cosmic-protocols" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d0e95be#d0e95be25e423cfe523b11111a3666ed7aaf0dc4" -dependencies = [ - "bitflags 2.11.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "wayland-server", -] - [[package]] name = "cosmic-protocols" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=160b086#160b086abe03cd34a8a375d7fbe47b24308d1f38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1492,7 +1469,6 @@ dependencies = [ [[package]] name = "cosmic-settings-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#e37160f14d1e7ee428f973cd2848b4e95f83dfe1" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1504,26 +1480,24 @@ 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#d5a972a2b63649fad11ea3a7e80f7dc4c592f01a" +version = "0.19.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "fontdb", "harfrust", "linebender_resource_handle", "log", "rangemap", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "self_cell", - "skrifa 0.40.0", + "skrifa", "smol_str", "swash", "sys-locale", @@ -1531,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#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "almost", "configparser", @@ -1544,12 +1518,18 @@ dependencies = [ "csscolorparser", "dirs 6.0.0", "palette", - "ron 0.12.0", + "ron 0.12.1", "serde", "serde_json", "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" @@ -1560,20 +1540,14 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.3.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "crc-catalog", + "libc", ] -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -1626,12 +1600,11 @@ 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", "lru", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "wgpu", ] @@ -1645,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" @@ -1664,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" @@ -1752,9 +1737,9 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "deflate64" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "deranged" @@ -1795,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]] @@ -1869,7 +1866,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1898,9 +1895,8 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "mime 0.1.0", "raw-window-handle", "smithay-client-toolkit", @@ -1925,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#a610ac9c7a72b39ff102ed4d946291618dc725b6" - -[[package]] -name = "drm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" -dependencies = [ - "bitflags 2.11.0", - "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" @@ -1974,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" @@ -2007,24 +1963,22 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", - "regex", ] [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", "env_filter", - "jiff", "log", ] @@ -2082,9 +2036,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -2133,29 +2087,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +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" @@ -2177,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]] @@ -2203,9 +2142,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed_decimal" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" dependencies = [ "displaydoc", "smallvec", @@ -2264,7 +2203,7 @@ dependencies = [ "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "self_cell", "smallvec", "unic-langid", @@ -2318,18 +2257,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.10.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "font-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ "bytemuck", ] @@ -2386,9 +2316,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "fork" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29714eb48a54d35d6f107e38cc58003f2e26825f222119db8369d28b24b79f2a" +checksum = "5bcc4b4161e53d499e41af904acb23950adf85682c772921ef3957cf1ecc98b3" dependencies = [ "libc", ] @@ -2423,7 +2353,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6d3a3635983a889f065aa9ce760384713f23a9b4a04f696f86c39a5d7a6a5a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "nom 8.0.0", ] @@ -2562,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" @@ -2590,11 +2510,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -2604,10 +2522,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -2642,9 +2562,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -2703,7 +2623,7 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -2798,7 +2718,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "gpu-alloc-types", ] @@ -2808,7 +2728,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -2829,7 +2749,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2840,14 +2760,14 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[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" @@ -2877,10 +2797,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "core_maths", - "read-fonts 0.37.0", + "read-fonts", "smallvec", ] @@ -2908,6 +2828,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.4.1" @@ -2940,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]] @@ -3041,7 +2976,6 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "dnd", "iced_accessibility", @@ -3062,7 +2996,6 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "accesskit", "accesskit_winit", @@ -3071,11 +3004,10 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "dnd", "glam", "lilt", @@ -3084,7 +3016,7 @@ dependencies = [ "num-traits", "palette", "raw-window-handle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "smol_str", "thiserror 2.0.18", @@ -3095,7 +3027,6 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "iced_core", "iced_futures", @@ -3105,12 +3036,11 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "futures", "iced_core", "log", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "tokio", "wasm-bindgen-futures", "wasmtimer", @@ -3119,9 +3049,8 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "cosmic-text", "half", @@ -3132,7 +3061,7 @@ dependencies = [ "log", "lyon_path", "raw-window-handle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "thiserror 2.0.18", "unicode-segmentation", ] @@ -3140,7 +3069,6 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "iced_graphics", "iced_runtime", @@ -3149,7 +3077,6 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3161,10 +3088,9 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "bytes", - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "dnd", "iced_core", "iced_futures", @@ -3176,7 +3102,6 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ "bytemuck", "cosmic-text", @@ -3185,7 +3110,7 @@ dependencies = [ "kurbo 0.10.4", "log", "resvg", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "softbuffer", "tiny-skia", ] @@ -3193,12 +3118,10 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ - "as-raw-xcb-connection", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "cryoglyph", "futures", "glam", @@ -3209,31 +3132,28 @@ dependencies = [ "lyon", "raw-window-handle", "resvg", - "rustc-hash 2.1.1", + "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#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "dnd", "iced_renderer", "iced_runtime", "log", "num-traits", "ouroboros", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "thiserror 2.0.18", "unicode-segmentation", "window_clipboard", @@ -3242,9 +3162,8 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" dependencies = [ - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "cursor-icon", "dnd", "iced_debug", @@ -3254,7 +3173,7 @@ dependencies = [ "iced_runtime", "log", "raw-window-handle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustix 0.38.44", "thiserror 2.0.18", "wasm-bindgen-futures", @@ -3273,9 +3192,9 @@ dependencies = [ [[package]] name = "icu" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ab713dd86fa032cb5487f9ac3a85d47b5dcf4c7b8c7dd00210b3cadd6a6551" +checksum = "00380f83691e089bcfa4aeb03a2d96a910b1c9ea406d6f822fc19dfb8b58d1ec" dependencies = [ "icu_calendar", "icu_casemap", @@ -3297,9 +3216,9 @@ dependencies = [ [[package]] name = "icu_calendar" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45" dependencies = [ "calendrical_calculations", "displaydoc", @@ -3315,15 +3234,15 @@ dependencies = [ [[package]] name = "icu_calendar_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" +checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d" [[package]] name = "icu_casemap" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ca9983e8bf51223c2f89014fa4eaa9e9b336c47f3af0d000538f86f841fba1" +checksum = "070f98b5b82798fcb93654bf96ed9f40064fc44c86f51a09ea711092cd5cc5be" dependencies = [ "icu_casemap_data", "icu_collections", @@ -3337,15 +3256,15 @@ dependencies = [ [[package]] name = "icu_casemap_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d4663d0f99b301033a19e0acf94e9d2fa4b107638580165e5a6ccc49ad1450" +checksum = "846b0857ca091204be3c874bc93daaf89d4777e8d2d20b0d3ffe8f671d98014b" [[package]] name = "icu_collator" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" +checksum = "b521b92a2666061ddda902769d8a4cf730b5c9529a845cc1b69770b12a6c9a71" dependencies = [ "icu_collator_data", "icu_collections", @@ -3362,19 +3281,20 @@ dependencies = [ [[package]] name = "icu_collator_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", "serde", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3382,9 +3302,9 @@ dependencies = [ [[package]] name = "icu_datetime" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5" +checksum = "989d56ea5bbc43ae2b4e0388874b002884eaf4ed3a76c84a6c8c5ad575e04d72" dependencies = [ "displaydoc", "fixed_decimal", @@ -3405,20 +3325,23 @@ dependencies = [ [[package]] name = "icu_datetime_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb" +checksum = "40d3cc1b690d9703202bc319692ac8a1f3a6390686f0930ff40542450fa34f0b" [[package]] name = "icu_decimal" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" dependencies = [ + "displaydoc", "fixed_decimal", "icu_decimal_data", "icu_locale", "icu_locale_core", + "icu_pattern", + "icu_plurals", "icu_provider", "serde", "writeable", @@ -3427,15 +3350,15 @@ dependencies = [ [[package]] name = "icu_decimal_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" [[package]] name = "icu_experimental" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ffa4d60b9cb8b024082afaf9e94d853184e483ec69322c74dc437bf8a882a5" +checksum = "0a881116e620fd635f564fd9cb9bc36c256b9da2221df8b3f55643d8ef32140f" dependencies = [ "displaydoc", "either", @@ -3466,15 +3389,15 @@ dependencies = [ [[package]] name = "icu_experimental_data" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bce39e12480e91c7ddb748218050c459e241f491d130ea6ee92c3e5cd254f7" +checksum = "f72090d4f08a2bc94565cb02de6d5b87939424e462d9927d73a34f6f8e5d1232" [[package]] name = "icu_list" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a0b7b126e2fc42777d3c348611553d540bd3683caa39b387c5dd1036bb21a8" +checksum = "aeeaf517689324395bed4767f7c65504f5455942ed4c14ee54c2087ca00b816e" dependencies = [ "icu_list_data", "icu_locale", @@ -3487,15 +3410,15 @@ dependencies = [ [[package]] name = "icu_list_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51044c242fe2a882cc0a464314bbdb9f441556a1cb238fb527fc47355ec2827b" +checksum = "ed62dbf114db9a4163481ed071509c4cd52cbcef9cb85979eba08a95549d73f3" [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -3508,9 +3431,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3522,15 +3445,15 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3545,15 +3468,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_pattern" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b" +checksum = "1c4c568054ffe735398a9f4c55aec37ad7c768844553cc0978f09cc9b933a1fb" dependencies = [ "displaydoc", "either", @@ -3565,9 +3488,9 @@ dependencies = [ [[package]] name = "icu_plurals" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" dependencies = [ "fixed_decimal", "icu_locale", @@ -3578,15 +3501,15 @@ dependencies = [ [[package]] name = "icu_plurals_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3599,15 +3522,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3622,9 +3545,9 @@ dependencies = [ [[package]] name = "icu_segmenter" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a807a7488f3f758629ae86d99d9d30dce24da2fb2945d74c80a4f4a62c71db73" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" dependencies = [ "core_maths", "icu_collections", @@ -3638,15 +3561,15 @@ dependencies = [ [[package]] name = "icu_segmenter_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebbb7321d9e21d25f5660366cb6c08201d0175898a3a6f7a41ee9685af21c80" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" [[package]] name = "icu_time" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8" +checksum = "ec3af0c141da0a61d4f6970cd1d5f4b388b17ea22f8124f8f6049d3d5147586a" dependencies = [ "calendrical_calculations", "displaydoc", @@ -3662,9 +3585,9 @@ dependencies = [ [[package]] name = "icu_time_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" +checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9" [[package]] name = "id-arena" @@ -3691,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", @@ -3725,7 +3648,7 @@ dependencies = [ "byteorder-lite", "color_quant", "exr", - "gif 0.14.1", + "gif 0.14.2", "image-webp", "moxcms", "num-traits", @@ -3736,7 +3659,16 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.1", - "zune-jpeg 0.5.13", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-extras" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d02eb2c9ccbbab470538fce34c7bc3be7b4e59268e65a3171367b296cdb842" +dependencies = [ + "image", ] [[package]] @@ -3757,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" @@ -3774,12 +3706,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3799,7 +3731,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "inotify-sys", "libc", ] @@ -3815,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]] @@ -3854,11 +3786,11 @@ 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.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -3910,21 +3842,21 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "ixdtf" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" +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", @@ -3936,10 +3868,21 @@ dependencies = [ ] [[package]] -name = "jiff-static" -version = "0.2.23" +name = "jiff-icu" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" +dependencies = [ + "icu_calendar", + "icu_time", + "jiff", +] + +[[package]] +name = "jiff-static" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c22e04db9c58f5136eb1757f3d5c49a7b187f49e52185228cbd2f5acdfcc08c" dependencies = [ "proc-macro2", "quote", @@ -3963,25 +3906,61 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-macros", + "jni-sys 0.4.1", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -3995,10 +3974,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -4198,7 +4179,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "serde", ] @@ -4221,9 +4202,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "known-folders" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770919970f7d2f74fea948900d35e2ef64f44129e8ae4015f59de1f0aca7c2a5" +checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ "windows-sys 0.61.2", ] @@ -4240,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", ] @@ -4289,25 +4270,24 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libcosmic" -version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic.git#9602dfd2f12b667e0afacdccd0e403d8152dde5a" +name = "libcosmic-yoda" +version = "0.1.0-yoda.2" dependencies = [ "apply", "ashpd 0.12.3", "auto_enums", - "cosmic-client-toolkit 0.2.0", + "cosmic-client-toolkit", "cosmic-config", "cosmic-freedesktop-icons", "cosmic-settings-config", @@ -4330,13 +4310,13 @@ dependencies = [ "iced_widget", "iced_winit", "image", + "image-extras", "jiff", "libc", "log", "mime 0.3.17", "palette", "phf 0.13.1", - "raw-window-handle", "rfd", "rust-embed", "rustix 1.1.4", @@ -4349,7 +4329,7 @@ dependencies = [ "tracing", "unicode-segmentation", "url", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -4380,14 +4360,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] @@ -4411,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" @@ -4425,9 +4399,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -4459,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" @@ -4488,9 +4462,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] name = "lyon" @@ -4504,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", @@ -4535,9 +4509,9 @@ dependencies = [ [[package]] name = "lyon_tessellation" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a35a7dd71b845ff317ce1834c4185506b79790294bde397df8d5c23031e357" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" dependencies = [ "float_next_after", "lyon_path", @@ -4546,12 +4520,11 @@ dependencies = [ [[package]] name = "lzma-rust2" -version = "0.15.7" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" +checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20" dependencies = [ - "crc", - "sha2", + "sha2 0.11.0", ] [[package]] @@ -4601,7 +4574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -4643,7 +4616,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -4655,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#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" dependencies = [ "smithay-clipboard", ] @@ -4694,9 +4666,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -4728,14 +4700,14 @@ checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "codespan-reporting", "half", "hashbrown 0.16.1", "hexf-parse", - "indexmap 2.13.0", + "indexmap 2.14.0", "libm", "log", "num-traits", @@ -4752,8 +4724,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.11.1", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -4773,7 +4745,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -4788,13 +4760,22 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", "memoffset", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -4826,7 +4807,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "fsevent-sys", "inotify", "kqueue", @@ -4853,16 +4834,16 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.12.0" +version = "4.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" dependencies = [ "futures-lite", "log", "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -4871,7 +4852,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -4895,9 +4876,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -5023,7 +5004,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -5039,7 +5020,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -5052,7 +5033,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5064,7 +5045,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "dispatch2", "objc2 0.6.4", @@ -5076,7 +5057,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "objc2-core-foundation", ] @@ -5099,7 +5080,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2-core-foundation", "objc2-core-graphics", ] @@ -5116,7 +5097,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -5128,7 +5109,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -5141,7 +5122,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5153,7 +5134,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5166,7 +5147,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -5195,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", @@ -5212,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", @@ -5222,9 +5203,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -5241,11 +5222,11 @@ dependencies = [ [[package]] name = "ordermap" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa78c92071bbd3628c22b1a964f7e0eb201dc1456555db072beb1662ecd6715" +checksum = "7f7476a5b122ff1fce7208e7ee9dccd0a516e835f5b8b19b8f3c98a34cf757c1" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_core", ] @@ -5367,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", ] @@ -5409,7 +5390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -5476,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", @@ -5519,9 +5500,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5548,7 +5529,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -5583,18 +5564,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "serde_core", "writeable", @@ -5697,7 +5678,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "chrono", "flate2", "procfs-core", @@ -5710,41 +5691,35 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "chrono", "hex", ] [[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", ] -[[package]] -name = "pure-rust-locales" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869675ad2d7541aea90c6d88c81f46a7f4ea9af8cd0395d38f11a95126998a0d" - [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -5787,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]] @@ -5822,9 +5797,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5833,9 +5808,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5918,7 +5893,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "simd_helpers", "thiserror 2.0.18", @@ -5949,9 +5924,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -5967,16 +5942,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "read-fonts" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "font-types 0.10.1", -] - [[package]] name = "read-fonts" version = "0.37.0" @@ -5985,7 +5950,7 @@ checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ "bytemuck", "core_maths", - "font-types 0.11.0", + "font-types", ] [[package]] @@ -6010,16 +5975,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -6156,7 +6121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" dependencies = [ "base64", - "bitflags 2.11.0", + "bitflags 2.11.1", "serde", "serde_derive", "unicode-ident", @@ -6164,11 +6129,11 @@ dependencies = [ [[package]] name = "ron" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "once_cell", "serde", "serde_derive", @@ -6212,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", ] @@ -6224,9 +6189,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustix" @@ -6234,7 +6208,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6247,7 +6221,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -6266,7 +6240,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "core_maths", "log", @@ -6344,9 +6318,9 @@ checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -6380,11 +6354,11 @@ 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.13.0", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -6405,24 +6379,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] [[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", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -6433,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", @@ -6450,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]] @@ -6461,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]] @@ -6492,9 +6489,19 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simd_helpers" @@ -6505,6 +6512,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.2" @@ -6516,19 +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" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts 0.35.0", -] +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" @@ -6537,7 +6540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", - "read-fonts 0.37.0", + "read-fonts", ] [[package]] @@ -6567,7 +6570,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "calloop", "calloop-wayland-source", @@ -6594,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", @@ -6625,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", @@ -6642,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]] @@ -6667,7 +6664,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -6697,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" @@ -6721,11 +6712,11 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" dependencies = [ - "skrifa 0.37.0", + "skrifa", "yazi", "zeno", ] @@ -6743,9 +6734,9 @@ dependencies = [ [[package]] name = "synchrony" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5f5d3091c2d998f6ab4c8b495c0d232ad0aecbc1fa9ac52c247a79d497e16" +checksum = "416090a4d8f6358526df5f9f65dfe28750b8b7bfd1fd8a5620f483fc4a75722c" dependencies = [ "futures-util", "loom", @@ -6773,14 +6764,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.7" +version = "7.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "version-compare", ] @@ -6798,9 +6789,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -6855,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", @@ -6865,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" @@ -6941,7 +6942,7 @@ dependencies = [ "half", "quick-error", "weezl", - "zune-jpeg 0.5.13", + "zune-jpeg 0.5.15", ] [[package]] @@ -7022,24 +7023,11 @@ 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.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -7063,9 +7051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -7080,9 +7068,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -7111,14 +7099,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", "winnow", @@ -7126,48 +7114,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.0.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.0.0+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tracing" @@ -7263,7 +7242,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -7280,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" @@ -7373,9 +7352,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-vo" @@ -7461,9 +7440,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", "serde_core", @@ -7527,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]] @@ -7540,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.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -7558,23 +7537,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7582,9 +7557,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -7595,9 +7570,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -7619,7 +7594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -7630,9 +7605,9 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] @@ -7652,9 +7627,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -7666,11 +7641,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -7682,16 +7657,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ "rustix 1.1.4", "wayland-client", @@ -7700,11 +7675,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.11" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7717,7 +7692,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7726,11 +7701,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7739,11 +7714,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7752,11 +7727,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7766,22 +7741,22 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.39.2", + "quick-xml 0.39.4", "quote", ] [[package]] name = "wayland-server" -version = "0.31.12" +version = "0.31.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63736a4a73e781cf6a736aa32c5d6773c3eb5389197562742a8c611b49b5e359" +checksum = "cc1846eb04c49182e04f4a099e2a830a2b745610bbc1d61246e206f29c7000a0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "downcast-rs", "rustix 1.1.4", "wayland-backend", @@ -7790,9 +7765,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -7802,9 +7777,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -7833,7 +7808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "document-features", @@ -7864,12 +7839,12 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "cfg_aliases", "document-features", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "naga", "once_cell", @@ -7924,7 +7899,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.11.0", + "bitflags 2.11.1", "block", "bytemuck", "cfg-if", @@ -7969,7 +7944,7 @@ version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "js-sys", "log", @@ -8017,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#3a7af79e54db6854d8aa9d9e2866a9288d0f95d5" dependencies = [ "clipboard-win", "clipboard_macos", "clipboard_wayland", - "clipboard_x11", "dnd", "mime 0.1.0", "raw-window-handle", @@ -8285,15 +8258,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -8339,21 +8303,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -8420,12 +8369,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8444,12 +8387,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8468,12 +8405,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8504,12 +8435,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8528,12 +8453,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8552,12 +8471,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8576,12 +8489,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8603,9 +8510,8 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg_aliases", "cursor-icon", "dpi", @@ -8623,16 +8529,14 @@ 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#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ "android-activity", - "bitflags 2.11.0", + "bitflags 2.11.1", "dpi", "ndk", "raw-window-handle", @@ -8644,9 +8548,8 @@ dependencies = [ [[package]] name = "winit-appkit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "dispatch2", "dpi", @@ -8666,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#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ "memmap2 0.9.10", "objc2 0.6.4", @@ -8674,16 +8576,14 @@ 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#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cursor-icon", "dpi", "keyboard-types", @@ -8695,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#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dpi", "libredox", "orbclient", "raw-window-handle", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", "smol_str", "tracing", "winit-core", @@ -8711,9 +8610,8 @@ dependencies = [ [[package]] name = "winit-uikit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2 0.6.2", "dispatch2", "dpi", @@ -8731,10 +8629,9 @@ dependencies = [ [[package]] name = "winit-wayland" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ "ahash", - "bitflags 2.11.0", + "bitflags 2.11.1", "calloop", "cursor-icon", "dpi", @@ -8757,10 +8654,9 @@ dependencies = [ [[package]] name = "winit-web" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ "atomic-waker", - "bitflags 2.11.0", + "bitflags 2.11.1", "concurrent-queue", "cursor-icon", "dpi", @@ -8779,9 +8675,8 @@ dependencies = [ [[package]] name = "winit-win32" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#a610ac9c7a72b39ff102ed4d946291618dc725b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cursor-icon", "dpi", "raw-window-handle", @@ -8792,34 +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#a610ac9c7a72b39ff102ed4d946291618dc725b6" -dependencies = [ - "bitflags 2.11.0", - "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" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -8833,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" @@ -8852,7 +8730,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -8882,8 +8760,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags 2.11.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -8902,7 +8780,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -8920,46 +8798,13 @@ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" 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" @@ -9055,7 +8900,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dlib", "log", "once_cell", @@ -9073,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" @@ -9118,9 +8963,9 @@ checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -9129,9 +8974,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -9163,10 +9008,10 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_repr", - "sha1", + "sha1 0.10.6", "static_assertions", "tracing", "uds_windows", @@ -9179,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", @@ -9208,9 +9053,9 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow", - "zbus_macros 5.14.0", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_macros 5.15.0", + "zbus_names 4.3.2", + "zvariant 5.11.0", ] [[package]] @@ -9220,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]] @@ -9234,7 +9079,7 @@ dependencies = [ "syn", "zbus-lockstep", "zbus_xml", - "zvariant 5.10.0", + "zvariant 5.11.0", ] [[package]] @@ -9252,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]] @@ -9278,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", - "zvariant 5.10.0", + "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]] @@ -9307,18 +9152,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -9327,18 +9172,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -9351,37 +9196,24 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -9391,9 +9223,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -9402,9 +9234,9 @@ dependencies = [ [[package]] name = "zip" -version = "7.2.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "aes", "bzip2", @@ -9412,15 +9244,14 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "generic-array", - "getrandom 0.3.4", + "getrandom 0.4.2", "hmac", - "indexmap 2.13.0", + "indexmap 2.14.0", "lzma-rust2", "memchr", "pbkdf2", "ppmd-rust", - "sha1", + "sha1 0.11.0", "time", "typed-path", "zeroize", @@ -9512,9 +9343,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core 0.5.1", ] @@ -9534,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", - "zvariant_derive 5.10.0", - "zvariant_utils 3.3.0", + "zvariant_derive 5.11.0", + "zvariant_utils 3.3.1", ] [[package]] @@ -9562,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]] @@ -9586,9 +9417,9 @@ 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", @@ -9596,3 +9427,7 @@ dependencies = [ "syn", "winnow", ] + +[[patch.unused]] +name = "winit-x11" +version = "0.31.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index be0b5ff..9e3fe98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "cosmic-files" -version = "1.0.8" +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" -chrono = { version = "0.4", features = ["unstable-locales"] } -icu = { version = "2.1.1", features = ["compiled_data"] } -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } +jiff = "0.2" +jiff-icu = "0.2" +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" -env_logger = "0.11" gio = { version = "0.21", optional = true } glib = { version = "0.21", optional = true } glob = "0.3" @@ -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.12", 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" -lzma-rust2 = { version = "0.15.7", optional = true } -ordermap = { version = "1.1.0", features = ["serde"] } +tar = "0.4.45" +lzma-rust2 = { version = "0.16", optional = true } +ordermap = { version = "1.2.0", features = ["serde"] } # Internationalization i18n-embed = { version = "0.16", features = [ "fluent-system", @@ -54,13 +54,18 @@ i18n-embed-fl = "0.10" rust-embed = "8" slotmap = "1.1.1" recently-used-xbel = "1.2.0" -zip = "7" -uzers = "0.12.2" +zip = "8" md-5 = "0.10.6" png = "0.18" 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.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] @@ -68,17 +73,18 @@ 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 = [ "about", + "advanced-shaping", "autosize", - "desktop", "multi-window", "tokio", - "winit", + "wayland", "surface-message", ] @@ -106,15 +112,15 @@ default = [ "wayland", "wgpu", ] -dbus-config = ["libcosmic/dbus-config"] -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 @@ -124,7 +130,8 @@ inherits = "release" debug = true [target.'cfg(unix)'.dependencies] -fork = "0.6" +fork = "0.7" +uzers = "0.12.2" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.18" @@ -139,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 0fc8e0e..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.8" +version = "1.0.13" edition = "2024" [dependencies] diff --git a/debian/changelog b/debian/changelog index c4358c8..e4697d2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +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 + + -- Jeremy Soller Tue, 14 Apr 2026 11:09:44 -0600 + +cosmic-files (1.0.9) noble; urgency=medium + + * Epoch 1.0.9 version update + + -- Jeremy Soller Mon, 06 Apr 2026 15:10:13 -0600 + cosmic-files (1.0.8) noble; urgency=medium * Epoch 1.0.8 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 687d070..df77069 100644 --- a/examples/dialog.rs +++ b/examples/dialog.rs @@ -1,17 +1,30 @@ -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; +use tracing_subscriber::util::SubscriberInitExt; fn main() -> Result<(), Box> { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let log_format = tracing_subscriber::fmt::format() + .pretty() + .without_time() + .with_line_number(true) + .with_file(true) + .with_target(false) + .with_thread_names(true); + + let log_layer = tracing_subscriber::fmt::Layer::default() + .with_writer(std::io::stderr) + .event_format(log_format); + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_env("RUST_LOG")) + .with(log_layer) + .init(); let settings = Settings::default(); app::run::(settings, ())?; @@ -148,7 +161,7 @@ impl Application for App { } fn view(&self) -> Element<'_, Message> { - let mut column = widget::column().spacing(8).padding(8); + let mut column = widget::column::with_capacity(8).spacing(8).padding(8); { let mut button = widget::button::standard("Open File"); if self.dialog_opt.is_none() { diff --git a/i18n/ar/cosmic_files.ftl b/i18n/ar/cosmic_files.ftl index f9b641d..c4e3292 100644 --- a/i18n/ar/cosmic_files.ftl +++ b/i18n/ar/cosmic_files.ftl @@ -22,10 +22,10 @@ empty-trash-warning = سيتم حذف العناصر الموجودة في مج ## New File/Folder Dialog -create-new-file = إنشاء ملف جديد -create-new-folder = إنشاء مجلد جديد +create-new-file = أنشئ ملف جديد +create-new-folder = أنشئ مجلَّد جديد file-name = اسم الملف -folder-name = اسم المجلد +folder-name = اسم المجلَّد file-already-exists = يوجد ملف بهذا الاسم بالفعل folder-already-exists = يوجد مجلد بهذا الاسم بالفعل name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية @@ -34,10 +34,10 @@ name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات م ## Open/Save Dialog -cancel = إلغاء -open = فتح -open-file = فتح ملف -open-folder = افتح مجلد +cancel = ألغِ +open = افتح +open-file = افتح ملف +open-folder = افتح مجلَّد open-in-new-tab = افتح في لسان جديد open-in-new-window = افتح في نافذة جديدة open-multiple-files = افتح عدة ملفات @@ -150,12 +150,12 @@ trashed-on = مهمل details = التفاصيل pause = ألبث resume = استئناف -create-archive = إنشاء أرشيف +create-archive = أنشئ أرشيف extract-password-required = كلمة السر مطلوبة extract-to = استخرِج إلى... extract-to-title = استخرِج إلى مجلّد mount-error = تعذر الوصول إلى القرص -create = إنشاء +create = أنشئ open-item-location = افتح مكان العنصر set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟ favorite-path-error-description = @@ -259,7 +259,7 @@ related-apps = تطبيقات ذات صلة selected-items = العناصر { $items } المحدّدة permanently-delete-question = احذف نهائيًا؟ delete = احذف -permanently-delete-warning = هل أنت متأكد من أنك تريد حذف { $target } نهائيًا؟ لا يمكن التراجع عن هذا الإجراء. +permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء. replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه. original-file = الملف الأصلي replace-with = استبدل بـ @@ -371,3 +371,15 @@ move-to = انقل إلى... 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 fde9f4f..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 @@ -176,7 +176,7 @@ set-executable-and-launch-description = Chcete povolit spouštění souboru „{ set-and-launch = Povolit a spustit open-with = Otevřít pomocí other = Ostatní -none = Žádný +none = Žádné icon-size-and-spacing = Velikost a rozestupy ikon grid-spacing = Rozestupy mřížky deleting = @@ -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 @@ -417,3 +417,13 @@ move-to-title = Vyberte cíl přesunutí 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 c300df6..993af2d 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -330,7 +330,7 @@ light = Hell type-to-search = Zum Suchen tippen type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner -type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein +type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein # Kontextmenü add-to-sidebar = Zur Seitenleiste hinzufügen compress = Komprimieren... @@ -376,9 +376,9 @@ select-all = Alles auswählen ## Ansicht -zoom-in = Vergrößern +zoom-in = Hineinzoomen default-size = Standardgröße -zoom-out = Verkleinern +zoom-out = Herauszoomen view = Ansicht grid-view = Rasteransicht list-view = Listenansicht @@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien... ## Sortieren -sort = Sortieren +sort = Sortierung sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Neueste zuerst @@ -424,3 +424,16 @@ clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren comment = Dateimanager für den COSMIC Desktop keywords = Ordner;Manager; move-to-button-label = Verschieben +move-to-title = Verschiebeziel auswählen +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 654e9ab..489ff7a 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -96,9 +96,17 @@ 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 +context-action = Context action +context-action-confirm-title = Run "{$name}"? +context-action-confirm-warning = This will run on {$items} {$items -> + [one] item + *[other] items + }. +run = Run ## Permanently delete Dialog selected-items = The {$items} selected items @@ -109,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 @@ -131,6 +140,12 @@ 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 ### Mode 1 (unusual) diff --git a/i18n/es-419/cosmic_files.ftl b/i18n/es-419/cosmic_files.ftl index ca17ec5..9f8ef72 100644 --- a/i18n/es-419/cosmic_files.ftl +++ b/i18n/es-419/cosmic_files.ftl @@ -5,7 +5,7 @@ no-results = No se encontraron resultados filesystem = Sistema de archivos home = Inicio networks = Redes -notification-in-progress = Las operaciones de archivo están en progreso. +notification-in-progress = Las operaciones de archivo están en progreso trash = Papelera recents = Recientes undo = Deshacer diff --git a/i18n/eu/cosmic_files.ftl b/i18n/eu/cosmic_files.ftl new file mode 100644 index 0000000..e69de29 diff --git a/i18n/fi/cosmic_files.ftl b/i18n/fi/cosmic_files.ftl index 105bff6..f1c3c58 100644 --- a/i18n/fi/cosmic_files.ftl +++ b/i18n/fi/cosmic_files.ftl @@ -5,7 +5,7 @@ no-results = Ei tuloksia filesystem = Tiedostojärjestelmä home = Koti networks = Verkot -notification-in-progress = Tiedostotoimintoja käynnissä. +notification-in-progress = Tiedostotoimintoja käynnissä trash = Roskakori recents = Viimeaikaiset undo = Kumoa @@ -16,7 +16,7 @@ today = Tänään desktop-view-options = Työpöytänäkymän asetukset… show-on-desktop = Näytä työpöydällä desktop-folder-content = Työpöytäkansion sisältö -mounted-drives = Tiedostojärjestelmään liitetyt kovalevyt +mounted-drives = Liitetyt asemat trash-folder-icon = Roskakorikansion kuvake icon-size-and-spacing = Kuvakkeen koko ja välistys icon-size = Kuvakkeen koko @@ -38,7 +38,7 @@ create-archive = Luo arkisto ## Empty Trash Dialog empty-trash = Tyhjennä roskakori -empty-trash-warning = Haluatko varmasti tyhjentää koko roskakorin pysyvästi? +empty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi ## Mount Error Dialog @@ -73,8 +73,8 @@ save-file = Tallenna tiedosto ## Open With Dialog -open-with-title = Kuinka haluat avata kohteen "{ $name }"? -browse-store = Selaa { $store } +open-with-title = Miten haluat avata kohteen "{ $name }"? +browse-store = Selaa { $store }a ## Rename Dialog @@ -88,15 +88,15 @@ replace-title = "{ $filename }" on jo olemassa tässä sijainnissa replace-warning = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön. replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön. original-file = Alkuperäinen tiedosto -replace-with = Korvaa kohteella -apply-to-all = Sovella kaikkiin +replace-with = Korvaa käyttäen +apply-to-all = Toteuta kaikkiin keep-both = Pidä molemmat skip = Ohita ## Set as Executable and Launch Dialog set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä -set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettväksi ja käynnistää sen? +set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettäväksi ja käynnistää sen? set-and-launch = Aseta ja käynnistä ## Metadata Dialog @@ -116,9 +116,9 @@ other = Muut add-network-drive = Lisää verkkolevy connect = Yhdistä connect-anonymously = Yhdistä nimettömästi -connecting = Yhdistää… +connecting = Yhdistetään… domain = Verkkotunnus -enter-server-address = Syötä palvelimen osoite +enter-server-address = Kirjoita palvelimen osoite network-drive-description = Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen. Esimerkkejä: ssh://192.168.0.1, ftp://[2001:db8::1] @@ -126,91 +126,91 @@ network-drive-description = ### Make sure to keep the comma which separates the columns network-drive-schemes = - Saatavissa olevat protokollat,Etuliite + Saatavilla olevat yhteyskäytännöt,Etuliite AppleTalk,afp:// - File Transfer Protocol,ftp:// or ftps:// + File Transfer Protocol,ftp:// tai ftps:// Network File System,nfs:// Server Message Block,smb:// - SSH File Transfer Protocol,sftp:// or ssh:// - WebDav,dav:// or davs:// -network-drive-error = Verkkolevy saavuttamattomissa + SSH File Transfer Protocol,sftp:// tai ssh:// + WebDav,dav:// tai davs:// +network-drive-error = Verkkolevy ei saatavilla password = Salasana remember-password = Muista salasana try-again = Yritä uudelleen -username = Käyttäjänimi +username = Käyttäjätunnus ## Operations edit-history = Muokkaa historiaa history = Historia no-history = Historia on tyhjä. -pending = Odottaa käsittelyä -failed = Epäonnistui -complete = Valmis +pending = Jonossa +failed = Epäonnistuneet +complete = Valmiit compressing = - Tiivistetään { $items } { $items -> + Pakataan { $items } { $items -> [one] kohde - *[other] kohteita - } lähteestä "{ $from }" arkistoon "{ $to }" + *[other] kohdetta + } sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })… compressed = - Tiivistetty { $items } { $items -> + Pakattu { $items } { $items -> [one] kohde - *[other] kohteet - } lähteestä "{ $from }" arkistoon "{ $to }" + *[other] kohdetta + } sijainnista "{ $from }" arkistoon "{ $to }" copy_noun = Kopio -creating = Luodaan kohdetta "{ $name }" kohteen "{ $parent }" alle -created = Luotu kohde "{ $name }" kohteen "{ $parent }" alle +creating = Luodaan "{ $name }" kohteen "{ $parent }" alle +created = Luotu "{ $name }" kohteen "{ $parent }" alle copying = Kopioidaan { $items } { $items -> [one] kohde - *[other] kohteita - } lähteestä "{ $from }" kohteeseen "{ $to }" + *[other] kohdetta + } sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… copied = Kopioitu { $items } { $items -> [one] kohde - *[other] kohteet - } lähteestä "{ $from }" kohteeseen "{ $to }" -emptying-trash = Tyhjennetään { trash } + *[other] kohdetta + } sijainnista "{ $from }" kohteeseen "{ $to }" +emptying-trash = Tyhjennetään { trash } ({ $progress })… emptied-trash = Tyhjennetty { trash } extracting = Puretaan { $items } { $items -> [one] kohde - *[other] kohteet - } arkistosta "{ $from }" kohteeseen "{ $to }" + *[other] kohdetta + } arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })… extracted = Purettu { $items } { $items -> [one] kohde - *[other] kohteet + *[other] kohdetta } arkistosta "{ $from }" kohteeseen "{ $to }" setting-executable-and-launching = Asetetaan "{ $name }" käynnistettäväksi ja käynnistetään set-executable-and-launched = Asetettu "{ $name }" käynnistettäväksi ja käynnistetty moving = Siirretään { $items } { $items -> [one] kohde - *[other] kohteet - } lähteestä "{ $from }" kohteeseen "{ $to }" + *[other] kohdetta + } sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… moved = Siirretty { $items } { $items -> [one] kohde - *[other] kohteet - } lähteestä "{ $from }" kohteeseen "{ $to }" + *[other] kohdetta + } sijainnista "{ $from }" kohteeseen "{ $to }" renaming = Nimetään kohde "{ $from }" muotoon "{ $to }" renamed = Nimetty kohde "{ $from }" muotoon "{ $to }" restoring = Palautetaan { $items } { $items -> [one] kohde - *[other] kohteet - } { trash }sta + *[other] kohdetta + } roskakorista ({ $progress })… restored = Palautettu { $items } { $items -> [one] kohde - *[other] kohteet - } { trash }sta -unknown-folder = Tuntematon kansio + *[other] kohdetta + } roskakorista +unknown-folder = tuntematon kansio ## Open with -menu-open-with = Avaa ohjelmalla… +menu-open-with = Avaa sovelluksella… default-app = { $name } (oletus) ## Show details @@ -225,14 +225,14 @@ settings = Asetukset appearance = Ulkoasu theme = Teema -match-desktop = Sovita yhteen työpöydän kanssa +match-desktop = Sovita työpöytään dark = Tumma light = Vaalea # Context menu add-to-sidebar = Lisää sivupalkkiin -compress = Pakkaa +compress = Pakkaa… extract-here = Pura new-file = Uusi tiedosto… new-folder = Uusi kansio… @@ -241,9 +241,9 @@ move-to-trash = Siirrä roskakoriin restore-from-trash = Palauta roskakorista remove-from-sidebar = Poista sivupalkista sort-by-name = Järjestä nimen mukaan -sort-by-modified = Järjestä muokkauspäivämäärän mukaan +sort-by-modified = Järjestä muokkausajan mukaan sort-by-size = Järjestä koon mukaan -sort-by-trashed = Järjestä poistamispäivämäärän mukaan +sort-by-trashed = Järjestä poistamisajan mukaan ## Desktop @@ -281,9 +281,9 @@ grid-view = Ruudukkonäkymä list-view = Listanäkymä show-hidden-files = Näytä piilotetut tiedostot list-directories-first = Näytä kansiot ensin -gallery-preview = Gallerian esinäkymä +gallery-preview = Gallerian esikatselu menu-settings = Asetukset… -menu-about = Tietoa COSMIC Tiedostoista… +menu-about = Tietoa COSMICin tiedostonhallinnasta… ## Sort @@ -318,3 +318,99 @@ move-to-button-label = Siirrä clear-recents-history = Tyhjennä viimeaikaisten historia copy-path = Kopioi polku dismiss = Hylkää viesti +operations-running = + { $running } { $running -> + [one] toiminto + *[other] toimintoa + } käynnissä ({ $percent } %)... +operations-running-finished = + { $running } { $running -> + [one] toiminto + *[other] toimintoa + } käynnissä ({ $percent } %), { $finished } valmistunut… +pause = Keskeytä +extract-to = Pura sijaintiin… +permanently-delete-warning = { $target } tullaan poistamaan pysyvästi. Tätä toimintoa ei voi perua. +execute-only = Vain suoritus +write-only = Vain kirjoitus +write-execute = Kirjoita ja suorita +read-only = Vain luku +read-execute = Lue ja suorita +read-write = Lue ja kirjoita +read-write-execute = Lue, kirjoita sekä suorita +calculating = Lasketaan… +single-click = Yhden napsautuksen avaus +type-to-search = Kirjoita etsiäksesi +type-to-search-recursive = Etsii nykyisestä kansiosta ja kaikista alikansioista +remove-from-recents = Poista viimeaikaisista +selected-items = { $items } valittua kohdetta +show-recents = Viimeaikaisten kansio sivupalkissa +copy-to = Kopioi… +move-to = Siirrä… +details = Yksityiskohdat +grid-spacing = Ruudukkovälit +none = Ei mitään +favorite-path-error = Virhe avattaessa kansiota +favorite-path-error-description = + Polun { $path } avaaminen ei onnistunut + "{ $path }" ei välttämättä ole olemassa tai oikeutesi eivät riitä sen avaamiseen + + Haluatko poistaa sen sivupalkista? +keep = Pidä +repository = Tietovarasto +support = Tuki +progress = { $percent } % +progress-cancelled = { $percent } %, peruttu +progress-failed = { $percent } %, epäonnistui +progress-paused = { $percent } %, keskeytetty +setting-permissions = Asetetaan kohteen "{ $name }" käyttöoikeudeksi { $mode } +set-permissions = Asetettu kohteen { $name } käyttöoikeudeksi { $mode } +permanently-deleting = + Poistetaan pysyvästi { $items } { $items -> + [one] kohde + *[other] kohdetta + } +permanently-deleted = + Poistettu pysyvästi { $items } { $items -> + [one] kohde + *[other] kohdetta + } +items = Kohteita: { $items } +item-accessed = Käytetty: { $accessed } +type-to-search-enter-path = Kirjoittaa polun kansioon tai tiedostoon +eject = Poista asemasta +copy-to-title = Valitse mihin kopioidaan +move-to-title = Valitse mihin siirretään +pasted-image = Liitetty kuva +pasted-text = Liitetty teksti +pasted-video = Liitetty video +type-to-search-select = Valitsee ensimmäisen täsmäävän tiedoston tai kansion +deleting = + Poistetaan { $items } { $items -> + [one] kohde + *[other] kohdetta + } roskakorista ({ $progress })… +deleted = + Poistettu { $items } { $items -> + [one] kohde + *[other] kohdetta + } roskakorista +removing-from-recents = + Poistetaan { $items } { $items -> + [one] kohde + *[other] kohdetta + } viimeaikaisista +removed-from-recents = + Poistettu { $items } { $items -> + [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 fa54fd7..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 @@ -436,3 +442,12 @@ keywords = Dossier;Gestionnaire; 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 7a9cfdf..04d546f 100644 --- a/i18n/ga/cosmic_files.ftl +++ b/i18n/ga/cosmic_files.ftl @@ -433,3 +433,12 @@ keywords = Fillteán;Bainisteoir; 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 dcf9f84..2cf31e6 100644 --- a/i18n/hu/cosmic_files.ftl +++ b/i18n/hu/cosmic_files.ftl @@ -436,3 +436,12 @@ move-to = Áthelyezés ide… 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 9a1e8c4..aefff98 100644 --- a/i18n/id/cosmic_files.ftl +++ b/i18n/id/cosmic_files.ftl @@ -318,3 +318,12 @@ comment = Pengelola berkas untuk desktop COSMIC 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/kab/cosmic_files.ftl b/i18n/kab/cosmic_files.ftl index e759fa5..233c47d 100644 --- a/i18n/kab/cosmic_files.ftl +++ b/i18n/kab/cosmic_files.ftl @@ -1 +1,322 @@ change-wallpaper = Beddel aɣrab n ugdil… +cosmic-files = Ifuyla COSMIC +empty-folder = Akaram d ilem +empty-folder-hidden = Akaram d ilem (yesɛa iferdisen yeffren) +no-results = Ulac igmaḍ yettwafen +home = Agejdan +networks = Iẓeḍwa +notification-in-progress = Timhalin ɣef ifuyla la tteddunt +trash = Iḍumman +recents = Melmi kan +undo = Ssemmet +today = Ass-a +desktop-view-options = Iɣewwaṛen n tmeẓri n tnarit… +show-on-desktop = Sken deg tnarit +desktop-folder-content = Agbur n ukaram n tnarit +trash-folder-icon = Tignit n ukaram n iḍumman +icon-size-and-spacing = Tiddi n tignit akked tallunt +icon-size = Tiddi n tignit +name = Isem +modified = Ittusnifel +trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman +size = Tiddi +details = Talqayt +pause = Serǧu +resume = Kemmel +create-archive = Snulfu-d aɣbaṛ +extract-to = Ssef ɣer... +extract-to-title = Ssef ɣer ukaram +empty-trash = Silem iḍumman +rename-folder = Snifel isem n ukaram +filesystem = Anagraw n yifuyla +dismiss = Zgel izen +empty-trash-title = Silem iḍumman? +empty-trash-warning = Iferdisen n ukaram n iḍumman ad ttwakksen i lebda +create-new-file = Snulfu-d afaylu amaynut +create-new-folder = Snulfu-d akaram amaynut +file-name = Isem n ufaylu +folder-name = Isem n ukaram +file-already-exists = Afaylu s yisem-agi yella yakan +folder-already-exists = Akaram s yisem-agi yella yakan +name-hidden = Ismawen ibeddun s "." ad ttwaffren +name-invalid = Isem ur yezmir ara ad yili "{ $filename }" +cancel = Sefsex +create = Snulfu-d +open = Ldi +open-file = Ldi afaylu +open-folder = Ldi akaram +open-in-new-tab = Ldi deg yiccer amaynut +open-in-new-window = Ldi deg usfaylu amaynut +open-item-location = Ldi adig n uferdis +open-multiple-files = Ldi aget n ifuyla +mounted-drives = imeɣriyen yettuserkben +mount-error = Ulamek anekcum ɣer umeɣri +operations-running = + { $running } { $running -> + [one] n temhelt la tteddu + *[other] n temhal la tteddunt + } ({ $percent }%)... +operations-running-finished = + { $running } { $running -> + [one] n temhelt la tteddu + *[other] n temhal la tteddunt + } ({ $percent }%), { $finished } { $finished -> + [one] tfukk + *[other] fukkent + }... +copy-to-title = Fren taɣerwaḍt n unɣel +copy-to-button-label = Nɣel +move-to-title = Fren taɣerwaḍt n usmutti +move-to-button-label = Smutti +comment = Amsefrak n yifuyla i tnarit COSMIC +keywords = Akaram;Amsefrak; +delete = kkes +replace = Semselsi +support = Tallalt +settings = Iɣewwaṛen +appearance = Timeẓri +theme = Asentel +dark = Aɣmayan +light = Aceɛlal +paste = Senteḍ +select-all = Fren akk +zoom-in = Asemɣeṛ +default-size = Tiddi tamezwert +zoom-out = Asemẓi +view = Wali +menu-settings = Iɣewwaṛen… +save = Sekles +match-desktop = Amṣada d tnarit +file = Afaylu +new-window = Asfaylu amaynut +quit = Tuffɣa +edit = Ẓreg +cut = Gzem +copy = Nɣel +repository = Asarsay +pasted-image = Tettwasenṭeḍ tugna +pasted-text = Yettwasenṭeḍ uḍris +pasted-video = Tettwasenṭeḍ tvidyut +compress = Skussem… +grid-spacing = Tallunt n iẓiki +extract-password-required = Awal uffir yettwasra +permanently-delete-question = Kkes s wudem imezgi? +permanently-delete-warning = { $target } ad yettwakkes s wudem imezgi. Tigawt-agi ur tezmir ara ad tettwasefsex. +rename-file = Snifel isem n ufaylu +replace-title = "{ $filename }" yella yakan deg wadig-a +replace-warning = Tebɣiḍ ad t-tsemselsiḍ s win ara teskelseḍ? Asemselsi-ines ad yaru sennig ugbur-is. +replace-warning-operation = Tebɣiḍ ad t-tsemselsiḍ? Asemselsi-ines ad yaru sennig ugbur-is. +original-file = Afaylu aneṣli +replace-with = Semselsi s +apply-to-all = Snes i meṛṛa +keep-both = Eǧǧ-iten deg sin +skip = Zgel +set-executable-and-launch = Sbeddet am umselkam syinna senker +set-executable-and-launch-description = Tebɣiḍ ad tesbeddeḍ "{ $name }" am umselkam syinna ad tessenkreḍ? +set-and-launch = Sbadu sakin senker +add-network-drive = Rnu ameɣri n uẓeṭṭa +connect = Qqen +connect-anonymously = Qqen s wudem udrig +connecting = Tuqqna… +domain = Taɣult +enter-server-address = Sekcem tansa n uqeddac +network-drive-description = + Tansiwin n uqeddac gebrent azwir n uneggaf akked tansa. + Imedyaten: ssh://192.168.0.1, ftp://[2001:db8::1] +network-drive-error = Ur izmir ara ad yekcem ɣer umeɣri n uzeṭṭa +password = Awal uffir +remember-password = Cfu ɣef wawal uffir +try-again = ɛreḍ tikelt nniḍen +username = Isem n useqdac +cancelled = Yettwasefsex +edit-history = Ẓreg amazray +history = Amazray +no-history = Ulac iferdisen deg umazray. +pending = Yettṛaǧu +progress-cancelled = { $percent }%, yettwasefsex +progress-failed = { $percent }%, ur yeddi ara +failed = Ur yeddi ara +renaming = Asnifel n yisem "{ $from }" ɣer "{ $to }" +renamed = Yettwasenfel yisem n "{ $from }" ɣer "{ $to }" +unknown-folder = akaram arussin +menu-open-with = Ldi s… +default-app = { $name } (amezwer) +show-details = Sken talqayt +type = Anaw: { $mime } +items = Iferdisen: { $items } +item-size = Tiddi: { $size } +item-created = Yettwarna: { $created } +item-modified = Ittusnifel: { $modified } +item-accessed = Yettwakcem: { $accessed } +calculating = Asiḍen… +single-click = Asiti asuf i ulday +type-to-search = Aru iwakken ad tnadiḍ +type-to-search-recursive = Nadi akaram amiran akked ikaramen inaddawen meṛṛa +type-to-search-enter-path = Sekcem abrid ɣer ukaram neɣ afaylu +add-to-sidebar = Rnu ɣer ufeggag adisan +delete-permanently = Kkes i lebda +grid-view = Askan s iẓiki +list-view = Askan s tebdart +show-hidden-files = Sken ifuyla uffiren +gallery-preview = Taskant n temidelt +menu-about = Ɣef Ifuyla COSMIC… +sort = Asmizzwer +sort-newest-first = Amaynut d amezwaru +sort-oldest-first = Aqbur d amezwaru +sort-smallest-to-largest = Seg umeẓyan akk ɣer umeqqran +sort-largest-to-smallest = Seg umeqqran akk ɣer umeẓyan +open-multiple-folders = Ldi usgit n ikaramen +save-file = Sekles afaylu +open-with-title = Amek tebɣiḍ ad teldiḍ "{ $name }"? +browse-store = Snirem { $store } +other-apps = Isnasen-nniḍen +emptying-trash = Silem { trash } ({ $progress })… +emptied-trash = D ilem { trash } +set-permissions = Sbadu isirigen i "{ $name }" ɣer { $mode } +eject = Ḍeqqer +extract-here = Ssef +new-file = Afaylu amaynut… +new-folder = Akaram amaynut… +open-in-terminal = Ldi deg yixef +move-to-trash = Smutti ɣer tqecwalt n yiḍumman +restore-from-trash = Err-d seg tqecwalt n yiḍumman +remove-from-sidebar = Kkes seg ugalis adisan +sort-by-name = Smizzwer s yisem +sort-by-modified = Asmizzwer s usnifel +sort-by-size = Asmizzwer s tiddi +sort-by-trashed = Asmizzwer s wakud n tukksa +remove-from-recents = Kkes seg ineggura +desktop-appearance = Timeẓri n tnarit… +display-settings = Iɣewwaṛen n ubeqqeḍ... +new-tab = Iccer amaynut +reload-folder = Ales asali n ukaram +rename = Snifel isem... +close-tab = Mdel iccer +list-directories-first = Sken di tazwara ikaramen +open-with = Ldi s +owner = Bab +group = Agraw +other = Ayen nniḍen +none = Ula Yiwen +execute-only = Selkem kan +write-only = Aru kan +write-execute = Aru u selkem +read-only = I tɣuri kan +read-execute = Ɣeṛ u selkem +read-write = Ɣeṛ u aru +read-write-execute = Ɣeṛ, aru, u selkem +favorite-path-error = Tuccḍa deg ulday n ukaram +remove = Kkes +keep = Eǧǧ +progress = { $percent }% +progress-paused = { $percent }%, ibedd +complete = Immed +copy_noun = Nɣel +creating = Asnulfu n "{ $name }" deg "{ $parent }" +created = Yettwarna "{ $name }" deg "{ $parent }" +setting-executable-and-launching = Asbadu n "{ $name }" am umselkam syin ad yettwasekker +set-executable-and-launched = Sbadu "{ $name }" am umselkam syin sekker-it +setting-permissions = Asbadu n tsirag i "{ $name }" ɣer { $mode } +related-apps = Isnasen icudden +favorite-path-error-description = + Ur izmir ara ad yeldi "{ $path }" + "{ $path }" yezmer lḥal ulac-it neɣ ahat ur tesɛiḍ ara tisirag akken ad t-teldiḍ + + Tebɣiḍ ad t-tekkseḍ seg ufeggag adisan? +compressing = + La issekkussum { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" ({ $progress })... +compressed = + Yekussem { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" +copying = + Anɣal { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" ({ $progress })... +copied = + Yettwanɣel { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" +deleting = + Tukksa { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { trash } ({ $progress })... +deleted = + Yettwakkes { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { trash } +extracting = + Tussfa { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" ({ $progress })... +extracted = + Yettusef { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" +moving = + Asmutti { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" ({ $progress })... +moved = + Yettwasenkez { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg "{ $from }" ɣer "{ $to }" +permanently-deleting = + Tukksa i lebda { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } +permanently-deleted = + I lebda yettwakkes { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } +removing-from-recents = + Tukksa { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { recents } +removed-from-recents = + Yettwakkes { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { recents } +restoring = + Tiririt { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { trash } ({ $progress })… +restored = + Yettwarr-d { $items } { $items -> + [one] n uferdis + *[other] n yiferdisen + } seg { trash } +network-drive-schemes = + Ineggafen iwejden, azwir + AppleTalk,afp:// + Aneggaf n usiweḍ n yifuyla,ftp:// neɣ ftps:// + Anagraw n yifuyla n uzeṭṭa,nfs:// + Iḥder n yizen n uqeddac,smb:// + SSH Aneggaf n usiweḍ n yifuyla,sftp:// neɣ ssh:// + WebDAV:// neɣ davs:// +sort-a-z = A-Ẓ +sort-z-a = Ẓ-A +selected-items = { $items } n yiferdisen yettwafernen +type-to-search-select = Fren afaylu amezwaru neɣ akaram yemṣadan +copy-to = Nɣel ɣer... +move-to = Smutti ɣer… +show-recents = Akaram n melmi kan deg ufeggag adisan +clear-recents-history = Sfeḍ azray n melmi kan +copy-path = Nɣel abrid diff --git a/i18n/kk/cosmic_files.ftl b/i18n/kk/cosmic_files.ftl index 6afb314..8aabf01 100644 --- a/i18n/kk/cosmic_files.ftl +++ b/i18n/kk/cosmic_files.ftl @@ -256,7 +256,7 @@ type-to-search-recursive = Ағымдағы бума мен барлық ішк type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды add-to-sidebar = Бүйірлік панельге қосу -compress = Сығу +compress = Сығу... delete-permanently = Біржолата өшіру eject = Шығару extract-here = Тарқату @@ -318,3 +318,12 @@ keywords = Folder;Manager;Бума;Басқарушы; 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 aec0e3f..5683c39 100644 --- a/i18n/ko/cosmic_files.ftl +++ b/i18n/ko/cosmic_files.ftl @@ -110,7 +110,7 @@ dismiss = 메시지 무시 copy_noun = 복사 progress = { $percent }% related-apps = 관련 앱 -compress = 압축 +compress = 압축... network-drive-error = 네트워크 드라이브에 접근할 수 없음 icon-size-and-spacing = 아이콘 크기 및 간격 password = 암호 @@ -272,3 +272,9 @@ network-drive-schemes = WebDAV,dav:// 또는 davs:// type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다 comment = COSMIC 데스크톱용 위한 파일 관리자 +keywords = 폴더;관리자; +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 8faf6d7..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ą @@ -252,7 +252,7 @@ type-to-search = Norint ieškoti, pradėkite rašyti type-to-search-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose type-to-search-enter-path = Įvedamas aplanko ar failo kelias add-to-sidebar = Pridėti į šonjuostę -compress = Suspausti +compress = Suspausti... delete-permanently = Ištrinti visam laikui eject = Išstumti extract-here = Išskleisti @@ -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/pa/cosmic_files.ftl b/i18n/pa/cosmic_files.ftl index e5c829f..be7163e 100644 --- a/i18n/pa/cosmic_files.ftl +++ b/i18n/pa/cosmic_files.ftl @@ -32,7 +32,7 @@ pause = ਵਿਰਾਮ resume = ਮੁੜ-ਚਾਲੂ create-archive = ਅਕਾਇਵ ਬਣਾਓ extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ -extract-to = Extract To... +extract-to = ਖਿਲਾਰੋ... extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ? @@ -113,7 +113,7 @@ replace-title = "{ $filename }" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣ favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ -connecting = Connecting... +connecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ... domain = ਡੋਮੇਨ enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ password = ਪਾਸਵਰਡ @@ -138,9 +138,9 @@ sort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ -change-wallpaper = Change wallpaper... -desktop-appearance = Desktop appearance... -display-settings = Display settings... +change-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ... +desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ... +display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ... file = ਫ਼ਾਇਲ new-tab = ਨਵੀਂ ਟੈਬ new-window = ਨਵੀਂ ਵਿੰਡੋ @@ -195,11 +195,11 @@ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ compress = ਕੰਪਰੈਸ -copy-to = Copy to... +copy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ... delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ eject = ਬਾਹਰ extract-here = ਖਿਲਾਰੋ -move-to = Move to... +move-to = ਇੱਥੇ ਭੇਜੋ... remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ list-view = ਸੂਚੀ ਝਲਕ diff --git a/i18n/pl/cosmic_files.ftl b/i18n/pl/cosmic_files.ftl index daf1007..9af6027 100644 --- a/i18n/pl/cosmic_files.ftl +++ b/i18n/pl/cosmic_files.ftl @@ -437,3 +437,12 @@ move-to = Przenieś do… 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 732d0fb..6d33279 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -424,8 +424,8 @@ sort-largest-to-smallest = Do maior para o menor empty-trash-title = Esvaziar a lixeira? type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente pasted-image = Imagem colada -pasted-text = Texto copiado -pasted-video = Vídeo copiado +pasted-text = Texto colado +pasted-video = Vídeo colado copy-to-title = Selecione o destino da cópia copy-to-button-label = Copiar move-to-title = Selecione o destino da movimentação @@ -433,6 +433,15 @@ move-to-button-label = Mover copy-to = Copiar para... move-to = Mover para... keywords = Pasta;Gerenciador; -show-recents = Pasta Recentes na barra lateral +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 15b8eb1..f8e76c1 100644 --- a/i18n/ru/cosmic_files.ftl +++ b/i18n/ru/cosmic_files.ftl @@ -195,7 +195,7 @@ dark = Тёмная light = Светлая # Context menu add-to-sidebar = Добавить на боковую панель -compress = Сжать +compress = Сжать... extract-here = Распаковать new-file = Новый файл… new-folder = Новая папка… @@ -380,3 +380,12 @@ keywords = Папка;Менеджер; 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 2527c4e..974c7dc 100644 --- a/i18n/sv/cosmic_files.ftl +++ b/i18n/sv/cosmic_files.ftl @@ -101,6 +101,7 @@ open-with = Öppna med owner = Ägare group = Grupp other = Andra +mixed = Blandade # Listvy name = Namn modified = Ändrad @@ -409,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 1413460..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 }» надано права на виконання і відкрито @@ -317,7 +323,7 @@ single-click = Відкривати одним клацанням type-to-search = Введіть для пошуку type-to-search-recursive = Шукає у поточній теці та всіх підтеках type-to-search-enter-path = Вводить шлях до каталогу або файлу -compress = Стиснути +compress = Стиснути... delete-permanently = Остаточно видалити eject = Безпечно вилучити extract-here = Видобути @@ -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 0002c17..abc8fda 100644 --- a/i18n/zh-CN/cosmic_files.ftl +++ b/i18n/zh-CN/cosmic_files.ftl @@ -11,7 +11,7 @@ recents = 最近访问 undo = 撤销 today = 今天 # Desktop view options -desktop-view-options = 桌面视图选项... +desktop-view-options = 桌面视图选项… show-on-desktop = 在桌面显示 desktop-folder-content = 桌面文件夹内容 mounted-drives = 已装载驱动器 @@ -31,7 +31,7 @@ operations-running = 正在进行 { $running } { $running -> [one] 个操作 *[other] 个操作 - }({ $percent }%)... + }({ $percent }%)… operations-running-finished = 正在进行 { $running } { $running -> [one] 个操作 @@ -50,7 +50,7 @@ create-archive = 创建压缩包 ## Extract Dialog extract-password-required = 需要密码 -extract-to = 提取到... +extract-to = 提取到… extract-to-title = 提取到文件夹 ## Empty Trash Dialog @@ -167,7 +167,7 @@ read-write-execute = 读取、写入和执行 ## Favorite Path Error Dialog -favorite-path-error = 打开路径时出错 +favorite-path-error = 打开目录时出错 favorite-path-error-description = 无法打开 "{ $path }" 。 "{ $path }" 可能不存在或您没有权限打开它。 @@ -317,7 +317,7 @@ unknown-folder = 未知文件夹 ## Open with -menu-open-with = 打开方式... +menu-open-with = 打开方式… default-app = { $name }(默认) ## Show details @@ -329,7 +329,7 @@ item-size = 文件大小:{ $size } item-created = 创建于:{ $created } item-modified = 修改于:{ $modified } item-accessed = 访问于:{ $accessed } -calculating = 计算中... +calculating = 计算中… ## Settings @@ -348,15 +348,15 @@ light = 亮色模式 type-to-search = 输入即可搜索 type-to-search-recursive = 搜索当前文件夹及其所有子文件夹 -type-to-search-enter-path = 输入文件夹或文件路径 +type-to-search-enter-path = 输入文件夹或文件目录 # Context menu add-to-sidebar = 加入侧边栏 compress = 压缩… delete-permanently = 永久删除 eject = 弹出 extract-here = 解压到此处 -new-file = 新建文件... -new-folder = 新建文件夹... +new-file = 新建文件… +new-folder = 新建文件夹… open-in-terminal = 在终端模拟器中打开 move-to-trash = 移动到回收站 restore-from-trash = 从回收站中还原 @@ -369,9 +369,9 @@ remove-from-recents = 从最近访问中移除 ## Desktop -change-wallpaper = 更改壁纸... -desktop-appearance = 桌面外观... -display-settings = 显示设置... +change-wallpaper = 更改壁纸… +desktop-appearance = 桌面外观… +display-settings = 显示设置… # Menu @@ -382,7 +382,7 @@ file = 文件 new-tab = 新建标签 new-window = 新建窗口 reload-folder = 刷新文件夹 -rename = 重命名... +rename = 重命名… close-tab = 关闭标签 quit = 退出 @@ -405,7 +405,7 @@ list-view = 列表视图 show-hidden-files = 显示隐藏文件 list-directories-first = 优先列出目录 gallery-preview = 图库预览 -menu-settings = 设置... +menu-settings = 设置… menu-about = 关于 COSMIC 文件… ## Sort @@ -435,4 +435,13 @@ comment = COSMIC 桌面的文件管理器 keywords = 文件夹;管理器; clear-recents-history = 清除最近访问历史 copy-path = 复制文件路径 -show-recents = 侧边栏显示最近访问 +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 1149a62..b356db3 100644 --- a/i18n/zh-TW/cosmic_files.ftl +++ b/i18n/zh-TW/cosmic_files.ftl @@ -1,11 +1,11 @@ -cosmic-files = COSMIC 檔案總管 +cosmic-files = COSMIC 檔案 empty-folder = 空資料夾 empty-folder-hidden = 空資料夾(包含隱藏項目) no-results = 找不到結果 filesystem = 檔案系統 -home = 主目錄 +home = 家目錄 networks = 網路 -notification-in-progress = 檔案操作正在進行中。 +notification-in-progress = 檔案操作正在進行中 trash = 垃圾桶 recents = 最近使用 undo = 復原 @@ -25,7 +25,7 @@ create-archive = 建立壓縮檔案 ## Empty Trash Dialog empty-trash = 清空垃圾桶 -empty-trash-warning = 你確定要永久刪除垃圾桶中的所有項目嗎? +empty-trash-warning = 垃圾桶中的項目將被永久刪除 ## New File/Folder Dialog @@ -33,11 +33,11 @@ create-new-file = 建立新檔案 create-new-folder = 建立新資料夾 file-name = 檔案名稱 folder-name = 資料夾名稱 -file-already-exists = 已存在同名檔案。 -folder-already-exists = 已存在同名資料夾。 -name-hidden = 以「.」開頭的名稱將會被隱藏。 -name-invalid = 名稱不能是 「{ $filename }」。 -name-no-slashes = 名稱不能包含斜線。 +file-already-exists = 相同名稱的檔案已經存在 +folder-already-exists = 相同名稱的資料夾已經存在 +name-hidden = 以「.」開頭的名稱將會被隱藏 +name-invalid = 名稱不能是「{ $filename }」 +name-no-slashes = 名稱不能包含斜線 ## Open/Save Dialog @@ -62,12 +62,12 @@ rename-folder = 重新命名資料夾 ## Replace Dialog replace = 取代 -replace-title = 檔案「{ $filename }」已存在於此位置。 +replace-title = 「{ $filename }」已存在於此位置 replace-warning = 你要取代它嗎?取代將覆蓋其內容。 replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。 original-file = 原始檔案 replace-with = 取代為 -apply-to-all = 套用至所有項目 +apply-to-all = 套用至全部 keep-both = 保留兩者 skip = 跳過 @@ -95,13 +95,13 @@ network-drive-description = 伺服器地址包括協定前綴和地址。 範例:ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = - 可用協定,前綴 - AppleTalk,afp:// - 檔案傳輸協定,ftp:// 或 ftps:// - 網路檔案系統,nfs:// - 伺服器訊息區塊,smb:// - SSH 檔案傳輸協定,sftp:// 或 ssh:// - WebDav,dav:// 或 davs:// + 可用協定,前綴 + AppleTalk,afp:// + 檔案傳輸協定,ftp:// 或 ftps:// + 網路檔案系統,nfs:// + 伺服器訊息區塊,smb:// + SSH 檔案傳輸協定,sftp:// 或 ssh:// + WebDav,dav:// 或 davs:// network-drive-error = 無法存取網路磁碟機 password = 密碼 remember-password = 記住密碼 @@ -112,7 +112,7 @@ username = 使用者名稱 edit-history = 編輯歷史 history = 歷史紀錄 -no-history = 沒有歷史項目。 +no-history = 無歷史記錄項目。 pending = 待處理 failed = 失敗 complete = 完成 @@ -133,51 +133,51 @@ copying = 正在複製 { $items } { $items -> [one] 項目 *[other] 項目 - } 從「{ $from }」到「{ $to }」({ $progress })... + }從「{ $from }」到「{ $to }」({ $progress })... copied = 已複製 { $items } { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」 -emptying-trash = 正在清空{ trash }({ $progress })… -emptied-trash = 已清空{ trash } +emptying-trash = 正在清空 { trash }({ $progress })… +emptied-trash = 已經清空 { trash } extracting = 正在解壓縮 { $items } 項目 { $items -> [one] 項目 *[other] 項目 - } 從「{ $from }」到「{ $to }」({ $progress })... + }從「{ $from }」至「{ $to }」({ $progress })... extracted = 已解壓縮 { $items } 項目 { $items -> [one] 項目 *[other] 項目 - } 從 { $from } 到 { $to } + }從「{ $from }」到「{ $to }」 moving = 正在移動 { $items } { $items -> [one] 項目 *[other] 項目 - } 從「{ $from }」到「{ $to }」({ $progress })... + }從「{ $from }」到「{ $to }」({ $progress })... moved = - 已移動 { $items } { $items -> + 已經移動 { $items } { $items -> [one] 項目 *[other] 項目 - } 從「{ $from }」到「{ $to }」 -renaming = 正在重新命名「{ $from }」為「{ $to }」 -renamed = 已重新命名 { $from } 為 { $to } + } 從「{ $from }」至「{ $to }」 +renaming = 正在重新命名「{ $from }」至「{ $to }」 +renamed = 已經重新命名「{ $from }」至「{ $to }」 restoring = 正在還原 { $items } 項目 { $items -> [one] 項目 *[other] 項目 - } 從{ trash }({ $progress })... + }自 { trash } ({ $progress })... restored = - 已還原 { $items } 項目 { $items -> + 已經還原 { $items } 項目 { $items -> [one] 項目 *[other] 項目 - } 從{ trash } -unknown-folder = 未知資料夾 + }從 { trash } +unknown-folder = 不明資料夾 ## Open with -menu-open-with = 開啟方式... +menu-open-with = 開啟檔案... default-app = { $name } (預設) ## Show details @@ -192,15 +192,15 @@ settings = 設定 appearance = 外觀 theme = 主題 -match-desktop = 與桌面一致 -dark = 暗色模式 -light = 亮色模式 +match-desktop = 符合桌面 +dark = 深色 +light = 淺色 # Context menu -add-to-sidebar = 加入側邊欄 -compress = 壓縮 -extract-here = 解壓縮至此 -new-file = 新檔案... -new-folder = 新資料夾... +add-to-sidebar = 添加至側邊欄 +compress = 壓縮… +extract-here = 解壓縮 +new-file = 新建檔案... +new-folder = 新建資料夾... open-in-terminal = 在終端機中開啟 move-to-trash = 移動至垃圾桶 restore-from-trash = 從垃圾桶還原 @@ -215,8 +215,8 @@ sort-by-size = 依大小排序 ## File file = 檔案 -new-tab = 新分頁 -new-window = 新視窗 +new-tab = 新建分頁 +new-window = 新建視窗 rename = 重新命名... close-tab = 關閉分頁 quit = 退出 @@ -238,36 +238,36 @@ view = 檢視 grid-view = 網格檢視 list-view = 列表檢視 show-hidden-files = 顯示隱藏檔案 -list-directories-first = 優先列出目錄 +list-directories-first = 目錄優先列出 menu-settings = 設定... -menu-about = 關於 COSMIC 檔案總管... +menu-about = 關於 COSMIC 檔案... ## Sort sort = 排序 sort-a-z = A-Z sort-z-a = Z-A -sort-newest-first = 最新的在前 -sort-oldest-first = 最舊的在前 -sort-smallest-to-largest = 由小至大 -sort-largest-to-smallest = 由大至小 +sort-newest-first = 最新優先 +sort-oldest-first = 最舊優先 +sort-smallest-to-largest = 從小到大 +sort-largest-to-smallest = 從大到小 deleted = - 已刪除 { $items } { $items -> + 已經刪除 { $items } { $items -> [one] 項目 *[other] 項目 - }從{ trash } + }從 { trash } permanently-deleting = 正在永久刪除 { $items } { $items -> [one] 项目 *[other] 项目 } permanently-deleted = - 已永久刪除 { $items } { $items -> + 已經永久刪除 { $items } { $items -> [one] 项目 *[other] 项目 } removing-from-recents = - 正在從{ recents }中移除 { $items } { $items -> + 正在從 { recents } 中移除 { $items } { $items -> [one] 项目 *[other] 项目 } @@ -275,9 +275,120 @@ deleting = 正在刪除 { $items } { $items -> [one] 项目 *[other] 项目 - }從{ trash }({ $progress })… + }從 { trash }({ $progress })… removed-from-recents = - 已從{ recents }中移除 { $items } { $items -> + 已經從 { recents } 中移除 { $items } { $items -> [one] 项目 *[other] 项目 } +repository = 軟體庫源 +desktop-view-options = 桌面檢視選項... +show-on-desktop = 顯示在桌面 +desktop-folder-content = 桌面資料夾內容 +mounted-drives = 已經掛載的磁碟機 +trash-folder-icon = 垃圾桶圖示 +trashed-on = 遺棄時間 +icon-size-and-spacing = 圖示大小與間距 +icon-size = 圖示大小 +grid-spacing = 網格間距 +details = 詳情 +dismiss = 撤停訊息 +delete = 刪除 +remove = 移除 +support = 支援 +cancelled = 已取消 +keywords = 資料夾;管理器; +empty-trash-title = 清空垃圾桶? +pause = 暫停 +resume = 繼續 +extract-password-required = 需要密碼 +extract-to = 解壓縮至... +extract-to-title = 解壓縮至資料夾 +mount-error = 無法存取磁碟機 +open-with-title = 您要如何開啟「{ $name }」? +browse-store = 瀏覽 { $store } +other-apps = 其他應用程式 +related-apps = 相關應用程式 +permanently-delete-question = 永久刪除? +set-executable-and-launch = 設定為可以執行並啟動 +read-only = 唯讀 +read-execute = 讀取和執行 +read-write = 讀取和寫入 +read-write-execute = 讀取、寫入和執行 +favorite-path-error = 開啟目錄時發生錯誤 +set-executable-and-launch-description = 您是否要將「{ $name }」設為可執行並啟動它? +set-and-launch = 設定並啟動 +none = 無 +execute-only = 僅執行 +write-only = 僅寫入 +write-execute = 寫入和執行 +operations-running = + { $running } { $running -> + [one] 個操作 + *[other] 個操作 + }正在執行({ $percent }%)... +operations-running-finished = + { $running } { $running -> + [one] 個操作 + *[other] 個操作 + }正在執行({ $percent }%), { $finished } 個已經完成... +permanently-delete-warning = 「{ $target }」將被永久刪除。此操作無法復原。 +open-with = 開啟檔案 +selected-items = 已經選定 { $items } 個項目 +copy-to-title = 選擇複製目的地 +copy-to-button-label = 複製 +move-to-title = 選擇移動目的地 +move-to-button-label = 移動 +keep = 保留 +progress = { $percent }% +progress-cancelled = { $percent }%,已經取消 +progress-failed = { $percent }%,失敗 +progress-paused = { $percent }%,已經暫停 +favorite-path-error-description = + 無法開啟「{ $path }」 + 「{ $path }」可能不存在,或您可能沒有權限開啟它。 + + 您是否要將它從側邊欄移除? +comment = COSMIC 桌面檔案管理器 +pasted-image = 已經貼上的圖片 +pasted-text = 已經貼上的文字 +pasted-video = 已經貼上的影片 +sort-by-trashed = 依丟入時間排序 +calculating = 計算中... +single-click = 點按以開啟 +type-to-search = 輸入進行搜尋 +type-to-search-recursive = 搜尋目前資料夾及全部子資料夾 +type-to-search-enter-path = 輸入目錄或檔案的目錄 +delete-permanently = 永久刪除 +eject = 彈出 +remove-from-recents = 從最近項目中移除 +change-wallpaper = 變更桌布... +desktop-appearance = 桌面外觀... +display-settings = 顯示設定... +reload-folder = 重新載入資料夾 +gallery-preview = 圖庫預覽 +type = 類型:{ $mime } +items = 項目:{ $items } +item-size = 大小:{ $size } +item-created = 建立時間:{ $created } +item-modified = 修改時間:{ $modified } +item-accessed = 存取時間:{ $accessed } +type-to-search-select = 選取第一個符合條件的檔案或資料夾 +copy-to = 複製至... +move-to = 移動至... +show-recents = 側邊欄中的最近使用資料夾 +clear-recents-history = 清除最近使用歷史記錄 +copy-path = 複製路徑 +setting-executable-and-launching = 設定「{ $name }」為可以執行並進行啟動 +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 4e20f48..0fc82ae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +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, @@ -12,46 +29,20 @@ use cosmic::iced::{ Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface, }, }; -#[cfg(all(feature = "wayland", feature = "desktop-applet"))] -use cosmic::iced_winit::commands::overlap_notify::overlap_notify; -use cosmic::{ - Application, ApplicationExt, Element, - app::{self, Core, Task, context_drawer}, - cosmic_config::{self, ConfigSet}, - cosmic_theme, - desktop::fde::DesktopEntry, - executor, - 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}, - }, - iced_runtime::clipboard, - iced_widget::{button::focus, scrollable::AbsoluteOffset}, - 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}, @@ -62,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; @@ -70,35 +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, - }, - 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, - }, - 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")); @@ -109,6 +98,9 @@ static DELETE_TRASH_BUTTON_ID: LazyLock = static CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("confirm-open-with-button")); +static CONFIRM_CONTEXT_ACTION_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("confirm-context-action-button")); + static EMPTY_TRASH_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("empty-trash-button")); @@ -141,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, @@ -180,6 +271,7 @@ pub enum Action { OpenItemLocation, OpenTerminal, OpenWith, + RunContextAction(usize), Paste, PermanentlyDelete, Preview, @@ -252,6 +344,9 @@ impl Action { Self::OpenItemLocation => Message::OpenItemLocation(entity_opt), Self::OpenTerminal => Message::OpenTerminal(entity_opt), Self::OpenWith => Message::OpenWithDialog(entity_opt), + Self::RunContextAction(action) => { + Message::TabMessage(entity_opt, tab::Message::RunContextAction(*action)) + } Self::Paste => Message::Paste(entity_opt), Self::PermanentlyDelete => Message::PermanentlyDelete(entity_opt), Self::Preview => Message::Preview(entity_opt), @@ -297,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 { @@ -323,6 +418,7 @@ pub enum NavMenuAction { OpenInNewTab(segmented_button::Entity), OpenInNewWindow(segmented_button::Entity), Preview(segmented_button::Entity), + RunContextAction(segmented_button::Entity, usize), RemoveFromSidebar(segmented_button::Entity), } @@ -398,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), @@ -412,12 +509,14 @@ pub enum Message { CheckClipboardImage, CheckClipboardVideo, CheckClipboardText, + RetryCheckClipboard(ClipboardCache), ClipboardCached(ClipboardCache), PendingCancel(u64), PendingCancelAll, PendingComplete(u64, OperationSelection), PendingDismiss, PendingError(u64, OperationError), + PendingResults(Vec<(u64, OperationSelection)>, Vec<(u64, OperationError)>), PendingPause(u64, bool), PendingPauseAll(bool), PermanentlyDelete(Option), @@ -436,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), @@ -449,7 +564,7 @@ pub enum Message { TabRescan( Entity, Location, - Option, + Option>, Vec, Option>, ), @@ -531,6 +646,7 @@ pub enum DialogPage { }, EmptyTrash, FailedOperation(u64), + FailedOperations(Vec), ExtractPassword { id: u64, password: String, @@ -556,11 +672,18 @@ pub enum DialogPage { name: String, dir: bool, }, + RunContextAction { + action: usize, + paths: Box<[PathBuf]>, + }, OpenWith { path: PathBuf, 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]>, @@ -575,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, @@ -718,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, @@ -817,9 +945,12 @@ impl App { // First launch apps that can be launched directly if mime == "application/x-desktop" { - // Try opening desktop application - Self::launch_desktop_entries(&paths); - continue; + #[cfg(feature = "desktop")] + { + // Try opening desktop application + Self::launch_desktop_entries(&paths); + continue; + } } else if mime == "application/x-executable" || mime == "application/vnd.appimage" { // Try opening executable for path in paths { @@ -880,31 +1011,43 @@ impl App { Task::batch(tasks) } + #[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", @@ -1059,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.), @@ -1109,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; @@ -1175,14 +1306,14 @@ impl App { entity.id() }; - ( - entity, - Task::batch([ - self.update_title(), - self.update_watcher(), - self.update_tab(entity, location, selection_paths), - ]), - ) + let mut tasks = Vec::with_capacity(4); + if activate { + tasks.push(task::widget(unfocus())); + } + tasks.push(self.update_title()); + tasks.push(self.update_watcher()); + tasks.push(self.update_tab(entity, location, selection_paths)); + (entity, Task::batch(tasks)) } fn open_tab( @@ -1250,34 +1381,183 @@ impl App { .insert(id, (operation.clone(), controller.clone())); // Use a task to send operations to the compio runtime thread. - cosmic::Task::stream(cosmic::iced_futures::stream::channel( - 4, - move |msg_tx| async move { - let (tx, rx) = tokio::sync::oneshot::channel(); + cosmic::Task::stream(cosmic::iced::stream::channel(4, move |msg_tx| async move { + let (tx, rx) = tokio::sync::oneshot::channel(); - let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); + let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); - let msg_tx_clone = msg_tx.clone(); + let msg_tx_clone = msg_tx.clone(); - _ = compio_tx - .send(Box::pin(async move { - let msg = match operation.perform(&msg_tx_clone, controller).await { - Ok(result_paths) => Message::PendingComplete(id, result_paths), - Err(err) => Message::PendingError(id, err), - }; + _ = compio_tx + .send(Box::pin(async move { + let msg = match operation.perform(&msg_tx_clone, controller).await { + Ok(result_paths) => Message::PendingComplete(id, result_paths), + Err(err) => Message::PendingError(id, err), + }; - _ = tx.send(msg); - })) - .await; + _ = tx.send(msg); + })) + .await; - if let Ok(msg) = rx.await { - let _ = msg_tx.lock().await.send(msg).await; - } - }, - )) + if let Ok(msg) = rx.await { + let _ = msg_tx.lock().await.send(msg).await; + } + })) .map(cosmic::Action::App) } + /// Will join operations together into a single task that will return a single + /// Message::PendingResults message when all operations are complete. + fn join_operations(&mut self, operations: Vec) -> Task { + Task::batch( + operations + .into_iter() + .map(|operation| self.operation(operation)), + ) + .collect() + .map(|messages| { + let results = messages.into_iter().fold( + Message::PendingResults(Vec::new(), Vec::new()), + |mut acc, message| { + if let Message::PendingResults(completed, errors) = &mut acc { + match message { + cosmic::Action::App(Message::PendingComplete(id, selection)) => { + completed.push((id, selection)); + } + cosmic::Action::App(Message::PendingError(id, err)) => { + errors.push((id, err)); + } + _ => {} + } + } + acc + }, + ); + cosmic::Action::App(results) + }) + } + + fn handle_completed_operations( + &mut self, + completed: Vec<(u64, OperationSelection)>, + ) -> Task { + let mut commands = Vec::with_capacity(4 * completed.len()); + let mut op_sel = OperationSelection::default(); + for (id, op_sel_pending) in completed { + op_sel.ignored.extend(op_sel_pending.ignored); + op_sel.selected.extend(op_sel_pending.selected); + if let Some((op, _)) = self.pending_operations.remove(&id) { + // Show toast for some operations + if let Some(description) = op.toast() { + if let Operation::Delete { ref paths } = op { + let paths: Arc<[PathBuf]> = Arc::from(paths.as_slice()); + commands.push( + self.toasts + .push( + widget::toaster::Toast::new(description) + .action(fl!("undo"), move |tid| { + Message::UndoTrash(tid, paths.clone()) + }), + ) + .map(cosmic::Action::App), + ); + } else { + commands.push( + self.toasts + .push(widget::toaster::Toast::new(description)) + .map(cosmic::Action::App), + ); + } + } + + // If a favorite for a path has been renamed or moved, update it. + if let Operation::Rename { ref from, ref to } = op { + if self.update_favorites([(from, to)].as_slice()) { + commands.push(self.update_config()); + } + } else if let Operation::Move { + ref paths, ref to, .. + } = op + { + let path_changes: Box<[_]> = paths + .iter() + .filter_map(|from| from.file_name().map(|name| (from, to.join(name)))) + .collect(); + if self.update_favorites(&path_changes) { + commands.push(self.update_config()); + } + } + + if matches!(op, Operation::RemoveFromRecents { .. }) { + commands.push(self.rescan_recents()); + } + + self.complete_operations.insert(id, op); + } + } + // Close progress notification if all relevant operations are finished + if !self + .pending_operations + .values() + .any(|(op, _)| op.show_progress_notification()) + { + self.progress_operations.clear(); + } + // Potentially show a notification + commands.push(self.update_notification()); + // Rescan and select based on operation + commands.push(self.rescan_operation_selection(op_sel)); + // Manually rescan any trash tabs after any operation is completed + commands.push(self.rescan_trash()); + + Task::batch(commands) + } + + fn handle_operation_errors(&mut self, errors: Vec<(u64, OperationError)>) -> Task { + let mut tasks = Vec::new(); + let mut failed = Vec::new(); + for (id, err) in errors.into_iter() { + if let Some((op, controller)) = self.pending_operations.remove(&id) { + // Only show dialog if not cancelled + if !controller.is_cancelled() { + match err.kind { + OperationErrorType::Generic(_) => failed.push(id), + OperationErrorType::PasswordRequired => { + tasks.push(self.dialog_pages.push_back(DialogPage::ExtractPassword { + id, + password: String::new(), + })); + } + } + } + + // Remove from progress + self.progress_operations.remove(&id); + self.failed_operations + .insert(id, (op, controller, err.to_string())); + } + } + if !failed.is_empty() { + tasks.push( + self.dialog_pages + .push_back(DialogPage::FailedOperations(failed)), + ); + tasks.push(widget::text_input::focus(self.dialog_text_input.clone())); + } + + // Close progress notification if all relevant operations are finished + if !self + .pending_operations + .values() + .any(|(op, _)| op.show_progress_notification()) + { + self.progress_operations.clear(); + } + // Manually rescan any trash tabs after any operation is completed + tasks.push(self.rescan_trash()); + Task::batch(tasks) + } + fn remove_window(&mut self, id: &window::Id) { if let Some(window) = self.windows.remove(id) { match window.kind { @@ -1342,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 @@ -1452,7 +1738,7 @@ impl App { }; search_location.map(|search_location| { - return ( + ( Location::Search( search_location, term, @@ -1460,7 +1746,7 @@ impl App { Instant::now(), ), true, - ); + ) }) } None => match &tab.location { @@ -1517,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 @@ -1530,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| { @@ -1600,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 { @@ -1628,7 +1963,7 @@ impl App { nav_model = nav_model.insert(|b| { b.text(fl!("trash")) - .icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16))) + .icon(icon::icon(Trash::icon_symbolic(16))) .data(Location::Trash) .divider_above() }); @@ -1775,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); @@ -1806,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 { @@ -1832,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 { @@ -1843,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(); @@ -1899,7 +2229,8 @@ impl App { let progress = controller.progress(); section = section.add(widget::column::with_children([ widget::row::with_children([ - widget::progress_bar(0.0..=1.0, progress) + widget::determinate_linear(progress) + .width(Length::Fill) .girth(progress_bar_height) .into(), if controller.is_paused() { @@ -1977,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()); @@ -2010,7 +2341,9 @@ impl App { match (selected.next(), selected.next()) { // At least two selected items - (Some(_), Some(_)) => Some(tab.multi_preview_view()), + (Some(_), Some(_)) => { + Some(tab.multi_preview_view(Some(&self.mime_app_cache))) + } // Exactly one selected item (Some(item), None) => { Some(item.preview_view(Some(&self.mime_app_cache), military_time)) @@ -2045,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 { @@ -2054,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| { @@ -2067,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 { @@ -2102,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(); @@ -2281,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, @@ -2439,6 +2898,28 @@ impl Application for App { NavMenuAction::OpenInNewWindow(entity), )); } + if let Some(path) = location_opt.and_then(Location::path_opt) { + let selected_dir = usize::from(path.is_dir()); + let action_items: Vec<_> = self + .config + .context_actions + .iter() + .enumerate() + .filter(|(_, action)| action.matches_selection(1, selected_dir)) + .map(|(i, action)| { + cosmic::widget::menu::Item::Button( + action.name.clone(), + None, + NavMenuAction::RunContextAction(entity, i), + ) + }) + .collect(); + + if !action_items.is_empty() { + items.push(cosmic::widget::menu::Item::Divider); + items.extend(action_items); + } + } items.push(cosmic::widget::menu::Item::Divider); if matches!(location_opt, Some(Location::Path(..))) { items.push(cosmic::widget::menu::Item::Button( @@ -2464,9 +2945,7 @@ impl Application for App { )); } - if matches!(location_opt, Some(Location::Trash)) - && !trash::os_limited::is_empty().unwrap_or(true) - { + if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() { items.push(cosmic::widget::menu::Item::Button( fl!("empty-trash"), None, @@ -2767,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(); } } @@ -2781,7 +3262,12 @@ impl Application for App { tab.refresh_cut(&[]); } let paths = self.selected_paths(entity_opt); - let contents = ClipboardCopy::new(ClipboardKind::Copy, paths); + self.clipboard_cache = ClipboardCache::Files(ClipboardPaste { + paths: paths.map(|p| p.to_path_buf()).collect(), + kind: ClipboardKind::Copy, + }); + let contents = + ClipboardCopy::new(ClipboardKind::Copy, self.selected_paths(entity_opt)); return clipboard::write_data(contents); } Message::CopyPath(entity_opt) => { @@ -2822,7 +3308,15 @@ impl Application for App { Message::Cut(entity_opt) => { self.set_cut(entity_opt); let paths = self.selected_paths(entity_opt); - let contents = ClipboardCopy::new(ClipboardKind::Cut { is_dnd: false }, paths); + self.clipboard_cache = ClipboardCache::Files(ClipboardPaste { + paths: paths.map(|p| p.to_path_buf()).collect(), + kind: ClipboardKind::Cut { is_dnd: false }, + }); + let contents = ClipboardCopy::new( + ClipboardKind::Cut { is_dnd: false }, + self.selected_paths(entity_opt), + ); + return clipboard::write_data(contents); } Message::CloseToast(id) => { @@ -2966,6 +3460,9 @@ impl Application for App { DialogPage::FailedOperation(id) => { log::warn!("TODO: retry operation {id}"); } + DialogPage::FailedOperations(_ids) => { + log::warn!("TODO: retry operations"); + } DialogPage::ExtractPassword { id, password } => { let (operation, _, _err) = self.failed_operations.get(&id).unwrap(); let new_op = match &operation { @@ -3015,10 +3512,14 @@ impl Application for App { Operation::NewFile { path } })); } + DialogPage::RunContextAction { action, paths } => { + context_action::run(&self.config.context_actions, action, &paths); + } DialogPage::OpenWith { path, mime, selected, + set_default, .. } => { let available_apps = self.get_apps_for_mime(&mime); @@ -3037,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!( @@ -3156,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))); } } @@ -3659,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()), ); @@ -3670,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) @@ -3680,6 +4194,24 @@ impl Application for App { // Use cached clipboard data if available (needed for Wayland popups) match &self.clipboard_cache { ClipboardCache::Files(contents) => { + if contents.paths.is_empty() { + return iced::Task::future(tokio::time::sleep( + std::time::Duration::from_millis(300), + )) + .discard() + .chain( + clipboard::read_data::().map( + move |contents_opt| match contents_opt { + Some(contents) => cosmic::action::app( + Message::PasteContents(to.clone(), contents), + ), + None => { + cosmic::action::app(Message::PasteImage(to.clone())) + } + }, + ), + ); + } return self .update(Message::PasteContents(to.clone(), contents.clone())); } @@ -3827,10 +4359,13 @@ impl Application for App { // Check if clipboard has any paste-able content and cache it return clipboard::read_data::().map(|contents_opt| { match contents_opt { + Some(contents) if contents.paths.is_empty() => cosmic::action::app( + Message::RetryCheckClipboard(ClipboardCache::Files(contents)), + ), Some(contents) => cosmic::action::app(Message::ClipboardCached( ClipboardCache::Files(contents), )), - None => cosmic::action::app(Message::CheckClipboardImage), + _ => cosmic::action::app(Message::CheckClipboardImage), } }); } @@ -3862,6 +4397,28 @@ impl Application for App { })) }); } + Message::RetryCheckClipboard(cache) => { + let mut cmds = Vec::new(); + cmds.push(self.update(Message::ClipboardCached(cache))); + + cmds.push( + iced::Task::future(tokio::time::sleep(Duration::from_millis(300))) + .discard() + .chain( + clipboard::read_data::().map(|contents_opt| { + match contents_opt { + Some(contents) if !contents.paths.is_empty() => { + cosmic::action::app(Message::ClipboardCached( + ClipboardCache::Files(contents), + )) + } + _ => cosmic::action::app(Message::CheckClipboardImage), + } + }), + ), + ); + return Task::batch(cmds); + } Message::ClipboardCached(cache) => { self.clipboard_cache = cache; } @@ -3878,106 +4435,19 @@ impl Application for App { } } Message::PendingComplete(id, op_sel) => { - let mut commands = Vec::with_capacity(4); - if let Some((op, _)) = self.pending_operations.remove(&id) { - // Show toast for some operations - if let Some(description) = op.toast() { - if let Operation::Delete { ref paths } = op { - let paths: Arc<[PathBuf]> = Arc::from(paths.as_slice()); - commands.push( - self.toasts - .push( - widget::toaster::Toast::new(description) - .action(fl!("undo"), move |tid| { - Message::UndoTrash(tid, paths.clone()) - }), - ) - .map(cosmic::Action::App), - ); - } else { - commands.push( - self.toasts - .push(widget::toaster::Toast::new(description)) - .map(cosmic::Action::App), - ); - } - } - - // If a favorite for a path has been renamed or moved, update it. - if let Operation::Rename { ref from, ref to } = op { - if self.update_favorites([(from, to)].as_slice()) { - commands.push(self.update_config()); - } - } else if let Operation::Move { - ref paths, ref to, .. - } = op - { - let path_changes: Box<[_]> = paths - .iter() - .filter_map(|from| from.file_name().map(|name| (from, to.join(name)))) - .collect(); - if self.update_favorites(&path_changes) { - commands.push(self.update_config()); - } - } - - if matches!(op, Operation::RemoveFromRecents { .. }) { - commands.push(self.rescan_recents()); - } - - self.complete_operations.insert(id, op); - } - // Close progress notification if all relevant operations are finished - if !self - .pending_operations - .values() - .any(|(op, _)| op.show_progress_notification()) - { - self.progress_operations.clear(); - } - // Potentially show a notification - commands.push(self.update_notification()); - // Rescan and select based on operation - commands.push(self.rescan_operation_selection(op_sel)); - // Manually rescan any trash tabs after any operation is completed - commands.push(self.rescan_trash()); - - return Task::batch(commands); + return self.handle_completed_operations(vec![(id, op_sel)]); } Message::PendingDismiss => { self.progress_operations.clear(); } Message::PendingError(id, err) => { - let mut tasks = Vec::new(); - if let Some((op, controller)) = self.pending_operations.remove(&id) { - // Only show dialog if not cancelled - if !controller.is_cancelled() { - tasks.push(self.dialog_pages.push_back(match err.kind { - OperationErrorType::Generic(_) => DialogPage::FailedOperation(id), - OperationErrorType::PasswordRequired => DialogPage::ExtractPassword { - id, - password: String::new(), - }, - })); - } - tasks.push(widget::text_input::focus(self.dialog_text_input.clone())); - - // Remove from progress - self.progress_operations.remove(&id); - self.failed_operations - .insert(id, (op, controller, err.to_string())); - } - // Close progress notification if all relevant operations are finished - if !self - .pending_operations - .values() - .any(|(op, _)| op.show_progress_notification()) - { - self.progress_operations.clear(); - } - // Manually rescan any trash tabs after any operation is completed - tasks.push(self.rescan_trash()); - return Task::batch(tasks); + return self.handle_operation_errors(vec![(id, err)]); + } + Message::PendingResults(completed, errors) => { + return Task::batch(vec![ + self.handle_completed_operations(completed), + self.handle_operation_errors(errors), + ]); } Message::PendingPause(id, pause) => { if let Some((_, controller)) = self.pending_operations.get(&id) { @@ -4071,10 +4541,8 @@ impl Application for App { .is_some_and(|loc| matches!(loc, Location::Trash)) }); if let Some(entity) = maybe_entity { - self.nav_model.icon_set( - entity, - icon::icon(tab::trash_helpers::trash_icon_symbolic(16)), - ); + self.nav_model + .icon_set(entity, icon::icon(Trash::icon_symbolic(16))); } return Task::batch([self.rescan_trash(), self.update_desktop()]); @@ -4096,12 +4564,14 @@ impl Application for App { .collect(); if !selected.is_empty() { //TODO: batch rename - let tasks = selected + let mut last_name = String::new(); + let tasks: Vec<_> = selected .into_iter() .filter_map(|path| { let parent = path.parent()?.to_path_buf(); let name = path.file_name()?.to_str()?.to_string(); let dir = path.is_dir(); + last_name = name.clone(); Some(self.dialog_pages.push_back(DialogPage::RenameItem { from: path, parent, @@ -4109,9 +4579,15 @@ impl Application for App { dir, })) }) - .chain(std::iter::once(widget::text_input::focus( + .collect(); + let tasks = tasks.into_iter().chain([ + widget::text_input::focus(self.dialog_text_input.clone()), + widget::text_input::select_until_last( self.dialog_text_input.clone(), - ))); + &last_name, + '.', + ), + ]); return Task::batch(tasks); } } @@ -4184,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(); @@ -4345,7 +4901,7 @@ impl Application for App { use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ Anchor, Gravity, }; - use cosmic::iced_runtime::platform_specific::wayland::popup::{ + use cosmic::iced::runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; let window_id = WindowId::unique(); @@ -4356,6 +4912,7 @@ impl Application for App { widget::Id::unique(), )), ); + commands.push(self.update(Message::CheckClipboard)); commands.push(self.update(Message::Surface( cosmic::surface::action::app_popup( move |app: &mut Self| -> SctkPopupSettings { @@ -4412,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, @@ -4422,6 +4987,25 @@ impl Application for App { tab::Command::ExecEntryAction(entry, action) => { Self::exec_entry_action(&entry, action); } + tab::Command::RunContextAction(action) => { + let paths: Box<[_]> = self.selected_paths(Some(entity)).collect(); + if let Some(preset) = self.config.context_actions.get(action) { + if preset.confirm { + commands.push(self.push_dialog( + DialogPage::RunContextAction { action, paths }, + Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), + )); + } else { + context_action::run( + &self.config.context_actions, + action, + &paths, + ); + } + } else { + log::warn!("invalid context action index `{action}`"); + } + } tab::Command::Iced(iced_command) => { commands.push(iced_command.0.map(move |x| { cosmic::action::app(Message::TabMessage(Some(entity), x)) @@ -4465,6 +5049,19 @@ impl Application for App { tab::Command::SetPermissions(path, mode) => { commands.push(self.operation(Operation::SetPermissions { path, mode })); } + tab::Command::SetMultiplePermissions(permissions) => { + commands.push( + self.join_operations( + permissions + .into_iter() + .map(|(path, mode)| Operation::SetPermissions { + path, + mode, + }) + .collect(), + ), + ); + } tab::Command::WindowDrag => { if let Some(window_id) = self.core.main_window_id() { commands.push(window::drag(window_id)); @@ -4902,6 +5499,7 @@ impl Application for App { .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), + set_default: false, }, None, ); @@ -4916,6 +5514,30 @@ impl Application for App { } } } + NavMenuAction::RunContextAction(entity, action) => { + if let Some(path) = self + .nav_model + .data::(entity) + .and_then(Location::path_opt) + .cloned() + { + let paths = vec![path]; + if let Some(preset) = self.config.context_actions.get(action) { + if preset.confirm { + return self.push_dialog( + DialogPage::RunContextAction { + action, + paths: paths.into_boxed_slice(), + }, + Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), + ); + } + context_action::run(&self.config.context_actions, action, &paths); + } else { + log::warn!("invalid context action index `{action}`"); + } + } + } NavMenuAction::OpenInNewTab(entity) => { let open_task = match self.nav_model.data::(entity) { Some(Location::Network(uri, display_name, path)) => self.open_tab( @@ -4987,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); } @@ -5166,7 +5788,11 @@ impl Application for App { }; } // Check clipboard when window gains focus - return self.update(Message::CheckClipboard); + // HACK: Wait a moment for the data to be available. + return cosmic::task::future(async { + _ = tokio::time::sleep(Duration::from_millis(300)).await; + cosmic::action::app(Message::CheckClipboard) + }); } Message::Surface(action) => { return cosmic::task::message(cosmic::Action::Cosmic( @@ -5290,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 { @@ -5427,6 +6053,25 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } + DialogPage::FailedOperations(ids) => { + let errors: Vec = ids + .iter() + .filter_map(|id| match self.failed_operations.get(id) { + Some((operation, _, err)) => Some(format!("{operation:#?}\n{err}")), + _ => None, + }) + .collect(); + + //TODO: nice description of error + widget::dialog() + .title("Failed operations") + .body(errors.join("\n\n")) + .icon(icon::from_name("dialog-error").size(64)) + //TODO: retry action + .primary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + } DialogPage::ExtractPassword { id, password } => widget::dialog() .title(fl!("extract-password-required")) .icon(icon::from_name("dialog-error").size(64)) @@ -5673,12 +6318,32 @@ impl Application for App { .spacing(space_xxs), ) } + DialogPage::RunContextAction { action, paths } => { + let name = self + .config + .context_actions + .get(*action) + .map_or_else(|| fl!("context-action"), |preset| preset.name.clone()); + + widget::dialog() + .title(fl!("context-action-confirm-title", name = name)) + .body(fl!("context-action-confirm-warning", items = paths.len())) + .icon(icon::from_name("dialog-error").size(64)) + .primary_action( + widget::button::suggested(fl!("run")) + .on_press(Message::DialogComplete) + .id(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + } DialogPage::OpenWith { path, mime, selected, store_opt, - .. + set_default, } => { let name = match path.file_name() { Some(file_name) => file_name.to_str(), @@ -5763,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( @@ -5869,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( @@ -5885,6 +6564,7 @@ impl Application for App { .into(), widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) + .double_click_select_delimiter('.') .on_input(move |name| { Message::DialogUpdate(DialogPage::RenameItem { from: from.clone(), @@ -6012,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; @@ -6060,8 +6740,9 @@ impl Application for App { //TODO: get height from theme? let progress_bar_height = Length::Fixed(4.0); - let progress_bar = - widget::progress_bar(0.0..=1.0, total_progress).girth(progress_bar_height); + let progress_bar = widget::determinate_linear(total_progress) + .width(Length::Fill) + .girth(progress_bar_height); let container = widget::layer_container(widget::column::with_children([ widget::row::with_children([ @@ -6167,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); @@ -6194,11 +6878,11 @@ impl Application for App { .button_height(32) .button_spacing(space_xxs) .enable_tab_drag(String::from("x-cosmic-files/tab-dnd")) - .on_reorder(move |event| Message::ReorderTab(event)) + .on_reorder(Message::ReorderTab) .tab_drag_threshold(25.) .on_activate(Message::TabActivate) .on_close(|entity| Message::TabClose(Some(entity))) - .on_dnd_enter(|entity, mimes| Message::DndEnterTab(entity, mimes)) + .on_dnd_enter(Message::DndEnterTab) .on_dnd_leave(|_| Message::DndExitTab) .on_dnd_drop(|entity, data, action| { Message::DndDropTab(entity, data, action) @@ -6211,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 @@ -6218,6 +6932,7 @@ impl Application for App { &self.key_binds, &self.modifiers, self.clipboard_has_content(), + &self.config.context_actions, ) .map(move |message| Message::TabMessage(Some(entity), message)); tab_column = tab_column.push(tab_view); @@ -6246,6 +6961,7 @@ impl Application for App { &self.key_binds, &window.modifiers, self.clipboard_has_content(), + &self.config.context_actions, ) .map(|x| Message::TabMessage(Some(*entity), x)), id.clone(), @@ -6263,6 +6979,7 @@ impl Application for App { &self.key_binds, &window.modifiers, self.clipboard_has_content(), + &self.config.context_actions, ) .map(move |message| Message::TabMessage(Some(*entity), message)), None => widget::space::vertical().into(), @@ -6524,14 +7241,7 @@ impl Application for App { }, ); - // TODO: Trash watching support for Windows, macOS, and other OSes - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_os = "ios"), - not(target_os = "android") - ))] - match (watcher_res, trash::os_limited::trash_folders()) { + match (watcher_res, Trash::folders()) { (Ok(mut watcher), Ok(trash_bins)) => { // Watch the "bins" themselves as well as the files folder where // trashed items are placed. This allows us to avoid recursively @@ -6688,8 +7398,7 @@ impl Application for App { |_| { stream::channel( 1, - move |msg_tx: futures::channel::mpsc::Sender<_>| async move { - let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); + move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move { tokio::task::spawn_blocking(move || { match notify_rust::Notification::new() .summary(&fl!("notification-in-progress")) @@ -6699,8 +7408,6 @@ impl Application for App { Ok(notification) => { let _ = futures::executor::block_on(async { msg_tx - .lock() - .await .send(Message::Notification(Arc::new( Mutex::new(notification), ))) @@ -6767,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 5786ff7..cff0c1e 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,17 +1,14 @@ -use crate::{ - mime_icon::mime_for_path, - operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}, -}; -use chrono::TimeZone; -use chrono::{Datelike, Timelike}; +use crate::mime_icon::mime_for_path; +use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}; use cosmic::iced::futures; -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] = &[ @@ -113,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>( @@ -190,6 +188,8 @@ fn zip_extract>( if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { let mut target = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut target)?; + // File no longer needed, drop to allow reading target on windows + drop(file); #[cfg(unix)] { @@ -200,11 +200,15 @@ fn zip_extract>( #[cfg(windows)] { let Ok(target) = String::from_utf8(target) else { - return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target")); + return Err(ZipError::InvalidArchive( + "Invalid UTF-8 as symlink target".into(), + )); }; - let target = target.into_boxed_str(); - let target_is_dir_from_archive = - archive.shared.files.contains_key(&target) && is_dir(&target); + let target_is_dir_from_archive = match password { + None => archive.by_name(&target), + Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()), + } + .map_or(false, |x| x.is_dir()); let target_path = directory.as_ref().join(OsString::from(target.to_string())); let target_is_dir = if target_is_dir_from_archive { true @@ -285,25 +289,25 @@ fn zip_extract>( } fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option { - let date = chrono::NaiveDate::from_ymd_opt( - date_time.year() as i32, - date_time.month() as u32, - date_time.day() as u32, - )?; - let time = chrono::NaiveTime::from_hms_opt( - date_time.hour() as u32, - date_time.minute() as u32, - date_time.second() as u32, - )?; - let naive = chrono::NaiveDateTime::new(date, time); - chrono::Local - .from_local_datetime(&naive) - .latest() + let dt = DateTime::new( + date_time.year() as i16, + date_time.month() as i8, + date_time.day() as i8, + date_time.hour() as i8, + date_time.minute() as i8, + date_time.second() as i8, + 0, + ) + .ok()?; + TimeZone::system() + .to_ambiguous_zoned(dt) + .later() + .ok() .map(SystemTime::from) } pub fn system_time_to_zip_date_time(system_time: SystemTime) -> Option { - let date_time: chrono::DateTime = system_time.into(); + let date_time = Zoned::try_from(system_time).ok()?; zip::DateTime::from_date_and_time( date_time.year() as u16, diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..2a295f9 --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,73 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +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) { + let channel = Arc::new(Channel { + queue: Mutex::new(VecDeque::default()), + notify: tokio::sync::Notify::const_new(), + closed: AtomicBool::new(false), + }); + + (Sender(channel.clone()), Receiver(channel)) +} + +/// A channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque. +struct Channel { + pub(self) queue: Mutex>, + /// Set when a new message has been stored. + pub(self) notify: tokio::sync::Notify, + /// Set when the receiver is dropped. + pub(self) closed: AtomicBool, +} + +pub struct Sender(Arc>); + +impl Sender { + pub fn send(&self, message: Message) { + self.0.queue.lock().unwrap().push_back(message); + self.0.notify.notify_one(); + } +} + +impl Drop for Sender { + fn drop(&mut self) { + self.0.closed.store(true, Ordering::SeqCst); + self.0.notify.notify_one(); + } +} + +pub struct Receiver(Arc>); + +impl Receiver { + /// Returns a value until the sender is dropped. + pub async fn recv(&self) -> Option { + loop { + { + let mut queue = self.0.queue.lock().unwrap(); + if let Some(value) = queue.pop_front() { + if queue.capacity() - queue.len() > 32 { + let capacity = queue.len().next_power_of_two(); + queue.shrink_to(capacity); + } + drop(queue); + return Some(value); + } + } + + if self.0.closed.load(Ordering::SeqCst) { + return None; + } + + self.0.notify.notified().await; + } + } + + pub fn try_recv(&self) -> Option { + self.0.queue.lock().unwrap().pop_front() + } +} diff --git a/src/clipboard.rs b/src/clipboard.rs index 637303e..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)] @@ -128,10 +126,15 @@ impl TryFrom<(Vec, String)> for ClipboardPaste { // Assume the kind is Copy if not provided by the mime type let mut kind = ClipboardKind::Copy; let mut paths = Vec::new(); + match mime.as_str() { "text/uri-list" => { let text = str::from_utf8(&data)?; - 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), @@ -141,6 +144,7 @@ impl TryFrom<(Vec, String)> for ClipboardPaste { } "x-special/gnome-copied-files" => { let text = str::from_utf8(&data)?; + for (i, line) in text.lines().enumerate() { if i == 0 { kind = match line { diff --git a/src/config.rs b/src/config.rs index cbe84a6..8f493c0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,20 +1,20 @@ // 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}; pub const CONFIG_VERSION: u64 = 1; @@ -164,11 +164,17 @@ pub struct Config { pub app_theme: AppTheme, pub dialog: DialogConfig, pub desktop: DesktopConfig, + pub context_actions: Vec, pub thumb_cfg: ThumbCfg, pub favorites: Vec, 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, } @@ -220,6 +226,7 @@ impl Default for Config { app_theme: AppTheme::System, desktop: DesktopConfig::default(), dialog: DialogConfig::default(), + context_actions: Vec::new(), thumb_cfg: ThumbCfg::default(), favorites: vec![ Favorite::Home, @@ -232,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 new file mode 100644 index 0000000..34dfb6e --- /dev/null +++ b/src/context_action.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::mime_app; +use crate::spawn_detached::spawn_detached; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub enum ContextActionSelection { + #[default] + #[serde(alias = "any")] + Any, + #[serde(alias = "files")] + Files, + #[serde(alias = "folders")] + Folders, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default)] +pub struct ContextActionPreset { + pub name: String, + pub confirm: bool, + pub selection: ContextActionSelection, + pub steps: Vec, +} + +impl ContextActionPreset { + pub fn matches_selection(&self, selected: usize, selected_dir: usize) -> bool { + if selected == 0 { + return false; + } + + match self.selection { + ContextActionSelection::Any => true, + ContextActionSelection::Files => selected_dir == 0, + ContextActionSelection::Folders => selected_dir == selected, + } + } + + pub fn run(&self, paths: &[PathBuf]) { + if self.steps.is_empty() { + log::warn!("context action {:?} has no steps", self.name); + return; + } + + for step in &self.steps { + 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, + step + ); + return; + }; + + for mut command in commands { + if let Err(err) = spawn_detached(&mut command) { + log::warn!( + "failed to run context action {:?} step {:?}: {}", + self.name, + step, + err + ); + return; + } + } + } + } +} + +pub fn run(actions: &[ContextActionPreset], action: usize, paths: &[PathBuf]) { + if let Some(preset) = actions.get(action) { + preset.run(paths); + } else { + log::warn!("invalid context action index `{action}`"); + } +} diff --git a/src/dialog.rs b/src/dialog.rs index 6f0e5cb..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::{ - 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, - }, - iced_core::widget::operation, - iced_widget::scrollable::AbsoluteOffset, - iced_winit::{self, SurfaceIdWrapper}, - 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); @@ -203,7 +191,7 @@ impl> From for DialogLabel { impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> { fn from(label: &'a DialogLabel) -> Self { - let mut iced_spans: Vec> = + let mut iced_spans: Vec> = Vec::with_capacity(label.spans.len()); for span in &label.spans { iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline)); @@ -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); @@ -595,6 +583,7 @@ impl App { col = col.push( widget::text_input("", filename) .id(self.filename_id.clone()) + .double_click_select_delimiter('.') .on_input(Message::Filename) .on_submit(|_| Message::Save(false)), ); @@ -717,7 +706,7 @@ impl App { match (selected.next(), selected.next()) { // At least two selected items - (Some(_), Some(_)) => Some(self.tab.multi_preview_view()), + (Some(_), Some(_)) => Some(self.tab.multi_preview_view(None)), // Exactly one selected item (Some(item), None) => Some(item.preview_view(None, military_time)), // No selected items @@ -743,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 @@ -798,7 +793,7 @@ impl App { }; search_location.map(|search_location| { - return ( + ( Location::Search( search_location, term, @@ -806,7 +801,7 @@ impl App { Instant::now(), ), true, - ); + ) }) } None => match &self.tab.location { @@ -836,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 { @@ -889,16 +885,20 @@ impl App { fn update_nav_model(&mut self) { let mut nav_model = segmented_button::ModelBuilder::default(); - nav_model = nav_model.insert(|b| { - b.text(fl!("recents")) - .icon(widget::icon::from_name("document-open-recent-symbolic")) - .data(Location::Recents) - }); + if self.flags.config.show_recents { + nav_model = nav_model.insert(|b| { + b.text(fl!("recents")) + .icon(widget::icon::from_name("document-open-recent-symbolic")) + .data(Location::Recents) + }); + } for favorite in &self.flags.config.favorites { 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 { @@ -1122,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 { @@ -1206,7 +1206,9 @@ impl Application for App { .icon(widget::icon::from_name("dialog-question").size(64)) .body(fl!("replace-warning")) .primary_action( - widget::button::suggested(fl!("replace")).on_press(Message::DialogComplete), + widget::button::suggested(fl!("replace")) + .on_press(Message::DialogComplete) + .id(REPLACE_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -1398,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(); } } @@ -1453,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) @@ -1787,7 +1792,7 @@ impl Application for App { use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ Anchor, Gravity, }; - use cosmic::iced_runtime::platform_specific::wayland::popup::{ + use cosmic::iced::runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; use cosmic::iced::Rectangle; @@ -1829,6 +1834,7 @@ impl Application for App { &app.key_binds, &app.modifiers, false, // Paste not used in dialogs + &app.flags.config.context_actions, ) .map(Message::TabMessage) .map(cosmic::Action::App), @@ -1952,6 +1958,16 @@ impl Application for App { if self.search_get().is_some() { return widget::text_input::focus(self.search_id.clone()); } + if let DialogKind::SaveFile { filename } = &self.flags.kind { + return Task::batch([ + widget::text_input::focus(self.filename_id.clone()), + widget::text_input::select_until_last( + self.filename_id.clone(), + filename, + '.', + ), + ]); + } return widget::text_input::focus(self.filename_id.clone()); } } @@ -2001,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); @@ -2022,7 +2038,7 @@ impl Application for App { col = col.push( self.tab - .view(&self.key_binds, &self.modifiers, false) + .view(&self.key_binds, &self.modifiers, false, &[]) .map(Message::TabMessage), ); diff --git a/src/key_bind.rs b/src/key_bind.rs index 8340580..f47d403 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -1,11 +1,10 @@ -use cosmic::{ - iced::keyboard::Key, - iced_core::keyboard::key::Named, - 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 159af08..44b87f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,23 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use cosmic::{app::Settings, iced::Limits}; -use std::{env, fs, path::PathBuf, process}; +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}; +use crate::config::{Config, State}; +use crate::tab::Location; -use app::{App, Flags}; pub mod app; mod archive; +pub mod channel; pub mod clipboard; -use config::Config; pub mod config; +mod context_action; pub mod dialog; mod key_bind; pub(crate) mod large_image; @@ -22,16 +30,15 @@ mod mounter; mod mouse_area; pub mod operation; mod spawn_detached; -use tab::Location; -mod zoom; - -use crate::config::State; pub mod tab; mod thumbnail_cacher; mod thumbnailer; +pub(crate) mod trash; +mod zoom; pub(crate) type FxOrderMap = ordermap::OrderMap; +#[cfg(feature = "gvfs")] pub(crate) fn err_str(err: T) -> String { err.to_string() } @@ -72,7 +79,22 @@ pub fn is_wayland() -> bool { /// Runs application in desktop mode #[rustfmt::skip] pub fn desktop() -> Result<(), Box> { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let log_format = tracing_subscriber::fmt::format() + .pretty() + .without_time() + .with_line_number(true) + .with_file(true) + .with_target(false) + .with_thread_names(true); + + let log_layer = tracing_subscriber::fmt::Layer::default() + .with_writer(std::io::stderr) + .event_format(log_format); + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_env("RUST_LOG")) + .with(log_layer) + .init(); localize::localize(); @@ -107,7 +129,21 @@ pub fn desktop() -> Result<(), Box> { /// Runs application with these settings #[rustfmt::skip] pub fn main() -> Result<(), Box> { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let log_format = tracing_subscriber::fmt::format() + .pretty() + .with_line_number(true) + .with_file(true) + .with_target(false) + .with_thread_names(true); + + let log_layer = tracing_subscriber::fmt::Layer::default() + .with_writer(std::io::stderr) + .event_format(log_format); + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(log_layer) + .init(); localize::localize(); @@ -157,7 +193,7 @@ pub fn main() -> Result<(), Box> { } if daemonize { - #[cfg(all(unix, not(target_os = "redox")))] + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] match fork::daemon(true, true) { Ok(fork::Fork::Child) => (), Ok(fork::Fork::Parent(_child_pid)) => process::exit(0), diff --git a/src/load_image.rs b/src/load_image.rs index 00e9bdf..88ceb1b 100644 --- a/src/load_image.rs +++ b/src/load_image.rs @@ -1,11 +1,10 @@ -use cosmic::{iced_core, iced_widget}; +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/main.rs b/src/main.rs index d4cb5bd..276cb9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,5 +6,6 @@ use tikv_jemallocator::Jemalloc; static GLOBAL: Jemalloc = Jemalloc; fn main() -> Result<(), Box> { + let _ = jxl_oxide::integration::register_image_decoding_hook(); cosmic_files::main() } diff --git a/src/menu.rs b/src/menu.rs index d025a0f..d167f41 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,29 +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, - fl, - tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab}, -}; +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")); @@ -37,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) ); @@ -60,6 +57,7 @@ pub fn context_menu<'a>( key_binds: &HashMap, modifiers: &Modifiers, clipboard_paste_available: bool, + context_actions: &[ContextActionPreset], ) -> Element<'a, tab::Message> { let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds { @@ -141,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); } _ => (), } @@ -157,6 +154,14 @@ pub fn context_menu<'a>( selected_types.sort_unstable(); selected_types.dedup(); selected_trash_only = selected_trash_only && selected == 1; + let context_action_items = |selected: usize, selected_dir: usize| { + context_actions + .iter() + .enumerate() + .filter(|(_, action)| action.matches_selection(selected, selected_dir)) + .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into()) + .collect::>>() + }; // Parse the desktop entry if it is the only selection #[cfg(feature = "desktop")] let selected_desktop_entry = selected_desktop_entry.and_then(|path| { @@ -183,14 +188,14 @@ pub fn context_menu<'a>( ) => { if selected_trash_only { children.push(menu_item(fl!("open"), Action::Open).into()); - if !trash::os_limited::is_empty().unwrap_or(true) { + 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(), )); } @@ -204,6 +209,11 @@ pub fn context_menu<'a>( } // Should this simply bypass trash and remove the shortcut? children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + let action_items = context_action_items(selected, selected_dir); + if !action_items.is_empty() { + children.push(divider::horizontal::light().into()); + children.extend(action_items); + } } else if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { children.push(menu_item(fl!("open"), Action::Open).into()); @@ -215,7 +225,7 @@ pub fn context_menu<'a>( .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); } } - if tab.location.is_recents() { + if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) { children.push( menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), ); @@ -226,6 +236,11 @@ pub fn context_menu<'a>( children .push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into()); } + let action_items = context_action_items(selected, selected_dir); + if !action_items.is_empty() { + children.push(divider::horizontal::light().into()); + children.extend(action_items); + } children.push(divider::horizontal::light().into()); if selected_mount_point == 0 { children.push(menu_item(fl!("rename"), Action::Rename).into()); @@ -559,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() } @@ -614,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 52b1a27..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"; @@ -19,6 +17,7 @@ struct MimeIconKey { struct MimeIconCache { cache: FxHashMap>, + #[cfg(unix)] shared_mime_info: xdg_mime::SharedMimeInfo, } @@ -26,10 +25,17 @@ impl MimeIconCache { pub fn new() -> Self { Self { cache: FxHashMap::default(), + #[cfg(unix)] shared_mime_info: xdg_mime::SharedMimeInfo::new(), } } + #[cfg(not(unix))] + pub fn get(&mut self, _key: MimeIconKey) -> Option { + None + } + + #[cfg(unix)] pub fn get(&mut self, key: MimeIconKey) -> Option { self.cache .entry(key) @@ -39,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(); @@ -53,6 +59,16 @@ impl MimeIconCache { static MIME_ICON_CACHE: LazyLock> = LazyLock::new(|| Mutex::new(MimeIconCache::new())); +#[cfg(not(unix))] +pub fn mime_for_path( + path: impl AsRef, + metadata_opt: Option<&fs::Metadata>, + remote: bool, +) -> Mime { + mime_guess::from_path(path).first_or_octet_stream() +} + +#[cfg(unix)] pub fn mime_for_path( path: impl AsRef, metadata_opt: Option<&fs::Metadata>, @@ -96,12 +112,20 @@ 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(), } } +#[cfg(not(unix))] +pub fn parent_mime_types(_mime: &Mime) -> Option> { + None +} + +#[cfg(unix)] pub fn parent_mime_types(mime: &Mime) -> Option> { let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); - mime_icon_cache.shared_mime_info.get_parents_aliased(mime) } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index cd4ea68..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 tokio::sync::{Mutex, mpsc}; +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"; @@ -199,6 +201,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { metadata, hidden, location_opt: Some(location), + image_dimensions: None, mime, icon_handle_grid, icon_handle_list, @@ -229,7 +232,10 @@ fn dir_info(uri: &str) -> Result<(String, String, Option), glib::Error> Ok((resolved_uri, info.display_name().into(), file.path())) } -fn mount_op(uri: String, event_tx: mpsc::UnboundedSender) -> gio::MountOperation { +fn mount_op( + uri: String, + event_tx: std::sync::Weak>, +) -> gio::MountOperation { let mount_op = gio::MountOperation::new(); mount_op.connect_ask_password( move |mount_op, message, default_user, default_domain, flags| { @@ -252,9 +258,9 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender) -> gio::MountOp .then_some(false), }; let (auth_tx, mut auth_rx) = mpsc::channel(1); - event_tx - .send(Event::NetworkAuth(uri.clone(), auth, auth_tx)) - .unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)); + } //TODO: async recv? if let Some(auth) = auth_rx.blocking_recv() { if auth.anonymous_opt == Some(true) { @@ -357,37 +363,45 @@ impl Item { pub struct Gvfs { command_tx: mpsc::UnboundedSender, - event_rx: Arc>>, + event_rx: Arc>, } impl Gvfs { pub fn new() -> Self { //TODO: switch to using gvfs-zbus which will better integrate with async rust let (command_tx, mut command_rx) = mpsc::unbounded_channel(); - let (event_tx, event_rx) = mpsc::unbounded_channel(); + let (event_tx, event_rx) = crate::channel::channel(); + let event_tx = Arc::new(event_tx); std::thread::spawn(move || { let main_loop = glib::MainLoop::new(None, false); main_loop.context().spawn_local(async move { + let event_tx = Arc::downgrade(&event_tx); let monitor = gio::VolumeMonitor::get(); { let event_tx = event_tx.clone(); monitor.connect_mount_changed(move |_monitor, mount| { log::info!("mount changed {}", MountExt::name(mount)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } { let event_tx = event_tx.clone(); monitor.connect_mount_added(move |_monitor, mount| { log::info!("mount added {}", MountExt::name(mount)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } { let event_tx = event_tx.clone(); monitor.connect_mount_removed(move |_monitor, mount| { log::info!("mount removed {}", MountExt::name(mount)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } @@ -395,21 +409,27 @@ impl Gvfs { let event_tx = event_tx.clone(); monitor.connect_volume_changed(move |_monitor, volume| { log::info!("volume changed {}", VolumeExt::name(volume)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } { let event_tx = event_tx.clone(); monitor.connect_volume_added(move |_monitor, volume| { log::info!("volume added {}", VolumeExt::name(volume)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } { let event_tx = event_tx.clone(); monitor.connect_volume_removed(move |_monitor, volume| { log::info!("volume removed {}", VolumeExt::name(volume)); - event_tx.send(Event::Changed).unwrap(); + if let Some(event_tx) = event_tx.upgrade() { + event_tx.send(Event::Changed); + } }); } @@ -419,7 +439,11 @@ impl Gvfs { items_tx.send(items(&monitor, sizes)).await.unwrap(); } Cmd::Rescan => { - event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap(); + let Some(event_tx) = event_tx.upgrade() else { + return; + }; + + event_tx.send(Event::Items(items(&monitor, IconSizes::default()))); } Cmd::Mount(mounter_item, complete_tx) => { let MounterItem::Gvfs(ref item) = mounter_item else { @@ -471,6 +495,9 @@ impl Gvfs { .ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE)) .unwrap_or(true); } + let Some(event_tx) = event_tx.upgrade() else { + return; + }; event_tx.send(Event::MountResult(updated_item, match res { Ok(()) => { _ = complete_tx.send(Ok(())); @@ -482,7 +509,7 @@ impl Gvfs { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) }} - })).unwrap(); + })); }, ); break; @@ -498,6 +525,9 @@ impl Gvfs { gio::Cancellable::NONE, move |res| { log::info!("network drive {uri}: result {res:?}"); + let Some(event_tx) = event_tx.upgrade() else { + return; + }; event_tx.send(Event::NetworkResult(uri, match res { Ok(()) => { _ = result_tx.send(Ok(())); @@ -508,7 +538,7 @@ impl Gvfs { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) }} - })).unwrap(); + })); } ); } @@ -532,6 +562,9 @@ impl Gvfs { // FIXME sometimes a uri can be mounted and then not recognized as mounted... // seems to be related to uri with a path items_tx.blocking_send(network_scan(&uri, sizes)).unwrap(); + let Some(event_tx) = event_tx.upgrade() else { + return; + }; event_tx.send(Event::NetworkResult(resolved_uri, match res { Ok(()) => { Ok(true) @@ -540,7 +573,7 @@ impl Gvfs { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) } - })).unwrap(); + })); } ); } else { @@ -596,7 +629,7 @@ impl Gvfs { }); Self { command_tx, - event_rx: Arc::new(Mutex::new(event_rx)), + event_rx: Arc::new(event_rx), } } } @@ -670,7 +703,7 @@ impl Mounter for Gvfs { let event_rx = self.event_rx.clone(); struct Wrapper { command_tx: mpsc::UnboundedSender, - event_rx: Arc>>, + event_rx: Arc>, } impl Hash for Wrapper { fn hash(&self, state: &mut H) { @@ -694,7 +727,7 @@ impl Mounter for Gvfs { MounterMessage, >| async move { command_tx.send(Cmd::Rescan).unwrap(); - while let Some(event) = event_rx.lock().await.recv().await { + while let Some(event) = event_rx.recv().await { match event { Event::Changed => command_tx.send(Cmd::Rescan).unwrap(), Event::Items(items) => { 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 dea4b25..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)] @@ -395,7 +391,7 @@ where update( self, - &event, + event, layout, cursor, shell, @@ -488,7 +484,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut cosmic::iced_core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles, ) { self.content.as_widget().drag_destinations( &state.children[0], diff --git a/src/operation/controller.rs b/src/operation/controller.rs index f4b26fe..691316c 100644 --- a/src/operation/controller.rs +++ b/src/operation/controller.rs @@ -1,7 +1,11 @@ -use std::sync::{Arc, Mutex}; +use atomic_float::AtomicF32; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicU16}; use tokio::sync::Notify; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u16)] pub enum ControllerState { Cancelled, Failed, @@ -11,8 +15,8 @@ pub enum ControllerState { #[derive(Debug)] struct ControllerInner { - state: Mutex, - progress: Mutex, + state: AtomicU16, + progress: AtomicF32, notify: Notify, } @@ -27,8 +31,8 @@ impl Default for Controller { Self { primary: true, inner: Arc::new(ControllerInner { - state: Mutex::new(ControllerState::Running), - progress: Mutex::new(0.0), + state: AtomicU16::new(ControllerState::Running.into()), + progress: AtomicF32::new(0.0), notify: Notify::new(), }), } @@ -50,19 +54,24 @@ impl Controller { } pub fn progress(&self) -> f32 { - *self.inner.progress.lock().unwrap() + self.inner.progress.load(atomic::Ordering::Relaxed) } pub fn set_progress(&self, progress: f32) { - *self.inner.progress.lock().unwrap() = progress; + self.inner + .progress + .swap(progress, atomic::Ordering::Relaxed); } pub fn state(&self) -> ControllerState { - *self.inner.state.lock().unwrap() + ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed)) + .unwrap_or(ControllerState::Failed) } pub fn set_state(&self, state: ControllerState) { - *self.inner.state.lock().unwrap() = state; + self.inner + .state + .store(state.into(), atomic::Ordering::Relaxed); self.inner.notify.notify_waiters(); } @@ -86,6 +95,35 @@ impl Controller { self.set_state(ControllerState::Paused); } + /// Returns when the state is paused. + /// + /// Use this to pause futures. + pub async fn until_paused(&self) { + loop { + if matches!(self.state(), ControllerState::Paused) { + return; + } + + self.inner.notify.notified().await; + } + } + + /// Returns when state is neither paused, cancelled, nor failed. + /// + /// Use this to resume futures. + pub async fn until_unpaused(&self) { + loop { + if !matches!( + self.state(), + ControllerState::Paused | ControllerState::Cancelled | ControllerState::Failed + ) { + return; + } + + self.inner.notify.notified().await; + } + } + pub fn unpause(&self) { if !self.is_cancelled() | !self.is_failed() { self.set_state(ControllerState::Running); diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 3aad6fc..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; @@ -22,6 +17,9 @@ use zip::AesMode::Aes256; pub use self::controller::{Controller, ControllerState}; pub mod controller; +pub use notifiers::*; +mod notifiers; + pub use self::reader::OpReader; pub mod reader; @@ -36,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; @@ -44,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; @@ -111,7 +109,7 @@ async fn copy_or_move( ); // Handle duplicate file names by renaming paths - let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths + let from_to_pairs_iter = paths .into_iter() .zip(std::iter::repeat(to.as_path())) .filter_map(|(from, to)| { @@ -129,36 +127,46 @@ async fn copy_or_move( //TODO: how to handle from missing file name? None } - }) - .collect(); + }); // Attempt quick and simple renames //TODO: allow rename to be used for directories in recursive context? - if matches!(method, Method::Move { .. }) { - from_to_pairs.retain(|(from, to)| { - //TODO: show replace dialog here? - if to.exists() { - return true; - } - //TODO: use compio::fs::rename? - match fs::rename(from, to) { - Ok(()) => { - log::info!("renamed {} to {}", from.display(), to.display()); - false + let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) { + from_to_pairs_iter + .map(|(from, to)| async move { + //TODO: show replace dialog here? + if to.exists() { + return Some((from, to)); } - Err(err) => { - log::info!( - "failed to rename {} to {}, fallback to recursive move: {}", - from.display(), - to.display(), - err - ); - true + + match compio::fs::rename(&from, &to).await { + Ok(()) => { + log::info!("renamed {} to {}", from.display(), to.display()); + None + } + Err(err) => { + log::info!( + "failed to rename {} to {}, fallback to recursive move: {}", + from.display(), + to.display(), + err + ); + Some((from, to)) + } } - } - }); - } + }) + .collect::>() + .fold(Vec::new(), |mut pairs, pair| async move { + if let Some(pair) = pair { + pairs.push(pair); + } + pairs + }) + .await + } else { + from_to_pairs_iter.collect() + }; let mut context = Context::new(controller.clone()); @@ -216,7 +224,7 @@ pub async fn sync_to_disk( } })) .buffer_unordered(32) - .collect::>() + .collect::<()>() .await; // Sync directories to disk @@ -226,7 +234,7 @@ pub async fn sync_to_disk( } })) .buffer_unordered(16) - .collect::>() + .collect::<()>() .await; } @@ -762,13 +770,12 @@ impl Operation { OperationError::from_err(e, &controller) })?; - if let Ok(modified) = metadata.modified() { - if let Some(last_modified) = + if let Ok(modified) = metadata.modified() + && let Some(last_modified) = archive::system_time_to_zip_date_time(modified) - { - zip_options = - zip_options.last_modified_time(last_modified); - } + { + zip_options = + zip_options.last_modified_time(last_modified); } #[cfg(unix)] @@ -1113,7 +1120,9 @@ impl Operation { #[cfg(target_os = "macos")] Self::Restore { .. } => { // TODO: add support for macos - return OperationError::from_msg("Restoring from trash is not supported on macos"); + return Err(OperationError::from_msg( + "Restoring from trash is not supported on macos", + )); } #[cfg(not(target_os = "macos"))] Self::Restore { items } => { @@ -1181,8 +1190,10 @@ impl Operation { .map_err(|s| OperationError::from_state(s, &controller))?; let controller_clone = controller.clone(); + let path_clone = path.clone(); compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { let controller = controller_clone; + let path = path_clone; //TODO: what to do on non-Unix systems? #[cfg(unix)] { @@ -1197,7 +1208,10 @@ impl Operation { .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; - Ok(OperationSelection::default()) + Ok(OperationSelection { + ignored: Vec::new(), + selected: vec![path], + }) } }; @@ -1224,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/notifiers.rs b/src/operation/notifiers.rs new file mode 100644 index 0000000..0e27ce9 --- /dev/null +++ b/src/operation/notifiers.rs @@ -0,0 +1,58 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock, Mutex}; +use tokio::sync::Notify; + +/// Monitor files which are being written to. +pub struct FileWritingNotifier { + data: Vec, + notify: Arc, +} + +static ACTIVELY_WRITING: LazyLock> = LazyLock::new(|| { + Mutex::new(FileWritingNotifier { + data: Vec::new(), + notify: Arc::new(Notify::new()), + }) +}); + +/// Append path that is being written to. +pub fn actively_writing_add(path: PathBuf) { + ACTIVELY_WRITING.lock().unwrap().data.push(path); +} + +/// Remove path to file that has finished writing and notify waiters. +pub fn actively_writing_remove(path: &Path) { + let mut guard = ACTIVELY_WRITING.lock().unwrap(); + guard.data.retain(|p| p != path); + guard.notify.notify_waiters(); +} + +/// Wait until the actively-writing queue is empty or a file has been removed. +pub async fn actively_writing_tick() { + let notify = (|| { + let guard = ACTIVELY_WRITING.lock().unwrap(); + + if !guard.data.is_empty() { + return Some(guard.notify.clone()); + } + + None + })(); + + if let Some(notify) = notify { + notify.notified().await + } +} + +/// Check if a file is being written to. Avoid thumbnail generation until after it is finished. +pub fn is_actively_writing_to(path: &Path) -> bool { + ACTIVELY_WRITING + .lock() + .unwrap() + .data + .iter() + .any(|p| p == path) +} 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 a807594..0f09bd6 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -1,15 +1,39 @@ -use compio::BufResult; -use compio::buf::{IntoInner, IoBuf}; -use compio::io::{AsyncReadAt, AsyncWriteAt}; -use std::future::Future; -use std::pin::Pin; -use std::time::Instant; -use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc}; -use walkdir::WalkDir; - -use crate::operation::{OperationError, sync_to_disk}; +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path}; +use crate::operation::{OperationError, sync_to_disk}; +use anyhow::Context as AnyhowContext; +use compio::BufResult; +use compio::buf::{IntoInner, IoBuf}; +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 walkdir::WalkDir; + +#[cfg(feature = "gvfs")] +use gio::prelude::FileExtManual; + +#[derive(thiserror::Error, Debug)] +pub enum GioCopyError { + #[error("controller state")] + Controller(OperationError), + #[cfg(feature = "gvfs")] + #[error("gio copy failed")] + GLib(#[from] glib::Error), +} pub enum Method { Copy, @@ -313,136 +337,28 @@ impl Op { }) } - async fn run( - &mut self, - ctx: &mut Context, - mut progress: Progress, - ) -> Result> { + async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result> { if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) { return Ok(true); } match self.kind { OpKind::Copy => { - // Remove `to` if overwriting and it is an existing file - if self.to.is_file() { - match ctx.replace(self).await? { - ControlFlow::Continue(to) => { - self.to = to; - } - ControlFlow::Break(ret) => { - return Ok(ret); - } - } + crate::operation::actively_writing_add(self.to.clone()); + let result = self.copy(ctx, progress).await; + + if result.is_err() { + _ = compio::fs::remove_file(&self.to).await; } - let (from_file, metadata, mut to_file) = cosmic::iced::futures::try_join!( - async { - compio::fs::OpenOptions::new() - .read(true) - .open(&self.from) - .await - }, - compio::fs::metadata(&self.from), - // This is atomic and ensures `to` is not created by any other process - async { - compio::fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(&self.to) - .await - } - )?; - - progress.total_bytes = Some(metadata.len()); - (ctx.on_progress)(self, &progress); - if let Err(err) = to_file.set_permissions(metadata.permissions()).await { - // This error is not propagated upwards as some filesystems do not support setting permissions - log::warn!( - "failed to set permissions for {}: {}", - self.to.display(), - err - ); - } - - // Prevent spamming the progress callbacks. - let mut last_progress_update = Instant::now(); - // io_uring/IOCP requires transferring ownership of the buffer to the kernel. - let mut buf_in = std::mem::take(&mut ctx.buf); - // Track where the current read/write position is at. - let mut pos = 0; - - loop { - let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await; - - let count = match result { - Ok(0) => { - ctx.buf = buf_out; - break; - } - Ok(count) => count, - Err(why) => { - ctx.buf = buf_out; - return Err(why.into()); - } - }; - - let BufResult(result, buf_out_slice) = - to_file.write_at(buf_out.slice(..count), pos).await; - let buf_out = buf_out_slice.into_inner(); - - if let Err(why) = result { - ctx.buf = buf_out; - return Err(why.into()); - } - - progress.current_bytes += count as u64; - pos += count as u64; - - // Avoid spamming progress messages too early. - let current = Instant::now(); - if current.duration_since(last_progress_update).as_millis() > 49 { - last_progress_update = current; - (ctx.on_progress)(self, &progress); - - // Also check if the progress was cancelled. - if let Err(state) = ctx.controller.check().await { - ctx.buf = buf_out; - return Err(OperationError::from_state(state, &ctx.controller).into()); - } - } - - buf_in = buf_out; - } - - let mut times = fs::FileTimes::new(); - { - use std::os::unix::prelude::MetadataExt; - log::info!("{}", metadata.mtime()); - } - if let Ok(time) = dbg!(metadata.modified()) { - times = times.set_modified(time); - } - if let Ok(time) = dbg!(metadata.accessed()) { - times = times.set_accessed(time); - } - //TODO: upstream set_times implementation to compio? - { - use compio::driver::{ToSharedFd, op::AsyncifyFd}; - let op = - AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| { - BufResult(file.set_times(times).map(|_| 0), ()) - }); - match compio::runtime::submit(op).await.0.map(|_| ()) { - Ok(()) => { - log::info!("set times for {} to {:?}", self.to.display(), times); - } - Err(err) => { - log::warn!("failed to set times for {}: {}", self.to.display(), err); - } - } - } + crate::operation::actively_writing_remove(&self.to); + return result; } OpKind::Move { cross_device_copy } => { + // Do not clean up if cross_device_copy is set + if cross_device_copy { + self.skipped.cleanup.set(true); + } + // Remove `to` if overwriting and it is an existing file if self.to.is_file() { match ctx.replace(self).await? { @@ -520,4 +436,268 @@ impl Op { } Ok(true) } + + async fn copy( + &mut self, + ctx: &mut Context, + mut progress: Progress, + ) -> Result> { + // Remove `to` if overwriting and it is an existing file + if self.to.is_file() { + match ctx.replace(self).await? { + ControlFlow::Continue(to) => { + self.to = to; + } + ControlFlow::Break(ret) => { + return Ok(ret); + } + } + } + + let (from_file, metadata, to_file) = cosmic::iced::futures::join!( + async { + compio::fs::OpenOptions::new() + .read(true) + .open(&self.from) + .await + .with_context(|| format!("failed to open {} for reading", self.from.display(),)) + }, + async { compio::fs::metadata(&self.from).await.ok() }, + // This is atomic and ensures `to` is not created by any other process + async { + compio::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&self.to) + .await + .with_context(|| format!("failed to open {} for writing", self.to.display())) + } + ); + + let from_file = from_file?; + let mut to_file = to_file?; + progress.total_bytes = metadata.as_ref().map(|m| m.len()); + (ctx.on_progress)(self, &progress); + + 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(),); + } + } + + // Prevent spamming the progress callbacks. + let mut last_progress_update = Instant::now(); + // io_uring/IOCP requires transferring ownership of the buffer to the kernel. + let mut buf_in = std::mem::take(&mut ctx.buf); + // Track where the current read/write position is at. + let mut pos = 0; + + loop { + let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await; + + let count = match result { + Ok(0) => { + buf_in = buf_out; + break; + } + Ok(count) => count, + Err(why) => { + ctx.buf = buf_out; + tracing::error!("failed to read: {:?}", why); + _ = futures::future::join(from_file.close(), to_file.close()).await; + return Err(why).context("failed to read")?; + } + }; + + let BufResult(result, buf_out_slice) = + to_file.write_at(buf_out.slice(..count), pos).await; + let buf_out = buf_out_slice.into_inner(); + + if let Err(why) = result { + #[cfg(feature = "gvfs")] + if let std::io::ErrorKind::Unsupported = why.kind() { + ctx.buf = buf_out; + _ = futures::future::join(from_file.close(), to_file.close()).await; + return self + .gio_file_copy(ctx, progress) + .await + .map(|_| true) + .map_err(Into::into); + } + + tracing::error!("failed to write: {:?}", why); + ctx.buf = buf_out; + _ = futures::future::join(from_file.close(), to_file.close()).await; + return Err(why).context("failed to write")?; + } + + progress.current_bytes += count as u64; + pos += count as u64; + + // Avoid spamming progress messages too early. + let current = Instant::now(); + if current.duration_since(last_progress_update).as_millis() > 49 { + last_progress_update = current; + (ctx.on_progress)(self, &progress); + + // Also check if the progress was cancelled. + if let Err(state) = ctx.controller.check().await { + ctx.buf = buf_out; + tracing::warn!( + "operation to copy from {:?} to {:?} cancelled", + self.from, + self.to + ); + _ = futures::future::join(from_file.close(), to_file.close()).await; + return Err(OperationError::from_state(state, &ctx.controller).into()); + } + } + + buf_in = buf_out; + } + + ctx.buf = buf_in; + + if let Some(metadata) = metadata.as_ref() { + let mut times = fs::FileTimes::new(); + if let Ok(time) = metadata.modified() { + times = times.set_modified(time); + } + if let Ok(time) = metadata.accessed() { + times = times.set_accessed(time); + } + //TODO: upstream set_times implementation to compio? + let op = AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| { + BufResult(file.set_times(times).map(|_| 0), ()) + }); + match compio::runtime::submit(op).await.0.map(|_| ()) { + Ok(()) => { + tracing::info!("set times for {} to {:?}", self.to.display(), times); + } + Err(why) => { + if !matches!(why.kind(), std::io::ErrorKind::Unsupported) { + tracing::error!(?why, "failed to set times for {}", self.to.display()); + } + } + } + } + + _ = to_file.close().await; + + Ok(true) + } + + /// Fallback mechanism in the event that unsupported I/O error errors occur. + /// Fixes unsupported errors when copying large files over MTP. + /// TODO: Find what Gio.File does to work around this. + #[cfg(feature = "gvfs")] + async fn gio_file_copy( + &self, + ctx: &mut Context, + mut progress: Progress, + ) -> Result<(), GioCopyError> { + _ = compio::fs::remove_file(&self.to).await; + + let from = gio::File::for_path(&self.from); + let to = gio::File::for_path(&self.to); + let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel(); + let (tx, rx) = tokio::sync::oneshot::channel(); + let (pause_tx, mut pause_rx) = tokio::sync::watch::channel(false); + + let task = compio::runtime::spawn_blocking(move || { + let glib_context = glib::MainContext::new(); + let glib_loop = glib::MainLoop::new(Some(&glib_context), false); + glib_context.with_thread_default(move || { + let glib_loop2 = glib_loop.clone(); + glib::MainContext::ref_thread_default().spawn_local(async move { + // Create a future for copying the file with `gio::File`. This also creates a progress stream. + let (gio_copy_fut, mut progress_stream) = from.copy_future( + &to, + gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::ALL_METADATA, + glib::Priority::LOW, + ); + + let mut copy_fut = gio_copy_fut + .map(|result| result.map_err(GioCopyError::GLib)) + .fuse(); + + let progress_fut = std::pin::pin!(async { + while let Some((current_bytes, _)) = progress_stream.next().await { + _ = progress_tx.send(current_bytes); + } + + drop(progress_tx); + futures::future::pending::<()>().await; + }); + + let mut progress_fut = progress_fut.fuse(); + let mut pause_rx2 = pause_rx.clone(); + + loop { + let until_paused = std::pin::pin!(pause_rx.wait_for(|paused| *paused)); + futures::select! { + _ = &mut progress_fut => {}, + + result = &mut copy_fut => { + _ = tx.send(result.map(|_| ())); + glib_loop2.quit(); + return; + } + + _ = until_paused.fuse() => { + _ = pause_rx2.wait_for(|paused| !*paused).await; + } + } + } + }); + + glib_loop.run(); + }) + }); + + let mut last_progress_update = Instant::now(); + let mut task = task.fuse(); + let mut rx = rx.fuse(); + + loop { + let until_paused = std::pin::pin!(ctx.controller.until_paused()); + futures::select! { + value = progress_rx.recv().fuse() => { + if let Some(current_bytes) = value { + progress.current_bytes = current_bytes as u64; + let current = Instant::now(); + if current.duration_since(last_progress_update).as_millis() > 49 { + last_progress_update = current; + (ctx.on_progress)(self, &progress); + // Also check if the progress was cancelled. + if let Err(state) = ctx.controller.check().await { + tracing::warn!( + "operation to copy from {:?} to {:?} cancelled", + self.from, + self.to + ); + return Err::<(), GioCopyError>(GioCopyError::Controller( + OperationError::from_state(state, &ctx.controller), + )); + } + } + } + } + + result = rx => return result.unwrap(), + + _ = task => (), + + _ = until_paused.fuse() => { + // Pauses an active copy while the controller state is paused. + _ = pause_tx.send(true); + ctx.controller.until_unpaused().await; + _ = pause_tx.send(false); + } + } + } + } } diff --git a/src/tab.rs b/src/tab.rs index d36aff0..d7339f8 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1,91 +1,72 @@ -use chrono::{Datelike, Timelike, Utc}; -use cosmic::{ - Apply, Element, cosmic_theme, - desktop::fde::{DesktopEntry, get_languages_from_env}, - font, - iced::{ - Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector, - advanced::{ - graphics, - text::{self, Paragraph}, - }, - alignment::{Horizontal, Vertical}, - clipboard::dnd::DndAction, - event, - futures::{self, SinkExt}, - keyboard::Modifiers, - padding, stream, - widget::{ - rule, - scrollable::{self, AbsoluteOffset, Viewport}, - stack, - }, - window, - }, - iced_core::{mouse::ScrollDelta, widget::tree}, - theme, - widget::{ - self, DndDestination, DndSource, Id, RcElementWrapper, Space, Widget, - menu::{action::MenuAction, key_bind::KeyBind}, - space, - }, +#[cfg(feature = "desktop")] +use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env}; +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::{Date, DateTime, Time}, - options::TimePrecision, - }, - locale::preferences::extensions::unicode::keywords::HourCycle, -}; -use image::{DynamicImage, ImageDecoder, ImageReader}; -use jxl_oxide::integration::JxlDecoder; +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 regex::Regex; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use std::{ - borrow::Cow, - cell::Cell, - cmp::{Ordering, Reverse}, - collections::{BTreeMap, HashMap}, - error::Error, - fmt::{self, Display}, - fs::{self, File, Metadata}, - hash::Hash, - io::{BufRead, BufReader}, - os::unix::fs::MetadataExt, - path::{self, Path, PathBuf}, - sync::{Arc, LazyLock, RwLock, atomic}, - time::{Duration, Instant, SystemTime}, -}; +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::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::{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, +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 uzers::{get_group_by_gid, get_user_by_uid}; +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() @@ -444,25 +451,10 @@ impl<'a> FormatTime<'a> { impl Display for FormatTime<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let datetime = chrono::DateTime::::from(self.time); - let now = chrono::Local::now(); - let icu_datetime = DateTime { - date: Date::try_new_gregorian( - datetime.year(), - datetime.month() as u8, - datetime.day() as u8, - ) - .unwrap(), - time: Time::try_new( - datetime.hour() as u8, - datetime.minute() as u8, - datetime.second() as u8, - 0, - ) - .unwrap(), - }; - - if datetime.date_naive() == now.date_naive() { + let zoned = jiff::Zoned::try_from(self.time).unwrap(); + let now = jiff::Zoned::now(); + let icu_datetime = DateTime::convert_from(zoned.datetime()); + if zoned.date() == now.date() { f.write_str(fl!("today").as_str())?; f.write_str(", ")?; self.time_formatter.format(&icu_datetime).fmt(f) @@ -572,6 +564,12 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind { FsKind::Local } +#[cfg(not(feature = "desktop"))] +fn get_desktop_file_display_name(_path: &Path) -> Option { + None +} + +#[cfg(feature = "desktop")] fn get_desktop_file_display_name(path: &Path) -> Option { let locales = get_languages_from_env(); let entry = match DesktopEntry::from_path(path, Some(&locales)) { @@ -585,6 +583,12 @@ fn get_desktop_file_display_name(path: &Path) -> Option { entry.name(&locales).map(|s| s.into_owned()) } +#[cfg(not(feature = "desktop"))] +fn get_desktop_file_icon(_path: &Path) -> Option { + None +} + +#[cfg(feature = "desktop")] fn get_desktop_file_icon(path: &Path) -> Option { let entry = match DesktopEntry::from_path::<&str>(path, None) { Ok(ok) => ok, @@ -604,10 +608,14 @@ 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() } } +#[cfg(feature = "desktop")] pub fn parse_desktop_file(path: &Path) -> (Option, Option) { let locales = get_languages_from_env(); let entry = match DesktopEntry::from_path(path, Some(&locales)) { @@ -667,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" { @@ -686,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); @@ -723,6 +724,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS children_opt, }, hidden, + image_dimensions: (!remote && mime.type_() == mime::IMAGE) + .then(|| image::image_dimensions(&path).ok()) + .flatten(), location_opt: Some(Location::Path(path)), mime, icon_handle_grid, @@ -758,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); @@ -806,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; @@ -822,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); @@ -858,6 +860,7 @@ pub fn item_from_entry( }, hidden, location_opt: Some(Location::Path(path)), + image_dimensions: None, mime, icon_handle_grid, icon_handle_list, @@ -911,6 +914,9 @@ pub fn item_from_trash_entry( metadata: ItemMetadata::Trash { metadata, entry }, hidden: false, location_opt: None, + image_dimensions: (mime.type_() == mime::IMAGE) + .then(|| image::image_dimensions(&original_path).ok()) + .flatten(), mime, icon_handle_grid, icon_handle_list, @@ -953,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")] { @@ -1176,135 +1185,7 @@ pub fn scan_search bool + Sync>( } } SearchLocation::Trash => { - trash_helpers::scan_search_trash(callback, ®ex); - } - } -} - -// This config statement is from trash::os_limited, inverted -#[cfg(not(any( - target_os = "windows", - all( - unix, - not(target_os = "macos"), - not(target_os = "ios"), - not(target_os = "android") - ) -)))] -mod trash_helpers { - use super::*; - - pub fn trash_entries() -> usize { - 0 - } - - pub fn trash_icon(icon_size: u16) -> widget::icon::Handle { - widget::icon::from_name("user-trash") - .size(icon_size) - .handle() - } - - pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle { - widget::icon::from_name("user-trash-symbolic") - .size(icon_size) - .handle() - } - - pub fn scan_trash(_sizes: IconSizes) -> Vec { - log::warn!("viewing trash not supported on this platform"); - Vec::new() - } - - pub fn scan_search_trash bool + Sync>(callback: F, regex: &Regex) {} -} - -// This config statement is from trash::os_limited -#[cfg(any( - target_os = "windows", - all( - unix, - not(target_os = "macos"), - not(target_os = "ios"), - not(target_os = "android") - ) -))] -pub mod trash_helpers { - use super::*; - - pub fn trash_entries() -> usize { - match trash::os_limited::list() { - Ok(entries) => entries.len(), - Err(_err) => 0, - } - } - - pub fn trash_icon(icon_size: u16) -> widget::icon::Handle { - widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) { - "user-trash" - } else { - "user-trash-full" - }) - .size(icon_size) - .handle() - } - - pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle { - widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) { - "user-trash-symbolic" - } else { - "user-trash-full-symbolic" - }) - .size(icon_size) - .handle() - } - - pub fn scan_trash(sizes: IconSizes) -> Vec { - let entries = match trash::os_limited::list() { - Ok(entry) => entry, - Err(err) => { - log::warn!("failed to read trash items: {err}"); - return Vec::new(); - } - }; - let mut items: Vec<_> = entries - .into_iter() - .filter_map(|entry| { - let metadata = trash::os_limited::metadata(&entry) - .inspect_err(|err| { - log::warn!("failed to get metadata for trash item {entry:?}: {err}") - }) - .ok()?; - Some(item_from_trash_entry(entry, metadata, sizes)) - }) - .collect(); - items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - _ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name), - }); - items - } - - pub fn scan_search_trash bool + Sync>(callback: F, regex: &Regex) { - let entries = match trash::os_limited::list() { - Ok(entries) => entries, - Err(err) => { - log::warn!("failed to read trash items: {err}"); - return; - } - }; - - for entry in entries { - if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| { - log::warn!("failed to get metadata for trash item {entry:?}: {err}") - }) { - let name = entry.name.to_string_lossy(); - if regex.is_match(&name) { - if !callback(SearchItem::Trash(entry, metadata)) { - break; - } - } - } + Trash::scan_search(callback, ®ex); } } } @@ -1340,8 +1221,8 @@ pub fn scan_recents(sizes: IconSizes) -> Vec { .into_iter() .filter_map(|bookmark| { let path = uri_to_path(bookmark.href)?; - let last_edit = bookmark.modified.parse::>().ok()?; - let last_visit = bookmark.visited.parse::>().ok()?; + let last_edit = bookmark.modified.parse::().ok()?; + let last_visit = bookmark.visited.parse::().ok()?; if path.exists() { let file_name = path.file_name()?; @@ -1442,15 +1323,15 @@ pub fn scan_desktop( let display_name = Item::display_name(&name); let metadata = ItemMetadata::SimpleDir { - entries: trash_helpers::trash_entries() as u64, + entries: Trash::entries() as u64, }; let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { ( "inode/directory".parse().unwrap(), - trash_helpers::trash_icon(sizes.grid()), - trash_helpers::trash_icon(sizes.list()), - trash_helpers::trash_icon(sizes.list_condensed()), + Trash::icon(sizes.grid()), + Trash::icon(sizes.list()), + Trash::icon(sizes.list_condensed()), ) }; @@ -1461,6 +1342,7 @@ pub fn scan_desktop( metadata, hidden: false, location_opt: Some(Location::Trash), + image_dimensions: None, mime, icon_handle_grid, icon_handle_list, @@ -1680,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) @@ -1690,13 +1572,13 @@ impl Location { // Search is done incrementally Vec::new() } - Self::Trash => trash_helpers::scan_trash(sizes), + Self::Trash => Trash::scan(sizes), Self::Recents => scan_recents(sizes), Self::Network(uri, _, _) => scan_network(uri, sizes), }; 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 @@ -1805,6 +1687,7 @@ pub enum Command { ContextMenu(Option, Option), Delete(Vec), DropFiles(PathBuf, ClipboardPaste), + ClearRecents, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(cosmic::desktop::DesktopEntryData, usize), @@ -1814,8 +1697,10 @@ pub enum Command { OpenInNewWindow(PathBuf), OpenTrash, Preview(PreviewKind), + RunContextAction(usize), SetOpenWith(Mime, String), SetPermissions(PathBuf, u32), + SetMultiplePermissions(Vec<(PathBuf, u32)>), SetSort(String, HeadingOptions, bool), WindowDrag, WindowToggleMaximize, @@ -1842,6 +1727,7 @@ pub enum Message { EditLocationSubmit, EditLocationTab, OpenInNewTab(PathBuf), + ClearRecents, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(Option, usize), @@ -1871,7 +1757,9 @@ pub enum Message { SelectFirst, SelectLast, SetOpenWith(Mime, String), + RunContextAction(usize), SetPermissions(PathBuf, u32), + ShiftPermissions(Option<(PathBuf, u32)>, u32, u32), SetSort(HeadingOptions, bool), TabComplete(PathBuf, Vec<(String, PathBuf)>), Thumbnail(PathBuf, ItemThumbnail), @@ -2113,55 +2001,26 @@ impl ItemThumbnail { } tried_supported_file = true; - let dyn_img: Option = match mime.subtype().as_str() { - "jxl" => match File::open(path) { - Ok(file) => match JxlDecoder::new(file) { - Ok(mut decoder) => { - let mut limits = image::Limits::default(); - let max_ram = max_mem * 1000 * 1000 / jobs as u64; - limits.max_alloc = Some(max_ram); - let _ = decoder.set_limits(limits); - match image::DynamicImage::from_decoder(decoder) { - Ok(img) => Some(img), - Err(err) => { - log::warn!("failed to decode jxl {}: {}", path.display(), err); - None - } - } - } + let dyn_img = match image::ImageReader::open(path) + .and_then(image::ImageReader::with_guessed_format) + { + Ok(mut reader) => { + let mut limits = image::Limits::default(); + let max_ram = max_mem * 1000 * 1000 / jobs as u64; + limits.max_alloc = Some(max_ram); + reader.limits(limits); + match reader.decode() { + Ok(reader) => Some(reader), Err(err) => { - log::warn!("failed to create jxl decoder {}: {}", path.display(), err); - None - } - }, - Err(err) => { - log::warn!("failed to open path {}: {}", path.display(), err); - None - } - }, - _ => { - match image::ImageReader::open(path) - .and_then(image::ImageReader::with_guessed_format) - { - Ok(mut reader) => { - let mut limits = image::Limits::default(); - let max_ram = max_mem * 1000 * 1000 / jobs as u64; - limits.max_alloc = Some(max_ram); - reader.limits(limits); - match reader.decode() { - Ok(reader) => Some(reader), - Err(err) => { - log::warn!("failed to decode {}: {}", path.display(), err); - None - } - } - } - Err(err) => { - log::warn!("failed to read {}: {}", path.display(), err); + log::warn!("failed to decode {}: {}", path.display(), err); None } } } + Err(err) => { + log::warn!("failed to read {}: {}", path.display(), err); + None + } }; if let Some(dyn_img) = dyn_img { @@ -2231,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 @@ -2361,6 +2240,7 @@ pub struct Item { pub hidden: bool, pub location_opt: Option, pub mime: Mime, + pub image_dimensions: Option<(u32, u32)>, pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle, @@ -2424,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) @@ -2485,9 +2365,9 @@ impl Item { space_xxxs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); - let mut column = widget::column().spacing(space_m); + let mut column = widget::column::with_capacity(4).spacing(space_m); column = column.push( widget::container(self.preview()) @@ -2495,7 +2375,7 @@ impl Item { .max_height(THUMBNAIL_SIZE as f32), ); - let mut details = widget::column().spacing(space_xxxs); + let mut details = widget::column::with_capacity(8).spacing(space_xxxs); details = details.push(widget::text::heading(self.name.clone())); details = details.push(widget::text::body(fl!( "type", @@ -2575,7 +2455,7 @@ impl Item { let mode = metadata.mode(); - let user_name = get_user_by_uid(metadata.uid()) + let user_name = uzers::get_user_by_uid(metadata.uid()) .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(); let user_path = path.clone(); @@ -2598,7 +2478,7 @@ impl Item { )), ); - let group_name = get_group_by_gid(metadata.gid()) + let group_name = uzers::get_group_by_gid(metadata.gid()) .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(); let group_path = path.clone(); @@ -2645,13 +2525,12 @@ impl Item { } column = column.push(details); - if let Some(path) = self.path_opt() { - if self.selected { - column = column.push( - widget::button::standard(fl!("open")) - .on_press(Message::Open(Some(path.clone()))), - ); - } + if let Some(path) = self.path_opt() + && self.selected + { + column = column.push( + widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))), + ); } if !settings.is_empty() { @@ -2664,12 +2543,12 @@ 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().spacing(space_xxxs); + let mut row = widget::row::with_capacity(2).spacing(space_xxxs); row = row.push(self.preview()); - let mut column = widget::column().spacing(space_xxxs); + let mut column = widget::column::with_capacity(3).spacing(space_xxxs); column = column.push(widget::text::heading(heading)); //TODO: translate! @@ -2804,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, @@ -3060,7 +2939,7 @@ impl Tab { /// Returns true if an item was selected. pub fn select_by_prefix(&mut self, prefix: &str) -> bool { let prefix_lower = prefix.to_lowercase(); - self.select_focus = None; + let focus = self.select_focus.take(); if let Some(ref mut items) = self.items_opt { // First, deselect all items @@ -3068,18 +2947,104 @@ impl Tab { item.selected = false; } - // Find first matching item - for (i, item) in items.iter_mut().enumerate() { - if item.name.to_lowercase().starts_with(&prefix_lower) { - item.selected = true; - self.select_focus = Some(i); - return true; - } + // Determine the start index of the search. When the index is before the currently focused item, it will be + // considered first, otherwise last. Consider the focused item last when only a single character has been + // typed, so we eagerly switch focus on the first character and stay on the same item as long as the prefix + // matches. + let single_char = prefix_lower.chars().count() == 1; + let start = if single_char { + Self::index_after_focus(focus, self.sort_direction) + } else { + Self::index_before_focus(focus, self.sort_direction) + }; + self.select_focus = Self::select_first_prefix_from_index( + &prefix_lower, + items, + start, + self.sort_direction, + ); + + if self.select_focus.is_some() || single_char { + return self.select_focus.is_some(); } + + let mut chars = prefix_lower.chars(); + let Some(first) = chars.next() else { + log::error!("search term is empty"); + return self.select_focus.is_some(); + }; + + // Check if all entered characters are the same + if !chars.all(|c| c == first) { + return self.select_focus.is_some(); + } + + // Search for a single character when all entered characters are the same. + // This allows cycling through items starting with the same character by repeatedly pressing a key. + let start = Self::index_after_focus(focus, self.sort_direction); + self.select_focus = Self::select_first_prefix_from_index( + &first.to_string(), + items, + start, + self.sort_direction, + ); + + return self.select_focus.is_some(); } false } + fn index_before_focus(current_focus: Option, forward: bool) -> usize { + current_focus.map_or(0, |i| if forward { i } else { i + 1 }) + } + + fn index_after_focus(current_focus: Option, forward: bool) -> usize { + current_focus.map_or(0, |i| if forward { i + 1 } else { i }) + } + + fn select_first_prefix_from_index( + prefix_lower: &str, + items: &mut [Item], + start: usize, + forward: bool, + ) -> Option { + // Order the search item so they begin at `start`. + let Some((until, after)) = items.split_at_mut_checked(start) else { + log::error!( + "invalid start index {start} for items of length {}", + items.len() + ); + return None; + }; + let search_items = after + .iter_mut() + .enumerate() + .map(|(i, item)| (i + start, item)) + .chain(until.iter_mut().enumerate()); + + if forward { + Self::select_first_prefix_match(prefix_lower, search_items) + } else { + Self::select_first_prefix_match(prefix_lower, search_items.rev()) + } + } + + /// Selects the first item in the given iterator whose name starts with the given prefix. + /// + /// The `prefix` must be lowercase. + fn select_first_prefix_match<'a>( + prefix: &str, + items: impl Iterator, + ) -> Option { + for (i, item) in items { + if item.name.to_lowercase().starts_with(prefix) { + item.selected = true; + return Some(i); + } + } + None + } + pub fn select_paths(&mut self, paths: Vec) { self.select_focus = None; if let Some(ref mut items) = self.items_opt { @@ -3614,6 +3579,11 @@ impl Tab { commands.push(Command::Action(action)); } + Message::RunContextAction(action) => { + self.context_menu = None; + + commands.push(Command::RunContextAction(action)); + } Message::ContextMenu(point_opt, _) => { self.edit_location = None; self.context_menu = point_opt; @@ -3663,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) => { @@ -3753,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); } @@ -4385,6 +4358,28 @@ impl Tab { Message::SetPermissions(path, mode) => { commands.push(Command::SetPermissions(path, mode)); } + 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), + )), + // Shift permissions on all selected items + None => { + let mut permissions = Vec::new(); + for item in self.items_opt().map_or(Vec::new(), |items| { + items.iter().filter(|item| item.selected).collect() + }) { + #[cfg(unix)] + if let (Some(path), Some(mode)) = ( + item.path_opt(), + item.file_metadata().map(|metadata| metadata.mode()), + ) { + permissions.push((path.clone(), set_mode_part(mode, shift, bits))); + } + } + commands.push(Command::SetMultiplePermissions(permissions)); + } + }, Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; @@ -4795,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; @@ -4851,14 +4846,14 @@ impl Tab { let content: cosmic::Element<'_, Message> = if let Some(error_msg) = error_msg_opt { - widget::column() + widget::column::with_capacity(2) .push(widget::image(image_handle)) .push(widget::text(format!("⚠ {}", error_msg)).size(13)) .padding(space_xs) .align_x(cosmic::iced::Alignment::Center) .into() } else if is_loading { - widget::column() + widget::column::with_capacity(2) .push(widget::image(image_handle)) .push(widget::text("Loading higher resolution...").size(14)) .padding(space_xs) @@ -4993,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)); @@ -5328,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 { @@ -5346,7 +5341,7 @@ impl Tab { }) .into(), ]), - Mode::Desktop => widget::column(), + Mode::Desktop => widget::column::with_capacity(0), } .align_x(Alignment::Center) .spacing(space_xxs), @@ -5368,7 +5363,7 @@ impl Tab { space_xxs, space_xxxs, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let TabConfig { show_hidden, @@ -5478,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( @@ -5554,7 +5548,7 @@ impl Tab { // Add a spacer if the row is empty, so scroll works if grid_elements[row].is_empty() { grid_elements[row].push(Element::from( - widget::column() + widget::column::with_capacity(0) .width(Length::Fill) .height(Length::Fixed(item_height as f32)), )); @@ -5714,7 +5708,7 @@ impl Tab { ) { let cosmic_theme::Spacing { space_s, space_xxs, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); let TabConfig { show_hidden, @@ -5961,12 +5955,13 @@ impl Tab { }; let button_row = button(row.into()); - let button_row: Element<_> = - if item.metadata.is_dir() && item.location_opt.is_some() { - self.dnd_dest(item.location_opt.as_ref().unwrap(), button_row) - } else { - button_row.into() - }; + let button_row: Element<_> = if item.metadata.is_dir() + && let Some(location) = item.location_opt.as_ref() + { + self.dnd_dest(location, button_row) + } else { + button_row.into() + }; if item.selected || !drag_items.is_empty() { let dnd_row = if !item.selected { @@ -6049,7 +6044,7 @@ impl Tab { button_row } else { - widget::column() + widget::column::with_capacity(0) .width(Length::Fill) .height(Length::Fixed(f32::from(row_height))) .into() @@ -6105,6 +6100,7 @@ impl Tab { modifiers: &'a Modifiers, size: Size, clipboard_paste_available: bool, + context_actions: &'a [ContextActionPreset], ) -> Element<'a, Message> { // Update cached size self.size_opt.set(Some(size)); @@ -6114,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 @@ -6192,8 +6188,13 @@ impl Tab { if let Some(point) = self.context_menu && (!cfg!(feature = "wayland") || !crate::is_wayland()) { - let context_menu = - menu::context_menu(self, key_binds, modifiers, clipboard_paste_available); + let context_menu = menu::context_menu( + self, + key_binds, + modifiers, + clipboard_paste_available, + context_actions, + ); popover = popover .popup(context_menu) .position(widget::popover::Position::Point(point)); @@ -6239,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([ @@ -6266,7 +6285,7 @@ impl Tab { tab_view = tab_view.style(|t| { let mut a = widget::container::Style::default(); let c = t.cosmic(); - a.border = cosmic::iced_core::Border { + a.border = cosmic::iced::core::Border { color: (c.accent_color()).into(), width: 1., radius: c.radius_0().into(), @@ -6297,14 +6316,17 @@ impl Tab { dnd_dest.into() } - pub fn multi_preview_view<'a>(&'a self) -> Element<'a, Message> { + pub fn multi_preview_view<'a>( + &'a self, + mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>, + ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, space_m, .. - } = theme::active().cosmic().spacing; + } = theme::spacing(); - let mut column = widget::column().spacing(space_m); + let mut column = widget::column::with_capacity(4).spacing(space_m); let handle = widget::icon::from_name("text-x-generic") .size(IconSizes::default().grid()) @@ -6333,8 +6355,7 @@ impl Tab { if item.selected { item.location_opt .as_ref() - .map(Location::path_opt) - .flatten() + .and_then(Location::path_opt) .is_some() } else { false @@ -6343,7 +6364,7 @@ impl Tab { .collect() }); - let mut details = widget::column().spacing(space_xxxs); + let mut details = widget::column::with_capacity(3).spacing(space_xxxs); details = details.push(widget::text::body(fl!( "items", items = selected_items.len() @@ -6351,6 +6372,11 @@ impl Tab { let mut total_size: u64 = 0; let mut mime_type_counts: BTreeMap = BTreeMap::new(); + let mut user_name: BTreeSet = BTreeSet::new(); + let mut mode_user: BTreeSet = BTreeSet::new(); + let mut group_name: BTreeSet = BTreeSet::new(); + let mut mode_group: BTreeSet = BTreeSet::new(); + let mut mode_other: BTreeSet = BTreeSet::new(); let mut calculating_dir_size = false; let mut dir_size_error: Option = None; @@ -6374,6 +6400,23 @@ impl Tab { } else { total_size = total_size.saturating_add(metadata.len()); } + #[cfg(unix)] + { + let mode = metadata.mode(); + user_name.insert( + uzers::get_user_by_uid(metadata.uid()) + .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(), + ); + mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER)); + group_name.insert( + uzers::get_group_by_gid(metadata.gid()) + .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(), + ); + mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP)); + mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER)); + } } } let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect(); @@ -6412,6 +6455,122 @@ impl Tab { column = column.push(widget::button::standard(fl!("open")).on_press(Message::Open(None))); + let mut settings = Vec::new(); + // Only allow modifying open-with if all mime types are the same + if mime_types.len() == 1 + && let Some(mime) = mime_types + .first() + .and_then(|(mime, _)| mime.parse::().ok()) + && 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()) + }), + ), + ); + } + } + + #[cfg(unix)] + { + // Only return mode part if it's the only one + fn selected_mode_part(mut modes: BTreeSet) -> Option { + match (modes.pop_first(), modes.pop_first()) { + (Some(mode), None) => Some(mode.try_into().unwrap()), + _ => None, + } + } + + // Convert a limited number of values from a set into a comma separated list + fn join_set(set: BTreeSet) -> String { + let limit = 5; + let mut title = set.into_iter().collect::>(); + if title.len() > limit { + title.truncate(limit); + title.push("...".to_string()); + } + title.join(", ") + } + + let mode_part_user = selected_mode_part(mode_user); + settings.push( + widget::settings::item::builder(join_set(user_name)) + .description(fl!("owner")) + .control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_user, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_USER, + selected.try_into().unwrap(), + ) + }, + ) + .placeholder(fl!("mixed")), + ), + ); + + let mode_part_group = selected_mode_part(mode_group); + settings.push( + widget::settings::item::builder(join_set(group_name)) + .description(fl!("group")) + .control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_group, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_GROUP, + selected.try_into().unwrap(), + ) + }, + ) + .placeholder(fl!("mixed")), + ), + ); + + let mode_part_other = selected_mode_part(mode_other); + settings.push( + widget::settings::item::builder(fl!("other")).control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_other, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_OTHER, + selected.try_into().unwrap(), + ) + }, + ) + .placeholder(fl!("mixed")), + ), + ); + } + + if !settings.is_empty() { + let mut section = widget::settings::section(); + section = section.extend(settings); + column = column.push(section); + } + column.into() } pub fn view<'a>( @@ -6419,10 +6578,17 @@ impl Tab { key_binds: &'a HashMap, modifiers: &'a Modifiers, clipboard_paste_available: bool, + context_actions: &'a [ContextActionPreset], ) -> Element<'a, Message> { widget::responsive(move |size| { widget::id_container( - self.view_responsive(key_binds, modifiers, size, clipboard_paste_available), + self.view_responsive( + key_binds, + modifiers, + size, + clipboard_paste_available, + context_actions, + ), Id::new(format!( "tab-{}-{}", self.scrollable_id, self.location_title @@ -6487,13 +6653,13 @@ impl Tab { // Determine effective memory budget based on image size let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE { - match image::image_dimensions(&path) { - Ok((width, height)) => { + match item.image_dimensions { + Some((width, height)) => { let (_use_dedicated, eff_mb, eff_jobs) = should_use_dedicated_worker(width, height, max_mb, max_jobs); (eff_mb, eff_jobs) } - Err(_) => (max_mb, max_jobs), + None => (max_mb, max_jobs), } } else { (max_mb, max_jobs) @@ -6536,6 +6702,10 @@ impl Tab { stream::channel( 1, move |mut output: futures::channel::mpsc::Sender<_>| async move { + while crate::operation::is_actively_writing_to(&path) { + crate::operation::actively_writing_tick().await; + } + let message = { let path = path.clone(); @@ -6720,9 +6890,8 @@ impl Tab { .await .unwrap(); - let output = Arc::new(tokio::sync::Mutex::new(output)); + let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true); { - let output = output.clone(); tokio::task::spawn_blocking(move || { scan_search( &search_location, @@ -6732,17 +6901,15 @@ impl Tab { // Don't send if the result is too old if let Some(last_modified) = *last_modified_opt.read().unwrap() - { - if let SearchItem::Path(_, _, ref metadata) = + && let SearchItem::Path(_, _, ref metadata) = search_item - { - if let Ok(modified) = metadata.modified() { - if modified < last_modified { - return true; - } - } else { + { + if let Ok(modified) = metadata.modified() { + if modified < last_modified { return true; } + } else { + return true; } } @@ -6752,14 +6919,7 @@ impl Tab { true } else { // Wake up update method - futures::executor::block_on(async { - output - .lock() - .await - .send(Message::SearchReady(false)) - .await - }) - .is_ok() + watch_tx.send(false).is_ok() } } Err(_) => false, @@ -6772,13 +6932,16 @@ impl Tab { search_location, start.elapsed(), ); - }) - .await - .unwrap(); + }); + } + + while watch_rx.changed().await.is_ok() { + let is_ready = *watch_rx.borrow_and_update(); + let _ = output.send(Message::SearchReady(is_ready)).await; } // Send final ready - let _ = output.lock().await.send(Message::SearchReady(true)).await; + let _ = output.send(Message::SearchReady(true)).await; std::future::pending().await }, @@ -6877,7 +7040,7 @@ pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> fn text_editor_class( theme: &cosmic::Theme, status: cosmic::widget::text_editor::Status, -) -> cosmic::iced_widget::text_editor::Style { +) -> cosmic::iced::widget::text_editor::Style { let cosmic = theme.cosmic(); let container = theme.current_container(); @@ -6890,9 +7053,9 @@ fn text_editor_class( let placeholder = placeholder.into(); match status { - cosmic::iced_widget::text_editor::Status::Active - | cosmic::iced_widget::text_editor::Status::Disabled => { - cosmic::iced_widget::text_editor::Style { + cosmic::iced::widget::text_editor::Status::Active + | cosmic::iced::widget::text_editor::Status::Disabled => { + cosmic::iced::widget::text_editor::Style { background: background.into(), border: cosmic::iced::Border { radius: cosmic.corner_radii.radius_m.into(), @@ -6904,9 +7067,9 @@ fn text_editor_class( selection, } } - cosmic::iced_widget::text_editor::Status::Hovered - | cosmic::iced_widget::text_editor::Status::Focused { .. } => { - cosmic::iced_widget::text_editor::Style { + cosmic::iced::widget::text_editor::Status::Hovered + | cosmic::iced::widget::text_editor::Status::Focused { .. } => { + cosmic::iced::widget::text_editor::Style { background: background.into(), border: cosmic::iced::Border { radius: cosmic.corner_radii.radius_m.into(), @@ -6923,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( @@ -7350,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 719bfc6..e42d343 100644 --- a/src/thumbnail_cacher.rs +++ b/src/thumbnail_cacher.rs @@ -1,15 +1,14 @@ use image::DynamicImage; use md5::{Digest, Md5}; use rustc_hash::FxHashMap; -use std::{ - error::Error, - fs::{self, File}, - io::{self, BufReader, BufWriter}, - os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, - sync::LazyLock, - time::UNIX_EPOCH, -}; +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::path::{Path, PathBuf}; +use std::sync::LazyLock; +use std::time::UNIX_EPOCH; use tempfile::NamedTempFile; use url::Url; @@ -93,6 +92,7 @@ impl ThumbnailCacher { } pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box> { + #[cfg(unix)] fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?; self.update_thumbnail_text_metadata(temp_file.path())?; fs::rename(temp_file.path(), &self.thumbnail_path)?; @@ -127,6 +127,7 @@ impl ThumbnailCacher { pub fn create_fail_marker(&self) -> Result<(), Box> { if let Some(dir) = self.thumbnail_fail_marker_path.parent() { fs::create_dir_all(dir)?; + #[cfg(unix)] fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?; } diff --git a/src/thumbnailer.rs b/src/thumbnailer.rs index f996f03..8e61447 100644 --- a/src/thumbnailer.rs +++ b/src/thumbnailer.rs @@ -1,15 +1,16 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +#[cfg(feature = "desktop")] 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 new file mode 100644 index 0000000..618e77c --- /dev/null +++ b/src/trash.rs @@ -0,0 +1,153 @@ +use cosmic::widget; +use regex::Regex; +use std::collections::HashSet; +use std::path::PathBuf; + +use crate::config::IconSizes; +use crate::tab::{Item, SearchItem}; + +pub trait TrashExt { + fn is_empty() -> bool { + true + } + + fn entries() -> usize { + 0 + } + + fn folders() -> Result, trash::Error> { + Err(trash::Error::Unknown { + description: "reading trash folders not supported on this platform".into(), + }) + } + + fn scan(_sizes: IconSizes) -> Vec { + log::warn!("viewing trash not supported on this platform"); + Vec::new() + } + + 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() { + "user-trash" + } else { + "user-trash-full" + }) + .size(icon_size) + .handle() + } + + fn icon_symbolic(icon_size: u16) -> widget::icon::Handle { + widget::icon::from_name(if Self::is_empty() { + "user-trash-symbolic" + } else { + "user-trash-full-symbolic" + }) + .size(icon_size) + .handle() + } +} + +pub struct Trash; + +// This config statement is from trash::os_limited +#[cfg(any( + target_os = "windows", + all( + unix, + not(target_os = "macos"), + not(target_os = "ios"), + not(target_os = "android") + ) +))] +impl TrashExt for Trash { + fn is_empty() -> bool { + trash::os_limited::is_empty().unwrap_or(true) + } + + fn entries() -> usize { + match trash::os_limited::list() { + Ok(entries) => entries.len(), + Err(_err) => 0, + } + } + + // Not available on Windows only + #[cfg(not(target_os = "windows"))] + fn folders() -> Result, trash::Error> { + trash::os_limited::trash_folders() + } + + fn scan(sizes: IconSizes) -> Vec { + use crate::localize::LANGUAGE_SORTER; + use crate::tab::item_from_trash_entry; + use std::cmp::Ordering; + + let entries = match trash::os_limited::list() { + Ok(entry) => entry, + Err(err) => { + log::warn!("failed to read trash items: {err}"); + return Vec::new(); + } + }; + let mut items: Vec<_> = entries + .into_iter() + .filter_map(|entry| { + let metadata = trash::os_limited::metadata(&entry) + .inspect_err(|err| { + log::warn!("failed to get metadata for trash item {entry:?}: {err}") + }) + .ok()?; + Some(item_from_trash_entry(entry, metadata, sizes)) + }) + .collect(); + items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name), + }); + items + } + + fn scan_search bool + Sync>(callback: F, regex: &Regex) { + let entries = match trash::os_limited::list() { + Ok(entries) => entries, + Err(err) => { + log::warn!("failed to read trash items: {err}"); + return; + } + }; + + for entry in entries { + if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| { + log::warn!("failed to get metadata for trash item {entry:?}: {err}") + }) { + let name = entry.name.to_string_lossy(); + if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) { + break; + } + } + } + } +} + +// This config statement is from trash::os_limited, inverted +#[cfg(not(any( + target_os = "windows", + all( + unix, + not(target_os = "macos"), + not(target_os = "ios"), + not(target_os = "android") + ) +)))] +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();