diff --git a/Cargo.lock b/Cargo.lock index 1cac6c0d..5ca8bed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,22 +192,6 @@ dependencies = [ "libc", ] -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - [[package]] name = "anyhow" version = "1.0.102" @@ -250,12 +234,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 = "ash" version = "0.38.0+1.3.281" @@ -531,25 +509,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.2", - "shlex", - "syn", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -652,7 +611,7 @@ dependencies = [ "libc", "log", "macaddr", - "nix 0.29.0", + "nix", "num-derive", "num-traits", "pin-project", @@ -803,25 +762,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -856,17 +796,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clipboard-win" version = "5.4.1" @@ -879,7 +808,6 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "objc", "objc-foundation", @@ -889,22 +817,12 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "dnd", "mime 0.1.0", "smithay-clipboard", ] -[[package]] -name = "clipboard_x11" -version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" -dependencies = [ - "thiserror 1.0.69", - "x11rb", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -977,21 +895,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - [[package]] name = "core-foundation" version = "0.9.4" @@ -1064,7 +967,7 @@ dependencies = [ [[package]] name = "cosmic-app-list" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-app-list-config", @@ -1075,7 +978,7 @@ dependencies = [ "i18n-embed", "i18n-embed-fl", "image", - "libcosmic", + "libcosmic-yoda", "memmap2 0.9.10", "rust-embed", "rustc-hash 2.1.2", @@ -1091,15 +994,15 @@ dependencies = [ [[package]] name = "cosmic-app-list-config" -version = "1.0.2" +version = "1.0.15" dependencies = [ - "libcosmic", + "libcosmic-yoda", "serde", ] [[package]] name = "cosmic-applet-a11y" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-client-toolkit", @@ -1108,7 +1011,7 @@ dependencies = [ "cosmic-settings-accessibility-subscription", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "tokio", "tracing", @@ -1118,12 +1021,14 @@ dependencies = [ [[package]] name = "cosmic-applet-audio" -version = "1.0.2" +version = "1.0.15" dependencies = [ - "cosmic-settings-sound-subscription", + "cosmic-settings-audio-client", + "futures", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "intmap", + "libcosmic-yoda", "mpris2-zbus", "rust-embed", "serde", @@ -1134,22 +1039,23 @@ dependencies = [ "url", "urlencoding", "zbus", + "zlink", ] [[package]] name = "cosmic-applet-battery" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-applets-config", "cosmic-config", "cosmic-settings-daemon-subscription", "cosmic-settings-upower-subscription", - "drm 0.14.1", + "drm", "futures", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "rustc-hash 2.1.2", "serde", @@ -1163,7 +1069,7 @@ dependencies = [ [[package]] name = "cosmic-applet-bluetooth" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "bluer", @@ -1171,7 +1077,7 @@ dependencies = [ "futures", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "rustc-hash 2.1.2", "tokio", @@ -1182,12 +1088,12 @@ dependencies = [ [[package]] name = "cosmic-applet-input-sources" -version = "1.0.2" +version = "1.0.15" dependencies = [ "cosmic-comp-config", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "tokio", "tracing", @@ -1198,13 +1104,13 @@ dependencies = [ [[package]] name = "cosmic-applet-minimize" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "i18n-embed", "i18n-embed-fl", "image", - "libcosmic", + "libcosmic-yoda", "memmap2 0.9.10", "rust-embed", "rustix 1.1.4", @@ -1216,7 +1122,7 @@ dependencies = [ [[package]] name = "cosmic-applet-network" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "async-fn-stream", @@ -1227,7 +1133,7 @@ dependencies = [ "i18n-embed", "i18n-embed-fl", "indexmap 2.14.0", - "libcosmic", + "libcosmic-yoda", "nmrs", "rust-embed", "rustc-hash 2.1.2", @@ -1242,7 +1148,7 @@ dependencies = [ [[package]] name = "cosmic-applet-notifications" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-notifications-config", @@ -1250,7 +1156,7 @@ dependencies = [ "futures-util", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "tokio", "tracing", @@ -1262,11 +1168,11 @@ dependencies = [ [[package]] name = "cosmic-applet-power" -version = "1.0.2" +version = "1.0.15" dependencies = [ "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "logind-zbus", "rust-embed", "rustix 1.1.4", @@ -1279,10 +1185,10 @@ dependencies = [ [[package]] name = "cosmic-applet-status-area" -version = "1.0.2" +version = "1.0.15" dependencies = [ "futures", - "libcosmic", + "libcosmic-yoda", "rustc-hash 2.1.2", "serde", "tokio", @@ -1294,7 +1200,7 @@ dependencies = [ [[package]] name = "cosmic-applet-tiling" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-client-toolkit", @@ -1302,7 +1208,7 @@ dependencies = [ "cosmic-protocols", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "rustc-hash 2.1.2", "tokio", @@ -1313,14 +1219,14 @@ dependencies = [ [[package]] name = "cosmic-applet-time" -version = "1.0.2" +version = "1.0.15" dependencies = [ "cosmic-applets-config", "i18n-embed", "i18n-embed-fl", "icu", "jiff", - "libcosmic", + "libcosmic-yoda", "logind-zbus", "rust-embed", "timedate-zbus", @@ -1333,14 +1239,14 @@ dependencies = [ [[package]] name = "cosmic-applet-workspaces" -version = "1.0.2" +version = "1.0.15" dependencies = [ "anyhow", "cosmic-client-toolkit", "futures", "i18n-embed", "i18n-embed-fl", - "libcosmic", + "libcosmic-yoda", "rust-embed", "tokio", "tracing", @@ -1350,7 +1256,7 @@ dependencies = [ [[package]] name = "cosmic-applets" -version = "1.0.2" +version = "1.0.15" dependencies = [ "cosmic-app-list", "cosmic-applet-a11y", @@ -1367,7 +1273,7 @@ dependencies = [ "cosmic-applet-time", "cosmic-applet-workspaces", "cosmic-panel-button", - "libcosmic", + "libcosmic-yoda", "tracing", "tracing-log", "tracing-subscriber", @@ -1376,7 +1282,7 @@ dependencies = [ [[package]] name = "cosmic-applets-config" -version = "1.0.2" +version = "1.0.15" dependencies = [ "cosmic-config", "serde", @@ -1385,7 +1291,6 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#c253ec1d6804afbcdf250f5cc37ae1194bba7bd2" dependencies = [ "bitflags 2.11.1", "cosmic-protocols", @@ -1397,8 +1302,7 @@ dependencies = [ [[package]] name = "cosmic-comp-config" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-comp.git?rev=5eb5af4#5eb5af46756f2f2bc5e812cf68fe4c4b79efd924" +version = "1.0.0" dependencies = [ "cosmic-config", "input", @@ -1408,7 +1312,6 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1429,7 +1332,6 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "quote", "syn", @@ -1438,7 +1340,6 @@ dependencies = [ [[package]] name = "cosmic-dbus-a11y" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "zbus", ] @@ -1446,7 +1347,6 @@ dependencies = [ [[package]] name = "cosmic-dbus-networkmanager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "bitflags 2.11.1", "derive_builder", @@ -1459,7 +1359,6 @@ dependencies = [ [[package]] name = "cosmic-freedesktop-icons" version = "0.4.0" -source = "git+https://github.com/pop-os/freedesktop-icons#9c562fe3ecf03241a46a60c0078cd6ea10bd75ce" dependencies = [ "bstr", "btoi", @@ -1473,7 +1372,6 @@ dependencies = [ [[package]] name = "cosmic-notifications-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-notifications#a899bfbc6715c36b1f02d7a0f4d3601a3ea0295f" dependencies = [ "cosmic-config", "serde", @@ -1482,10 +1380,9 @@ dependencies = [ [[package]] name = "cosmic-notifications-util" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-notifications#a899bfbc6715c36b1f02d7a0f4d3601a3ea0295f" dependencies = [ "fast_image_resize", - "libcosmic", + "libcosmic-yoda", "serde", "tl", "tracing", @@ -1495,10 +1392,10 @@ dependencies = [ [[package]] name = "cosmic-panel-button" -version = "1.0.2" +version = "1.0.15" dependencies = [ "cosmic-config", - "libcosmic", + "libcosmic-yoda", "rustc-hash 2.1.2", "serde", "tracing", @@ -1509,7 +1406,6 @@ dependencies = [ [[package]] name = "cosmic-panel-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-panel#2358f0473bf68b79f54a0906994a218de211de34" dependencies = [ "anyhow", "cosmic-config", @@ -1520,24 +1416,9 @@ dependencies = [ "xdg-shell-wrapper-config", ] -[[package]] -name = "cosmic-pipewire" -version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" -dependencies = [ - "intmap", - "libspa", - "libspa-sys", - "pipewire", - "serde", - "serde_json", - "tracing", -] - [[package]] name = "cosmic-protocols" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#c253ec1d6804afbcdf250f5cc37ae1194bba7bd2" dependencies = [ "bitflags 2.11.1", "wayland-backend", @@ -1551,7 +1432,6 @@ dependencies = [ [[package]] name = "cosmic-settings-a11y-manager-subscription" version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" dependencies = [ "cosmic-protocols", "iced_futures", @@ -1565,7 +1445,6 @@ dependencies = [ [[package]] name = "cosmic-settings-accessibility-subscription" version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" dependencies = [ "cosmic-dbus-a11y", "futures", @@ -1575,10 +1454,34 @@ dependencies = [ "zbus", ] +[[package]] +name = "cosmic-settings-audio-client" +version = "1.0.0" +dependencies = [ + "cosmic-settings-audio-core", + "dirs", + "futures-util", + "ron 0.12.1", + "serde", + "tokio", + "tokio-util", + "tracing", + "zlink", +] + +[[package]] +name = "cosmic-settings-audio-core" +version = "1.0.0" +dependencies = [ + "dirs", + "ron 0.12.1", + "serde", + "zlink", +] + [[package]] name = "cosmic-settings-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#716da6d6af0b252e2f78aba2ad72ee19ae0241e0" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1590,8 +1493,7 @@ 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", ] @@ -1599,7 +1501,6 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon-subscription" version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" dependencies = [ "futures", "iced_futures", @@ -1612,13 +1513,12 @@ dependencies = [ [[package]] name = "cosmic-settings-network-manager-subscription" version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" dependencies = [ "bitflags 2.11.1", "cosmic-dbus-networkmanager", "futures", "iced_futures", - "itertools 0.14.0", + "itertools", "nm-secret-agent-manager", "secret-service", "secure-string", @@ -1628,25 +1528,9 @@ dependencies = [ "zbus", ] -[[package]] -name = "cosmic-settings-sound-subscription" -version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" -dependencies = [ - "cosmic-pipewire", - "futures", - "intmap", - "libcosmic", - "numtoa", - "rustix 1.1.4", - "tokio", - "tracing", -] - [[package]] name = "cosmic-settings-upower-subscription" version = "1.0.7" -source = "git+https://github.com/pop-os/cosmic-settings#703a934b096b8681b1b8d16d8625118c8073151a" dependencies = [ "futures", "iced_futures", @@ -1660,8 +1544,6 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ "bitflags 2.11.1", "fontdb", @@ -1679,18 +1561,19 @@ 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#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "almost", "configparser", "cosmic-config", "csscolorparser", "dirs", + "hex_color", "palette", "ron 0.12.1", "serde", @@ -1731,7 +1614,6 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryoglyph" version = "0.1.0" -source = "git+https://github.com/iced-rs/cryoglyph.git?rev=e429a025df36ab8145708acb309080ae3deec17a#e429a025df36ab8145708acb309080ae3deec17a" dependencies = [ "cosmic-text", "etagere", @@ -1768,15 +1650,6 @@ dependencies = [ "uncased", ] -[[package]] -name = "ctor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" -dependencies = [ - "dtor", -] - [[package]] name = "cursor-icon" version = "1.2.0" @@ -2079,7 +1952,6 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "bitflags 2.11.1", "mime 0.1.0", @@ -2106,20 +1978,6 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dpi" version = "0.1.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" - -[[package]] -name = "drm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" -dependencies = [ - "bitflags 2.11.1", - "bytemuck", - "drm-ffi 0.7.1", - "drm-fourcc", - "rustix 0.38.44", -] [[package]] name = "drm" @@ -2129,29 +1987,19 @@ checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" dependencies = [ "bitflags 2.11.1", "bytemuck", - "drm-ffi 0.9.1", + "drm-ffi", "drm-fourcc", "libc", "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 0.6.1", - "rustix 0.38.44", -] - [[package]] name = "drm-ffi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" dependencies = [ - "drm-sys 0.8.1", + "drm-sys", "rustix 1.1.4", ] @@ -2161,16 +2009,6 @@ 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 = "drm-sys" version = "0.8.1" @@ -2181,12 +2019,6 @@ dependencies = [ "linux-raw-sys 0.9.4", ] -[[package]] -name = "dtor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -2327,7 +2159,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" dependencies = [ - "toml 0.5.11", + "toml", ] [[package]] @@ -2531,7 +2363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6d3a3635983a889f065aa9ce760384713f23a9b4a04f696f86c39a5d7a6a5a" dependencies = [ "indexmap 2.14.0", - "nom 8.0.0", + "nom", ] [[package]] @@ -2660,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" @@ -2753,12 +2575,6 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "glow" version = "0.16.0" @@ -2917,6 +2733,17 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0" +dependencies = [ + "arrayvec", + "rand 0.8.6", + "serde", +] + [[package]] name = "hexf-parse" version = "0.2.1" @@ -3035,7 +2862,6 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "dnd", "iced_accessibility", @@ -3056,7 +2882,6 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "accesskit", "accesskit_winit", @@ -3065,7 +2890,6 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "bitflags 2.11.1", "bytes", @@ -3082,6 +2906,7 @@ dependencies = [ "serde", "smol_str", "thiserror 2.0.18", + "unicode-segmentation", "web-time", "window_clipboard", ] @@ -3089,7 +2914,6 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "iced_core", "iced_futures", @@ -3099,7 +2923,6 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "futures", "iced_core", @@ -3113,7 +2936,6 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "bitflags 2.11.1", "bytemuck", @@ -3134,7 +2956,6 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "iced_graphics", "iced_runtime", @@ -3143,7 +2964,6 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3155,7 +2975,6 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3170,7 +2989,6 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "bytemuck", "cosmic-text", @@ -3187,9 +3005,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ - "as-raw-xcb-connection", "bitflags 2.11.1", "bytemuck", "cosmic-client-toolkit", @@ -3206,19 +3022,16 @@ dependencies = [ "rustc-hash 2.1.2", "rustix 0.38.44", "thiserror 2.0.18", - "tiny-xlib", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-sys", "wgpu", - "x11rb", ] [[package]] name = "iced_widget" version = "0.14.2" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3236,7 +3049,6 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" dependencies = [ "cosmic-client-toolkit", "cursor-icon", @@ -3792,9 +3604,9 @@ dependencies = [ [[package]] name = "input" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9" +checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d" dependencies = [ "bitflags 2.11.1", "input-sys", @@ -3845,15 +3657,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -4053,9 +3856,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "285efcf12ef41bec907b3000d5ffaeb54191d4d9d83c0d6157e6cbc2db255e64" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ "bitflags 2.11.1", "libc", @@ -4101,9 +3904,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libcosmic" -version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#4fab6c777dbd1a023440f08fb2b729c86492366c" +name = "libcosmic-yoda" +version = "0.1.0-yoda.2" dependencies = [ "apply", "ashpd 0.12.3", @@ -4117,6 +3919,7 @@ dependencies = [ "cosmic-theme", "css-color", "derive_setters", + "enumflags2", "float-cmp 0.10.0", "freedesktop-desktop-entry", "futures", @@ -4182,42 +3985,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.5", -] - -[[package]] -name = "libspa" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" -dependencies = [ - "bitflags 2.11.1", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix 0.30.1", - "nom 8.0.0", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" -dependencies = [ - "bindgen", - "cc", - "system-deps", + "redox_syscall 0.8.1", ] [[package]] @@ -4251,12 +4026,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.9.4" @@ -4452,7 +4221,6 @@ dependencies = [ [[package]] name = "mime" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "smithay-clipboard", ] @@ -4463,12 +4231,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -4504,7 +4266,6 @@ dependencies = [ [[package]] name = "mpris2-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "futures-util", "jiff", @@ -4588,31 +4349,18 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nm-secret-agent-manager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "zbus", ] [[package]] name = "nmrs" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5e0c4453d1edf1b13767c4a10905a55c847d942d751cb220a78074f194d523" +checksum = "f9493a0dfbe50783b53ef52ec8ddb8d1f85fd45098be8210205236ff0f43bb7b" dependencies = [ "async-trait", "base64", @@ -4628,16 +4376,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -4718,9 +4456,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -4796,12 +4534,6 @@ dependencies = [ "syn", ] -[[package]] -name = "numtoa" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4d8a81ede501fad07191e746a299f4d79f6dcd053bab1b97af4ff5a90099f2" - [[package]] name = "objc" version = "0.2.7" @@ -5245,18 +4977,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -5286,34 +5018,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pipewire" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "libc", - "libspa", - "libspa-sys", - "nix 0.30.1", - "once_cell", - "pipewire-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "pipewire-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - [[package]] name = "pkg-config" version = "0.3.33" @@ -5558,6 +5262,8 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -5567,10 +5273,20 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -5586,6 +5302,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] name = "rand_core" @@ -5625,6 +5344,16 @@ dependencies = [ "font-types", ] +[[package]] +name = "redox_event" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c07d0d6d291e3a951bd847b1cd4af32fc6243d64116cf7702838c02797688b7" +dependencies = [ + "bitflags 2.11.1", + "libredox", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -5636,9 +5365,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" dependencies = [ "bitflags 2.11.1", ] @@ -5897,6 +5626,12 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -6063,15 +5798,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_with" version = "3.20.0" @@ -6247,7 +5973,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", @@ -6278,14 +6003,11 @@ dependencies = [ [[package]] name = "softbuffer" version = "0.4.1" -source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#c2b2c19ddb38ff17495643699f97cb1f2064a1be" dependencies = [ - "as-raw-xcb-connection", "bytemuck", "cfg_aliases", "cocoa", "core-graphics", - "drm 0.11.1", "fastrand", "foreign-types", "js-sys", @@ -6295,14 +6017,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]] @@ -6399,7 +6119,6 @@ dependencies = [ [[package]] name = "switcheroo-control" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "zbus", ] @@ -6435,19 +6154,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 1.1.2+spec-1.1.0", - "version-compare", -] - [[package]] name = "taffy" version = "0.9.2" @@ -6460,12 +6166,6 @@ dependencies = [ "slotmap", ] -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - [[package]] name = "temp-dir" version = "0.1.16" @@ -6577,7 +6277,6 @@ dependencies = [ [[package]] name = "timedate-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "zbus", ] @@ -6608,19 +6307,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny-xlib" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" -dependencies = [ - "as-raw-xcb-connection", - "ctor", - "libloading", - "pkg-config", - "tracing", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -6691,6 +6377,20 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -6702,21 +6402,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -6747,12 +6432,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tracing" version = "0.1.44" @@ -6971,7 +6650,6 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "upower_dbus" version = "0.3.2" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" dependencies = [ "serde", "serde_repr", @@ -7055,12 +6733,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -7564,12 +7236,10 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "clipboard-win", "clipboard_macos", "clipboard_wayland", - "clipboard_x11", "dnd", "mime 0.1.0", "raw-window-handle", @@ -8009,7 +7679,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "cfg_aliases", @@ -8029,13 +7698,11 @@ dependencies = [ "winit-wayland", "winit-web", "winit-win32", - "winit-x11", ] [[package]] name = "winit-android" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "android-activity", "bitflags 2.11.1", @@ -8050,7 +7717,6 @@ dependencies = [ [[package]] name = "winit-appkit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "block2 0.6.2", @@ -8072,7 +7738,6 @@ dependencies = [ [[package]] name = "winit-common" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "memmap2 0.9.10", "objc2 0.6.4", @@ -8080,14 +7745,12 @@ dependencies = [ "smol_str", "tracing", "winit-core", - "x11-dl", "xkbcommon-dl", ] [[package]] name = "winit-core" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "cursor-icon", @@ -8101,14 +7764,13 @@ dependencies = [ [[package]] name = "winit-orbital" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "dpi", "libredox", "orbclient", "raw-window-handle", - "redox_syscall 0.7.5", + "redox_event", "smol_str", "tracing", "winit-core", @@ -8117,7 +7779,6 @@ dependencies = [ [[package]] name = "winit-uikit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "block2 0.6.2", @@ -8137,7 +7798,6 @@ dependencies = [ [[package]] name = "winit-wayland" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "ahash", "bitflags 2.11.1", @@ -8163,7 +7823,6 @@ dependencies = [ [[package]] name = "winit-web" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "atomic-waker", "bitflags 2.11.1", @@ -8185,7 +7844,6 @@ dependencies = [ [[package]] name = "winit-win32" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" dependencies = [ "bitflags 2.11.1", "cursor-icon", @@ -8198,34 +7856,11 @@ dependencies = [ "winit-core", ] -[[package]] -name = "winit-x11" -version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366" -dependencies = [ - "bitflags 2.11.1", - "bytemuck", - "calloop", - "cursor-icon", - "dpi", - "libc", - "percent-encoding", - "raw-window-handle", - "rustix 1.1.4", - "smol_str", - "tracing", - "winit-common", - "winit-core", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -8339,39 +7974,6 @@ dependencies = [ "either", ] -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.4", - "x11rb-protocol", - "xcursor", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "xcursor" version = "0.3.10" @@ -8387,7 +7989,6 @@ checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "xdg-shell-wrapper-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-panel#2358f0473bf68b79f54a0906994a218de211de34" dependencies = [ "serde", "wayland-protocols-wlr", @@ -8718,6 +8319,73 @@ dependencies = [ "syn", ] +[[package]] +name = "zlink" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b895b99588dceb73f4d349b8323eabad9a97d48ce83698d475c7223727c6148" +dependencies = [ + "zlink-smol", + "zlink-tokio", +] + +[[package]] +name = "zlink-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd12701bd1d42a982b931f0159cf5054bf13d90e7828a8377dfc02ed4b00342d" +dependencies = [ + "futures-util", + "itoa", + "libc", + "pin-project-lite", + "rustix 1.1.4", + "ryu", + "serde", + "serde_json", + "tracing", + "zlink-macros", +] + +[[package]] +name = "zlink-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f2416a5f504dfd7e04fee49f31abafe3314a3f62b4ddaa8e9a5fd496d4dd50" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zlink-smol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc53cd0d636ad753f759aab0abb1f456e985c3938a279d11ebda92340ae37b1" +dependencies = [ + "async-broadcast", + "async-channel", + "async-io", + "futures-lite", + "futures-util", + "pin-project-lite", + "zlink-core", +] + +[[package]] +name = "zlink-tokio" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cbf366ac77ab41bf9a8d43535d3d620a072f7957813e03355d3d010c16cc4f" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", + "zlink-core", +] + [[package]] name = "zmij" version = "1.0.21" @@ -8794,3 +8462,15 @@ dependencies = [ "syn", "winnow", ] + +[[patch.unused]] +name = "cosmic-settings-airplane-mode-subscription" +version = "1.0.7" + +[[patch.unused]] +name = "cosmic-settings-sound-subscription" +version = "1.0.7" + +[[patch.unused]] +name = "winit-x11" +version = "0.31.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index 569cb3b4..6f673cc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,11 @@ resolver = "3" [workspace.dependencies] anyhow = "1.0.102" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be" } +cctk = { package = "cosmic-client-toolkit", path = "../cosmic-protocols/client-toolkit" } cosmic-applets-config = { path = "cosmic-applets-config" } -cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [ +cosmic-protocols = { path = "../cosmic-protocols", default-features = false, features = [ "client", -], rev = "d0e95be" } +]} futures = "0.3" futures-util = "0.3" @@ -38,7 +38,7 @@ i18n-embed = { version = "0.16.0", features = [ "desktop-requester", ] } i18n-embed-fl = "0.10" -libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = [ +cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [ "applet", "applet-token", "dbus-config", @@ -48,6 +48,7 @@ libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = fa "desktop-systemd-scope", "winit", ] } +cosmic-comp-config = { path = "../cosmic-comp/cosmic-comp-config" } rust-embed = "8.11.0" rust-embed-utils = "8.11.0" rustc-hash = "2.1" @@ -58,8 +59,9 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } tracing-log = "0.2.0" tokio = { version = "1.49.0", features = ["full"] } # cosmic-config = { path = "../libcosmic/cosmic-config" } -cosmic-config = { git = "https://github.com/pop-os/libcosmic" } +cosmic-config = { path = "../libcosmic/cosmic-config" } serde = { version = "1.0.228", features = ["derive"] } +zlink = "0.5.0" [profile.release] opt-level = 3 @@ -81,12 +83,51 @@ ignored = ["libcosmic"] # winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } # winit = { path = "../winit" } +[patch."https://github.com/pop-os/libcosmic"] +cosmic-config = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" } +cosmic-theme = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-theme" } +iced = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced" } +iced_accessibility = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/accessibility" } +iced_core = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/core" } +iced_futures = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/futures" } +iced_graphics = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/graphics" } +iced_renderer = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/renderer" } +iced_runtime = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/runtime" } +iced_tiny_skia = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/tiny_skia" } +iced_wgpu = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/wgpu" } +iced_widget = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/widget" } +iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit" } + [patch."https://github.com/smithay/client-toolkit.git"] sctk = { package = "smithay-client-toolkit", version = "0.20.0" } [patch."https://github.com/pop-os/cosmic-protocols"] -cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" } -cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" } +cosmic-protocols = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols" } +cosmic-client-toolkit = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols/client-toolkit" } + +[patch."https://github.com/pop-os/cosmic-panel"] +cosmic-panel-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/cosmic-panel-config" } +xdg-shell-wrapper-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/xdg-shell-wrapper-config" } + +[patch."https://github.com/pop-os/cosmic-notifications"] +cosmic-notifications-config = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-config" } +cosmic-notifications-util = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-util" } + +[patch."https://github.com/pop-os/cosmic-settings"] +cosmic-settings-a11y-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/a11y-manager" } +cosmic-settings-accessibility-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/accessibility" } +cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" } +cosmic-settings-daemon-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/settings-daemon" } +cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" } +cosmic-settings-sound-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/sound" } +cosmic-settings-upower-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/upower" } + +[patch."https://github.com/pop-os/cosmic-settings/"] +cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" } +cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" } + +[patch."https://github.com/pop-os/cosmic-text.git"] +cosmic-text = { path = "../cosmic-text" } # [patch.'https://github.com/pop-os/dbus-settings-bindings'] # cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" } diff --git a/cosmic-app-list/Cargo.toml b/cosmic-app-list/Cargo.toml index 7c398b97..b3171a2c 100644 --- a/cosmic-app-list/Cargo.toml +++ b/cosmic-app-list/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-app-list" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -13,13 +13,13 @@ futures.workspace = true i18n-embed.workspace = true i18n-embed-fl.workspace = true image = { version = "0.25.9", default-features = false } -libcosmic.workspace = true +cosmic.workspace = true memmap2 = "0.9.10" fastrand = "2.3.0" rust-embed.workspace = true rustix.workspace = true rustc-hash.workspace = true -switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } +switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" } tokio.workspace = true tracing-log.workspace = true tracing-subscriber.workspace = true diff --git a/cosmic-app-list/cosmic-app-list-config/Cargo.toml b/cosmic-app-list/cosmic-app-list-config/Cargo.toml index 08d87f2e..0d5a21d7 100644 --- a/cosmic-app-list/cosmic-app-list-config/Cargo.toml +++ b/cosmic-app-list/cosmic-app-list-config/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "cosmic-app-list-config" -version = "1.0.2" +version = "1.0.15" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic.workspace = true +cosmic.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/cosmic-app-list/i18n/en/cosmic_app_list.ftl b/cosmic-app-list/i18n/en/cosmic_app_list.ftl index c412cd51..028a1558 100644 --- a/cosmic-app-list/i18n/en/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/en/cosmic_app_list.ftl @@ -5,4 +5,15 @@ quit-all = Quit All new-window = New Window run = Run run-on = Run on {$gpu} -run-on-default = (Default) \ No newline at end of file +run-on-default = (Default) +edit-launcher = Edit launcher +launcher-name = Name +launcher-command = Command +launcher-icon = Icon +launcher-icon-theme = Theme +launcher-icon-search = Search icons +launcher-icon-catalog-loading = Loading icons +launcher-icon-catalog-empty = No icons +launcher-icons = icons +save = Save +cancel = Cancel diff --git a/cosmic-app-list/i18n/fr/cosmic_app_list.ftl b/cosmic-app-list/i18n/fr/cosmic_app_list.ftl index 39cac491..16bb7465 100644 --- a/cosmic-app-list/i18n/fr/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/fr/cosmic_app_list.ftl @@ -6,3 +6,14 @@ new-window = Nouvelle fenêtre run = Exécuter run-on = Lancer avec { $gpu } run-on-default = (Défaut) +edit-launcher = Modifier le lanceur +launcher-name = Nom +launcher-command = Commande +launcher-icon = Icône +launcher-icon-theme = Thème +launcher-icon-search = Rechercher une icône +launcher-icon-catalog-loading = Chargement des icônes +launcher-icon-catalog-empty = Aucune icône +launcher-icons = icônes +save = Enregistrer +cancel = Annuler diff --git a/cosmic-app-list/i18n/kmr/cosmic_app_list.ftl b/cosmic-app-list/i18n/kmr/cosmic_app_list.ftl index e69de29b..793d1880 100644 --- a/cosmic-app-list/i18n/kmr/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/kmr/cosmic_app_list.ftl @@ -0,0 +1,8 @@ +run = Bixebitîne +run-on = Li ser { $gpu } bixebitîne +run-on-default = (Berdest) +pin = Bo destgeha sepanan bi dar bixe +quit = Biqedîne +quit-all = Hemûyan biqedîne +new-window = Çarçoveya nû +cosmic-app-list = Destgeha sepanan diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index 932ffd6c..55d815b8 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ - fl, + fl, icon_catalog, + launcher_edit::{self, LauncherEditRequest}, wayland_subscription::{ OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate, wayland_subscription, @@ -43,11 +44,11 @@ use cosmic::{ surface, theme::{self, Button, Container}, widget::{ - DndDestination, Image, button, container, divider, dnd_source, + DndDestination, Image, button, container, divider, dnd_source, grid, icon::{self, from_name}, image::Handle, rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription}, - svg, text, + scrollable, svg, text, text_input, }, }; use cosmic::{ @@ -64,6 +65,8 @@ use tokio::time::sleep; use url::Url; static MIME_TYPE: &str = "text/uri-list"; +const MAX_VISIBLE_ICON_CHOICES: usize = 120; +const ICON_CATALOG_COLUMNS: usize = 6; pub fn run() -> cosmic::iced::Result { cosmic::applet::run::(()) @@ -187,6 +190,11 @@ impl DockItem { dot_border_radius: [f32; 4], window_id: window::Id, filter: Option<&dyn Fn(&ToplevelInfo) -> bool>, + // Yoda: multiplier on the computed icon size (1.0 = default, + // >1.0 = magnified e.g. on hover for the macOS Tahoe effect). + // Applied to the icon's rendered width/height only — indicator + // dot and surrounding layout stay at base size. + icon_scale: f32, ) -> Element<'_, Message> { let Self { toplevels, @@ -205,17 +213,35 @@ impl DockItem { }; let toplevel_count = filtered_toplevels.len(); + // Cairo-like : pastille plus petite + atténuée quand toutes les fenêtres + // de cette app sont minimisées. + let all_minimized = toplevel_count > 0 + && filtered_toplevels + .iter() + .all(|(info, _)| info.state.contains(&State::Minimized)); + let app_icon = AppletIconData::new(applet); + // Yoda: scaled icon size for hover magnification. Clamped so + // tiny floats don't round to 0 and huge ones stay within u16. + let scaled_icon_size = ((f32::from(app_icon.icon_size) * icon_scale).round() as i32) + .clamp(1, u16::MAX as i32) as u16; let cosmic_icon = cosmic::widget::icon( fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(), ) // sets the preferred icon size variant .size(128) - .width(app_icon.icon_size.into()) - .height(app_icon.icon_size.into()); + .width(scaled_icon_size.into()) + .height(scaled_icon_size.into()); let indicator = { + // Padding réduit quand minimisée → pastille plus petite. + let effective_radius = if all_minimized { + (app_icon.dot_radius * 0.55).max(1.0) + } else { + app_icon.dot_radius + }; + let container = if toplevel_count <= 1 { vertical_space().height(Length::Fixed(0.0)) } else { @@ -229,22 +255,34 @@ impl DockItem { } } .apply(container) - .padding(app_icon.dot_radius); + .padding(effective_radius); if toplevel_count == 0 { container } else { - container.class(theme::Container::custom(move |theme| container::Style { - background: if is_focused { - Some(Background::Color(theme.cosmic().accent_color().into())) + container.class(theme::Container::custom(move |theme| { + let cosmic = theme.cosmic(); + let accent: iced::Color = cosmic.accent_color().into(); + let on_bg: iced::Color = cosmic.on_bg_color().into(); + // Teinte neutre atténuée quand toutes les fenêtres sont minimisées. + let muted = iced::Color { a: 0.45, ..on_bg }; + + let fill = if all_minimized { + muted + } else if is_focused { + accent } else { - Some(Background::Color(theme.cosmic().on_bg_color().into())) - }, - border: Border { - radius: dot_border_radius.into(), + on_bg + }; + + container::Style { + background: Some(Background::Color(fill)), + border: Border { + radius: dot_border_radius.into(), + ..Default::default() + }, ..Default::default() - }, - ..Default::default() + } })) } }; @@ -352,10 +390,35 @@ pub struct Popup { popup_type: PopupType, } +#[derive(Debug, Clone)] +struct LauncherEditState { + original_app_id: String, + source_path: PathBuf, + original_name: String, + original_exec: String, + name: String, + exec: String, + icon: String, + terminal: bool, + saving: bool, + error: Option, + icon_catalog: IconCatalogState, +} + +#[derive(Debug, Clone)] +struct IconCatalogState { + theme: String, + query: String, + entries: Vec, + loading: bool, + truncated: bool, +} + #[derive(Clone, Default)] struct CosmicAppList { core: cosmic::app::Core, popup: Option, + launcher_edit: Option, subscription_ctr: u32, item_ctr: u32, desktop_entries: Vec, @@ -374,6 +437,21 @@ struct CosmicAppList { output_list: FxHashMap, locales: Vec, hovered_toplevel: Option, + /// Yoda: which dock icon the pointer is currently over (for hover + /// magnification). None = no dock icon hovered. + hovered_dock_item: Option, + /// Yoda: animated "virtual cursor" center used by the fisheye + /// formula — lerps toward the real hovered icon's center on each + /// AnimTick, so the bell curve slides smoothly from one icon to the + /// next instead of snapping. + anim_hover_center: Option<(f32, f32)>, + /// Yoda: fade-in/out intensity of the magnification effect + /// (0.0 = icons flat, 1.0 = full fisheye). Targets 1.0 while the + /// pointer is over any dock icon, 0.0 otherwise. Lerped on AnimTick. + anim_hover_intensity: f32, + /// Yoda: timestamp of the last AnimTick, for dt-based exponential + /// smoothing. `None` on first tick. + anim_last_tick: Option, overflow_favorites_popup: Option, overflow_active_popup: Option, } @@ -382,13 +460,33 @@ struct CosmicAppList { pub enum PopupType { RightClickMenu, ToplevelList, + LauncherEditor, } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { Wayland(WaylandUpdate), PinApp(u32), UnpinApp(u32), + EditLauncher(u32), + LauncherNameChanged(String), + LauncherExecChanged(String), + LauncherIconChanged(String), + LauncherIconSearchChanged(String), + LauncherIconSelected(String), + ReloadLauncherIconCatalog, + LauncherIconCatalogLoaded(icon_catalog::IconCatalog), + SaveLauncherEdit, + CancelLauncherEdit, + LauncherEditSaved(Result), + /// Yoda: pointer entered (Some) or left (None) a dock icon — drives + /// the macOS Tahoe-style hover magnification effect. + DockItemHover(Option), + /// Yoda: ticked at ~60fps by the animation subscription. Advances + /// anim_hover_center + anim_hover_intensity toward their targets so + /// the fisheye effect transitions smoothly instead of snapping. + AnimTick(std::time::Instant), Popup(u32, window::Id), Pressed(window::Id), ToplevelListPopup(u32, window::Id), @@ -626,6 +724,143 @@ pub fn menu_control_padding() -> Padding { [spacing.space_xxs, spacing.space_s].into() } +fn launcher_icon_editor(edit: &LauncherEditState) -> Element<'_, Message> { + let spacing = theme::spacing(); + let selected_icon = edit.icon.trim(); + let query = edit.icon_catalog.query.trim().to_ascii_lowercase(); + let mut total_matches = 0usize; + let mut visible_count = 0usize; + let mut icon_grid = grid() + .width(Length::Fill) + .column_spacing(spacing.space_xxs) + .row_spacing(spacing.space_xxs); + + for entry in edit.icon_catalog.entries.iter().filter(|entry| { + query.is_empty() || entry.name.to_ascii_lowercase().contains(query.as_str()) + }) { + total_matches += 1; + if visible_count >= MAX_VISIBLE_ICON_CHOICES { + continue; + } + + if visible_count > 0 && visible_count % ICON_CATALOG_COLUMNS == 0 { + icon_grid = icon_grid.insert_row(); + } + + let selected = selected_icon == entry.name; + let icon_preview = cosmic::widget::icon( + fde::IconSource::from_unknown(entry.name.as_str()).as_cosmic_icon(), + ) + .size(32) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)); + + let label = text::caption(entry.name.as_str()) + .ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1))) + .width(Length::Fill) + .center(); + + let tile = column![icon_preview, label] + .align_x(Alignment::Center) + .spacing(4) + .width(Length::Fixed(70.0)); + + let tile_button = button::custom(tile) + .class(if selected { + Button::Suggested + } else { + Button::Image + }) + .selected(selected) + .on_press(Message::LauncherIconSelected(entry.name.clone())) + .padding(6) + .width(Length::Fixed(74.0)) + .height(Length::Fixed(76.0)); + + icon_grid = icon_grid.push(tile_button); + visible_count += 1; + } + + let current_icon = + cosmic::widget::icon(fde::IconSource::from_unknown(edit.icon.as_str()).as_cosmic_icon()) + .size(32) + .width(Length::Fixed(36.0)) + .height(Length::Fixed(36.0)); + + let icon_value = row![ + current_icon, + text_input("", edit.icon.as_str()) + .label(fl!("launcher-icon")) + .on_input(Message::LauncherIconChanged) + .on_submit(|_| Message::SaveLauncherEdit) + .width(Length::Fill) + .size(14), + button::icon(from_name("view-refresh-symbolic")) + .on_press(Message::ReloadLauncherIconCatalog) + .padding(spacing.space_xxs), + ] + .spacing(spacing.space_xs) + .align_y(Alignment::Center); + + let visible_total = if edit.icon_catalog.truncated { + format!( + "{}/{}+ {}", + visible_count, + total_matches, + fl!("launcher-icons") + ) + } else { + format!( + "{}/{} {}", + visible_count, + total_matches, + fl!("launcher-icons") + ) + }; + + let catalog_header = row![ + text::caption(format!( + "{}: {}", + fl!("launcher-icon-theme"), + edit.icon_catalog.theme + )), + horizontal_space(), + text::caption(visible_total), + ] + .align_y(Alignment::Center); + + let catalog_body: Element<_> = if edit.icon_catalog.loading { + container(text::body(fl!("launcher-icon-catalog-loading"))) + .center(Length::Fill) + .height(Length::Fixed(220.0)) + .into() + } else if total_matches == 0 { + container(text::body(fl!("launcher-icon-catalog-empty"))) + .center(Length::Fill) + .height(Length::Fixed(220.0)) + .into() + } else { + scrollable(icon_grid) + .height(Length::Fixed(240.0)) + .width(Length::Fill) + .into() + }; + + column![ + icon_value, + catalog_header, + text_input("", edit.icon_catalog.query.as_str()) + .label(fl!("launcher-icon-search")) + .on_input(Message::LauncherIconSearchChanged) + .width(Length::Fill) + .size(14), + catalog_body, + ] + .spacing(spacing.space_s) + .width(Length::Fill) + .into() +} + fn find_desktop_entries<'a>( desktop_entries: &'a [fde::DesktopEntry], app_ids: &'a [String], @@ -647,6 +882,79 @@ impl CosmicAppList { .collect::>(); } + /// Yoda: macOS-Tahoe fisheye-style magnification. Returns the + /// per-icon size multiplier based on the distance (in pixels) from + /// the currently hovered icon's center to this icon's center. + /// + /// Uses a gaussian bell curve so the hovered icon peaks at + /// 1.0 + PEAK, immediate neighbors still bulge noticeably, and icons + /// further away relax back to 1.0× — that's the smooth neighbour + /// deformation people associate with the macOS Dock. + /// + /// Falls back to binary 1.3×/1.0× when the rectangle tracker hasn't + /// populated yet (first render, or just after layout changes). + /// + /// Uses the animated hover center (anim_hover_center) and intensity + /// (anim_hover_intensity) so inter-icon transitions slide smoothly + /// and the whole effect fades in/out at the dock's edges. + fn icon_scale_for(&self, id: &DockItemId) -> f32 { + const PEAK: f32 = 0.35; + // sigma expressed in multiples of the hovered icon's size — + // 1.4 means the ±1 neighbors sit ~0.7σ away and still bulge + // visibly, while ±3+ has collapsed to ~1.0× (fisheye footprint + // close to 5 icons wide, Tahoe-ish). + const SIGMA_FACTOR: f32 = 1.4; + + // No intensity at all → skip the rest. + if self.anim_hover_intensity < 0.001 { + return 1.0; + } + + // Prefer the animated center (smooth); fall back to the real + // hovered icon's center (first render, before tick fires). + let hover_center = self.anim_hover_center.or_else(|| { + let hovered_id = self.hovered_dock_item.as_ref()?; + let r = self.rectangles.get(hovered_id)?; + Some((r.x + r.width / 2.0, r.y + r.height / 2.0)) + }); + let Some(hover_center) = hover_center else { + // No coords yet — visibly peak on the exact hovered id so + // the very first frame still responds. + return if self.hovered_dock_item.as_ref() == Some(id) { + 1.0 + PEAK * self.anim_hover_intensity + } else { + 1.0 + }; + }; + + let this_rect = match self.rectangles.get(id) { + Some(r) => r, + None => return 1.0, + }; + + let is_horizontal = matches!( + self.core.applet.anchor, + PanelAnchor::Top | PanelAnchor::Bottom + ); + let this_center = if is_horizontal { + this_rect.x + this_rect.width / 2.0 + } else { + this_rect.y + this_rect.height / 2.0 + }; + let hover_axis = if is_horizontal { hover_center.0 } else { hover_center.1 }; + let distance = (this_center - hover_axis).abs(); + let icon_extent = if is_horizontal { + this_rect.width + } else { + this_rect.height + } + .max(1.0); + let sigma = icon_extent * SIGMA_FACTOR; + // exp(-t²) bell curve, t = distance / sigma + let t = distance / sigma; + 1.0 + PEAK * self.anim_hover_intensity * (-t * t).exp() + } + fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool { use cosmic_app_list_config::ToplevelFilter; @@ -691,10 +999,45 @@ impl CosmicAppList { .collect(); } + fn sync_pinned_list_from_config(&mut self) { + for item in self.pinned_list.drain(..) { + if !item.toplevels.is_empty() { + self.active_list.push(item); + } + } + + self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites) + .zip(&self.config.favorites) + .map(|(de, original_id)| { + if let Some(p) = self + .active_list + .iter() + .position(|dock_item| dock_item.desktop_info.id() == de.id()) + { + let mut d = self.active_list.remove(p); + d.desktop_info = de.clone(); + d.original_app_id.clone_from(original_id); + d + } else { + self.item_ctr += 1; + DockItem { + id: self.item_ctr, + toplevels: Vec::new(), + desktop_info: de.clone(), + original_app_id: original_id.clone(), + } + } + }) + .collect(); + } + /// Close any open popups. fn close_popups(&mut self) -> Task> { let mut commands = Vec::new(); if let Some(popup) = self.popup.take() { + if popup.popup_type == PopupType::LauncherEditor { + self.launcher_edit = None; + } commands.push(destroy_popup(popup.id)); } if let Some(popup) = self.overflow_active_popup.take() { @@ -1068,6 +1411,198 @@ impl cosmic::Application for CosmicAppList { return destroy_popup(popup_id); } } + Message::EditLauncher(id) => { + let Some(dock_item) = self.pinned_list.iter().find(|t| t.id == id).cloned() else { + return Task::none(); + }; + let Some(exec) = dock_item.desktop_info.exec() else { + return Task::none(); + }; + let Some(existing_popup) = self.popup.as_mut() else { + return Task::none(); + }; + + let original_name = dock_item + .desktop_info + .desktop_entry("Name") + .map(ToString::to_string) + .or_else(|| { + dock_item + .desktop_info + .name(&self.locales) + .map(Cow::into_owned) + }) + .unwrap_or_else(|| dock_item.original_app_id.clone()); + let original_exec = exec.to_string(); + let original_icon = dock_item + .desktop_info + .icon() + .unwrap_or_default() + .to_string(); + let icon_theme = cosmic::icon_theme::default(); + + self.launcher_edit = Some(LauncherEditState { + original_app_id: dock_item.original_app_id.clone(), + source_path: dock_item.desktop_info.path.clone(), + original_name: original_name.clone(), + original_exec: original_exec.clone(), + name: original_name, + exec: original_exec, + icon: original_icon.clone(), + terminal: dock_item.desktop_info.terminal(), + saving: false, + error: None, + icon_catalog: IconCatalogState { + theme: icon_theme.clone(), + query: String::new(), + entries: Vec::new(), + loading: true, + truncated: false, + }, + }); + + existing_popup.dock_item = dock_item; + existing_popup.popup_type = PopupType::LauncherEditor; + + return Task::perform( + icon_catalog::load_icon_catalog(icon_theme, original_icon), + |catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)), + ); + } + Message::LauncherNameChanged(name) => { + if let Some(edit) = self.launcher_edit.as_mut() + && !edit.saving + { + edit.name = name; + edit.error = None; + } + } + Message::LauncherExecChanged(exec) => { + if let Some(edit) = self.launcher_edit.as_mut() + && !edit.saving + { + edit.exec = exec; + edit.error = None; + } + } + Message::LauncherIconChanged(icon) => { + if let Some(edit) = self.launcher_edit.as_mut() + && !edit.saving + { + edit.icon = icon; + edit.error = None; + } + } + Message::LauncherIconSearchChanged(query) => { + if let Some(edit) = self.launcher_edit.as_mut() { + edit.icon_catalog.query = query; + } + } + Message::LauncherIconSelected(icon) => { + if let Some(edit) = self.launcher_edit.as_mut() + && !edit.saving + { + edit.icon = icon; + edit.error = None; + } + } + Message::ReloadLauncherIconCatalog => { + if let Some(edit) = self.launcher_edit.as_mut() { + edit.icon_catalog.theme = cosmic::icon_theme::default(); + edit.icon_catalog.entries.clear(); + edit.icon_catalog.loading = true; + edit.icon_catalog.truncated = false; + + return Task::perform( + icon_catalog::load_icon_catalog( + edit.icon_catalog.theme.clone(), + edit.icon.clone(), + ), + |catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)), + ); + } + } + Message::LauncherIconCatalogLoaded(catalog) => { + if let Some(edit) = self.launcher_edit.as_mut() + && edit.icon_catalog.theme == catalog.theme + { + edit.icon_catalog.entries = catalog.entries; + edit.icon_catalog.loading = false; + edit.icon_catalog.truncated = catalog.truncated; + } + } + Message::SaveLauncherEdit => { + let Some(edit) = self.launcher_edit.as_mut() else { + return Task::none(); + }; + if edit.saving { + return Task::none(); + } + if let Err(error) = + launcher_edit::validate_launcher_fields(&edit.name, &edit.exec, &edit.icon) + { + edit.error = Some(error); + return Task::none(); + } + + let request = LauncherEditRequest { + current_app_id: edit.original_app_id.clone(), + source_path: edit.source_path.clone(), + name: edit.name.clone(), + exec: edit.exec.clone(), + icon: edit.icon.clone(), + terminal: edit.terminal, + replace_localized_name: edit.name.trim() != edit.original_name.trim(), + disable_dbus_activation: edit.exec.trim() != edit.original_exec.trim(), + }; + + edit.saving = true; + edit.error = None; + + return Task::perform(launcher_edit::save_launcher_edit(request), |result| { + cosmic::Action::App(Message::LauncherEditSaved(result)) + }); + } + Message::CancelLauncherEdit => { + return self.close_popups(); + } + Message::LauncherEditSaved(result) => match result { + Ok(result) => { + tracing::info!( + app_id = result.new_app_id, + path = ?result.path, + "saved editable launcher" + ); + + let mut favorites = self.config.favorites.clone(); + let mut favorites_changed = false; + for favorite in &mut favorites { + if *favorite == result.old_app_id && *favorite != result.new_app_id { + *favorite = result.new_app_id.clone(); + favorites_changed = true; + } + } + + if favorites_changed { + self.config.update_pinned( + favorites.clone(), + &Config::new(APP_ID, AppListConfig::VERSION).unwrap(), + ); + self.config.favorites = favorites; + } + + self.update_desktop_entries(); + self.sync_pinned_list_from_config(); + self.launcher_edit = None; + return self.close_popups(); + } + Err(error) => { + if let Some(edit) = self.launcher_edit.as_mut() { + edit.saving = false; + edit.error = Some(error); + } + } + }, Message::Activate(handle) => { if let Some(tx) = self.wayland_sender.as_ref() { let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle))); @@ -1520,6 +2055,9 @@ impl cosmic::Application for CosmicAppList { }, Message::ClosePopup => { if let Some(p) = self.popup.take() { + if p.popup_type == PopupType::LauncherEditor { + self.launcher_edit = None; + } return destroy_popup(p.id); } } @@ -1534,42 +2072,16 @@ impl cosmic::Application for CosmicAppList { } Message::ConfigUpdated(config) => { self.config = config; - // drain to active list - for item in self.pinned_list.drain(..) { - if !item.toplevels.is_empty() { - self.active_list.push(item); - } - } - - // pull back configured items into the favorites list - self.pinned_list = - find_desktop_entries(&self.desktop_entries, &self.config.favorites) - .zip(&self.config.favorites) - .map(|(de, original_id)| { - if let Some(p) = self - .active_list - .iter() - // match using heuristic id - .position(|dock_item| dock_item.desktop_info.id() == de.id()) - { - let mut d = self.active_list.remove(p); - // but use the id from the config - d.original_app_id.clone_from(original_id); - d - } else { - self.item_ctr += 1; - DockItem { - id: self.item_ctr, - toplevels: Vec::new(), - desktop_info: de.clone(), - original_app_id: original_id.clone(), - } - } - }) - .collect(); + self.update_desktop_entries(); + self.sync_pinned_list_from_config(); } Message::CloseRequested(id) => { - if Some(id) == self.popup.as_ref().map(|p| p.id) { + if let Some(popup) = &self.popup + && popup.id == id + { + if popup.popup_type == PopupType::LauncherEditor { + self.launcher_edit = None; + } self.popup = None; } if self.overflow_active_popup.is_some_and(|p| p == id) { @@ -1582,6 +2094,52 @@ impl cosmic::Application for CosmicAppList { Message::GpuRequest(gpus) => { self.gpus = gpus; } + Message::DockItemHover(id) => { + self.hovered_dock_item = id; + // Seed the animated center on the very first hover so + // the bell doesn't "fly in" from (0,0). + if self.anim_hover_center.is_none() + && let Some(hovered_id) = self.hovered_dock_item.as_ref() + && let Some(r) = self.rectangles.get(hovered_id) + { + self.anim_hover_center = Some(( + r.x + r.width / 2.0, + r.y + r.height / 2.0, + )); + } + } + Message::AnimTick(now) => { + // dt-based exponential smoothing: reach ~99% of the + // target in ~120ms at 60fps (about 7 ticks). + let dt = self + .anim_last_tick + .map(|prev| now.saturating_duration_since(prev).as_secs_f32()) + .unwrap_or(0.016) + .min(0.1); // clamp so a long pause doesn't snap + self.anim_last_tick = Some(now); + + // Intensity: target 1.0 when any icon is hovered, 0.0 else. + let intensity_target = if self.hovered_dock_item.is_some() { 1.0 } else { 0.0 }; + let tau = 0.060_f32; // time-constant (s); smaller = snappier + let alpha = 1.0 - (-dt / tau).exp(); + self.anim_hover_intensity += (intensity_target - self.anim_hover_intensity) * alpha; + + // Hovered-center smoothing: chase the real rect's center. + if let Some(hovered_id) = self.hovered_dock_item.as_ref() + && let Some(r) = self.rectangles.get(hovered_id) + { + let target = (r.x + r.width / 2.0, r.y + r.height / 2.0); + let current = self.anim_hover_center.unwrap_or(target); + self.anim_hover_center = Some(( + current.0 + (target.0 - current.0) * alpha, + current.1 + (target.1 - current.1) * alpha, + )); + } else if self.anim_hover_intensity < 0.01 { + // Nothing hovered + intensity faded out → forget the + // animated center so the next hover seeds fresh. + self.anim_hover_center = None; + } + } Message::OpenActive => { let create_new = self.overflow_active_popup.is_none(); let mut cmds = vec![self.close_popups()]; @@ -1763,7 +2321,9 @@ impl cosmic::Application for CosmicAppList { .filter(|(info, _)| self.is_on_current_monitor_and_workspace(info)) .any(|y| focused_item.contains(&y.0.foreign_toplevel)); - self.core + let dock_id = dock_item.id; + let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id)); + let tooltip = self.core .applet .applet_tooltip::( dock_item.as_icon( @@ -1776,6 +2336,7 @@ impl cosmic::Application for CosmicAppList { dot_radius, self.core.main_window_id().unwrap(), Some(&|info| self.is_on_current_monitor_and_workspace(info)), + icon_scale, ), dock_item .desktop_info @@ -1785,7 +2346,10 @@ impl cosmic::Application for CosmicAppList { self.popup.is_some(), Message::Surface, None, - ) + ); + cosmic::widget::mouse_area(tooltip) + .on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id)))) + .on_exit(Message::DockItemHover(None)) .into() }) .collect(); @@ -1836,6 +2400,10 @@ impl cosmic::Application for CosmicAppList { dot_radius, self.core.main_window_id().unwrap(), Some(&|info| self.is_on_current_monitor_and_workspace(info)), + // Yoda: no magnification on DnD-preview icons — these + // float around as the user drags, so static 1.0 is + // less visually confusing. + 1.0, ), ); } else if self.is_listening_for_dnd && self.pinned_list.is_empty() { @@ -1875,8 +2443,10 @@ impl cosmic::Application for CosmicAppList { .iter() .filter(|(info, _)| self.is_on_current_monitor_and_workspace(info)) .any(|y| focused_item.contains(&y.0.foreign_toplevel)); + let dock_id = dock_item.id; + let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id)); - self.core + let tooltip = self.core .applet .applet_tooltip( dock_item.as_icon( @@ -1889,6 +2459,7 @@ impl cosmic::Application for CosmicAppList { dot_radius, self.core.main_window_id().unwrap(), Some(&|info| self.is_on_current_monitor_and_workspace(info)), + icon_scale, ), dock_item .desktop_info @@ -1898,7 +2469,10 @@ impl cosmic::Application for CosmicAppList { self.popup.is_some(), Message::Surface, None, - ) + ); + cosmic::widget::mouse_area(tooltip) + .on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id)))) + .on_exit(Message::DockItemHover(None)) .into() }) .collect(); @@ -2179,6 +2753,19 @@ impl cosmic::Application for CosmicAppList { }), ); + if is_pinned && desktop_info.exec().is_some() { + content = content.push( + menu_button( + row![ + icon::icon(from_name("edit-symbolic").into()).size(16), + text::body(fl!("edit-launcher")) + ] + .spacing(8), + ) + .on_press(Message::EditLauncher(*id)), + ); + } + if !toplevels.is_empty() { content = content.push(divider::horizontal::light()); content = match toplevels.len() { @@ -2200,7 +2787,7 @@ impl cosmic::Application for CosmicAppList { //TODO: move style to libcosmic .class(theme::Container::custom(|theme| { let cosmic = theme.cosmic(); - let component = &cosmic.background.component; + let component = &cosmic.background(false).component; container::Style { icon_color: Some(component.on.into()), text_color: Some(component.on.into()), @@ -2225,6 +2812,84 @@ impl cosmic::Application for CosmicAppList { ) .into() } + PopupType::LauncherEditor => { + let Some(edit) = self.launcher_edit.as_ref() else { + return text::body("").into(); + }; + + let spacing = theme::spacing(); + let can_save = !edit.saving + && launcher_edit::validate_launcher_fields( + &edit.name, &edit.exec, &edit.icon, + ) + .is_ok(); + + let mut form = column![ + text::title4(fl!("edit-launcher")), + text_input("", edit.name.as_str()) + .label(fl!("launcher-name")) + .on_input(Message::LauncherNameChanged) + .on_submit(|_| Message::SaveLauncherEdit) + .width(Length::Fill) + .size(14), + text_input("", edit.exec.as_str()) + .label(fl!("launcher-command")) + .on_input(Message::LauncherExecChanged) + .on_submit(|_| Message::SaveLauncherEdit) + .width(Length::Fill) + .size(14), + launcher_icon_editor(edit), + ] + .spacing(spacing.space_s) + .width(Length::Fill); + + if let Some(error) = edit.error.as_ref() { + form = form.push(text::caption(error.as_str()).class( + cosmic::theme::Text::Color(theme.cosmic().destructive_color().into()), + )); + } + + let cancel = + button::custom(text::body(fl!("cancel")).center().width(Length::Fill)) + .on_press_maybe(if edit.saving { + None + } else { + Some(Message::CancelLauncherEdit) + }) + .padding([spacing.space_xxs, spacing.space_s]) + .width(142); + + let save = button::custom(text::body(fl!("save")).center().width(Length::Fill)) + .class(Button::Suggested) + .on_press_maybe(if can_save { + Some(Message::SaveLauncherEdit) + } else { + None + }) + .padding([spacing.space_xxs, spacing.space_s]) + .width(142); + + let actions = row![horizontal_space(), cancel, save] + .spacing(spacing.space_xxs) + .align_y(Alignment::Center); + + let content = column![form, actions] + .spacing(spacing.space_m) + .padding(spacing.space_m) + .width(Length::Fill); + + self.core + .applet + .popup_container(container(content).width(Length::Fill)) + .limits( + Limits::NONE + .min_width(480.) + .min_height(1.) + .max_width(520.) + .max_height(1000.), + ) + .into() + } PopupType::ToplevelList => match self.core.applet.anchor { PanelAnchor::Left | PanelAnchor::Right => { let mut content = @@ -2313,6 +2978,10 @@ impl cosmic::Application for CosmicAppList { dot_radius, id, Some(&|info| self.is_on_current_monitor_and_workspace(info)), + // Yoda: icons in the overflow popup are + // already smaller-grid — keep them at 1.0 + // so the popup doesn't reshuffle on hover. + 1.0, ), dock_item .desktop_info @@ -2421,6 +3090,10 @@ impl cosmic::Application for CosmicAppList { dot_radius, id, Some(&|info| self.is_on_current_monitor_and_workspace(info)), + // Yoda: popup icons stay at 1.0 (hover + // magnification is applied to the main + // dock row only). + 1.0, ), dock_item .desktop_info @@ -2479,7 +3152,21 @@ impl cosmic::Application for CosmicAppList { } fn subscription(&self) -> Subscription { + // Yoda: ~60fps animation ticks for the fisheye magnification. + // Only emitted when an animation is actually in progress (hover + // intensity >0 OR still fading out) — keeps the panel idle when + // the pointer is nowhere near the dock. + let anim_active = self.hovered_dock_item.is_some() + || self.anim_hover_intensity > 0.001; + let anim_subscription = if anim_active { + cosmic::iced::time::every(std::time::Duration::from_millis(16)) + .map(Message::AnimTick) + } else { + Subscription::none() + }; + Subscription::batch([ + anim_subscription, wayland_subscription().map(Message::Wayland), listen_with(|e, _, id| match e { cosmic::iced::core::Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/cosmic-app-list/src/icon_catalog.rs b/cosmic-app-list/src/icon_catalog.rs new file mode 100644 index 00000000..2cb05e1a --- /dev/null +++ b/cosmic-app-list/src/icon_catalog.rs @@ -0,0 +1,440 @@ +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet, VecDeque}, + fs, + path::{Component, Path, PathBuf}, +}; + +const FALLBACK_THEMES: &[&str] = &["Cosmic", "hicolor", "gnome", "Yaru"]; +const MAX_THEME_CHAIN: usize = 24; +const MAX_SCAN_DEPTH: usize = 5; +const MAX_CATALOG_ENTRIES: usize = 2_500; + +#[derive(Debug, Clone)] +pub struct IconCatalog { + pub theme: String, + pub entries: Vec, + pub truncated: bool, +} + +#[derive(Debug, Clone)] +pub struct IconCatalogEntry { + pub name: String, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +struct CandidateRank { + preferred: u8, + category: u8, + symbolic: u8, + theme_depth: usize, + extension: u8, +} + +impl Ord for CandidateRank { + fn cmp(&self, other: &Self) -> Ordering { + ( + self.preferred, + self.category, + self.symbolic, + self.theme_depth, + self.extension, + ) + .cmp(&( + other.preferred, + other.category, + other.symbolic, + other.theme_depth, + other.extension, + )) + } +} + +impl PartialOrd for CandidateRank { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Debug, Clone)] +struct Candidate { + name: String, + rank: CandidateRank, +} + +pub async fn load_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog { + build_icon_catalog(theme, preferred_icon) +} + +fn build_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog { + let theme = if theme.trim().is_empty() { + "Cosmic".to_string() + } else { + theme + }; + let preferred_name = icon_name_from_value(preferred_icon.trim()); + let theme_chain = theme_chain(&theme); + let mut candidates = HashMap::new(); + + for (theme_depth, theme_name) in theme_chain.iter().enumerate() { + for theme_dir in theme_dirs(theme_name) { + scan_icon_tree( + &theme_dir, + theme_depth, + preferred_name.as_deref(), + &mut candidates, + ); + } + } + + for pixmap_dir in pixmap_dirs() { + scan_icon_tree( + &pixmap_dir, + theme_chain.len() + 1, + preferred_name.as_deref(), + &mut candidates, + ); + } + + let mut entries = candidates.into_values().collect::>(); + entries.sort_by(|a, b| a.rank.cmp(&b.rank).then_with(|| a.name.cmp(&b.name))); + + let truncated = entries.len() > MAX_CATALOG_ENTRIES; + entries.truncate(MAX_CATALOG_ENTRIES); + + IconCatalog { + theme, + entries: entries + .into_iter() + .map(|candidate| IconCatalogEntry { + name: candidate.name, + }) + .collect(), + truncated, + } +} + +fn theme_chain(theme: &str) -> Vec { + let mut queue = VecDeque::from([theme.to_string()]); + let mut seen = HashSet::new(); + let mut chain = Vec::new(); + + while let Some(theme_name) = queue.pop_front() { + let key = theme_name.to_ascii_lowercase(); + if !seen.insert(key) { + continue; + } + + chain.push(theme_name.clone()); + if chain.len() >= MAX_THEME_CHAIN { + break; + } + + for parent in read_theme_inherits(&theme_name) { + queue.push_back(parent); + } + } + + for fallback in FALLBACK_THEMES { + if chain.len() >= MAX_THEME_CHAIN { + break; + } + let key = fallback.to_ascii_lowercase(); + if seen.insert(key) { + chain.push((*fallback).to_string()); + } + } + + chain +} + +fn read_theme_inherits(theme: &str) -> Vec { + theme_dirs(theme) + .into_iter() + .find_map(|dir| fs::read_to_string(dir.join("index.theme")).ok()) + .map(|contents| parse_inherits(&contents)) + .unwrap_or_default() +} + +fn parse_inherits(contents: &str) -> Vec { + let mut in_icon_theme = false; + + for raw_line in contents.lines() { + let line = raw_line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + if line.starts_with('[') && line.ends_with(']') { + in_icon_theme = line == "[Icon Theme]"; + continue; + } + + if in_icon_theme && let Some(value) = line.strip_prefix("Inherits=") { + return value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .collect(); + } + } + + Vec::new() +} + +fn scan_icon_tree( + root: &Path, + theme_depth: usize, + preferred_name: Option<&str>, + candidates: &mut HashMap, +) { + let mut stack = vec![(root.to_path_buf(), 0usize)]; + + while let Some((dir, depth)) = stack.pop() { + if depth > MAX_SCAN_DEPTH { + continue; + } + + let Ok(entries) = fs::read_dir(&dir) else { + continue; + }; + + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + + if file_type.is_dir() { + stack.push((path, depth + 1)); + } else if (file_type.is_file() || file_type.is_symlink()) + && let Some(candidate) = candidate_from_path(&path, theme_depth, preferred_name) + { + insert_candidate(candidates, candidate); + } + } + } +} + +fn insert_candidate(candidates: &mut HashMap, candidate: Candidate) { + let key = candidate.name.to_ascii_lowercase(); + match candidates.get_mut(&key) { + Some(existing) if candidate.rank < existing.rank => { + *existing = candidate; + } + None => { + candidates.insert(key, candidate); + } + _ => {} + } +} + +fn candidate_from_path( + path: &Path, + theme_depth: usize, + preferred_name: Option<&str>, +) -> Option { + let extension = extension_rank(path)?; + let name = path.file_stem()?.to_str()?.trim(); + if name.is_empty() { + return None; + } + + let symbolic = u8::from(name.ends_with("-symbolic") || path_has_component(path, "symbolic")); + let preferred = u8::from(preferred_name != Some(name)); + + Some(Candidate { + name: name.to_string(), + rank: CandidateRank { + preferred, + category: category_rank(path), + symbolic, + theme_depth, + extension, + }, + }) +} + +fn extension_rank(path: &Path) -> Option { + match path.extension()?.to_str()?.to_ascii_lowercase().as_str() { + "svg" => Some(0), + "png" => Some(1), + "xpm" => Some(2), + _ => None, + } +} + +fn category_rank(path: &Path) -> u8 { + for component in path.components().filter_map(component_str) { + match component { + "apps" | "applications" => return 0, + "categories" => return 1, + "places" => return 2, + "devices" => return 3, + "mimetypes" => return 4, + "actions" => return 5, + "status" => return 6, + _ => {} + } + } + + 7 +} + +fn path_has_component(path: &Path, needle: &str) -> bool { + path.components() + .filter_map(component_str) + .any(|component| component == needle) +} + +fn component_str(component: Component<'_>) -> Option<&str> { + component.as_os_str().to_str() +} + +fn icon_name_from_value(value: &str) -> Option { + if value.is_empty() { + return None; + } + + let path = Path::new(value); + if value.contains('/') { + return path + .file_stem() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned); + } + + path.file_stem() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned) + .or_else(|| Some(value.to_string())) +} + +fn theme_dirs(theme: &str) -> Vec { + icon_base_dirs() + .into_iter() + .map(|base| base.join(theme)) + .filter(|path| path.is_dir()) + .collect() +} + +fn icon_base_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Some(home) = std::env::home_dir() { + push_existing_unique(&mut dirs, home.join(".icons")); + } + + if let Some(data_home) = xdg_data_home() { + push_existing_unique(&mut dirs, data_home.join("icons")); + } + + for data_dir in xdg_data_dirs() { + push_existing_unique(&mut dirs, data_dir.join("icons")); + } + + dirs +} + +fn pixmap_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Some(data_home) = xdg_data_home() { + push_existing_unique(&mut dirs, data_home.join("pixmaps")); + } + + for data_dir in xdg_data_dirs() { + push_existing_unique(&mut dirs, data_dir.join("pixmaps")); + } + + push_existing_unique(&mut dirs, PathBuf::from("/usr/share/pixmaps")); + dirs +} + +fn xdg_data_home() -> Option { + std::env::var_os("XDG_DATA_HOME") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .or_else(|| std::env::home_dir().map(|home| home.join(".local/share"))) +} + +fn xdg_data_dirs() -> Vec { + std::env::var_os("XDG_DATA_DIRS") + .filter(|value| !value.is_empty()) + .map(|value| std::env::split_paths(&value).collect()) + .unwrap_or_else(|| { + vec![ + PathBuf::from("/usr/local/share"), + PathBuf::from("/usr/share"), + ] + }) +} + +fn push_existing_unique(dirs: &mut Vec, path: PathBuf) { + if path.exists() && !dirs.iter().any(|existing| existing == &path) { + dirs.push(path); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_inherits_only_from_icon_theme_section() { + let contents = r#" +[Other] +Inherits=Wrong + +[Icon Theme] +Name=Demo +Inherits=Cosmic, hicolor , Adwaita +"#; + + assert_eq!(parse_inherits(contents), ["Cosmic", "hicolor", "Adwaita"]); + } + + #[test] + fn normalizes_icon_names_from_plain_names_and_paths() { + assert_eq!(icon_name_from_value("firefox").as_deref(), Some("firefox")); + assert_eq!( + icon_name_from_value("/usr/share/icons/hicolor/scalable/apps/firefox.svg").as_deref(), + Some("firefox") + ); + } + + #[test] + fn keeps_the_best_duplicate_candidate() { + let mut candidates = HashMap::new(); + insert_candidate( + &mut candidates, + Candidate { + name: "demo".to_string(), + rank: CandidateRank { + preferred: 1, + category: 7, + symbolic: 1, + theme_depth: 4, + extension: 2, + }, + }, + ); + insert_candidate( + &mut candidates, + Candidate { + name: "demo".to_string(), + rank: CandidateRank { + preferred: 0, + category: 0, + symbolic: 0, + theme_depth: 0, + extension: 0, + }, + }, + ); + + assert_eq!(candidates["demo"].rank.preferred, 0); + assert_eq!(candidates["demo"].rank.category, 0); + } +} diff --git a/cosmic-app-list/src/launcher_edit.rs b/cosmic-app-list/src/launcher_edit.rs new file mode 100644 index 00000000..c354e1e6 --- /dev/null +++ b/cosmic-app-list/src/launcher_edit.rs @@ -0,0 +1,378 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + env, + ffi::OsString, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use tokio::fs; + +#[derive(Clone, Debug)] +pub struct LauncherEditRequest { + pub current_app_id: String, + pub source_path: PathBuf, + pub name: String, + pub exec: String, + pub icon: String, + pub terminal: bool, + pub replace_localized_name: bool, + pub disable_dbus_activation: bool, +} + +#[derive(Clone, Debug)] +pub struct LauncherEditResult { + pub old_app_id: String, + pub new_app_id: String, + pub path: PathBuf, +} + +#[derive(Clone, Debug)] +struct ValidLauncherEdit { + current_app_id: String, + source_path: PathBuf, + name: String, + exec: String, + icon: String, + terminal: bool, + replace_localized_name: bool, + disable_dbus_activation: bool, +} + +pub fn validate_launcher_fields(name: &str, exec: &str, icon: &str) -> Result<(), String> { + validate_required("Name", name)?; + validate_required("Command", exec)?; + validate_optional("Icon", icon)?; + Ok(()) +} + +pub async fn save_launcher_edit( + request: LauncherEditRequest, +) -> Result { + let request = validate_request(request)?; + let (new_app_id, target_path) = + target_launcher_path(&request.current_app_id, &request.source_path)?; + + let source = read_source_desktop_entry(&request).await?; + let rendered = render_editable_desktop_entry(&source, &request); + + let Some(parent) = target_path.parent() else { + return Err("Desktop file target has no parent directory".to_string()); + }; + + fs::create_dir_all(parent) + .await + .map_err(|err| format!("Could not create applications directory: {err}"))?; + + let tmp_path = temporary_path(&target_path)?; + fs::write(&tmp_path, rendered) + .await + .map_err(|err| format!("Could not write temporary desktop file: {err}"))?; + + if let Err(err) = fs::rename(&tmp_path, &target_path).await { + let _ = fs::remove_file(&tmp_path).await; + return Err(format!("Could not install desktop file: {err}")); + } + + Ok(LauncherEditResult { + old_app_id: request.current_app_id, + new_app_id, + path: target_path, + }) +} + +fn validate_request(request: LauncherEditRequest) -> Result { + let name = validate_required("Name", &request.name)?; + let exec = validate_required("Command", &request.exec)?; + let icon = validate_optional("Icon", &request.icon)?; + + Ok(ValidLauncherEdit { + current_app_id: request.current_app_id, + source_path: request.source_path, + name, + exec, + icon, + terminal: request.terminal, + replace_localized_name: request.replace_localized_name, + disable_dbus_activation: request.disable_dbus_activation, + }) +} + +fn validate_required(label: &str, value: &str) -> Result { + let value = validate_optional(label, value)?; + if value.is_empty() { + return Err(format!("{label} cannot be empty")); + } + Ok(value) +} + +fn validate_optional(label: &str, value: &str) -> Result { + if value.contains('\0') || value.contains('\n') || value.contains('\r') { + return Err(format!("{label} cannot contain line breaks")); + } + Ok(value.trim().to_string()) +} + +async fn read_source_desktop_entry(request: &ValidLauncherEdit) -> Result { + if request.source_path.as_os_str().is_empty() { + return Ok(minimal_desktop_entry()); + } + + match fs::read_to_string(&request.source_path).await { + Ok(source) => Ok(source), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(minimal_desktop_entry()), + Err(err) => Err(format!("Could not read source desktop file: {err}")), + } +} + +fn minimal_desktop_entry() -> String { + "[Desktop Entry]\nType=Application\n".to_string() +} + +fn user_applications_dir() -> Result { + if let Some(xdg_data_home) = env::var_os("XDG_DATA_HOME").filter(|value| !value.is_empty()) { + return Ok(PathBuf::from(xdg_data_home).join("applications")); + } + + let Some(home) = env::var_os("HOME").filter(|value| !value.is_empty()) else { + return Err("Neither XDG_DATA_HOME nor HOME is set".to_string()); + }; + + Ok(PathBuf::from(home).join(".local/share/applications")) +} + +fn target_launcher_path(app_id: &str, source_path: &Path) -> Result<(String, PathBuf), String> { + let applications_dir = user_applications_dir()?; + if source_path.starts_with(&applications_dir) { + return Ok((app_id.to_string(), source_path.to_path_buf())); + } + + let desktop_id = normalize_desktop_id(app_id)?; + Ok(( + desktop_id.clone(), + applications_dir.join(format!("{desktop_id}.desktop")), + )) +} + +fn normalize_desktop_id(app_id: &str) -> Result { + if app_id.contains('\0') || app_id.contains('\n') || app_id.contains('\r') { + return Err("Desktop ID cannot contain line breaks".to_string()); + } + + let desktop_id = app_id + .chars() + .map(|c| if c == '/' { '-' } else { c }) + .collect::(); + + if desktop_id.trim().is_empty() { + return Err("Desktop ID cannot be empty".to_string()); + } + + Ok(desktop_id) +} + +fn temporary_path(target_path: &Path) -> Result { + let Some(file_name) = target_path.file_name() else { + return Err("Desktop file target has no filename".to_string()); + }; + + let mut tmp_file_name = OsString::from("."); + tmp_file_name.push(file_name); + tmp_file_name.push(format!(".tmp-{}", std::process::id())); + + Ok(target_path.with_file_name(tmp_file_name)) +} + +fn render_editable_desktop_entry(source: &str, request: &ValidLauncherEdit) -> String { + let mut updates = vec![ + ("Type", "Application".to_string()), + ("Name", request.name.clone()), + ("Exec", request.exec.clone()), + ("Icon", request.icon.clone()), + ( + "Terminal", + if request.terminal { "true" } else { "false" }.to_string(), + ), + ("X-COSMIC-UserEditable", "true".to_string()), + ("X-COSMIC-SourceDesktopId", request.current_app_id.clone()), + ]; + + if !request.source_path.as_os_str().is_empty() { + updates.push(( + "X-COSMIC-SourceDesktopPath", + request.source_path.to_string_lossy().to_string(), + )); + } + + if request.disable_dbus_activation { + updates.push(("DBusActivatable", "false".to_string())); + } + + set_desktop_entry_keys( + source, + &updates, + request.replace_localized_name, + request.disable_dbus_activation, + ) +} + +fn set_desktop_entry_keys( + source: &str, + updates: &[(&str, String)], + replace_localized_name: bool, + remove_try_exec: bool, +) -> String { + let mut out = Vec::new(); + let mut seen = vec![false; updates.len()]; + let mut in_desktop_entry = false; + let mut saw_desktop_entry = false; + + for line in source.lines() { + if let Some(section) = section_name(line) { + if in_desktop_entry { + insert_missing_keys(&mut out, updates, &seen); + } + + in_desktop_entry = section == "Desktop Entry"; + saw_desktop_entry |= in_desktop_entry; + if in_desktop_entry { + seen.fill(false); + } + + out.push(line.to_string()); + continue; + } + + if in_desktop_entry && let Some(key) = desktop_entry_key(line) { + if replace_localized_name && key.starts_with("Name[") { + continue; + } + + if remove_try_exec && key == "TryExec" { + continue; + } + + if let Some(index) = updates + .iter() + .position(|(update_key, _)| *update_key == key) + { + out.push(format!("{}={}", updates[index].0, updates[index].1)); + seen[index] = true; + continue; + } + } + + out.push(line.to_string()); + } + + if in_desktop_entry { + insert_missing_keys(&mut out, updates, &seen); + } + + if !saw_desktop_entry { + let mut with_desktop_entry = Vec::with_capacity(out.len() + updates.len() + 2); + with_desktop_entry.push("[Desktop Entry]".to_string()); + insert_missing_keys( + &mut with_desktop_entry, + updates, + &vec![false; updates.len()], + ); + if !out.is_empty() { + with_desktop_entry.push(String::new()); + with_desktop_entry.extend(out); + } + out = with_desktop_entry; + } + + let mut rendered = out.join("\n"); + rendered.push('\n'); + rendered +} + +fn insert_missing_keys(out: &mut Vec, updates: &[(&str, String)], seen: &[bool]) { + for (index, (key, value)) in updates.iter().enumerate() { + if !seen[index] { + out.push(format!("{key}={value}")); + } + } +} + +fn section_name(line: &str) -> Option<&str> { + let trimmed = line.trim(); + trimmed + .strip_prefix('[') + .and_then(|value| value.strip_suffix(']')) +} + +fn desktop_entry_key(line: &str) -> Option<&str> { + let line = line.trim_start(); + if line.starts_with('#') || line.starts_with(';') { + return None; + } + + line.split_once('=').map(|(key, _)| key.trim_end()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn request() -> ValidLauncherEdit { + ValidLauncherEdit { + current_app_id: "org.example.App".to_string(), + source_path: PathBuf::from("/usr/share/applications/org.example.App.desktop"), + name: "Example".to_string(), + exec: "example --new".to_string(), + icon: "example-custom".to_string(), + terminal: false, + replace_localized_name: true, + disable_dbus_activation: true, + } + } + + #[test] + fn updates_desktop_entry_without_dropping_action_groups() { + let source = "\ +[Desktop Entry] +Type=Application +Name=Old +Name[fr]=Ancien +Exec=old +TryExec=old +Icon=old +DBusActivatable=true +Actions=new-window; + +[Desktop Action new-window] +Name=New Window +Exec=old --new-window +"; + + let rendered = render_editable_desktop_entry(source, &request()); + + assert!(rendered.contains("Name=Example\n")); + assert!(!rendered.contains("Name[fr]=")); + assert!(rendered.contains("Exec=example --new\n")); + assert!(rendered.contains("Icon=example-custom\n")); + assert!(rendered.contains("DBusActivatable=false\n")); + assert!(!rendered.contains("TryExec=")); + assert!(rendered.contains("[Desktop Action new-window]\n")); + assert!(rendered.contains("Exec=old --new-window\n")); + } + + #[test] + fn preserves_localized_names_when_name_is_unchanged() { + let mut edit = request(); + edit.replace_localized_name = false; + + let rendered = render_editable_desktop_entry( + "[Desktop Entry]\nName=Example\nName[fr]=Exemple\nExec=old\n", + &edit, + ); + + assert!(rendered.contains("Name=Example\n")); + assert!(rendered.contains("Name[fr]=Exemple\n")); + } +} diff --git a/cosmic-app-list/src/lib.rs b/cosmic-app-list/src/lib.rs index 51079cc4..f6893335 100644 --- a/cosmic-app-list/src/lib.rs +++ b/cosmic-app-list/src/lib.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only mod app; +mod icon_catalog; +mod launcher_edit; mod localize; mod wayland_handler; mod wayland_subscription; diff --git a/cosmic-app-list/src/wayland_handler.rs b/cosmic-app-list/src/wayland_handler.rs index 03c16973..d27409dc 100644 --- a/cosmic-app-list/src/wayland_handler.rs +++ b/cosmic-app-list/src/wayland_handler.rs @@ -388,7 +388,10 @@ impl CaptureData { }, ) .unwrap(); - self.conn.flush().unwrap(); + if let Err(err) = self.conn.flush() { + tracing::error!("Wayland flush failed during screencopy session create: {err}"); + return None; + } let formats = session .wait_while(|data| data.formats.is_none()) @@ -437,7 +440,10 @@ impl CaptureData { session: capture_session.clone(), }, ); - self.conn.flush().unwrap(); + if let Err(err) = self.conn.flush() { + tracing::error!("Wayland flush failed during screencopy capture: {err}"); + return None; + } // TODO: wait for server to release buffer? let res = session @@ -709,7 +715,10 @@ pub(crate) fn wayland_handler( if app_data.exit { break; } - event_loop.dispatch(None, &mut app_data).unwrap(); + if let Err(err) = event_loop.dispatch(None, &mut app_data) { + tracing::error!("Wayland event loop terminated: {err}"); + break; + } } } diff --git a/cosmic-applet-a11y/Cargo.toml b/cosmic-applet-a11y/Cargo.toml index a5bb12b4..711b4ec6 100644 --- a/cosmic-applet-a11y/Cargo.toml +++ b/cosmic-applet-a11y/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-a11y" -version = "1.0.2" +version = "1.0.15" edition = "2024" [dependencies] @@ -9,7 +9,7 @@ cctk.workspace = true cosmic-protocols.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true rust-embed.workspace = true tokio.workspace = true tracing-log.workspace = true @@ -17,9 +17,7 @@ tracing-subscriber.workspace = true tracing.workspace = true [dependencies.cosmic-settings-a11y-manager-subscription] -git = "https://github.com/pop-os/cosmic-settings" -# path = "../../cosmic-settings/subscriptions/a11y-manager" +path = "../../cosmic-settings/subscriptions/a11y-manager" [dependencies.cosmic-settings-accessibility-subscription] -git = "https://github.com/pop-os/cosmic-settings" -# path = "../../cosmic-settings/subscriptions/accessibility" +path = "../../cosmic-settings/subscriptions/accessibility" diff --git a/cosmic-applet-a11y/i18n/ja/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/ja/cosmic_applet_a11y.ftl index 75249579..564e0d4f 100644 --- a/cosmic-applet-a11y/i18n/ja/cosmic_applet_a11y.ftl +++ b/cosmic-applet-a11y/i18n/ja/cosmic_applet_a11y.ftl @@ -2,5 +2,5 @@ screen-reader = スクリーンリーダー magnifier = 拡大鏡 settings = アクセシビリティ設定… invert-colors = 色を反転 -filter-colors = 色のフィルター +filter-colors = カラーフィルター high-contrast = ハイコントラスト diff --git a/cosmic-applet-a11y/i18n/kmr/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/kmr/cosmic_applet_a11y.ftl index e69de29b..4c772791 100644 --- a/cosmic-applet-a11y/i18n/kmr/cosmic_applet_a11y.ftl +++ b/cosmic-applet-a11y/i18n/kmr/cosmic_applet_a11y.ftl @@ -0,0 +1,6 @@ +screen-reader = Xwînerê dîmenderê +magnifier = Mezinker +invert-colors = Rengan berevajî bike +settings = Sazkariyên gihîştinê... +filter-colors = Rengan parzûn bike +high-contrast = Dijbiriya bilind diff --git a/cosmic-applet-a11y/src/app.rs b/cosmic-applet-a11y/src/app.rs index ad5f2a1e..6a328339 100644 --- a/cosmic-applet-a11y/src/app.rs +++ b/cosmic-applet-a11y/src/app.rs @@ -26,10 +26,9 @@ use cosmic::{ }; use cosmic_settings_a11y_manager_subscription::{ - self as cosmic_a11y_manager, AccessibilityEvent, AccessibilityRequest, ColorFilter, + AccessibilityEvent, AccessibilityRequest, ColorFilter, }; use cosmic_settings_accessibility_subscription::{self as accessibility}; -use std::sync::LazyLock; use tokio::sync::mpsc::UnboundedSender; pub fn run() -> cosmic::iced::Result { @@ -52,6 +51,7 @@ struct CosmicA11yApplet { } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { TogglePopup, CloseRequested(window::Id), diff --git a/cosmic-applet-a11y/src/backend/wayland/mod.rs b/cosmic-applet-a11y/src/backend/wayland/mod.rs index fa9796f0..12340588 100644 --- a/cosmic-applet-a11y/src/backend/wayland/mod.rs +++ b/cosmic-applet-a11y/src/backend/wayland/mod.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use anyhow; -use cctk::sctk::reexports::calloop::{self, channel::SyncSender}; +use cctk::sctk::reexports::calloop::{self}; use cosmic::iced::{ self, Subscription, - futures::{self, SinkExt, StreamExt, channel::mpsc}, + futures::{self, SinkExt}, stream, }; -use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1::Filter; use cosmic_settings_a11y_manager_subscription::{ self as thread, AccessibilityEvent, AccessibilityRequest, }; diff --git a/cosmic-applet-audio/Cargo.toml b/cosmic-applet-audio/Cargo.toml index 96d37cad..3e67d3f5 100644 --- a/cosmic-applet-audio/Cargo.toml +++ b/cosmic-applet-audio/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "cosmic-applet-audio" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] +futures.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true -mpris2-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" } -# mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" } +cosmic.workspace = true +mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" } rust-embed.workspace = true serde.workspace = true tokio.workspace = true @@ -19,7 +19,9 @@ tracing.workspace = true url = "2" urlencoding = "2.1.3" zbus.workspace = true +zlink.workspace = true +intmap = "3.1.3" -[dependencies.cosmic-settings-sound-subscription] -git = "https://github.com/pop-os/cosmic-settings" -# path = "../../cosmic-settings/subscriptions/sound" +[dependencies.cosmic-settings-audio-client] +path = "../../cosmic-settings-daemon/audio-client" +features = ["codec"] diff --git a/cosmic-applet-audio/i18n/hu/cosmic_applet_audio.ftl b/cosmic-applet-audio/i18n/hu/cosmic_applet_audio.ftl index c26f9fb0..b3fdb11d 100644 --- a/cosmic-applet-audio/i18n/hu/cosmic_applet_audio.ftl +++ b/cosmic-applet-audio/i18n/hu/cosmic_applet_audio.ftl @@ -2,6 +2,6 @@ output = Kimenet input = Bemenet show-media-controls = Médiavezérlők megjelenítése a panelen sound-settings = Hangbeállítások… -disconnected = PulseAudio nincs csatlakozva -no-device = Nincs kiválasztott eszköz +disconnected = A PulseAudio-kapcsolat megszakadt +no-device = Nincs eszköz kiválasztva unknown-artist = Ismeretlen diff --git a/cosmic-applet-audio/i18n/kmr/cosmic_applet_audio.ftl b/cosmic-applet-audio/i18n/kmr/cosmic_applet_audio.ftl index e69de29b..373ab157 100644 --- a/cosmic-applet-audio/i18n/kmr/cosmic_applet_audio.ftl +++ b/cosmic-applet-audio/i18n/kmr/cosmic_applet_audio.ftl @@ -0,0 +1,7 @@ +output = Deran +input = Ketan +unknown-artist = Nenas +sound-settings = Sazkariyên dengê... +disconnected = Girêdana PulseAudio qut bû +no-device = Tu amûr nehatine hilbijartin +show-media-controls = Kontrolên mediyayê li ser destgehê nîşan bide diff --git a/cosmic-applet-audio/src/lib.rs b/cosmic-applet-audio/src/lib.rs index 5f49d0f9..bd62dfbd 100644 --- a/cosmic-applet-audio/src/lib.rs +++ b/cosmic-applet-audio/src/lib.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only mod localize; +mod model; mod mouse_area; use crate::localize::localize; use config::{AudioAppletConfig, amplification_sink, amplification_source}; use cosmic::{ - Element, Renderer, Task, Theme, app, + Apply, Element, Renderer, Task, Theme, app, applet::{ column as applet_column, cosmic_panel_config::PanelAnchor, @@ -18,18 +19,24 @@ use cosmic::{ cosmic_config::CosmicConfigEntry, cosmic_theme::Spacing, iced::{ - self, Alignment, Length, Subscription, + self, Alignment, Length, Rectangle, Subscription, futures::StreamExt, widget::{self, column, row, slider}, window, }, surface, theme, - widget::{Row, button, container, divider, icon, space, text, toggler}, + widget::{ + Row, button, container, divider, icon, + rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription}, + space, text, toggler, + }, }; -use cosmic_settings_sound_subscription as css; +use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy}; +use futures::SinkExt; use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup}; use mpris_subscription::{MprisRequest, MprisUpdate}; use mpris2_zbus::player::PlaybackStatus; +use std::{cell::RefCell, rc::Rc, sync::Arc}; mod config; mod mpris_subscription; @@ -50,8 +57,10 @@ pub struct Audio { core: cosmic::app::Core, /// Track the applet's popup window. popup: Option, - /// The model from cosmic-settings for managing pipewire devices. - model: css::Model, + /// Varlink connection to `com.system76.CosmicSettings.Audio`. + audio_client: Option>>, + /// Known audio device state + model: model::Model, /// Whether to expand the revealer of a source or sink device. is_open: IsOpen, /// Max slider volume for the sink device, as determined by the amplification property. @@ -68,12 +77,14 @@ pub struct Audio { player_status: Option, /// Used to request an activation token for opening cosmic-settings. token_tx: Option>, + rectangle_tracker: Option>, + rectangle: Option, } impl Audio { fn output_icon_name(&self) -> &'static str { - let volume = self.model.sink_volume; - let mute = self.model.sink_mute; + let volume = self.model.active_sink.volume; + let mute = self.model.active_sink.mute; if mute || volume == 0 { "audio-volume-muted-symbolic" } else if volume < 33 { @@ -88,8 +99,8 @@ impl Audio { } fn input_icon_name(&self) -> &'static str { - let volume = self.model.source_volume; - let mute = self.model.source_mute; + let volume = self.model.active_source.volume; + let mute = self.model.active_source.mute; if mute || volume == 0 { "microphone-sensitivity-muted-symbolic" } else if volume < 33 { @@ -110,8 +121,10 @@ enum IsOpen { Input, } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub enum Message { + /// Connection to `com.system76.CosmicSettings`. + Client(Arc), Ignore, SetSinkVolume(u32), SetSourceVolume(u32), @@ -129,8 +142,9 @@ pub enum Message { MprisRequest(MprisRequest), Token(TokenUpdate), OpenSettings, - Subscription(css::Message), + Subscription(audio_client::Event), Surface(surface::Action), + Rectangle(RectangleUpdate), } // TODO @@ -242,15 +256,9 @@ impl cosmic::Application for Audio { const APP_ID: &'static str = "com.system76.CosmicAppletAudio"; fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task) { - let mut model = css::Model::default(); - model.unplugged_text = "Unplugged".into(); - model.hd_audio_text = "HD Audio".into(); - model.usb_audio_text = "USB Audio".into(); - ( Self { core, - model, ..Default::default() }, Task::none(), @@ -271,6 +279,14 @@ impl cosmic::Application for Audio { fn update(&mut self, message: Message) -> app::Task { match message { + Message::Rectangle(u) => match u { + RectangleUpdate::Rectangle(r) => { + self.rectangle = Some(r.1); + } + RectangleUpdate::Init(tracker) => { + self.rectangle_tracker.replace(tracker); + } + }, Message::Ignore => {} Message::TogglePopup => { if let Some(p) = self.popup.take() { @@ -291,14 +307,21 @@ impl cosmic::Application for Audio { (100, &[][..]) }; - let popup_settings = self.core.applet.get_popup_settings( + let mut popup_settings = self.core.applet.get_popup_settings( self.core.main_window_id().unwrap(), new_id, None, None, None, ); - + if let Some(r) = self.rectangle { + popup_settings.positioner.anchor_rect = Rectangle { + x: r.x as i32, + y: r.y as i32, + width: r.width as i32, + height: r.height as i32, + }; + } return get_popup(popup_settings); } } @@ -318,52 +341,74 @@ impl cosmic::Application for Audio { } } Message::Subscription(message) => { - return self - .model - .update(message) - .map(|message| Message::Subscription(message).into()); + self.model.update(message); } Message::SetDefaultSink(pos) => { - return self - .model - .set_default_sink(pos) - .map(|message| Message::Subscription(message).into()); + if let Some(&pos) = self.model.sinks.sorted_index.get(pos) + && let Some(&node_id) = self.model.sinks.id.get(pos as usize) + && let Some(client) = self.audio_client.as_mut() + { + futures::executor::block_on(async { + _ = client.borrow_mut().conn.set_default(node_id, true).await; + }); + } } Message::SetDefaultSource(pos) => { - return self - .model - .set_default_source(pos) - .map(|message| Message::Subscription(message).into()); + if let Some(&pos) = self.model.sources.sorted_index.get(pos) + && let Some(&node_id) = self.model.sources.id.get(pos as usize) + && let Some(client) = self.audio_client.as_mut() + { + futures::executor::block_on(async { + _ = client.borrow_mut().conn.set_default(node_id, true).await; + }); + } } - Message::ToggleSinkMute => self.model.toggle_sink_mute(), + Message::ToggleSinkMute => { + if let Some(ref mut client) = self.audio_client { + futures::executor::block_on(async { + _ = client.borrow_mut().conn.sink_mute_toggle().await; + }); + } + } - Message::ToggleSourceMute => self.model.toggle_source_mute(), + Message::ToggleSourceMute => { + if let Some(ref mut client) = self.audio_client { + futures::executor::block_on(async { + _ = client.borrow_mut().conn.source_mute_toggle().await; + }); + } + } Message::SetSinkVolume(volume) => { - return self - .model - .set_sink_volume(volume) - .map(|message| Message::Subscription(message).into()); + if let Some(ref mut client) = self.audio_client { + self.model.active_sink.volume = volume; + self.model.active_sink.volume_text = volume.to_string(); + futures::executor::block_on(async { + _ = client.borrow_mut().conn.set_sink_volume(volume).await; + }); + } } Message::SetSourceVolume(volume) => { - return self - .model - .set_source_volume(volume) - .map(|message| Message::Subscription(message).into()); + if let Some(ref mut client) = self.audio_client { + self.model.active_source.volume = volume; + self.model.active_source.volume_text = volume.to_string(); + futures::executor::block_on(async { + _ = client.borrow_mut().conn.set_source_volume(volume).await; + }); + } } Message::ToggleMediaControlsInTopPanel(enabled) => { self.config.show_media_controls_in_top_panel = enabled; if let Ok(helper) = cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION) + && let Err(err) = self.config.write_entry(&helper) { - if let Err(err) = self.config.write_entry(&helper) { - tracing::error!(?err, "Error writing config"); - } + tracing::error!(?err, "Error writing config"); } } Message::CloseRequested(id) => { @@ -461,6 +506,12 @@ impl cosmic::Application for Audio { cosmic::app::Action::Surface(a), )); } + Message::Client(client) => { + if let Some(client) = Arc::into_inner(client) { + self.audio_client = Some(Rc::new(RefCell::new(client))); + self.model = model::Model::default(); + } + } } Task::none() @@ -476,7 +527,50 @@ impl cosmic::Application for Audio { }), mpris_subscription::mpris_subscription(0).map(Message::Mpris), activation_token_subscription(0).map(Message::Token), - Subscription::run(|| css::watch().map(Message::Subscription)), + Subscription::run(|| { + iced::stream::channel( + 1, + move |mut emitter: futures::channel::mpsc::Sender<_>| async move { + loop { + let mut client = match audio_client::connect().await { + Ok(client) => client, + Err(why) => { + if let zlink::Error::Io(ref why) = why + && why.kind() == std::io::ErrorKind::NotFound + { + tracing::error!( + "cosmic-settings-daemon varlink service not found." + ); + } else { + tracing::error!( + ?why, + "failed to connect to cosmic-settings's varlink service" + ); + } + + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + continue; + } + }; + + if let Ok(Ok(mut stream)) = client.recv_events().await { + _ = emitter.send(Message::Client(Arc::new(client))).await; + while let Some(message) = stream.next().await { + match message { + Ok(event) => { + _ = emitter.send(Message::Subscription(event)).await; + } + Err(why) => { + tracing::error!(?why, "event error"); + } + } + } + } + } + }, + ) + }), + rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)), ]) } @@ -487,63 +581,94 @@ impl cosmic::Application for Audio { .icon_button(self.output_icon_name()) .on_press_down(Message::TogglePopup); - const WHEEL_STEP: f32 = 5.0; // 5% per wheel event + const WHEEL_STEP: f32 = 5.0; // 5% par cran logique + // Wayland axis_v120 envoie un cran physique en rafale de plusieurs + // ScrollDelta::Pixels (5–8 events ~15–20px), pour ~120px par cran. On + // accumule ces sub-events dans un thread_local et on n'émet qu'au + // passage d'un seuil — sinon `signum()` faisait croire que chaque + // sub-event = un cran, et un seul cran physique faisait chuter le + // volume jusqu'à 0 ("coupe le son"). + const PIXEL_THRESHOLD: f32 = 15.0; // px par cran logique + std::thread_local! { + static PIXEL_ACC: std::cell::Cell = const { std::cell::Cell::new(0.0) }; + } let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| { let scroll_vector = match delta { - iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1 - iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1 + iced::mouse::ScrollDelta::Lines { y, .. } => { + PIXEL_ACC.with(|a| a.set(0.0)); + y * WHEEL_STEP + } + iced::mouse::ScrollDelta::Pixels { y, .. } => { + PIXEL_ACC.with(|acc_cell| { + let acc = acc_cell.get() + y; + let steps = (acc / PIXEL_THRESHOLD).trunc(); + acc_cell.set(acc - steps * PIXEL_THRESHOLD); + steps * WHEEL_STEP + }) + } }; if scroll_vector == 0.0 { return Message::Ignore; } - let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64)) + let new_volume = (self.model.active_sink.volume as f64 + (scroll_vector as f64)) .clamp(0.0, self.max_sink_volume as f64); - Message::SetSinkVolume(new_volume as u32) + Message::SetSinkVolume(new_volume.round() as u32) }); + let mut has_playback_buttons = false; let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| { // if we have a configure for width and height, we're in a overflow popup c.width > 0. && c.height > 0. })) .then(|| self.playback_buttons()); - self.core - .applet - .autosize_window( - if let Some(playback_buttons) = playback_buttons - && !playback_buttons.is_empty() - { - match self.core.applet.anchor { - PanelAnchor::Left | PanelAnchor::Right => Element::from( - applet_column::Column::with_children(playback_buttons) - .push(btn) - .align_x(Alignment::Center) - .height(Length::Shrink) - // TODO configurable variable from the panel? - .spacing( - -(self.core.applet.suggested_padding(true).0 as f32) - * self.core.applet.padding_overlap, - ), + let mut ret = if let Some(playback_buttons) = playback_buttons + && !playback_buttons.is_empty() + { + has_playback_buttons = true; + Element::from(match self.core.applet.anchor { + PanelAnchor::Left | PanelAnchor::Right => Element::from( + applet_column::Column::with_children(playback_buttons) + .push(btn) + .align_x(Alignment::Center) + .height(Length::Shrink) + // TODO configurable variable from the panel? + .spacing( + -(self.core.applet.suggested_padding(true).0 as f32) + * self.core.applet.padding_overlap, ), - PanelAnchor::Top | PanelAnchor::Bottom => { - applet_row::Row::with_children(playback_buttons) - .push(btn) - .align_y(Alignment::Center) - .width(Length::Shrink) - // TODO configurable variable from the panel? - .spacing( - -(self.core.applet.suggested_padding(true).0 as f32) - * self.core.applet.padding_overlap, - ) - .into() - } - } - } else { - btn.into() - }, - ) - .into() + ), + PanelAnchor::Top | PanelAnchor::Bottom => { + applet_row::Row::with_children(playback_buttons) + .push(btn) + .align_y(Alignment::Center) + .width(Length::Shrink) + // TODO configurable variable from the panel? + .spacing( + -(self.core.applet.suggested_padding(true).0 as f32) + * self.core.applet.padding_overlap, + ) + .into() + } + }) + } else { + btn.into() + }; + + if let Some(tracker) = self.rectangle_tracker.as_ref() + && has_playback_buttons + { + ret = tracker.container(0, ret).into() + } + + if !self.core.applet.suggested_bounds.as_ref().is_some_and(|c| { + // if we have a configure for width and height, we're in a overflow popup + c.width > 0. && c.height > 0. + }) { + ret = self.core.applet.autosize_window(ret).into(); + } + ret } fn view_window(&self, _id: window::Id) -> Element<'_, Message> { @@ -551,19 +676,21 @@ impl cosmic::Application for Audio { space_xxs, space_s, .. } = theme::active().cosmic().spacing; - let sink = self + let sink: Option<&str> = self .model - .active_sink() - .and_then(|pos| self.model.sinks().get(pos)); - let source = self + .sinks + .active() + .map(|pos| self.model.sinks.sorted_display[pos].as_ref()); + let source: Option<&str> = self .model - .active_source() - .and_then(|pos| self.model.sources().get(pos)); + .sources + .active() + .map(|pos| self.model.sources.sorted_display[pos].as_ref()); let mut audio_content = { let output_slider = slider( 0..=self.max_sink_volume, - self.model.sink_volume, + self.model.active_sink.volume, Message::SetSinkVolume, ) .width(Length::FillPortion(5)) @@ -571,7 +698,7 @@ impl cosmic::Application for Audio { let input_slider = slider( 0..=self.max_source_volume, - self.model.source_volume, + self.model.active_source.volume, Message::SetSourceVolume, ) .width(Length::FillPortion(5)) @@ -590,7 +717,7 @@ impl cosmic::Application for Audio { .line_height(24) .on_press(Message::ToggleSinkMute), output_slider, - container(text(&self.model.sink_volume_text).size(16)) + container(text(&self.model.active_sink.volume_text).size(16)) .width(Length::FillPortion(1)) .align_x(Alignment::End) ] @@ -609,7 +736,7 @@ impl cosmic::Application for Audio { .line_height(24) .on_press(Message::ToggleSourceMute), input_slider, - container(text(&self.model.source_volume_text).size(16)) + container(text(&self.model.active_source.volume_text).size(16)) .width(Length::FillPortion(1)) .align_x(Alignment::End) ] @@ -624,7 +751,7 @@ impl cosmic::Application for Audio { Some(sink) => sink.to_owned(), None => fl!("no-device"), }, - self.model.sinks(), + &self.model.sinks.sorted_display, Message::OutputToggle, Message::SetDefaultSink, ), @@ -635,7 +762,7 @@ impl cosmic::Application for Audio { Some(source) => source.to_owned(), None => fl!("no-device"), }, - self.model.sources(), + &self.model.sources.sorted_display, Message::InputToggle, Message::SetDefaultSource, ) @@ -691,19 +818,15 @@ impl cosmic::Application for Audio { } if let Some(play) = self.is_play() { control_elements.push( - button::icon( - icon::from_name(if play { PLAY } else { PAUSE }) - .size(32) - .symbolic(true), - ) - .extra_small() - .class(cosmic::theme::Button::AppletIcon) - .on_press(if play { - Message::MprisRequest(MprisRequest::Play) - } else { - Message::MprisRequest(MprisRequest::Pause) - }) - .into(), + button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true)) + .extra_small() + .class(cosmic::theme::Button::AppletIcon) + .on_press(if play { + Message::MprisRequest(MprisRequest::Play) + } else { + Message::MprisRequest(MprisRequest::Pause) + }) + .into(), ); } if let Some(go_next) = self.go_next(32) { @@ -754,20 +877,21 @@ impl cosmic::Application for Audio { } } -fn revealer( +fn revealer<'a>( open: bool, title: String, selected: String, - devices: &[String], + devices: &'a [Arc], toggle: Message, mut change: impl FnMut(usize) -> Message + 'static, -) -> widget::Column<'static, Message, crate::Theme, Renderer> { +) -> widget::Column<'a, Message, crate::Theme, Renderer> { if open { - devices.iter().cloned().enumerate().fold( + devices.iter().enumerate().fold( column![revealer_head(open, title, selected, toggle)].width(Length::Fill), - |col, (id, name)| { + move |col, (id, name)| { col.push( - menu_button(text::body(name)) + text::body(name.as_ref()) + .apply(menu_button) .on_press(change(id)) .width(Length::Fill) .padding([8, 48]), @@ -785,9 +909,9 @@ fn revealer_head( selected: String, toggle: Message, ) -> cosmic::widget::Button<'static, Message> { - menu_button(column![ - text::body(title).width(Length::Fill), - text::caption(selected), - ]) - .on_press(toggle) + cosmic::widget::column::with_capacity(2) + .push(text::body(title).width(Length::Fill)) + .push(text::caption(selected)) + .apply(menu_button) + .on_press(toggle) } diff --git a/cosmic-applet-audio/src/model.rs b/cosmic-applet-audio/src/model.rs new file mode 100644 index 00000000..42c76b3c --- /dev/null +++ b/cosmic-applet-audio/src/model.rs @@ -0,0 +1,393 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::sync::Arc; + +use cosmic_settings_audio_client::{self as audio_client, Availability, RouteInfo}; +use intmap::IntMap; + +pub type DeviceId = u32; +pub type NodeId = u32; + +#[derive(Debug, Default)] +pub struct Model { + pub device_routes: IntMap>, + pub node_devices: IntMap>, + pub sinks: Nodes, + pub sources: Nodes, + pub active_sink: ActiveNode, + pub active_source: ActiveNode, + pub default_sink: Option, + pub default_source: Option, +} + +#[derive(Debug, Default)] +pub struct Nodes { + active: Option, + pub sorted_display: Box<[Arc]>, + pub sorted_index: Box<[u16]>, + pub balance: Vec>, + pub card_profile_device: Vec>, + pub description: Vec, + pub devices: Vec>, + pub display: Vec>, + pub mute: Vec, + pub name: Vec, + pub id: Vec, + pub volume: Vec, +} + +impl Nodes { + pub fn active(&self) -> Option { + self.active.and_then(|active| { + self.sorted_index + .iter() + .position(|idx| *idx as usize == active) + }) + } + + pub fn dropdown_sort(&mut self) { + let mut enumerated_displays = self + .display + .clone() + .into_iter() + .enumerate() + .map(|(index, display)| (index as u16, display)) + .collect::>(); + enumerated_displays.sort_by_key(|v| v.1.clone()); + let (indexes, displays): (Vec<_>, Vec<_>) = enumerated_displays.into_iter().unzip(); + self.sorted_display = displays.into_boxed_slice(); + self.sorted_index = indexes.into_boxed_slice(); + } + + pub fn remove(&mut self, node_id: u32) -> bool { + let Some(pos) = self.id.iter().position(|id| node_id == *id) else { + return false; + }; + self.balance.remove(pos); + self.card_profile_device.remove(pos); + self.description.remove(pos); + self.devices.remove(pos); + self.display.remove(pos); + self.mute.remove(pos); + self.name.remove(pos); + self.id.remove(pos); + self.volume.remove(pos); + self.dropdown_sort(); + if self.active == Some(pos) { + self.active = None; + } + true + } +} + +#[derive(Debug, Default)] +pub struct ActiveNode { + pub volume_text: String, + pub volume: u32, + pub mute: bool, +} + +impl Model { + pub fn update(&mut self, event: audio_client::Event) { + tracing::debug!(?event, "update"); + match event { + audio_client::Event::NodeMute(node_id, mute) => { + if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) { + self.sinks.mute[pos] = mute; + if self.sinks.active == Some(pos) { + self.active_sink.mute = mute; + } + } else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) { + self.sources.mute[pos] = mute; + if self.sources.active == Some(pos) { + self.active_source.mute = mute; + } + } + } + + audio_client::Event::NodeVolume(node_id, volume, balance) => { + if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) { + self.sinks.volume[pos] = volume; + self.sinks.balance[pos] = balance; + if self.default_sink.as_ref().is_some_and(|&id| id == node_id) + && let Some(pos) = self.sinks.active + { + self.active_sink.mute = self.sinks.mute[pos]; + self.active_sink.volume = self.sinks.volume[pos]; + self.active_sink.volume_text = self.active_sink.volume.to_string(); + } + } else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) { + self.sources.volume[pos] = volume; + self.sources.balance[pos] = balance; + if self + .default_source + .as_ref() + .is_some_and(|&id| id == node_id) + && let Some(pos) = self.sources.active + { + self.active_source.mute = self.sources.mute[pos]; + self.active_source.volume = self.sources.volume[pos]; + self.active_source.volume_text = self.active_source.volume.to_string(); + } + } + } + + audio_client::Event::DefaultSink(node_id) => { + self.default_sink = Some(node_id); + if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id) { + self.sinks.active = Some(pos); + self.active_sink.mute = self.sinks.mute[pos]; + self.active_sink.volume = self.sinks.volume[pos]; + self.active_sink.volume_text = self.active_sink.volume.to_string(); + } + } + + audio_client::Event::DefaultSource(node_id) => { + self.default_source = Some(node_id); + if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) { + self.sources.active = Some(pos); + self.active_source.mute = self.sources.mute[pos]; + self.active_source.volume = self.sources.volume[pos]; + self.active_source.volume_text = self.active_source.volume.to_string(); + } + } + + audio_client::Event::Node(node_id, node) => { + self.node_devices.insert(node_id, node.device_id); + if node.is_sink { + let pos = if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id) + { + self.sinks.description[pos] = self.translate(&node.description); + self.sinks.name[pos] = node.name; + self.sinks.card_profile_device[pos] = node.card_profile_device; + pos + } else { + self.sinks.display.push(Arc::default()); + self.sinks + .description + .push(self.translate(&node.description)); + self.sinks.id.push(node_id); + self.sinks.volume.push(0); + self.sinks.balance.push(None); + self.sinks.mute.push(false); + self.sinks.name.push(node.name); + self.sinks.devices.push(node.device_id); + self.sinks + .card_profile_device + .push(node.card_profile_device); + self.sinks.id.len() - 1 + }; + + self.sinks.display[pos] = node + .device_id + .zip(node.card_profile_device) + .and_then(|(device_id, node_card_profile_device)| { + let routes = self.device_routes.get(device_id)?; + for route in routes { + if matches!(route.availability, Availability::No) || !route.is_sink + { + continue; + } + + if route.devices.contains(&node_card_profile_device) { + return Some(node_name( + &self.translate(&route.description), + &self.sinks.description[pos], + )); + } + } + + None + }) + .unwrap_or_else(|| { + node_name( + &node.device_profile_description, + &self.sinks.description[pos], + ) + }); + + self.sinks.dropdown_sort(); + + if let Some(default_node_id) = self.default_sink + && default_node_id == node_id + { + self.sinks.active = Some(pos); + self.active_sink.mute = self.sinks.mute[pos]; + self.active_sink.volume = self.sinks.volume[pos]; + self.active_sink.volume_text = self.active_sink.volume.to_string(); + } + } else { + let pos = + if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) { + self.sources.description[pos] = self.translate(&node.description); + self.sources.name[pos] = node.name; + self.sources.card_profile_device[pos] = node.card_profile_device; + pos + } else { + self.sources + .description + .push(self.translate(&node.description)); + self.sources.display.push(Arc::default()); + self.sources.id.push(node_id); + self.sources.volume.push(0); + self.sources.balance.push(None); + self.sources.mute.push(false); + self.sources.name.push(node.name); + self.sources.devices.push(node.device_id); + self.sources + .card_profile_device + .push(node.card_profile_device); + self.sources.id.len() - 1 + }; + + if let Some(name) = node + .device_id + .zip(node.card_profile_device) + .map(|(device_id, node_card_profile_device)| { + let routes = self.device_routes.get(device_id)?; + for route in routes { + if route.is_sink || matches!(route.availability, Availability::No) { + continue; + } + + if route.devices.contains(&node_card_profile_device) { + return Some(node_name( + &self.translate(&route.description), + &self.sources.description[pos], + )); + } + } + + None + }) + .unwrap_or_else(|| { + Some(node_name( + &node.device_profile_description, + &self.sources.description[pos], + )) + }) + { + self.sources.display[pos] = name; + self.sources.dropdown_sort(); + } else { + // Remove sources that are unplugged. + self.sources.remove(node_id); + return; + } + + if let Some(default_node_id) = self.default_source + && default_node_id == node_id + { + self.sources.active = Some(pos); + self.active_source.mute = self.sources.mute[pos]; + self.active_source.volume = self.sources.volume[pos]; + self.active_source.volume_text = self.active_source.volume.to_string(); + } + } + } + + audio_client::Event::ActiveRoute(device_id, _index, route) => { + self.update_device_names(device_id, &route); + } + + audio_client::Event::Route(device_id, index, route) => { + let routes = self.device_routes.entry(device_id).or_default(); + if index == 0 { + *routes = vec![route]; + } else { + if routes.len() < index as usize + 1 { + let additional = (index as usize + 1) - routes.capacity(); + routes.reserve_exact(additional); + routes.extend(std::iter::repeat_n(RouteInfo::default(), additional)); + } + routes[index as usize] = route; + } + } + + audio_client::Event::RemoveNode(node_id) => { + self.node_devices.remove(node_id); + + if !self.sinks.remove(node_id) { + self.sources.remove(node_id); + } + } + + audio_client::Event::RemoveDevice(device_id) => { + self.device_routes.remove(device_id); + } + + _ => (), + } + } + + fn update_device_names(&mut self, device_id: DeviceId, route: &RouteInfo) { + if matches!(route.availability, Availability::No) { + return; + } + + let compatible_nodes = self.node_devices.iter().filter_map(|(node, &dev_id)| { + if dev_id? == device_id { + Some(node) + } else { + None + } + }); + + if route.is_sink { + for n_id in compatible_nodes { + let Some(pos) = self.sinks.id.iter().position(|&node| node == n_id) else { + continue; + }; + + let Some(card_profile_device) = self.sinks.card_profile_device[pos] else { + continue; + }; + + if route.devices.contains(&card_profile_device) { + self.sinks.display[pos] = node_name( + &self.translate(&route.description), + &self.sinks.description[pos], + ); + self.sinks.dropdown_sort(); + break; + } + } + } else { + for n_id in compatible_nodes { + let Some(pos) = self.sources.id.iter().position(|&node| node == n_id) else { + continue; + }; + + let Some(card_profile_device) = self.sources.card_profile_device[pos] else { + continue; + }; + + if route.devices.contains(&card_profile_device) { + self.sources.display[pos] = node_name( + &self.translate(&route.description), + &self.sources.description[pos], + ); + self.sources.dropdown_sort(); + break; + } + } + } + } + + pub fn translate(&self, description: &str) -> String { + description + .replace("High Definition", "HD") + .replace("DisplayPort", "DP") + .replace("Controller", "") + } +} + +fn node_name(route: &str, node: &str) -> Arc { + if route.is_empty() { + node.to_owned() + } else { + [route, " - ", node].concat() + } + .into() +} diff --git a/cosmic-applet-audio/src/mouse_area.rs b/cosmic-applet-audio/src/mouse_area.rs index 6ae8547a..38306e45 100644 --- a/cosmic-applet-audio/src/mouse_area.rs +++ b/cosmic-applet-audio/src/mouse_area.rs @@ -5,7 +5,7 @@ use cosmic::iced::core::Point; use cosmic::iced::core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, - event::{self, Event}, + event::Event, layout, mouse, overlay, renderer, touch, widget::{Operation, Tree, tree}, }; diff --git a/cosmic-applet-battery/Cargo.toml b/cosmic-applet-battery/Cargo.toml index 2218f638..3310f8fa 100644 --- a/cosmic-applet-battery/Cargo.toml +++ b/cosmic-applet-battery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-battery" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -12,7 +12,7 @@ drm = "0.14.1" futures.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true rust-embed.workspace = true rustc-hash.workspace = true tokio.workspace = true @@ -24,9 +24,7 @@ zbus.workspace = true serde.workspace = true [dependencies.cosmic-settings-upower-subscription] -git = "https://github.com/pop-os/cosmic-settings" -# path = "../../cosmic-settings/subscriptions/upower" +path = "../../cosmic-settings/subscriptions/upower" [dependencies.cosmic-settings-daemon-subscription] -git = "https://github.com/pop-os/cosmic-settings" -# path = "../../cosmic-settings/subscriptions/settings-daemon" +path = "../../cosmic-settings/subscriptions/settings-daemon" diff --git a/cosmic-applet-battery/i18n/hu/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/hu/cosmic_applet_battery.ftl index 814d3605..01626a96 100644 --- a/cosmic-applet-battery/i18n/hu/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/hu/cosmic_applet_battery.ftl @@ -1,13 +1,13 @@ battery = Akkumulátor -battery-desc = Csökkentett energiafogyasztás és teljesítmény +battery-desc = Csökkentett energiafogyasztás és teljesítmény. balanced = Kiegyensúlyozott -balanced-desc = Normál teljesítmény és akkumulátorhasználat +balanced-desc = Normál teljesítmény és akkumulátorhasználat. performance = Nagy teljesítmény -performance-desc = Nagy teljesítmény és energiafogyasztás -max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra -seconds = másodperc -minutes = perc -hours = óra +performance-desc = Nagy teljesítmény és energiafogyasztás. +max-charge = Az akkumulátor élettartamának növelése érdekében állítsd a maximális töltési szintet 80%-ra +seconds = mp +minutes = p +hours = ó until-empty = a lemerülésig power-settings = Energia- és akkumulátorbeállítások… dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát diff --git a/cosmic-applet-battery/i18n/kmr/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/kmr/cosmic_applet_battery.ftl index e69de29b..8c0d92d1 100644 --- a/cosmic-applet-battery/i18n/kmr/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/kmr/cosmic_applet_battery.ftl @@ -0,0 +1,14 @@ +battery = Betarî +seconds = ç +minutes = x +hours = d +until-empty = Heya vala dibe +power-settings = Sazkariyên hêz û betariyê... +battery-desc = Bikaranîna hêzê kêmkirî û performans. +balanced = Hevseng +balanced-desc = Performansa standard û bikaranîna betariyê. +performance = Performansa bilind +performance-desc = Performansa bilind û bikaranîna hêzê. +max-charge = Jiyana betariyê xwe dirêj bike bi riya danîna sînorê herî mezin yê tijîkrinê %80 +dgpu-running = YKG a veqetandî çalak e û dikare jiyana betariyê kêm bike +dgpu-applications = Sepanên ku YKG a veqetandî ya { $gpu_name } bi kar tînin diff --git a/cosmic-applet-battery/src/app.rs b/cosmic-applet-battery/src/app.rs index 9233a33f..22d9e6cc 100644 --- a/cosmic-applet-battery/src/app.rs +++ b/cosmic-applet-battery/src/app.rs @@ -40,7 +40,7 @@ use cosmic_settings_upower_subscription::{ }; use rustc_hash::FxHashMap; -use std::{path::PathBuf, sync::LazyLock, time::Duration}; +use std::{path::PathBuf, time::Duration}; use tokio::sync::mpsc::UnboundedSender; // XXX improve @@ -169,9 +169,24 @@ impl CosmicBatteryApplet { self.charging_limit = Some(limit); self.update_battery(self.battery_percent, self.on_battery); } + + fn panel_icon_name(&self) -> &str { + if self.no_battery { + if self.screen_brightness.is_some() { + self.display_icon_name.as_str() + } else if self.kbd_brightness.is_some() { + "keyboard-brightness-symbolic" + } else { + "plugged-into-power-symbolic" + } + } else { + self.icon_name.as_str() + } + } } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { TogglePopup, CloseRequested(window::Id), @@ -511,7 +526,7 @@ impl cosmic::Application for CosmicBatteryApplet { Task::none() } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let is_horizontal = match self.core.applet.anchor { PanelAnchor::Top | PanelAnchor::Bottom => true, PanelAnchor::Left | PanelAnchor::Right => false, @@ -521,19 +536,9 @@ impl cosmic::Application for CosmicBatteryApplet { let applet_padding = self.core.applet.suggested_padding(true); let mut children = vec![ - icon::from_name(if self.no_battery { - if self.screen_brightness.is_some() { - self.display_icon_name.as_str() - } else if self.kbd_brightness.is_some() { - "keyboard-brightness-symbolic" - } else { - self.icon_name.as_str() - } - } else { - self.icon_name.as_str() - }) - .size(suggested_size.0) - .into(), + icon::from_name(self.panel_icon_name()) + .size(suggested_size.0) + .into(), ]; if self.config.show_percentage && !self.no_battery { diff --git a/cosmic-applet-bluetooth/Cargo.toml b/cosmic-applet-bluetooth/Cargo.toml index 03b67388..1ee209ee 100644 --- a/cosmic-applet-bluetooth/Cargo.toml +++ b/cosmic-applet-bluetooth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-bluetooth" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -10,7 +10,7 @@ bluer = { version = "0.17", features = ["bluetoothd", "id"] } futures.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true fastrand = "2.3.0" rust-embed.workspace = true rustc-hash.workspace = true diff --git a/cosmic-applet-bluetooth/i18n/hu/cosmic_applet_bluetooth.ftl b/cosmic-applet-bluetooth/i18n/hu/cosmic_applet_bluetooth.ftl index ada54d0b..f12b24b3 100644 --- a/cosmic-applet-bluetooth/i18n/hu/cosmic_applet_bluetooth.ftl +++ b/cosmic-applet-bluetooth/i18n/hu/cosmic_applet_bluetooth.ftl @@ -1,7 +1,7 @@ bluetooth = Bluetooth other-devices = Egyéb Bluetooth-eszközök settings = Bluetooth-beállítások… -connected = Csatlakoztatva +connected = Kapcsolódva confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal confirm = Megerősítés cancel = Mégse diff --git a/cosmic-applet-bluetooth/i18n/kmr/cosmic_applet_bluetooth.ftl b/cosmic-applet-bluetooth/i18n/kmr/cosmic_applet_bluetooth.ftl index e69de29b..988f00ca 100644 --- a/cosmic-applet-bluetooth/i18n/kmr/cosmic_applet_bluetooth.ftl +++ b/cosmic-applet-bluetooth/i18n/kmr/cosmic_applet_bluetooth.ftl @@ -0,0 +1,12 @@ +cancel = Têk bibe +bluetooth = Bluetooth +connected = Hat girêdan +other-devices = Amûrên Bluetooth ên din +settings = Sazkariyên Bluetooth... +confirm-pin = Tika ye piştrast bike ku PIN a jêrîn bi ya ku li ser { $deviceName } tê xuyakirin re li hev tê +confirm = Bipejirîne +check-device = Piştrast bike ku { $deviceName } çalak e, di hundir rêjeyê de ye, û amade ye ji bo cotkirinê. +try-again = Dîsa hewl bide +discoverable = Dîtbar +pairable = Cotbar +unsuccessful = Cotkirin têkçûyî ye diff --git a/cosmic-applet-bluetooth/src/app.rs b/cosmic-applet-bluetooth/src/app.rs index 2ca95ec5..484b8bf3 100644 --- a/cosmic-applet-bluetooth/src/app.rs +++ b/cosmic-applet-bluetooth/src/app.rs @@ -21,10 +21,10 @@ use cosmic::{ widget::{Column, column, container, row}, }, theme, - widget::{button, divider, icon, scrollable, text}, + widget::{button, divider, icon, indeterminate_circular, scrollable, text}, }; use futures::FutureExt; -use std::{collections::HashMap, sync::LazyLock, time::Duration}; +use std::{collections::HashMap, time::Duration}; use tokio::sync::mpsc::Sender; use crate::{ @@ -63,6 +63,7 @@ impl CosmicBluetoothApplet { } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { TogglePopup, CloseRequested(window::Id), @@ -145,7 +146,7 @@ impl cosmic::Application for CosmicBluetoothApplet { } Message::BluetoothEvent(e) => match e { BluerEvent::RequestResponse { - req, + req: _, state, err_msg, } => { @@ -367,14 +368,12 @@ impl cosmic::Application for CosmicBluetoothApplet { ); } BluerDeviceStatus::Paired => {} - BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => { - row = row.push( - icon::from_name("process-working-symbolic") - .size(24) - .symbolic(true), - ); + BluerDeviceStatus::Connecting + | BluerDeviceStatus::Disconnecting + | BluerDeviceStatus::Pairing => { + row = row.push(indeterminate_circular().size(24.0)); } - BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue, + BluerDeviceStatus::Disconnected => continue, } known_bluetooth.push( diff --git a/cosmic-applet-bluetooth/src/bluetooth.rs b/cosmic-applet-bluetooth/src/bluetooth.rs index b3648acd..3a859932 100644 --- a/cosmic-applet-bluetooth/src/bluetooth.rs +++ b/cosmic-applet-bluetooth/src/bluetooth.rs @@ -149,8 +149,6 @@ pub fn bluetooth_subscription( }, BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e), - - _ => return, }; _ = output.send(message).await; @@ -199,6 +197,7 @@ pub enum BluerRequest { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum BluerEvent { RequestResponse { req: BluerRequest, @@ -568,22 +567,21 @@ impl BluerSessionState { tick(&mut interval).await; let new_status = adapter_clone.is_powered().await.unwrap_or_default(); devices = build_device_list(devices, &adapter_clone).await; - if new_status != status { - status = new_status; - let state = BluerState { - devices: devices.clone(), - bluetooth_enabled: status, - }; - if state.bluetooth_enabled { - for d in &state.devices { - if d.paired_and_trusted() { - _ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await; - } + let power_changed = new_status != status; + status = new_status; + let state = BluerState { + devices: devices.clone(), + bluetooth_enabled: status, + }; + if power_changed && state.bluetooth_enabled { + for d in &state.devices { + if d.paired_and_trusted() { + _ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await; } } _ = wake_up_discover_tx.send(()).await; - let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await; } + let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await; } }); } @@ -674,6 +672,9 @@ impl BluerSessionState { } } + // Always refresh power state from the adapter + // to avoid stale cached is_powered values + is_powered = adapter_clone.is_powered().await.unwrap_or_default(); let _ = tx .send(BluerSessionEvent::ChangesProcessed(BluerState { devices: devices.clone(), @@ -853,13 +854,7 @@ async fn bluer_state(adapter: &Adapter) -> BluerState { #[inline(never)] async fn build_device_list(mut devices: Vec, adapter: &Adapter) -> Vec { - let addrs: Vec
= adapter - .device_addresses() - .await - .unwrap_or_default() - .into_iter() - .filter(|addr| !devices.iter().any(|d| d.address == *addr)) - .collect(); + let addrs = adapter.device_addresses().await.unwrap_or_default(); devices.clear(); if addrs.len() > devices.capacity() { diff --git a/cosmic-applet-input-sources/Cargo.toml b/cosmic-applet-input-sources/Cargo.toml index 92a7daf3..45b54857 100644 --- a/cosmic-applet-input-sources/Cargo.toml +++ b/cosmic-applet-input-sources/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "cosmic-applet-input-sources" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] -cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" } +cosmic-comp-config.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true rust-embed.workspace = true tokio.workspace = true tracing-log.workspace = true diff --git a/cosmic-applet-input-sources/i18n/kmr/cosmic_applet_input_sources.ftl b/cosmic-applet-input-sources/i18n/kmr/cosmic_applet_input_sources.ftl index e69de29b..737324c5 100644 --- a/cosmic-applet-input-sources/i18n/kmr/cosmic_applet_input_sources.ftl +++ b/cosmic-applet-input-sources/i18n/kmr/cosmic_applet_input_sources.ftl @@ -0,0 +1,2 @@ +show-keyboard-layout = Awaya kilîtdankê nîşan bide... +keyboard-settings = Sazkariyên kilîtdankê... diff --git a/cosmic-applet-input-sources/src/lib.rs b/cosmic-applet-input-sources/src/lib.rs index 93e84283..2c845de1 100644 --- a/cosmic-applet-input-sources/src/lib.rs +++ b/cosmic-applet-input-sources/src/lib.rs @@ -3,7 +3,6 @@ mod localize; -use cosmic::iced::{Alignment, Length}; use cosmic::{ app, app::Core, @@ -14,16 +13,14 @@ use cosmic::{ iced::{ Rectangle, Task, platform_specific::shell::commands::popup::{destroy_popup, get_popup}, - widget::{column, row}, window::Id, }, - iced::{core::window, runtime::Appearance}, + iced::core::window, prelude::*, surface, theme, widget::{ self, autosize, rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription}, - space, }, }; use cosmic_comp_config::CosmicCompConfig; diff --git a/cosmic-applet-minimize/Cargo.toml b/cosmic-applet-minimize/Cargo.toml index 783be11b..29ee2c8e 100644 --- a/cosmic-applet-minimize/Cargo.toml +++ b/cosmic-applet-minimize/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-minimize" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -9,7 +9,7 @@ anyhow.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true image = { version = "0.25.9", default-features = false } -libcosmic.workspace = true +cosmic.workspace = true memmap2 = "0.9.10" rust-embed.workspace = true rustix.workspace = true diff --git a/cosmic-applet-minimize/src/lib.rs b/cosmic-applet-minimize/src/lib.rs index e40a7551..307762b5 100644 --- a/cosmic-applet-minimize/src/lib.rs +++ b/cosmic-applet-minimize/src/lib.rs @@ -316,7 +316,7 @@ impl cosmic::Application for Minimize { } else { (cross_padding, major_padding) }; - let theme = self.core.system_theme().cosmic(); + let _theme = self.core.system_theme().cosmic(); let icon_buttons = self.apps[..max_icon_count].iter().map(|app| { self.core .applet @@ -420,7 +420,7 @@ impl cosmic::Application for Minimize { (cross_padding, major_padding) }; let theme = self.core.system_theme().cosmic(); - let space_xxs = theme.space_xxs(); + let _space_xxs = theme.space_xxs(); let icon_buttons = self.apps[max_icon_count..].iter().map(|app| { tooltip( Element::from(crate::window_image::WindowImage::new( diff --git a/cosmic-applet-minimize/src/wayland_handler.rs b/cosmic-applet-minimize/src/wayland_handler.rs index bf2eae30..562693e2 100644 --- a/cosmic-applet-minimize/src/wayland_handler.rs +++ b/cosmic-applet-minimize/src/wayland_handler.rs @@ -121,6 +121,7 @@ struct AppData { toplevel_info_state: ToplevelInfoState, toplevel_manager_state: ToplevelManagerState, seat_state: SeatState, + captured_toplevels: std::collections::HashSet, } struct CaptureData { @@ -351,6 +352,36 @@ impl AppData { } }); } + + fn handle_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1, is_new: bool) { + let Some(info) = self.toplevel_info_state.info(toplevel) else { + return; + }; + if info + .state + .contains(&zcosmic_toplevel_handle_v1::State::Minimized) + { + // Capture a thumbnail once, on the transition into minimized. + if self.captured_toplevels.insert(toplevel.clone()) { + self.send_image(toplevel.clone()); + } + let update = if is_new { + ToplevelUpdate::Add(info.clone()) + } else { + ToplevelUpdate::Update(info.clone()) + }; + let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(update))); + } else { + self.remove_toplevel(toplevel); + } + } + + fn remove_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1) { + self.captured_toplevels.remove(toplevel); + let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( + ToplevelUpdate::Remove(toplevel.clone()), + ))); + } } impl ToplevelInfoHandler for AppData { @@ -364,23 +395,7 @@ impl ToplevelInfoHandler for AppData { _qh: &QueueHandle, toplevel: &ExtForeignToplevelHandleV1, ) { - if let Some(info) = self.toplevel_info_state.info(toplevel) { - if info - .state - .contains(&zcosmic_toplevel_handle_v1::State::Minimized) - { - // spawn thread for sending the image - self.send_image(toplevel.clone()); - let _ = futures::executor::block_on( - self.tx - .send(WaylandUpdate::Toplevel(ToplevelUpdate::Add(info.clone()))), - ); - } else { - let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( - ToplevelUpdate::Remove(toplevel.clone()), - ))); - } - } + self.handle_toplevel(toplevel, true); } fn update_toplevel( @@ -389,21 +404,7 @@ impl ToplevelInfoHandler for AppData { _qh: &QueueHandle, toplevel: &ExtForeignToplevelHandleV1, ) { - if let Some(info) = self.toplevel_info_state.info(toplevel) { - if info - .state - .contains(&zcosmic_toplevel_handle_v1::State::Minimized) - { - self.send_image(toplevel.clone()); - let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( - ToplevelUpdate::Update(info.clone()), - ))); - } else { - let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( - ToplevelUpdate::Remove(toplevel.clone()), - ))); - } - } + self.handle_toplevel(toplevel, false); } fn toplevel_closed( @@ -412,9 +413,7 @@ impl ToplevelInfoHandler for AppData { _qh: &QueueHandle, toplevel: &ExtForeignToplevelHandleV1, ) { - let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( - ToplevelUpdate::Remove(toplevel.clone()), - ))); + self.remove_toplevel(toplevel); } } @@ -482,6 +481,7 @@ pub(crate) fn wayland_handler( toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh), queue_handle: qh, registry_state, + captured_toplevels: std::collections::HashSet::new(), }; loop { diff --git a/cosmic-applet-network/Cargo.toml b/cosmic-applet-network/Cargo.toml index 44f08b53..62efbaf9 100644 --- a/cosmic-applet-network/Cargo.toml +++ b/cosmic-applet-network/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "cosmic-applet-network" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-or-later" [dependencies] anyhow.workspace = true async-fn-stream = "0.3" -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } +cosmic-dbus-networkmanager = { path = "../../dbus-settings-bindings/networkmanager" } futures.workspace = true futures-util.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic = { workspace = true, features = [ +cosmic = { workspace = true, features = [ "applet", "applet-token", "tokio", @@ -34,5 +34,4 @@ nmrs = "3.1.3" [dependencies.cosmic-settings-network-manager-subscription] -git = "https://github.com/pop-os/cosmic-settings/" -# path = "../../cosmic-settings/subscriptions/network-manager" +path = "../../cosmic-settings/subscriptions/network-manager" diff --git a/cosmic-applet-network/data/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf b/cosmic-applet-network/data/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf new file mode 100644 index 00000000..8e98cedb --- /dev/null +++ b/cosmic-applet-network/data/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf @@ -0,0 +1,8 @@ + + + + + + diff --git a/cosmic-applet-network/i18n/hu/cosmic_applet_network.ftl b/cosmic-applet-network/i18n/hu/cosmic_applet_network.ftl index e9a61812..4ad41d3f 100644 --- a/cosmic-applet-network/i18n/hu/cosmic_applet_network.ftl +++ b/cosmic-applet-network/i18n/hu/cosmic_applet_network.ftl @@ -1,16 +1,16 @@ network = Hálózat airplane-mode = Repülőgép-üzemmód airplane-mode-on = A repülőgép-üzemmód be van kapcsolva -turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil szélessáv engedélyezéséhez. +turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil széles sáv engedélyezéséhez. wifi = Wi-Fi identity = Azonosító ipv4 = IPv4-cím ipv6 = IPv6-cím mac = MAC megabits-per-second = Mbps -connected = Csatlakoztatva -connecting = Csatlakozás… -connect = Csatlakozás +connected = Kapcsolódva +connecting = Kapcsolódás +connect = Kapcsolódás cancel = Mégse settings = Hálózati beállítások… visible-wireless-networks = Látható vezeték nélküli hálózatok diff --git a/cosmic-applet-network/i18n/kmr/cosmic_applet_network.ftl b/cosmic-applet-network/i18n/kmr/cosmic_applet_network.ftl index e69de29b..3dbb0266 100644 --- a/cosmic-applet-network/i18n/kmr/cosmic_applet_network.ftl +++ b/cosmic-applet-network/i18n/kmr/cosmic_applet_network.ftl @@ -0,0 +1,24 @@ +cancel = Têk bibe +connect = Girê bide +connected = Hat girêdan +network = Tor +airplane-mode = Awaya balafirê +airplane-mode-on = Awaya balafirê vekirî ye +turn-off-airplane-mode = Ji bo çalakirina Wi-Fi, Bluetooth û torên mobayîl, awaya balafirê bigire. +wifi = Wi-Fi +identity = Nasname +ipv4 = IPv4 +ipv6 = IPv6 +mac = MAC +megabits-per-second = Mbps +gigabits-per-second = Gbps +terabits-per-second = Tbps +connecting = Tê girêdan +settings = Sazkariyên torê... +visible-wireless-networks = Torên bêtêl ên xuya +vpn-connections = Girêdanên VPN +enter-password = Borînpeyvê yan jî kilîtê şîfrekirinê têxîne +router-wps-button = Tu dikarî bi pêlkirina bişkoka "WPS" ya li ser routerê jî girê bidî +unable-to-connect = Nikare bi torê ve were girêdan +check-wifi-connection = Piştrast bike ku Wi-Fi bi înternetê ve girêdayî ye û borînpeyv rast e +reset = Ji nû ve saz bike diff --git a/cosmic-applet-network/src/app.rs b/cosmic-applet-network/src/app.rs index 6939f5e3..0b6dbe24 100644 --- a/cosmic-applet-network/src/app.rs +++ b/cosmic-applet-network/src/app.rs @@ -35,7 +35,7 @@ use cosmic::{ widget::{ Id, button, column, container, divider, icon::{self, from_name}, - row, scrollable, secure_input, text, text_input, toggler, + indeterminate_circular, row, scrollable, secure_input, text, text_input, toggler, }, }; use cosmic_dbus_networkmanager::interface::enums::{ @@ -405,6 +405,7 @@ fn secret_agent_task(identifier: String) -> Task { cosmic::Task::stream(async_fn_stream::fn_stream(move |emitter| async move { let registration = SecretAgent::builder() .with_identifier(identifier) + .with_object_path("/org/freedesktop/NetworkManager/SecretAgent") .with_capabilities(SecretAgentCapabilities::VPN_HINTS) .register() .await; @@ -664,6 +665,13 @@ fn load_vpns(_conn: zbus::Connection) -> Task { let mut map: IndexMap = IndexMap::new(); for c in saved { + // Skip in-memory-only NM connections — assumed connections that NM + // auto-generated from externally-managed interfaces (e.g. one + // brought up by wg-quick@wg0.service) report unsaved=true and + // evaporate on deactivate, leaving the applet's toggle dead. + if c.unsaved { + continue; + } let uuid: UUID = Arc::from(c.uuid.as_str()); let entry = match c.summary { SettingsSummary::WireGuard { .. } => ConnectionSettings::Wireguard { id: c.id }, @@ -726,9 +734,10 @@ impl cosmic::Application for CosmicNetworkApplet { tasks.push(update_state(conn.clone())); tasks.push(update_devices(conn.clone())); tasks.push(load_vpns(conn)); + let uuid = uuid::Uuid::new_v4().to_string().replace("-", "_"); + let my_id = format!( - "com.system76.CosmicSettings.Applet.{}.NetworkManager.SecretAgent", - uuid::Uuid::new_v4() + "com.system76.CosmicSettings.Applet._{uuid}.NetworkManager.SecretAgent", ); tasks.push(secret_agent_task(my_id).map(Message::SecretAgent)); } @@ -1507,12 +1516,7 @@ impl cosmic::Application for CosmicNetworkApplet { ]; match state { ActiveConnectionState::Activating | ActiveConnectionState::Deactivating => { - btn_content.push( - icon::from_name("process-working-symbolic") - .size(24) - .symbolic(true) - .into(), - ); + btn_content.push(indeterminate_circular().size(24.0).into()); } ActiveConnectionState::Activated => btn_content.push( text::body(fl!("connected")) @@ -1720,12 +1724,7 @@ impl cosmic::Application for CosmicNetworkApplet { .into(), ); btn_content.push(ssid.into()); - btn_content.push( - icon::from_name("process-working-symbolic") - .size(24) - .symbolic(true) - .into(), - ); + btn_content.push(indeterminate_circular().size(24.0).into()); } else if matches!(known.state, DeviceState::Unavailable) { btn_content.push( icon::from_name("network-wireless-disconnected-symbolic") @@ -1897,10 +1896,7 @@ impl cosmic::Application for CosmicNetworkApplet { let connecting = padded_control( row::with_children([ Element::from(id), - icon::from_name("process-working-symbolic") - .size(24) - .symbolic(true) - .into(), + indeterminate_circular().size(24.0).into(), ]) .spacing(8), ); diff --git a/cosmic-applet-notifications/Cargo.toml b/cosmic-applet-notifications/Cargo.toml index 094cfa14..91752579 100644 --- a/cosmic-applet-notifications/Cargo.toml +++ b/cosmic-applet-notifications/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-notifications" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -8,7 +8,7 @@ license = "GPL-3.0-only" cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" } cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" } anyhow.workspace = true -libcosmic.workspace = true +cosmic.workspace = true tokio.workspace = true # cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" } # cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" } diff --git a/cosmic-applet-notifications/i18n/hu/cosmic_applet_notifications.ftl b/cosmic-applet-notifications/i18n/hu/cosmic_applet_notifications.ftl index ec1f5a91..0ff88aa1 100644 --- a/cosmic-applet-notifications/i18n/hu/cosmic_applet_notifications.ftl +++ b/cosmic-applet-notifications/i18n/hu/cosmic_applet_notifications.ftl @@ -1,14 +1,14 @@ hours-ago = { $duration -> - [0] Most - [one] 1 órával ezelőtt - *[other] { $duration } órával ezelőtt + [0] Épp most + [one] 1 órája + *[other] { $duration } órája } minutes-ago = { $duration -> - [0] Most - [one] 1 perccel ezelőtt - *[other] { $duration } perccel ezelőtt + [0] Épp most + [one] 1 perce + *[other] { $duration } perce } show-less = Kevesebb megjelenítése show-more = { $more } további megjelenítése diff --git a/cosmic-applet-notifications/i18n/kmr/cosmic_applet_notifications.ftl b/cosmic-applet-notifications/i18n/kmr/cosmic_applet_notifications.ftl index e69de29b..b7a7578a 100644 --- a/cosmic-applet-notifications/i18n/kmr/cosmic_applet_notifications.ftl +++ b/cosmic-applet-notifications/i18n/kmr/cosmic_applet_notifications.ftl @@ -0,0 +1,19 @@ +hours-ago = + { $duration -> + [0] Niha + [one] 1 demjimêr berê + *[other] { $duration } demjimêr berê + } +minutes-ago = + { $duration -> + [0] Niha + [one] 1 xulek berê + *[other] { $duration } xulek berê + } +show-less = Kêmtir nîşan bide +show-more = Bêtir { $more } nîşan bide +clear-group = Komê pak bike +clear-all = Hemû agahdariyan pak bike +do-not-disturb = Rawestandina balkêşiyê +notification-settings = Sazkariyên agahdariyê.... +no-notifications = Agahdarî tune ne diff --git a/cosmic-applet-power/Cargo.toml b/cosmic-applet-power/Cargo.toml index 041c0474..85f520f9 100644 --- a/cosmic-applet-power/Cargo.toml +++ b/cosmic-applet-power/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "cosmic-applet-power" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true logind-zbus = "5.3.2" rust-embed.workspace = true rustix.workspace = true diff --git a/cosmic-applet-power/i18n/ca/cosmic_applet_power.ftl b/cosmic-applet-power/i18n/ca/cosmic_applet_power.ftl index 2c45ddba..c1084630 100644 --- a/cosmic-applet-power/i18n/ca/cosmic_applet_power.ftl +++ b/cosmic-applet-power/i18n/ca/cosmic_applet_power.ftl @@ -1,2 +1,4 @@ cancel = Cancel·lar confirm = Confirmar +restart = Reinicia +suspend = Suspèn diff --git a/cosmic-applet-power/i18n/hu/cosmic_applet_power.ftl b/cosmic-applet-power/i18n/hu/cosmic_applet_power.ftl index 9019650a..9791f43f 100644 --- a/cosmic-applet-power/i18n/hu/cosmic_applet_power.ftl +++ b/cosmic-applet-power/i18n/hu/cosmic_applet_power.ftl @@ -33,4 +33,4 @@ confirm-body = [lock-screen] zárolni fogja a képernyőt [log-out] kijelentkezik *[other] alkalmazni fogja a kiválasztott műveletet - } { $countdown } másodperc múlva + } { $countdown } másodperc múlva. diff --git a/cosmic-applet-power/i18n/kmr/cosmic_applet_power.ftl b/cosmic-applet-power/i18n/kmr/cosmic_applet_power.ftl index e69de29b..003f4a23 100644 --- a/cosmic-applet-power/i18n/kmr/cosmic_applet_power.ftl +++ b/cosmic-applet-power/i18n/kmr/cosmic_applet_power.ftl @@ -0,0 +1,36 @@ +cancel = Têk bibe +confirm = Bipejirîne +power = Hêz +restart = Ji nû ve bide destpêkirin +settings = Sazkarî... +shutdown = Vemrîne +suspend = Rawestîne +log-out = Derkeve +lock-screen = Dîmenderê kilît bike +lock-screen-shortcut = Super + Escape +log-out-shortcut = Super + Shift + Escape +confirm-button = + { $action -> + [restart] { restart } + [suspend] { suspend } + [shutdown] Vemrîne + [log-out] { log-out } + *[other] { confirm } + } +confirm-title = + { $action -> + [restart] { restart } + [suspend] { suspend } + [shutdown] { shutdown } + [log-out] Hemû sepanan bigire û derkeve + *[other] Çalakiyê hilbijartî bisepîne + } niha? +confirm-body = + Pergal wê xweber were { $action -> + [restart] jinûvedestpêkkirin + [suspend] rawestandin + [shutdown] vemirandin + [lock-screen] kilîtkirin + [log-out] derketin + *[other] çalakiyê hilbijartî bisepîne + }di { $countdown } çirke de. diff --git a/cosmic-applet-power/src/lib.rs b/cosmic-applet-power/src/lib.rs index 67725fbc..4a5829df 100644 --- a/cosmic-applet-power/src/lib.rs +++ b/cosmic-applet-power/src/lib.rs @@ -16,9 +16,8 @@ use cosmic::{ window, }, surface, theme, - widget::{Space, button, divider, icon, space, text}, + widget::{button, divider, icon, space, text}, }; -use std::sync::LazyLock; use logind_zbus::{ manager::ManagerProxy, @@ -35,9 +34,6 @@ pub mod session_manager; use crate::{cosmic_session::CosmicSessionProxy, session_manager::SessionManagerProxy}; -static SUBSURFACE_ID: LazyLock = - LazyLock::new(|| cosmic::widget::Id::new("subsurface")); - pub fn run() -> cosmic::iced::Result { localize::localize(); @@ -49,7 +45,6 @@ struct Power { icon_name: String, popup: Option, token_tx: Option>, - subsurface_id: window::Id, } #[derive(Debug, Clone, Copy)] @@ -75,6 +70,7 @@ impl PowerAction { } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { Action(PowerAction), TogglePopup, @@ -104,7 +100,6 @@ impl cosmic::Application for Power { Self { core, icon_name: "system-shutdown-symbolic".to_string(), - subsurface_id: window::Id::unique(), token_tx: None, popup: Option::default(), }, diff --git a/cosmic-applet-status-area/Cargo.toml b/cosmic-applet-status-area/Cargo.toml index fe85ad48..d96913ab 100644 --- a/cosmic-applet-status-area/Cargo.toml +++ b/cosmic-applet-status-area/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "cosmic-applet-status-area" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] futures.workspace = true -libcosmic.workspace = true +cosmic.workspace = true serde.workspace = true rustc-hash.workspace = true tokio.workspace = true diff --git a/cosmic-applet-status-area/src/components/app.rs b/cosmic-applet-status-area/src/components/app.rs index 1071d7ac..7bedc653 100644 --- a/cosmic-applet-status-area/src/components/app.rs +++ b/cosmic-applet-status-area/src/components/app.rs @@ -9,11 +9,14 @@ use cosmic::{ }, cctk::sctk::reexports::calloop, iced::{ - self, Length, Subscription, + self, + Event::Mouse, + Length, Subscription, event, + mouse::{self, ScrollDelta}, platform_specific::shell::commands::popup::{destroy_popup, get_popup}, - theme::Style, window, }, + scroll::{DiscreteScrollDelta, DiscreteScrollState}, surface, widget::{container, mouse_area}, }; @@ -25,6 +28,7 @@ use crate::{ }; #[derive(Clone, Debug)] +#[allow(dead_code)] pub enum Msg { None, Activate(usize), @@ -33,7 +37,8 @@ pub enum Msg { StatusMenu((usize, status_menu::Msg)), StatusNotifier(status_notifier_watcher::Event), TogglePopup(usize), - Hovered(usize), + Hovered(Option), + WheelScrolled(ScrollDelta), Surface(surface::Action), ToggleOverflow, HoveredOverflow, @@ -49,6 +54,8 @@ pub(crate) struct App { max_menu_id: usize, popup: Option, overflow_popup: Option, + hovered_menu: Option, + scroll_states: BTreeMap, token_tx: Option>, } @@ -103,9 +110,10 @@ impl App { let overflow_index = self.overflow_index().unwrap_or(0); let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| { mouse_area( - menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(*id)), + menu_icon_button(&self.core.applet, menu).on_press_down(Msg::TogglePopup(*id)), ) - .on_enter(Msg::Hovered(*id)) + .on_enter(Msg::Hovered(Some(*id))) + .on_exit(Msg::Hovered(None)) .into() }); @@ -210,6 +218,7 @@ impl cosmic::Application for App { { let id = *id; self.menus.remove(&id); + self.scroll_states.remove(&id); if self.open_menu == Some(id) { self.open_menu = None; if let Some(popup_id) = self.popup { @@ -312,6 +321,10 @@ impl cosmic::Application for App { } }, Msg::Hovered(id) => { + self.hovered_menu = id; + let Some(id) = id else { + return Task::none(); + }; let mut cmds = Vec::new(); if let Some(old_id) = self.open_menu.take() { if old_id != id { @@ -361,6 +374,19 @@ impl cosmic::Application for App { cmds.push(get_popup(popup_settings)); Task::batch(cmds) } + Msg::WheelScrolled(delta) => { + let Some(id) = self.hovered_menu else { + return Task::none(); + }; + let discrete_delta = self.scroll_states.entry(id).or_default().update(delta); + let Some((delta, orientation)) = discrete_scroll_delta(discrete_delta) else { + return Task::none(); + }; + let Some(menu) = self.menus.get(&id) else { + return Task::none(); + }; + scroll(id, menu.item.item_proxy().clone(), delta, orientation) + } Msg::Surface(a) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), @@ -405,6 +431,7 @@ impl cosmic::Application for App { } } Msg::HoveredOverflow => { + self.hovered_menu = None; let mut cmds = Vec::new(); if self.overflow_popup.is_some() { // If we already have an overflow popup, do nothing @@ -460,6 +487,12 @@ impl cosmic::Application for App { subscriptions.push(menu.subscription(is_open).with(*id).map(Msg::StatusMenu)); } subscriptions.push(activation_token_subscription(0).map(Msg::Token)); + subscriptions.push(event::listen_with(|e, status, _| match (e, status) { + (Mouse(mouse::Event::WheelScrolled { delta }), event::Status::Ignored) => { + Some(Msg::WheelScrolled(delta)) + } + _ => None, + })); iced::Subscription::batch(subscriptions) } @@ -472,9 +505,10 @@ impl cosmic::Application for App { .iter() .take(overflow_index.unwrap_or(self.menus.len())) .map(|(id, menu)| { - mouse_area(menu_icon_button(&self.core.applet, &menu).on_press(Msg::Activate(*id))) + mouse_area(menu_icon_button(&self.core.applet, menu).on_press(Msg::Activate(*id))) .on_right_press(Msg::TogglePopup(*id)) - .on_enter(Msg::Hovered(*id)) + .on_enter(Msg::Hovered(Some(*id))) + .on_exit(Msg::Hovered(None)) .into() }); @@ -570,6 +604,33 @@ fn activate( }) } +fn scroll( + id: usize, + item_proxy: crate::subscriptions::status_notifier_item::StatusNotifierItemProxy<'static>, + delta: i32, + orientation: &'static str, +) -> Task> { + Task::future(async move { + match item_proxy.scroll(delta, orientation).await { + Ok(_) => cosmic::action::app(Msg::None), + Err(err) => { + tracing::error!("Scroll failed for {}: {}", id, err); + cosmic::action::app(Msg::None) + } + } + }) +} + +fn discrete_scroll_delta(delta: DiscreteScrollDelta) -> Option<(i32, &'static str)> { + if delta.y != 0 { + Some((delta.y as i32, "vertical")) + } else if delta.x != 0 { + Some((delta.x as i32, "horizontal")) + } else { + None + } +} + fn menu_icon_button<'a>( applet: &'a cosmic::applet::Context, menu: &'a status_menu::State, @@ -577,7 +638,7 @@ fn menu_icon_button<'a>( let icon = menu.icon_handle().clone(); let theme = cosmic::theme::active(); - let theme = theme.cosmic(); + let _theme = theme.cosmic(); let suggested = applet.suggested_size(true); let padding = applet.suggested_padding(true).1; @@ -595,7 +656,7 @@ fn menu_icon_button<'a>( .class(if symbolic { cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| { cosmic::iced::widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), + color: Some(theme.cosmic().background(false).on.into()), } })) } else { @@ -614,3 +675,43 @@ fn menu_icon_button<'a>( pub fn main() -> iced::Result { cosmic::applet::run::(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn discrete_scroll_prefers_vertical_delta() { + assert_eq!( + discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 4, y: -2 }), + Some((-2, "vertical")) + ); + } + + #[test] + fn discrete_scroll_uses_horizontal_delta_when_vertical_is_zero() { + assert_eq!( + discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 3, y: 0 }), + Some((3, "horizontal")) + ); + } + + #[test] + fn discrete_scroll_ignores_zero_delta() { + assert_eq!( + discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 0, y: 0 }), + None + ); + } + + #[test] + fn pixel_scroll_accumulates_before_emitting_discrete_delta() { + let mut state = cosmic::scroll::DiscreteScrollState::default(); + + let first = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 }); + assert_eq!(discrete_scroll_delta(first), None); + + let second = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 }); + assert_eq!(discrete_scroll_delta(second), Some((1, "vertical"))); + } +} diff --git a/cosmic-applet-status-area/src/components/status_menu.rs b/cosmic-applet-status-area/src/components/status_menu.rs index fedd79e4..e706f37b 100644 --- a/cosmic-applet-status-area/src/components/status_menu.rs +++ b/cosmic-applet-status-area/src/components/status_menu.rs @@ -26,6 +26,7 @@ pub struct State { expanded: Option, // TODO handle icon with multiple sizes? icon_handle: icon::Handle, + icon_theme_path: Option, click_event: Option<(i32, bool)>, } @@ -39,6 +40,7 @@ impl State { icon_handle: icon::from_name("application-default") .prefer_svg(true) .handle(), + icon_theme_path: None, click_event: None, }, iced::Task::none(), @@ -63,6 +65,7 @@ impl State { } Msg::Icon(update) => { let icon_name = update.name.unwrap_or_default(); + self.icon_theme_path = update.theme_path; // Use the icon pixmap if an icon was not defined by name. if icon_name.is_empty() { @@ -93,13 +96,18 @@ impl State { self.icon_handle = if Path::new(&icon_name).exists() { icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true) } else { - icon::from_name(icon_name) - .prefer_svg(true) - .fallback(Some(IconFallback::Names(vec![ + let mut builder = icon::from_name(icon_name).prefer_svg(true).fallback(Some( + IconFallback::Names(vec![ "application-default".into(), "application-x-executable".into(), - ]))) - .handle() + ]), + )); + + if let Some(ref theme_path) = self.icon_theme_path { + builder = builder.with_extra_paths(vec![theme_path.clone()]); + } + + builder.handle() }; iced::Task::none() diff --git a/cosmic-applet-status-area/src/status_notifier_watcher.rs b/cosmic-applet-status-area/src/status_notifier_watcher.rs index 1bb75d79..5c8b6175 100644 --- a/cosmic-applet-status-area/src/status_notifier_watcher.rs +++ b/cosmic-applet-status-area/src/status_notifier_watcher.rs @@ -10,21 +10,18 @@ //! can be socket-activated and not conflict with anything else running as a status notifier //! watcher. //! -//! The daemon runs as long as as there is at least one client still connected. Which it checks -//! for every `REFRESH_INTERVAL`. +//! The daemon runs as long as there is at least one client still connected. use crate::subscriptions::status_notifier_watcher::server::create_service; use crate::unique_names::UniqueNames; use futures::StreamExt; -use std::{collections::HashSet, time::Duration}; +use std::collections::HashSet; use zbus::fdo; use zbus::message::Header; const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher"; const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher"; -const REFRESH_INTERVAL: Duration = Duration::from_secs(60); - /// Run daemon pub fn run() -> cosmic::iced::Result { if let Err(err) = run_inner() { @@ -42,7 +39,7 @@ pub async fn cosmic_register(conn: &zbus::Connection) -> zbus::Result<()> { tokio::spawn(async move { while let Some(value) = stream.next().await { if let Some(_unique_name) = value { - /// Register with new owner + // Register with new owner. let _ = cosmic_watcher.register_applet().await; } } diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs index fd51390e..2c3d4904 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_item.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use std::hash::Hash; +use std::path::PathBuf; use cosmic::iced::{self, Subscription}; use futures::{FutureExt, StreamExt}; @@ -27,7 +28,7 @@ pub struct Icon { pub struct IconUpdate { pub name: Option, pub pixmap: Option>, - // pub theme_path: Option, + pub theme_path: Option, } impl StatusNotifierItem { @@ -118,11 +119,11 @@ impl StatusNotifierItem { async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate { let icon_name = item_proxy.icon_name().await; let icon_pixmap = item_proxy.icon_pixmap().await; - // let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from); + let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from); IconUpdate { name: icon_name.ok(), pixmap: icon_pixmap.ok(), - // theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()), + theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()), } } @@ -201,6 +202,8 @@ pub trait StatusNotifierItem { fn activate(&self, x: i32, y: i32) -> zbus::Result<()>; fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>; + + fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>; } #[derive(Clone, Debug)] diff --git a/cosmic-applet-tiling/Cargo.toml b/cosmic-applet-tiling/Cargo.toml index 23e689e0..7767a997 100644 --- a/cosmic-applet-tiling/Cargo.toml +++ b/cosmic-applet-tiling/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "cosmic-applet-tiling" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] -libcosmic.workspace = true +cosmic.workspace = true anyhow.workspace = true cctk.workspace = true -cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" } +cosmic-comp-config.workspace = true cosmic-protocols.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true diff --git a/cosmic-applet-tiling/i18n/hu/cosmic_applet_tiling.ftl b/cosmic-applet-tiling/i18n/hu/cosmic_applet_tiling.ftl index 7d622463..a2d0d4bc 100644 --- a/cosmic-applet-tiling/i18n/hu/cosmic_applet_tiling.ftl +++ b/cosmic-applet-tiling/i18n/hu/cosmic_applet_tiling.ftl @@ -3,11 +3,11 @@ tile-current = Jelenlegi munkaterület csempézése shortcuts = Gyorsbillentyűk navigate-windows = Ablakok navigálása move-window = Ablak mozgatása -toggle-floating-window = Az ablakok lebegtetésének be- és kikapcsolása +toggle-floating-window = Lebegő ablak be/ki view-all-shortcuts = Az összes gyorsbillentyű megtekintése… active-hint = Aktív ablak kiemelése gaps = Hézagok -floating-window-exceptions = Lebegő ablak kivételek… +floating-window-exceptions = Lebegőablak-kivételek… window-management-settings = Ablakkezelési beállítások… all-workspaces = Összes munkaterület per-workspace = Munkaterületenként diff --git a/cosmic-applet-tiling/i18n/kmr/cosmic_applet_tiling.ftl b/cosmic-applet-tiling/i18n/kmr/cosmic_applet_tiling.ftl index e69de29b..e3f124ff 100644 --- a/cosmic-applet-tiling/i18n/kmr/cosmic_applet_tiling.ftl +++ b/cosmic-applet-tiling/i18n/kmr/cosmic_applet_tiling.ftl @@ -0,0 +1,20 @@ +tile-windows = Çarçoveyan xweber berhev bike +tile-current = Qada xebatê heyî berhev bike +shortcuts = Kurterê +navigate-windows = Di nav çarçoveyan de bigere +move-window = Çarçoveyê bilivîne +toggle-floating-window = Rewşê çarçoveyê herikbar biguhêrîne +view-all-shortcuts = Hemû kurteriyan nîşan bide... +active-hint = Pêşniyara çalak +gaps = Valahî +floating-window-exceptions = Awarteyên çarçoveyê herikbar... +window-management-settings = Sazkariyên rêveberiya çarçoveyê... +all-workspaces = Hemû qadên xebatê +per-workspace = Li gorî qada xebatê +super = Super +shift = Shift +arrow-keys = tîr +tiled = Berhevkirî +floating = Herikbar +autotile-behavior = Li ser qadên xebatê çarçoveyan berhev bike +new-workspace = Tevgera qada xebatê ya nû diff --git a/cosmic-applet-tiling/src/window.rs b/cosmic-applet-tiling/src/window.rs index 327d5d75..7da430e3 100644 --- a/cosmic-applet-tiling/src/window.rs +++ b/cosmic-applet-tiling/src/window.rs @@ -26,7 +26,7 @@ use cosmic::{ }; use cosmic_comp_config::{CosmicCompConfig, TileBehavior}; use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState; -use std::{thread, time::Instant}; +use std::thread; use tracing::error; const ID: &str = "com.system76.CosmicAppletTiling"; @@ -46,6 +46,7 @@ pub struct Window { } #[derive(Clone, Debug)] +#[allow(dead_code)] pub enum Message { TogglePopup, PopupClosed(Id), diff --git a/cosmic-applet-time/Cargo.toml b/cosmic-applet-time/Cargo.toml index ff5e6298..3c85ee07 100644 --- a/cosmic-applet-time/Cargo.toml +++ b/cosmic-applet-time/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applet-time" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -9,7 +9,7 @@ cosmic-applets-config.workspace = true jiff = "0.2" i18n-embed-fl.workspace = true i18n-embed.workspace = true -libcosmic.workspace = true +cosmic.workspace = true rust-embed.workspace = true tokio.workspace = true tracing-log.workspace = true @@ -17,5 +17,5 @@ tracing-subscriber.workspace = true tracing.workspace = true icu = { version = "2.1.1", features = ["compiled_data"] } zbus.workspace = true -timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" } +timedate-zbus = { path = "../../dbus-settings-bindings/timedate" } logind-zbus = "5.3.2" diff --git a/cosmic-applet-time/i18n/kmr/cosmic_applet_time.ftl b/cosmic-applet-time/i18n/kmr/cosmic_applet_time.ftl index e69de29b..e6ff41f9 100644 --- a/cosmic-applet-time/i18n/kmr/cosmic_applet_time.ftl +++ b/cosmic-applet-time/i18n/kmr/cosmic_applet_time.ftl @@ -0,0 +1 @@ +datetime-settings = Sazkariyên dîrok, dem û salnameyê... diff --git a/cosmic-applet-time/src/window.rs b/cosmic-applet-time/src/window.rs index a44d81ee..ddd37d31 100644 --- a/cosmic-applet-time/src/window.rs +++ b/cosmic-applet-time/src/window.rs @@ -94,6 +94,7 @@ pub struct Window { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum Message { TogglePopup, CloseRequested(window::Id), @@ -361,7 +362,7 @@ impl cosmic::Application for Window { } fn subscription(&self) -> Subscription { - fn time_subscription(mut show_seconds: watch::Receiver) -> Subscription { + fn time_subscription(show_seconds: watch::Receiver) -> Subscription { struct Wrapper { inner: watch::Receiver, id: &'static str, @@ -376,7 +377,7 @@ impl cosmic::Application for Window { inner: show_seconds, id: "time-sub", }, - |Wrapper { inner, id }| { + |Wrapper { inner, id: _ }| { let mut show_seconds = inner.clone(); stream::channel(1, move |mut output: mpsc::Sender| async move { // Mark this receiver's state as changed so that it always receives an initial diff --git a/cosmic-applet-workspaces/Cargo.toml b/cosmic-applet-workspaces/Cargo.toml index ee9e04db..103680e2 100644 --- a/cosmic-applet-workspaces/Cargo.toml +++ b/cosmic-applet-workspaces/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "cosmic-applet-workspaces" -version = "1.0.2" +version = "1.0.15" authors = ["Ashley Wulber "] edition = "2024" license = "GPL-3.0-only" [dependencies] -libcosmic.workspace = true +cosmic.workspace = true cctk.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/cosmic-applet-workspaces/i18n/kmr/cosmic_applet_workspaces.ftl b/cosmic-applet-workspaces/i18n/kmr/cosmic_applet_workspaces.ftl index e69de29b..e262ce29 100644 --- a/cosmic-applet-workspaces/i18n/kmr/cosmic_applet_workspaces.ftl +++ b/cosmic-applet-workspaces/i18n/kmr/cosmic_applet_workspaces.ftl @@ -0,0 +1 @@ +cosmic-applet-workspaces = Qadên xebatê yên COSMIC diff --git a/cosmic-applet-workspaces/src/components/app.rs b/cosmic-applet-workspaces/src/components/app.rs index dc4ccdc4..a9bafb79 100644 --- a/cosmic-applet-workspaces/src/components/app.rs +++ b/cosmic-applet-workspaces/src/components/app.rs @@ -84,6 +84,7 @@ impl IcedWorkspacesApplet { } #[derive(Debug, Clone)] +#[allow(dead_code)] enum Message { WorkspaceUpdate(WorkspacesUpdate), WorkspacePressed(ExtWorkspaceHandleV1), diff --git a/cosmic-applets-config/Cargo.toml b/cosmic-applets-config/Cargo.toml index b6999f16..724da32a 100644 --- a/cosmic-applets-config/Cargo.toml +++ b/cosmic-applets-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applets-config" -version = "1.0.2" +version = "1.0.15" edition = "2024" [dependencies] diff --git a/cosmic-applets/Cargo.toml b/cosmic-applets/Cargo.toml index 2d950afc..f204a80c 100644 --- a/cosmic-applets/Cargo.toml +++ b/cosmic-applets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-applets" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" @@ -23,7 +23,7 @@ cosmic-applet-time = { path = "../cosmic-applet-time" } cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" } cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" } cosmic-panel-button = { path = "../cosmic-panel-button" } -libcosmic.workspace = true +cosmic.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tracing-log.workspace = true diff --git a/cosmic-applets/src/main.rs b/cosmic-applets/src/main.rs index f62bbf21..dfd541d1 100644 --- a/cosmic-applets/src/main.rs +++ b/cosmic-applets/src/main.rs @@ -12,26 +12,44 @@ fn main() -> cosmic::iced::Result { }; let start = applet.rfind('/').map_or(0, |v| v + 1); - let cmd = &applet.as_str()[start..]; + let cmd = applet.as_str()[start..].to_string(); tracing::info!("Starting `{cmd}` with version {VERSION}"); - match cmd { - "cosmic-app-list" => cosmic_app_list::run(), - "cosmic-applet-a11y" => cosmic_applet_a11y::run(), - "cosmic-applet-audio" => cosmic_applet_audio::run(), - "cosmic-applet-battery" => cosmic_applet_battery::run(), - "cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(), - "cosmic-applet-minimize" => cosmic_applet_minimize::run(), - "cosmic-applet-network" => cosmic_applet_network::run(), - "cosmic-applet-notifications" => cosmic_applet_notifications::run(), - "cosmic-applet-power" => cosmic_applet_power::run(), - "cosmic-applet-status-area" => cosmic_applet_status_area::run(), - "cosmic-applet-tiling" => cosmic_applet_tiling::run(), - "cosmic-applet-time" => cosmic_applet_time::run(), - "cosmic-applet-workspaces" => cosmic_applet_workspaces::run(), - "cosmic-applet-input-sources" => cosmic_applet_input_sources::run(), - "cosmic-panel-button" => cosmic_panel_button::run(), - _ => Ok(()), + let cmd_for_run = cmd.clone(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || { + match cmd_for_run.as_str() { + "cosmic-app-list" => cosmic_app_list::run(), + "cosmic-applet-a11y" => cosmic_applet_a11y::run(), + "cosmic-applet-audio" => cosmic_applet_audio::run(), + "cosmic-applet-battery" => cosmic_applet_battery::run(), + "cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(), + "cosmic-applet-minimize" => cosmic_applet_minimize::run(), + "cosmic-applet-network" => cosmic_applet_network::run(), + "cosmic-applet-notifications" => cosmic_applet_notifications::run(), + "cosmic-applet-power" => cosmic_applet_power::run(), + "cosmic-applet-status-area" => cosmic_applet_status_area::run(), + "cosmic-applet-tiling" => cosmic_applet_tiling::run(), + "cosmic-applet-time" => cosmic_applet_time::run(), + "cosmic-applet-workspaces" => cosmic_applet_workspaces::run(), + "cosmic-applet-input-sources" => cosmic_applet_input_sources::run(), + "cosmic-panel-button" => cosmic_panel_button::run(), + _ => Ok(()), + } + })); + + match result { + Ok(r) => r, + Err(payload) => { + let msg = payload + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "".to_string()); + tracing::error!( + "`{cmd}` panicked (likely compositor disconnect), exiting cleanly: {msg}" + ); + Ok(()) + } } } diff --git a/cosmic-panel-button/Cargo.toml b/cosmic-panel-button/Cargo.toml index dc6ffd9a..69074433 100644 --- a/cosmic-panel-button/Cargo.toml +++ b/cosmic-panel-button/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "cosmic-panel-button" -version = "1.0.2" +version = "1.0.15" edition = "2024" license = "GPL-3.0-only" [dependencies] -libcosmic.workspace = true +cosmic.workspace = true rustc-hash.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/cosmic-panel-button/src/lib.rs b/cosmic-panel-button/src/lib.rs index 45ee1ca2..5c875719 100644 --- a/cosmic-panel-button/src/lib.rs +++ b/cosmic-panel-button/src/lib.rs @@ -48,7 +48,7 @@ impl Button { icon: cosmic::widget::icon::Handle, ) -> cosmic::widget::Button<'a, Message> { let theme = cosmic::theme::active(); - let theme = theme.cosmic(); + let _theme = theme.cosmic(); let suggested = self.core.applet.suggested_size(icon.symbolic); let (major_padding, applet_padding_minor_axis) = @@ -66,7 +66,7 @@ impl Button { .class(if symbolic { cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| { cosmic::iced::widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), + color: Some(theme.cosmic().background(false).on.into()), } })) } else { diff --git a/debian/changelog b/debian/changelog index 147363a2..3d46c583 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cosmic-applets (1.0.15) noble; urgency=medium + + * Update changelog + + -- Michael Murphy Thu, 28 May 2026 19:33:02 +0200 + cosmic-applets (1.0.12) noble; urgency=medium * Update changelog diff --git a/i18n/hu/desktop_entries.ftl b/i18n/hu/desktop_entries.ftl index 637e262e..82b4bfdb 100644 --- a/i18n/hu/desktop_entries.ftl +++ b/i18n/hu/desktop_entries.ftl @@ -40,7 +40,7 @@ cosmic-applet-power-comment = Képernyő zárolása, kijelentkezés, felfüggesz cosmic-app-list-comment = Rögzített alkalmazások indítása és a megnyitott ablakok kezelése cosmic-applet-status-area-comment = Alkalmazásjelzők, melyek menüt is megjeleníthetnek a panelen cosmic-applet-status-area-keywords = COSMIC;kisalkalmazás;alkalmazás;app;jelző;értesítések;tálca;állapot; -cosmic-applet-tiling-comment = Az aktív ablak kiemelésének, valamint az aktuális és munkaterületenkénti automatikus csempézés kezelése +cosmic-applet-tiling-comment = Az aktív ablakkiemelés, valamint a jelenlegi és munkaterületenkénti automatikus csempézés kezelése cosmic-applet-tiling-keywords = COSMIC;kisalkalmazás;csempézés;kiemelés;munkaterületek; cosmic-applet-time-comment = Az aktuális idő megjelenítése a panelen, felugró naptárral cosmic-applet-time-keywords = COSMIC;kisalkalmazás;dátum;idő;naptár; diff --git a/i18n/ja/desktop_entries.ftl b/i18n/ja/desktop_entries.ftl index e69de29b..c5b41c0a 100644 --- a/i18n/ja/desktop_entries.ftl +++ b/i18n/ja/desktop_entries.ftl @@ -0,0 +1 @@ +cosmic-applet-a11y = アクセシビリティ diff --git a/i18n/kmr/desktop_entries.ftl b/i18n/kmr/desktop_entries.ftl index e69de29b..0a42fc18 100644 --- a/i18n/kmr/desktop_entries.ftl +++ b/i18n/kmr/desktop_entries.ftl @@ -0,0 +1,51 @@ +cosmic-applet-audio = Deng +cosmic-app-list = Destgeha sepanan +cosmic-applet-a11y = Gihîştin +cosmic-applet-bluetooth = Bluetooth +cosmic-applet-bluetooth-comment = Amûrên Bluetooth bi rê ve bibe +cosmic-applet-bluetooth-keywords = COSMIC;Sepanok;Bluetooth; +cosmic-applet-input-sources = Çavkaniyên ketanê +cosmic-applet-input-sources-comment = Di navbera çavkaniyên ketanê de biguhêrîne +cosmic-applet-input-sources-keywords = COSMIC;Sepanok;Ketan;Çavkanî; +cosmic-app-list-comment = Sepanên darxistî veke û çarçoveyên vekirî bi rê ve bibe +cosmic-app-list-keywords = COSMIC;Sepanok;Sepan;Destgeh;Lîste;Erk;Bend; +cosmic-applet-a11y-comment = Sazkariyên gihîştinê ji destgehê saz bike +cosmic-applet-a11y-keywords = COSMIC;Sepanok;Gihîştin;Dîmender;A11y;Xwîner;Dîmender;Mezinker;Bijberî;Reng; +cosmic-applet-audio-comment = Hilbijartina amûra dengê, kontrola dengê, û kontrolên mediyaya MPRIS +cosmic-applet-audio-keywords = COSMIC;Sepanok;Deng;Deng;MPRIS; +cosmic-applet-battery = Hêz & Betarî +cosmic-applet-battery-comment = Awayên hêzê û vebijêrkên parastina hêzê +cosmic-applet-battery-keywords = COSMIC;Sepanok;Hêz;Betarî; +cosmic-applet-minimize = Çarçoveyên biçûk +cosmic-applet-minimize-comment = Çarçoveyên biçûk bi rê ve bibe +cosmic-applet-minimize-keywords = COSMIC;Sepanok;Biçûkkirin; +cosmic-applet-network = Tor +cosmic-applet-network-comment = Girêdanên torê bi rê ve bibe +cosmic-applet-network-keywords = COSMIC;Sepanok;Tor; +cosmic-applet-notifications = Navenda Agahdariyan +cosmic-applet-notifications-comment = Agahdarî û rawestandina balkişiyê bi rê ve bibe +cosmic-applet-notifications-keywords = COSMIC;Sepanok;Agahdarî; +cosmic-applet-power = Danaşîna bikarhêner +cosmic-applet-power-comment = Dîmenderê kilît bike, derkeve, rawestîne, ji nû ve bide destpêkirin, û vemrîne +cosmic-applet-power-keywords = COSMIC;Sepanok;Bikarhêner;Danaşîn;Kilîtkirin;Derketin;Destpêkirin;Vemirandin;Rawestandin; +cosmic-applet-status-area = Desgeha agahdariyan +cosmic-applet-status-area-comment = Nîşanderên sepanan ên ku dibe ku menuyan bo destgehê derxînin +cosmic-applet-status-area-keywords = COSMIC;Sepanok;Sepan;Nîşander;Agahdarî;Destgeh;Rewş; +cosmic-applet-tiling = Berhevkirin +cosmic-applet-tiling-comment = Berhevkirina xweber a çalak, ya niha û ya her qada xebatê bi rê ve bibe +cosmic-applet-tiling-keywords = COSMIC;Sepanok;Berhevkirin;Nîşander;Navenda xebatê; +cosmic-applet-time = Dîrok, dem & Salname +cosmic-applet-time-comment = Demê niha li ser destgehê bi salnameyeke vebûyî nîşan bide +cosmic-applet-time-keywords = COSMIC;Sepanok;Dîrok;Dem;Salname; +cosmic-applet-workspaces = Qada xebatê jimarkirî +cosmic-applet-workspaces-comment = Di navbera qada xebatê jimarkirî de di destgehê de biguherîne +cosmic-applet-workspaces-keywords = COSMIC;Sepanok;Qada xebatê; +cosmic-panel-app-button = Bişkoka pirtûkxaneya sepanan +cosmic-panel-app-button-comment = Pirtûkxaneya sepanan ji bo vekirina sepanên sazkirî veke +cosmic-panel-app-button-keywords = COSMIC;Sepanok;Sepan;Pirtûkxane;Destgeh; +cosmic-panel-launcher-button = Bişkoka destpêker +cosmic-panel-launcher-button-comment = Destpêkerê ji bo lêgerîna sepanan û xebitandina fermanan veke +cosmic-panel-launcher-button-keywords = COSMIC;Sepanok;Destpêker;Rêveber; +cosmic-panel-workspaces-button = Bişkoka qadên xebatê +cosmic-panel-workspaces-button-comment = Pêşdîtina qadên xebatê veke ji bo bi rê ve bibî û biguhêrînî +cosmic-panel-workspaces-button-keywords = COSMIC;Sepan;Qada xebatê;Pêşdîtin; diff --git a/i18n/ko/desktop_entries.ftl b/i18n/ko/desktop_entries.ftl index 7a9abf42..17b758c9 100644 --- a/i18n/ko/desktop_entries.ftl +++ b/i18n/ko/desktop_entries.ftl @@ -46,3 +46,6 @@ cosmic-panel-launcher-button-keywords = COSMIC;애플릿;런처;실행기; cosmic-panel-workspaces-button = 작업 공간 버튼 cosmic-panel-workspaces-button-comment = 작업 공간 관리 및 전환을 위해 작업 공간 개요 열기 cosmic-panel-workspaces-button-keywords = COSMIC;애플릿;작업 공간;개요; +cosmic-applet-workspaces-comment = 패널에서 번호가 지정된 작업 공간 간 전환 +cosmic-applet-tiling-comment = 활성 창 강조, 현재 및 작업 공간별 자동 타일링 관리 +cosmic-applet-workspaces = 번호가 지정된 작업 공간 diff --git a/i18n/pt/desktop_entries.ftl b/i18n/pt/desktop_entries.ftl index de93fad1..1fe912c3 100644 --- a/i18n/pt/desktop_entries.ftl +++ b/i18n/pt/desktop_entries.ftl @@ -20,3 +20,8 @@ cosmic-applet-workspaces-comment = Alternar entre áreas de trabalho numeradas n cosmic-applet-workspaces-keywords = COSMIC;Miniaplicação;Applet;Área;Trabalho; cosmic-panel-workspaces-button-comment = Abrir a visão geral das áreas de trabalho para gerir e alternar entre elas cosmic-panel-workspaces-button-keywords = COSMIC;Miniaplicação;Applet;Área;Trabalho;Visão;Geral; +cosmic-applet-a11y-comment = Configurar definições de acessibilidade a partir do painel +cosmic-applet-battery = Energia e Bateria +cosmic-app-list-keywords = COSMIC;Miniaplicação;Applet;Aplicação;Bandeja;Lista;Tarefa;Barra; +cosmic-applet-a11y-keywords = COSMIC;Miniaplicação;Applet;Acessibilidade;A11y;Ecrã;Leitor;Lupa;Ampliador;Contraste;Cor; +cosmic-applet-audio-keywords = COSMIC;Miniaplicação;Applet;Som;Áudio;MPRIS; diff --git a/i18n/uk/desktop_entries.ftl b/i18n/uk/desktop_entries.ftl index c6088440..72bfb5ba 100644 --- a/i18n/uk/desktop_entries.ftl +++ b/i18n/uk/desktop_entries.ftl @@ -32,7 +32,7 @@ cosmic-applet-minimize-comment = Керування згорнутими вік cosmic-applet-minimize-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Згорнути;Згорнуті;Вікна;Вікно; cosmic-applet-network-comment = Керування мережевими з’єднаннями cosmic-applet-network-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Мережа;Інтернет; -cosmic-applet-notifications-comment = Керування сповіщеннями та режимом «Не турбувати» +cosmic-applet-notifications-comment = Керування сповіщеннями та режимом Не турбувати cosmic-applet-notifications-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Сповіщення; cosmic-applet-power-comment = Блокування екрана, вихід із сеансу, призупинення, перезапуск і вимкнення cosmic-applet-power-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Сеанс;Користувач;Блокування;Вихід;Перезапуск;Вимкнення;Призупинення;Сон; diff --git a/justfile b/justfile index 83a9f819..99c53104 100644 --- a/justfile +++ b/justfile @@ -59,13 +59,16 @@ _install_status_notifier_watcher: sed "s|@bindir@|{{ prefixdir }}|" cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service install -Dm0644 cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service {{ libdir }}/systemd/user/com.system76.CosmicStatusNotifierWatcher.service +_install_secret_agent_policy: + install -Dm0644 cosmic-applet-network/data/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf {{ sharedir }}/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf + # Installs files into the system -install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') _install_metainfo _install_status_notifier_watcher +install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') _install_metainfo _install_status_notifier_watcher _install_secret_agent_policy # Vendor Cargo dependencies locally vendor: mkdir -p .cargo - cargo vendor | head -n -1 > .cargo/config + cargo vendor --locked | head -n -1 > .cargo/config echo 'directory = "vendor"' >> .cargo/config tar pcf vendor.tar vendor rm -rf vendor diff --git a/redeploy.sh b/redeploy.sh new file mode 100755 index 00000000..686bf3cb --- /dev/null +++ b/redeploy.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Recompile et déploie le fork local cosmic-applets (multiplexor cosmic-applets + +# binaire séparé cosmic-app-list). +# +# Cibles : /usr/local/bin/cosmic-applets et /usr/local/bin/cosmic-app-list +# (binaires compilés manuellement, hors pacman, qui priment sur ceux du paquet +# via $PATH /usr/local/bin avant /usr/bin). +# +# Branche de déploiement : yoda-dock-magnification (contient les commits +# magnification + le fix Wayland error handling). + +set -euo pipefail + +REPO="/home/lionel/Projets/COSMIC/cosmic-applets" +TARGETS=( + "cosmic-applets" + "cosmic-app-list" + "cosmic-panel-button" +) +BIN_DIR="/usr/local/bin" + +cd "$REPO" + +echo "==> Branche actuelle : $(git branch --show-current)" +echo "==> Build release (workspace)..." +cargo build --release --workspace + +STAMP=$(date +%Y%m%d-%H%M%S) +for bin in "${TARGETS[@]}"; do + src="$REPO/target/release/$bin" + dst="$BIN_DIR/$bin" + + if [[ ! -f "$src" ]]; then + echo "!! Binaire absent après build : $src" >&2 + exit 1 + fi + + if [[ -f "$dst" ]]; then + echo "==> Backup $dst" + sudo cp -a "$dst" "${dst}.bak.${STAMP}" + fi + + echo "==> Install $src -> $dst" + sudo install -m755 "$src" "$dst" +done + +echo "==> Kill des process applet en cours (cosmic-panel relancera à la demande)" +pkill -u "$USER" -f "/usr/local/bin/cosmic-applet" || true +pkill -u "$USER" -f "/usr/local/bin/cosmic-app-list" || true +pkill -u "$USER" -f "/usr/local/bin/cosmic-panel-button" || true + +echo "==> Vérif" +for bin in "${TARGETS[@]}"; do + file "$BIN_DIR/$bin" +done +echo "OK — relogin recommandé pour repartir sur des process applet propres." diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 79d20990..075062e5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.93" +channel = "1.93.0"