From cdb60ec82854bc5281e327ad41d2c9a2b25f7a7b Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:02:46 +0200 Subject: [PATCH 1/8] feat: Move power menu to the panel, i18n, systemd support Power menu is now in a pop-over (menu button) in the panel (top-right). Power menu can understand how to work with systemd. This enables better feature parity between what systemd can do and what the power menu knows how to do. The power menu is also aware of what button is "dangerous" (powers off the systemrharish101/ReGreet) and asks for confirmation Closes rharish101/ReGreet#108. --- Cargo.lock | 1385 ++++++++++++++++++++------ Cargo.toml | 13 +- i18n.toml | 10 + i18n/en/regreet.ftl | 16 + i18n/uk/regreet.ftl | 16 + regreet.sample.toml | 10 +- src/config.rs | 18 + src/gui/component.rs | 18 +- src/gui/icons.rs | 18 + src/gui/messages.rs | 2 - src/gui/mod.rs | 2 + src/gui/model.rs | 54 +- src/gui/templates.rs | 64 +- src/gui/widget/power_menu/mod.rs | 64 ++ src/gui/widget/power_menu/systemd.rs | 267 +++++ src/i18n.rs | 60 ++ src/main.rs | 16 + src/tomlutils.rs | 6 +- 18 files changed, 1626 insertions(+), 413 deletions(-) create mode 100644 i18n.toml create mode 100644 i18n/en/regreet.ftl create mode 100644 i18n/uk/regreet.ftl create mode 100644 src/gui/icons.rs create mode 100644 src/gui/widget/power_menu/mod.rs create mode 100644 src/gui/widget/power_menu/systemd.rs create mode 100644 src/i18n.rs diff --git a/Cargo.lock b/Cargo.lock index b386e60..1d07122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -32,12 +23,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -49,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -64,43 +49,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -109,48 +101,57 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backtrace" -version = "0.3.74" +name = "basic-toml" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", + "serde", ] [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cairo-rs" -version = "0.20.7" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" +checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" dependencies = [ "bitflags", "cairo-sys-rs", @@ -160,9 +161,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df" +checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" dependencies = [ "glib-sys", "libc", @@ -171,18 +172,19 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.6" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-expr" -version = "0.17.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", "target-lexicon", @@ -190,27 +192,26 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets", + "windows-link", ] [[package]] name = "clap" -version = "4.5.23" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -218,9 +219,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -230,9 +231,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -242,21 +243,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", "konst", @@ -279,11 +280,20 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -303,15 +313,46 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "educe" version = "0.6.0" @@ -346,9 +387,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "field-offset" @@ -370,16 +411,75 @@ dependencies = [ "flate2", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "flume" version = "0.11.1" @@ -394,15 +494,15 @@ dependencies = [ [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "fragile" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] name = "futures" @@ -495,9 +595,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6efc7705f7863d37b12ad6974cbb310d35d054f5108cdc1e69037742f573c4c" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" dependencies = [ "gdk-pixbuf-sys", "gio", @@ -507,9 +607,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f2587c9202bf997476bbba6aaed4f78a11538a2567df002a5f57f5331d0b5c" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" dependencies = [ "gio-sys", "glib-sys", @@ -520,9 +620,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0196720118f880f71fe7da971eff58cc43a89c9cf73f46076b7cb1e60889b15" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -535,9 +635,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b0e1340bd15e7a78810cf39fed9e5d85f0a8f80b1d999d384ca17dcc452b60" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -550,11 +650,21 @@ dependencies = [ "system-deps", ] +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -563,17 +673,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "gio" -version = "0.20.7" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a517657589a174be9f60c667f1fec8b7ac82ed5db4ebf56cf073a3b5955d8e2e" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" dependencies = [ "futures-channel", "futures-core", @@ -588,9 +692,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" dependencies = [ "glib-sys", "gobject-sys", @@ -601,9 +705,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.7" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" dependencies = [ "bitflags", "futures-channel", @@ -622,9 +726,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.7" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" dependencies = [ "heck", "proc-macro-crate", @@ -635,9 +739,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" dependencies = [ "libc", "system-deps", @@ -645,15 +749,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gobject-sys" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" dependencies = [ "glib-sys", "libc", @@ -662,9 +766,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39d3bcd2e24fd9c2874a56f277b72c03e728de9bdc95a8d4ef4c962f10ced98" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" dependencies = [ "glib", "graphene-sys", @@ -673,9 +777,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a68d39515bf340e879b72cecd4a25c1332557757ada6e8aba8654b4b81d23a" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" dependencies = [ "glib-sys", "libc", @@ -698,9 +802,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b9188db0a6219e708b6b6e7225718e459def664023dbddb8395ca1486d8102" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" dependencies = [ "cairo-rs", "gdk4", @@ -713,9 +817,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca10fc65d68528a548efa3d8747934adcbe7058b73695c9a7f43a25352fce14" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -729,9 +833,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697ff938136625f6acf75f01951220f47a45adcf0060ee55b4671cf734dac44" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" dependencies = [ "cairo-rs", "field-offset", @@ -762,9 +866,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af4b680cee5d2f786a2f91f1c77e95ecf2254522f0ca4edf3a2dce6cb35cecf" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -781,15 +885,21 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.5.0" @@ -798,9 +908,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "humantime-serde" @@ -812,16 +922,84 @@ dependencies = [ "serde", ] +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "locale_config", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -837,12 +1015,31 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", ] [[package]] @@ -853,41 +1050,44 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.1.16" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8" +checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5" dependencies = [ "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", "serde", "windows-sys 0.59.0", ] [[package]] name = "jiff-tzdb" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -914,27 +1114,70 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libadwaita" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500135d29c16aabf67baafd3e7741d48e8b8978ca98bac39e589165c8dc78191" +dependencies = [ + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6680988058c2558baf3f548a370e4e78da3bf7f08469daa822ac414842c912db" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -942,14 +1185,23 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -962,22 +1214,23 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -991,11 +1244,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1023,25 +1276,51 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ - "memchr", + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "pango" -version = "0.20.7" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e89bd74250a03a05cec047b43465469102af803be2bf5e5a1088f8b8455e087" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" dependencies = [ "gio", "glib", @@ -1051,9 +1330,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.7" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71787e0019b499a5eda889279e4adb455a4f3fdd6870cd5ab7f4a5aa25df6699" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" dependencies = [ "glib-sys", "gobject-sys", @@ -1061,11 +1340,34 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1075,60 +1377,106 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "powerfmt" -version = "0.2.0" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "proc-macro-crate" -version = "3.2.0" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ - "toml_edit", + "portable-atomic", ] [[package]] -name = "proc-macro2" -version = "1.0.92" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" -dependencies = [ - "unicode-ident", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "pwd" -version = "1.4.0" +name = "proc-macro-crate" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "libc", - "thiserror 1.0.69", + "toml_edit 0.23.7", ] [[package]] -name = "quote" -version = "1.0.38" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", + "quote", ] [[package]] -name = "regex" -version = "1.11.1" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ - "aho-corasick", + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pwd" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", "memchr", "regex-automata", "regex-syntax", @@ -1136,9 +1484,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1147,9 +1495,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regreet" @@ -1163,22 +1511,26 @@ dependencies = [ "greetd_ipc", "gtk4", "humantime-serde", + "i18n-embed", + "i18n-embed-fl", "jiff", "lazy_static", "lru", "pwd", "regex", "relm4", + "rust-embed", "serde", "shlex", "test-case", - "thiserror 2.0.9", + "thiserror 2.0.17", "tokio", - "toml", + "toml 0.8.23", "tracing", "tracing-appender", "tracing-subscriber", "tracker", + "unic-langid", ] [[package]] @@ -1191,6 +1543,7 @@ dependencies = [ "fragile", "futures", "gtk4", + "libadwaita", "once_cell", "relm4-css", "relm4-macros", @@ -1216,10 +1569,50 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rust-embed" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -1230,11 +1623,26 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "scopeguard" @@ -1242,26 +1650,51 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.0", +] + +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + [[package]] name = "semver" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1270,25 +1703,46 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1305,28 +1759,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "slab" -version = "0.4.9" +name = "signal-hook-registry" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ - "autocfg", + "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1346,9 +1812,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.93" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -1357,22 +1823,22 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.3" +version = "7.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +checksum = "c236d79f20808ca0084bfcd1a2fd6c686216b7f7a0c4fc39deb0cbf5eaab3713" dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 0.9.8", "version-compare", ] [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "test-case" @@ -1418,11 +1884,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.17", ] [[package]] @@ -1438,9 +1904,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -1449,19 +1915,18 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "time" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1476,69 +1941,146 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" -version = "1.43.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", ] [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tracing" version = "0.1.41" @@ -1564,9 +2106,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -1575,9 +2117,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -1629,11 +2171,45 @@ dependencies = [ "syn", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-xid" @@ -1649,9 +2225,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-compare" @@ -1659,28 +2235,46 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -1692,9 +2286,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1702,9 +2296,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -1715,26 +2309,101 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -1743,7 +2412,25 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -1752,14 +2439,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1768,53 +2472,116 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "zerofrom", +] diff --git a/Cargo.toml b/Cargo.toml index 824feae..8abb4b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ name = "regreet" version = "0.1.3" authors = ["Harish Rajagopal "] edition = "2021" -rust-version = "1.75" +rust-version = "1.80" description = "Clean and customizable greeter for greetd" repository = "https://github.com/rharish101/ReGreet/" license = "GPL-3.0-or-later" @@ -22,21 +22,28 @@ glob = "0.3" greetd_ipc = { version = "0.10", features = ["tokio-codec"] } gtk4 = "0.9" humantime-serde = "1.1.1" +i18n-embed = { version = "0.15.2", features = [ + "desktop-requester", + "fluent-system", +] } +i18n-embed-fl = "0.9.2" jiff = "0.1.14" lazy_static = "1.5.0" lru = "0.12" pwd = "1.4.0" regex = "1.10" -relm4 = "0.9" +relm4 = { version = "0.9", features = ["adw", "libadwaita"] } +rust-embed = "8.5.0" serde = { version = "1.0", features = ["derive"] } shlex = "1.3" thiserror = "2.0" -tokio = { version = "1.43", features = ["net", "time"] } +tokio = { version = "1.43", features = ["net", "process", "time"] } toml = "0.8" tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["local-time"] } tracker = "0.2" +unic-langid = "0.9.5" [features] gtk4_8 = ["gtk4/v4_8"] diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 0000000..9e70cc8 --- /dev/null +++ b/i18n.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Docs: https://crates.io/crates/cargo-i18n/ + +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/i18n/en/regreet.ftl b/i18n/en/regreet.ftl new file mode 100644 index 0000000..c0650a6 --- /dev/null +++ b/i18n/en/regreet.ftl @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +dialog-cancel = Cancel + +# Power menu + +power-menu-tooltip = Power menu +power-menu-poweroff = Poweroff +power-menu-reboot = Reboot +power-menu-reboot-firmware = Reboot into firmware +power-menu-suspend = Suspend +power-menu-hibernate = Hibernate +power-menu-hybrid-sleep = Hybrid sleep +power-menu-confirm-dialog-heading = Really { $what }? \ No newline at end of file diff --git a/i18n/uk/regreet.ftl b/i18n/uk/regreet.ftl new file mode 100644 index 0000000..a813024 --- /dev/null +++ b/i18n/uk/regreet.ftl @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +dialog-cancel = Відмінити + +# Power menu + +power-menu-tooltip = Керування живленням +power-menu-poweroff = Вимкнути +power-menu-reboot = Перезапустити +power-menu-reboot-firmware = Перезапустити до меню загрузки +power-menu-suspend = Сон +power-menu-hibernate = Гибернація +power-menu-hybrid-sleep = Гібрідний сон +power-menu-confirm-dialog-heading = Точно { $what }? \ No newline at end of file diff --git a/regreet.sample.toml b/regreet.sample.toml index b85f8b0..26068b8 100644 --- a/regreet.sample.toml +++ b/regreet.sample.toml @@ -12,10 +12,12 @@ path = "/usr/share/backgrounds/greeter.jpg" # NOTE: This is ignored if ReGreet isn't compiled with GTK v4.8 support. fit = "Contain" + # The entries defined in this section will be passed to the session as environment variables when it is started [env] ENV_VARIABLE = "value" + [GTK] # Whether to use the dark theme application_prefer_dark_theme = true @@ -43,7 +45,7 @@ reboot = ["systemctl", "reboot"] poweroff = ["systemctl", "poweroff"] # The command prefix for X11 sessions to start the X server -x11_prefix = [ "startx", "/usr/bin/env" ] +x11_prefix = ["startx", "/usr/bin/env"] [appearance] # The message that initially displays on startup @@ -65,3 +67,9 @@ timezone = "America/Chicago" # Ask GTK to make the label at least this wide. This helps keeps the parent element layout and width consistent. # Experiment with different widths, the interpretation of this value is entirely up to GTK. label_width = 150 + + +# The power menu widget +[widget.power_menu.systemd] +# Everything except `["hybernate", "hybrid-sleep"]` +actions = ["poweroff", "reboot", "reboot-firmware", "suspend"] diff --git a/src/config.rs b/src/config.rs index cd05323..e8c2411 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::constants::{GREETING_MSG, POWEROFF_CMD, REBOOT_CMD, X11_CMD_PREFIX}; use crate::gui::widget::clock::ClockConfig; +use crate::gui::widget::power_menu::PowerMenuConfig; use crate::tomlutils::load_toml; #[derive(Deserialize, Serialize)] @@ -130,6 +131,9 @@ pub struct Config { pub struct WidgetConfig { #[serde(default)] pub(crate) clock: ClockConfig, + + #[serde(default)] + pub(crate) power_menu: PowerMenuConfig, } impl Config { @@ -162,3 +166,17 @@ impl Config { self.appearance.greeting_msg.clone() } } + +#[cfg(test)] +mod tests { + use crate::tomlutils::{load_raw_toml, TomlFileResult}; + + use super::*; + + #[test] + fn sample_config_works() -> TomlFileResult<()> { + let _: Config = load_raw_toml("regreet.sample.toml")?; + + Ok(()) + } +} diff --git a/src/gui/component.rs b/src/gui/component.rs index eb8385e..a3cb161 100644 --- a/src/gui/component.rs +++ b/src/gui/component.rs @@ -22,7 +22,7 @@ use super::model::{Greeter, InputMode, Updates}; use super::templates::Ui; /// Load GTK settings from the greeter config. -fn setup_settings(model: &Greeter, root: >k::ApplicationWindow) { +fn setup_settings(model: &Greeter, root: &adw::ApplicationWindow) { let settings = root.settings(); let config = if let Some(config) = model.config.get_gtk_settings() { config @@ -116,7 +116,7 @@ impl AsyncComponent for Greeter { view! { // The `view!` macro needs a proper widget, not a template, as the root. #[name = "window"] - gtk::ApplicationWindow { + adw::ApplicationWindow { set_visible: true, // Name the UI widget, otherwise the inner children cannot be accessed by name. @@ -131,6 +131,14 @@ impl AsyncComponent for Greeter { model.clock.widget(), }, + #[template_child] + panel_end { + append = model.power_menu.widget() { + set_halign: gtk::Align::End, + set_hexpand: false, + }, + }, + #[template_child] message_label { #[track(model.updates.changed(Updates::message()))] @@ -309,10 +317,6 @@ impl AsyncComponent for Greeter { #[track(model.updates.changed(Updates::error()))] set_label: model.updates.error.as_ref().unwrap_or(&"".to_string()), }, - #[template_child] - reboot_button { connect_clicked => Self::Input::Reboot }, - #[template_child] - poweroff_button { connect_clicked => Self::Input::PowerOff }, } } } @@ -416,8 +420,6 @@ impl AsyncComponent for Greeter { Self::Input::ToggleManualSess => self .updates .set_manual_sess_mode(!self.updates.manual_sess_mode), - Self::Input::Reboot => self.reboot_click_handler(&sender), - Self::Input::PowerOff => self.poweroff_click_handler(&sender), } } diff --git a/src/gui/icons.rs b/src/gui/icons.rs new file mode 100644 index 0000000..45b24e8 --- /dev/null +++ b/src/gui/icons.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +macro_rules! icons { + ($($name:ident = $value:expr),+$(,)?) => { + $(pub const $name: &str = $value;)+ + }; +} + +icons![ + POWER_MENU = self::POWEROFF, + POWEROFF = "system-shutdown-symbolic", + REBOOT = "system-reboot-symbolic", + REBOOT_FIRMWARE = "application-x-firmware-symbolic", + SUSPEND = "system-suspend-symbolic", + HIBERNATE = "system-hibernate-symbolic", +]; diff --git a/src/gui/messages.rs b/src/gui/messages.rs index f5c6d9b..7d766d4 100644 --- a/src/gui/messages.rs +++ b/src/gui/messages.rs @@ -56,8 +56,6 @@ pub enum InputMsg { ToggleManualUser, /// Toggle manual entry of session. ToggleManualSess, - Reboot, - PowerOff, } #[derive(Debug)] diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 1ef2eba..2fd03c5 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -10,7 +10,9 @@ mod model; mod templates; pub(crate) mod widget { pub mod clock; + pub mod power_menu; } +pub mod icons; pub use component::GreeterInit; pub use model::Greeter; diff --git a/src/gui/model.rs b/src/gui/model.rs index 258c6da..71f8dde 100644 --- a/src/gui/model.rs +++ b/src/gui/model.rs @@ -9,7 +9,6 @@ //! The main logic for the greeter use std::path::Path; -use std::process::Command; use std::sync::Arc; use std::time::Duration; @@ -30,7 +29,7 @@ use crate::sysutil::{SessionInfo, SessionType, SysUtil}; use super::{ messages::{CommandMsg, UserSessInfo}, - widget::clock::Clock, + widget::{clock::Clock, power_menu::PowerMenu}, }; const ERROR_MSG_CLEAR_DELAY: u64 = 5; @@ -96,6 +95,7 @@ pub struct Greeter { pub(super) demo: bool, pub(super) clock: Controller, + pub(super) power_menu: Controller, } impl Greeter { @@ -125,6 +125,10 @@ impl Greeter { .launch(config.widget.clock.clone()) .detach(); + let power_menu = PowerMenu::builder() + .launch(config.widget.power_menu.clone()) + .detach(); + Self { greetd_client, sys_util: SysUtil::new(&config).expect("Couldn't read available users and sessions"), @@ -134,6 +138,7 @@ impl Greeter { updates, demo, clock, + power_menu, } } @@ -176,51 +181,6 @@ impl Greeter { self.updates.set_monitor(chosen_monitor); } - /// Run a command and log any errors in a background thread. - fn run_cmd(command: &[String], sender: &AsyncComponentSender) { - let mut process = Command::new(&command[0]); - process.args(command[1..].iter()); - // Run the command and check its output in a separate thread, so as to not block the GUI. - sender.spawn_command(move |_| match process.output() { - Ok(output) => { - if !output.status.success() { - if let Ok(err) = std::str::from_utf8(&output.stderr) { - error!("Failed to launch command: {err}") - } else { - error!("Failed to launch command: {:?}", output.stderr) - } - } - } - Err(err) => error!("Failed to launch command: {err}"), - }); - } - - /// Event handler for clicking the "Reboot" button - /// - /// This reboots the PC. - #[instrument(skip_all)] - pub(super) fn reboot_click_handler(&self, sender: &AsyncComponentSender) { - if self.demo { - info!("demo: skip reboot"); - return; - } - info!("Rebooting"); - Self::run_cmd(&self.config.get_sys_commands().reboot, sender); - } - - /// Event handler for clicking the "Power-Off" button - /// - /// This shuts down the PC. - #[instrument(skip_all)] - pub(super) fn poweroff_click_handler(&self, sender: &AsyncComponentSender) { - if self.demo { - info!("demo: skip shutdown"); - return; - } - info!("Shutting down"); - Self::run_cmd(&self.config.get_sys_commands().poweroff, sender); - } - /// Event handler for clicking the "Cancel" button /// /// This cancels the created session and goes back to the user/session chooser. diff --git a/src/gui/templates.rs b/src/gui/templates.rs index f8f944d..5d094a3 100644 --- a/src/gui/templates.rs +++ b/src/gui/templates.rs @@ -8,17 +8,6 @@ use gtk::prelude::*; use relm4::{gtk, RelmWidgetExt, WidgetTemplate}; -/// Button that ends the greeter (eg. Reboot) -#[relm4::widget_template(pub)] -impl WidgetTemplate for EndButton { - view! { - gtk::Button { - set_focusable: true, - add_css_class: "destructive-action", - } - } -} - /// Label for an entry/combo box #[relm4::widget_template(pub)] impl WidgetTemplate for EntryLabel { @@ -153,22 +142,34 @@ impl WidgetTemplate for Ui { }, }, - /// Clock widget - #[name = "clock_frame"] - add_overlay = >k::Frame { - set_halign: gtk::Align::Center, + add_overlay = >k::CenterBox { + set_halign: gtk::Align::Fill, set_valign: gtk::Align::Start, + set_hexpand:true, + + /// Clock widget + #[name = "clock_frame"] + #[wrap(Some)] + set_center_widget = >k::Frame { + add_css_class: "background", + + // Make it fit cleanly onto the top edge of the screen. + inline_css: " + border-top-right-radius: 0px; + border-top-left-radius: 0px; + border-top-width: 0px; + ", + }, - add_css_class: "background", - - // Make it fit cleanly onto the top edge of the screen. - inline_css: " - border-top-right-radius: 0px; - border-top-left-radius: 0px; - border-top-width: 0px; - ", + #[name = "panel_end"] + #[wrap(Some)] + set_end_widget = >k::Box { + set_hexpand: true, + set_halign: gtk::Align::End, + }, }, + /// Collection of widgets appearing at the bottom add_overlay = >k::Box { set_orientation: gtk::Orientation::Vertical, @@ -198,23 +199,6 @@ impl WidgetTemplate for Ui { }, } }, - - /// Collection of buttons that close the greeter (eg. Reboot) - gtk::Box { - set_halign: gtk::Align::Center, - set_homogeneous: true, - set_spacing: 15, - - /// Button to reboot - #[name = "reboot_button"] - #[template] - EndButton { set_label: "Reboot" }, - - /// Button to power-off - #[name = "poweroff_button"] - #[template] - EndButton { set_label: "Power Off" }, - }, }, } } diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs new file mode 100644 index 0000000..b7f13c1 --- /dev/null +++ b/src/gui/widget/power_menu/mod.rs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! A [serde-configurable][`PowerMenuConfig`] power menu. + +use relm4::prelude::*; +use serde::Deserialize; +use systemd::{SystemdPowerMenu, SystemdPowerMenuConfig}; + +use crate::{fl, gui::icons}; + +mod systemd; + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PowerMenuConfig { + /// Systemd-aware widget + Systemd(SystemdPowerMenuConfig), +} + +impl Default for PowerMenuConfig { + fn default() -> Self { + Self::Systemd(Default::default()) + } +} + +pub struct PowerMenu { + menu: AsyncController, +} + +#[relm4::component(pub)] +impl Component for PowerMenu { + type Init = PowerMenuConfig; + type Input = (); + type Output = (); + type CommandOutput = (); + + view! { + gtk::MenuButton { + set_icon_name: icons::POWER_MENU, + set_tooltip: &fl!("power-menu-tooltip"), + + #[wrap(Some)] + set_popover = >k::Popover { + model.menu.widget() { }, + }, + } + } + + fn init( + PowerMenuConfig::Systemd(systemd_config): Self::Init, + root: Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let model = Self { + menu: SystemdPowerMenu::builder().launch(systemd_config).detach(), + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } +} diff --git a/src/gui/widget/power_menu/systemd.rs b/src/gui/widget/power_menu/systemd.rs new file mode 100644 index 0000000..942485d --- /dev/null +++ b/src/gui/widget/power_menu/systemd.rs @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! A systemd-aware power menu widget. + +use std::collections::HashSet; + +use relm4::{ + gtk::{glib::markup_escape_text, prelude::*}, + prelude::{AsyncComponentParts, *}, + AsyncComponentSender, +}; +use tokio::process::Command; + +use crate::{demo, fl, i18n::lowercase_first_char}; + +macro_rules! actions { + ($($variant:ident = $systemctl_args:expr; $icon:ident),+ $(,)?) => { + #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[serde(rename_all = "snake_case")] + pub enum Action { + $($variant),+ + } + + impl Action { + /// Every variant as a [`Vec`]. + pub fn all() -> Vec { + // It is easier to use a macro than adding a complex dependency. + [$(Action::$variant),+].into() + } + + pub fn systemctl_args(&self) -> &'static[&'static str] { + match self { + $(Self::$variant => &$systemctl_args[..]),+ + } + } + + pub fn icon(&self) -> &'static str { + match self { + $(Self::$variant => crate::gui::icons::$icon),+ + } + } + } + + }; +} + +actions! { + Poweroff = ["poweroff"]; POWEROFF, + Reboot = ["reboot"]; REBOOT, + RebootFirmware = ["reboot", "--firmware-setup"]; REBOOT_FIRMWARE, + Suspend = ["suspend"]; SUSPEND, + Hibernate = ["hibernate"]; HIBERNATE, + HybridSleep = ["hybrid-sleep"]; HIBERNATE, // TODO: Is there an icon for it? +} + +impl Action { + /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{snake-case}")` + fn fl(&self) -> String { + use Action as A; + match self { + A::Poweroff => fl!("power-menu-poweroff"), + A::Reboot => fl!("power-menu-reboot"), + A::RebootFirmware => fl!("power-menu-reboot-firmware"), + A::Suspend => fl!("power-menu-suspend"), + A::Hibernate => fl!("power-menu-hibernate"), + A::HybridSleep => fl!("power-menu-hybrid-sleep"), + } + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct SystemdPowerMenuConfig { + /// The list of actions to show. Order is preserved. The first unique occurance is used, with duplicates discarded. + /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. + /// + /// See [`Action`] for all available actions and what they do. + #[serde(default = "Action::all")] + pub actions: Vec, +} + +impl Default for SystemdPowerMenuConfig { + fn default() -> Self { + Self { + actions: Action::all(), + } + } +} + +#[derive(PartialEq, Eq)] +pub enum SystemdPowerMenu { + /// The default state with all buttons shown. + Menu, + + /// The confimation widget for a specific action. + Confirm(Action), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SystemdPowerMenuMsg { + /// Sent when the user selected a power menu button. If the action requires confirmation (involves a poweroff of the + /// system) a confirmation screen is shown ([`SystemdPowerMenu::Confirm`] state). + Request(Action), + + /// A confirmation of a [`Self::Request`] was cancelled. Has no effect if no confirmation is requested. + Cancel, + + /// Confirms an action that was requested in [`Self::Request`]. Has no effect if no confirmation is requested. + Confirm, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for SystemdPowerMenu { + type Init = SystemdPowerMenuConfig; + type Input = SystemdPowerMenuMsg; + type Output = (); + type CommandOutput = (); + + view! { + gtk::Box { + #[name = "menu"] + #[transition(Crossfade)] + match model { + Self::Menu => >k::Box::new(gtk::Orientation::Vertical, 15) { + gtk::Label { + set_markup: &format!("{} (Systemd)", fl!("power-menu-tooltip")), + }, + + #[iterate] + append: &action_buttons(actions, sender.clone()), + }, + + Self::Confirm(action) => >k::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 15, + + gtk::Label { + #[watch] + set_markup: &format!("{}", + markup_escape_text(&fl!("power-menu-confirm-dialog-heading", + what = lowercase_first_char(&action.fl())) + ) + ), + }, + + gtk::Box { + set_spacing: 15, + + gtk::Button { + set_label: &fl!("dialog-cancel"), + + connect_clicked => SystemdPowerMenuMsg::Cancel, + }, + + gtk::Button { + add_css_class: "destructive-action", + + #[watch] + set_label: &action.fl(), + + connect_clicked => SystemdPowerMenuMsg::Confirm, + } + } + }, + } + } + } + + async fn init( + SystemdPowerMenuConfig { actions }: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let model = Self::Menu; + let widgets = view_output!(); + + widgets.menu.set_vhomogeneous(false); + widgets.menu.set_hhomogeneous(false); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _: &Self::Root, + ) { + use SystemdPowerMenuMsg as M; + let action = match &message { + M::Request(action) => *action, + + M::Confirm => { + let Self::Confirm(what) = self else { return }; + *what + } + + M::Cancel => { + *self = Self::Menu; + return; + } + }; + + use Action as A; + let require_confirm = *self == Self::Menu + && matches!( + &message, + M::Request(A::Poweroff | A::Reboot | A::RebootFirmware) + ); + + if require_confirm { + *self = Self::Confirm(action); + return; + } + + if demo() { + info!("Demo mode: not doing {action:?}"); + *self = Self::Menu; + + return; + } + + let systemctl = Command::new("systemctl") + .args(action.systemctl_args()) + .status(); + + sender.oneshot_command(async move { + let Err(why) = systemctl.await else { return }; + debug!("Failed to {action:?}: {why}"); + }); + } +} + +fn action_buttons( + activated: Vec, + sender: AsyncComponentSender, +) -> Vec { + let len = activated.len(); + let mut mentioned = HashSet::new(); + + activated + .into_iter() + .fold(Vec::with_capacity(len), |mut acc, action| { + if !mentioned.insert(action) { + return acc; + } + + let label = gtk::Label::new(Some(&action.fl())); + let icon = gtk::Image::new(); + icon.set_icon_name(Some(action.icon())); + + let container = gtk::Box::new(gtk::Orientation::Horizontal, 15); + container.append(&icon); + container.append(&label); + + let button = gtk::Button::new(); + button.set_child(Some(&container)); + + let sender = sender.clone(); + button.connect_clicked(move |_| sender.input(SystemdPowerMenuMsg::Request(action))); + + acc.push(button); + acc + }) +} diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..57f5983 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! # Internationalization +//! +//! Firstly Initialize the language selection with [`init`]. Then use the [`fl`] macro to request a string by id. + +use std::sync::LazyLock; + +use i18n_embed::{ + fluent::{fluent_language_loader, FluentLanguageLoader}, + DefaultLocalizer, LanguageLoader as _, Localizer, +}; +use rust_embed::RustEmbed; +use unic_langid::LanguageIdentifier; + +#[derive(RustEmbed)] +#[folder = "i18n"] +struct Localizations; + +/// Applies the requested language(s) to be used when calling the `fl!()` macro. +pub fn init(requested_languages: &[LanguageIdentifier]) { + if let Err(err) = localizer().select(requested_languages) { + eprintln!("error while loading fluent localizations: {err}"); + } +} + +/// Get the `Localizer` to be used for localizing this library. +#[must_use] +fn localizer() -> Box { + Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +} + +pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { + let loader: FluentLanguageLoader = fluent_language_loader!(); + + loader + .load_fallback_language(&Localizations) + .expect("Error while loading fallback language"); + + loader +}); + +/// Request a localized string by ID. +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) + }}; + + ($message_id:literal, $($args:expr),*) => {{ + i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) + }}; +} + +/// Lowercase the first letter of the string. +pub(crate) fn lowercase_first_char(string: &str) -> String { + string[0..1].to_lowercase() + &string[1..] +} diff --git a/src/main.rs b/src/main.rs index 30028a4..b399710 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,15 +7,18 @@ mod client; mod config; mod constants; mod gui; +mod i18n; mod sysutil; mod tomlutils; use std::fs::{create_dir_all, OpenOptions}; use std::io::{Result as IoResult, Write}; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use clap::{Parser, ValueEnum}; use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; +use i18n_embed::DesktopLanguageRequester; use tracing::subscriber::set_global_default; use tracing_appender::{non_blocking, non_blocking::WorkerGuard}; use tracing_subscriber::{ @@ -31,6 +34,8 @@ extern crate tracing; extern crate lazy_static; #[macro_use] extern crate const_format; +#[macro_use(Deserialize)] +extern crate serde; #[cfg(test)] #[macro_use] @@ -39,6 +44,13 @@ extern crate test_case; const MAX_LOG_FILES: usize = 3; const MAX_LOG_SIZE: usize = 1024 * 1024; +static DEMO: OnceLock = OnceLock::new(); + +/// Get the demo mode status +fn demo() -> bool { + *DEMO.get().unwrap_or(&false) +} + #[derive(Clone, Debug, ValueEnum)] enum LogLevel { Off, @@ -78,7 +90,11 @@ struct Args { } fn main() { + i18n::init(&DesktopLanguageRequester::requested_languages()); + let args = Args::parse(); + DEMO.get_or_init(|| args.demo); + // Keep the guard alive till the end of the function, since logging depends on this. let _guard = init_logging(&args.logs, &args.log_level, args.verbose); diff --git a/src/tomlutils.rs b/src/tomlutils.rs index e8f1e06..8889fa2 100644 --- a/src/tomlutils.rs +++ b/src/tomlutils.rs @@ -17,16 +17,16 @@ pub enum TomlFileError { IO(#[from] std::io::Error), #[error("Error decoding UTF-8")] Utf8(#[from] std::str::Utf8Error), - #[error("Error decoding TOML file contents")] + #[error("Error decoding TOML file contents: {0}")] TomlDecode(#[from] toml::de::Error), - #[error("Error encoding into TOML")] + #[error("Error encoding into TOML: {0}")] TomlEncode(#[from] toml::ser::Error), } pub type TomlFileResult = Result; /// Load the TOML file from disk without any checks. -fn load_raw_toml(path: &Path) -> TomlFileResult { +pub(crate) fn load_raw_toml>(path: P) -> TomlFileResult { Ok(toml::from_str(std::str::from_utf8( read(path)?.as_slice(), )?)?) From 2ee0d13b09cb5fae6939f81fa32f19f8c568e8fb Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sun, 16 Feb 2025 21:31:37 +0200 Subject: [PATCH 2/8] feat: Add `shutdown` support to power menu, minor refactors `man 8 shutdown` is a generic Linux command for system poweroff and reboot. Some minor refactors and a fix of the lowercase_first_char function - now doesn't panic with non-ascii first character. --- i18n/en/regreet.ftl | 1 + i18n/uk/regreet.ftl | 1 + regreet.sample.toml | 8 +- src/config.rs | 16 +- src/constants.rs | 5 - src/gui/mod.rs | 2 + src/gui/templates.rs | 24 +-- src/gui/widget/power_menu/mod.rs | 33 +++- src/gui/widget/power_menu/systemd.rs | 134 ++++++++-------- src/gui/widget/power_menu/unix.rs | 221 +++++++++++++++++++++++++++ src/i18n.rs | 8 +- 11 files changed, 344 insertions(+), 109 deletions(-) create mode 100644 src/gui/widget/power_menu/unix.rs diff --git a/i18n/en/regreet.ftl b/i18n/en/regreet.ftl index c0650a6..4ab1abb 100644 --- a/i18n/en/regreet.ftl +++ b/i18n/en/regreet.ftl @@ -8,6 +8,7 @@ dialog-cancel = Cancel power-menu-tooltip = Power menu power-menu-poweroff = Poweroff +power-menu-halt = Halt power-menu-reboot = Reboot power-menu-reboot-firmware = Reboot into firmware power-menu-suspend = Suspend diff --git a/i18n/uk/regreet.ftl b/i18n/uk/regreet.ftl index a813024..01ac720 100644 --- a/i18n/uk/regreet.ftl +++ b/i18n/uk/regreet.ftl @@ -8,6 +8,7 @@ dialog-cancel = Відмінити power-menu-tooltip = Керування живленням power-menu-poweroff = Вимкнути +power-menu-halt = Зупинити power-menu-reboot = Перезапустити power-menu-reboot-firmware = Перезапустити до меню загрузки power-menu-suspend = Сон diff --git a/regreet.sample.toml b/regreet.sample.toml index 26068b8..d58a34e 100644 --- a/regreet.sample.toml +++ b/regreet.sample.toml @@ -70,6 +70,8 @@ label_width = 150 # The power menu widget -[widget.power_menu.systemd] -# Everything except `["hybernate", "hybrid-sleep"]` -actions = ["poweroff", "reboot", "reboot-firmware", "suspend"] +[widget.power_menu.systemd] # systemd-aware +# [widget.power_menu.unix] # Uses the generic shutdown command + +# Selects what actions are displayed in the power menu +actions = ["poweroff", "reboot"] diff --git a/src/config.rs b/src/config.rs index e8c2411..7ae2f81 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; -use crate::constants::{GREETING_MSG, POWEROFF_CMD, REBOOT_CMD, X11_CMD_PREFIX}; +use crate::constants::{GREETING_MSG, X11_CMD_PREFIX}; use crate::gui::widget::clock::ClockConfig; use crate::gui::widget::power_menu::PowerMenuConfig; use crate::tomlutils::load_toml; @@ -71,10 +71,6 @@ struct Background { /// Struct for various system commands #[derive(Deserialize, Serialize)] pub struct SystemCommands { - #[serde(default = "default_reboot_command")] - pub reboot: Vec, - #[serde(default = "default_poweroff_command")] - pub poweroff: Vec, #[serde(default = "default_x11_command_prefix")] pub x11_prefix: Vec, } @@ -82,21 +78,11 @@ pub struct SystemCommands { impl Default for SystemCommands { fn default() -> Self { SystemCommands { - reboot: default_reboot_command(), - poweroff: default_poweroff_command(), x11_prefix: default_x11_command_prefix(), } } } -fn default_reboot_command() -> Vec { - shlex::split(REBOOT_CMD).expect("Unable to lex reboot command") -} - -fn default_poweroff_command() -> Vec { - shlex::split(POWEROFF_CMD).expect("Unable to lex poweroff command") -} - fn default_x11_command_prefix() -> Vec { shlex::split(X11_CMD_PREFIX).expect("Unable to lex X11 command prefix") } diff --git a/src/constants.rs b/src/constants.rs index abacaef..3bbcd74 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -39,11 +39,6 @@ const LOG_DIR: &str = env_or!("LOG_DIR", concatcp!("/var/log/", GREETER_NAME)); /// Path to the log file pub const LOG_PATH: &str = concatcp!(LOG_DIR, "/log"); -/// Default command for rebooting -pub const REBOOT_CMD: &str = env_or!("REBOOT_CMD", "reboot"); -/// Default command for shutting down -pub const POWEROFF_CMD: &str = env_or!("POWEROFF_CMD", "poweroff"); - /// Default greeting message pub const GREETING_MSG: &str = "Welcome back!"; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 2fd03c5..ec13ce1 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -16,3 +16,5 @@ pub mod icons; pub use component::GreeterInit; pub use model::Greeter; + +const GAP: i32 = 15; diff --git a/src/gui/templates.rs b/src/gui/templates.rs index 5d094a3..78767d3 100644 --- a/src/gui/templates.rs +++ b/src/gui/templates.rs @@ -8,6 +8,8 @@ use gtk::prelude::*; use relm4::{gtk, RelmWidgetExt, WidgetTemplate}; +use crate::gui::GAP; + /// Label for an entry/combo box #[relm4::widget_template(pub)] impl WidgetTemplate for EntryLabel { @@ -35,18 +37,18 @@ impl WidgetTemplate for Ui { add_css_class: "background", gtk::Grid { - set_column_spacing: 15, - set_margin_bottom: 15, - set_margin_end: 15, - set_margin_start: 15, - set_margin_top: 15, - set_row_spacing: 15, + set_column_spacing: GAP as u32, + set_margin_bottom: GAP, + set_margin_end: GAP, + set_margin_start: GAP, + set_margin_top: GAP, + set_row_spacing: GAP as u32, set_width_request: 500, /// Widget to display messages to the user #[name = "message_label"] attach[0, 0, 3, 1] = >k::Label { - set_margin_bottom: 15, + set_margin_bottom: GAP, // Format all messages in boldface. #[wrap(Some)] @@ -121,7 +123,7 @@ impl WidgetTemplate for Ui { /// Collection of action buttons (eg. Login) attach[1, 3, 2, 1] = >k::Box { set_halign: gtk::Align::End, - set_spacing: 15, + set_spacing: GAP, /// Button to cancel password entry #[name = "cancel_button"] @@ -171,12 +173,10 @@ impl WidgetTemplate for Ui { /// Collection of widgets appearing at the bottom - add_overlay = >k::Box { - set_orientation: gtk::Orientation::Vertical, + add_overlay = >k::Box::new(gtk::Orientation::Vertical, GAP) { set_halign: gtk::Align::Center, set_valign: gtk::Align::End, - set_margin_bottom: 15, - set_spacing: 15, + set_margin_bottom: GAP, gtk::Frame { /// Notification bar for error messages diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs index b7f13c1..7793f1a 100644 --- a/src/gui/widget/power_menu/mod.rs +++ b/src/gui/widget/power_menu/mod.rs @@ -7,16 +7,19 @@ use relm4::prelude::*; use serde::Deserialize; use systemd::{SystemdPowerMenu, SystemdPowerMenuConfig}; +use unix::{UnixPowerMenu, UnixPowerMenuConfig}; use crate::{fl, gui::icons}; mod systemd; +mod unix; #[derive(Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum PowerMenuConfig { /// Systemd-aware widget Systemd(SystemdPowerMenuConfig), + Unix(UnixPowerMenuConfig), } impl Default for PowerMenuConfig { @@ -25,8 +28,9 @@ impl Default for PowerMenuConfig { } } -pub struct PowerMenu { - menu: AsyncController, +pub enum PowerMenu { + Systemd(AsyncController), + Unix(AsyncController), } #[relm4::component(pub)] @@ -43,18 +47,26 @@ impl Component for PowerMenu { #[wrap(Some)] set_popover = >k::Popover { - model.menu.widget() { }, + model.widget() { }, }, } } fn init( - PowerMenuConfig::Systemd(systemd_config): Self::Init, + init: Self::Init, root: Self::Root, _sender: ComponentSender, ) -> ComponentParts { - let model = Self { - menu: SystemdPowerMenu::builder().launch(systemd_config).detach(), + let model = match init { + Self::Init::Systemd(systemd_config) => { + Self::Systemd(SystemdPowerMenu::builder().launch(systemd_config).detach()) + } + + Self::Init::Unix(unix_power_menu_config) => Self::Unix( + UnixPowerMenu::builder() + .launch(unix_power_menu_config) + .detach(), + ), }; let widgets = view_output!(); @@ -62,3 +74,12 @@ impl Component for PowerMenu { ComponentParts { model, widgets } } } + +impl PowerMenu { + fn widget(&self) -> >k::Box { + match self { + Self::Systemd(controller) => controller.widget(), + Self::Unix(controller) => controller.widget(), + } + } +} diff --git a/src/gui/widget/power_menu/systemd.rs b/src/gui/widget/power_menu/systemd.rs index 942485d..fec8c60 100644 --- a/src/gui/widget/power_menu/systemd.rs +++ b/src/gui/widget/power_menu/systemd.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! A systemd-aware power menu widget. +//! +//! See supported actions as variants of [`Action`]. use std::collections::HashSet; @@ -13,61 +15,18 @@ use relm4::{ }; use tokio::process::Command; -use crate::{demo, fl, i18n::lowercase_first_char}; - -macro_rules! actions { - ($($variant:ident = $systemctl_args:expr; $icon:ident),+ $(,)?) => { - #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] - #[serde(rename_all = "snake_case")] - pub enum Action { - $($variant),+ - } - - impl Action { - /// Every variant as a [`Vec`]. - pub fn all() -> Vec { - // It is easier to use a macro than adding a complex dependency. - [$(Action::$variant),+].into() - } - - pub fn systemctl_args(&self) -> &'static[&'static str] { - match self { - $(Self::$variant => &$systemctl_args[..]),+ - } - } - - pub fn icon(&self) -> &'static str { - match self { - $(Self::$variant => crate::gui::icons::$icon),+ - } - } - } - - }; -} - -actions! { - Poweroff = ["poweroff"]; POWEROFF, - Reboot = ["reboot"]; REBOOT, - RebootFirmware = ["reboot", "--firmware-setup"]; REBOOT_FIRMWARE, - Suspend = ["suspend"]; SUSPEND, - Hibernate = ["hibernate"]; HIBERNATE, - HybridSleep = ["hybrid-sleep"]; HIBERNATE, // TODO: Is there an icon for it? -} - -impl Action { - /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{snake-case}")` - fn fl(&self) -> String { - use Action as A; - match self { - A::Poweroff => fl!("power-menu-poweroff"), - A::Reboot => fl!("power-menu-reboot"), - A::RebootFirmware => fl!("power-menu-reboot-firmware"), - A::Suspend => fl!("power-menu-suspend"), - A::Hibernate => fl!("power-menu-hibernate"), - A::HybridSleep => fl!("power-menu-hybrid-sleep"), - } - } +use crate::{demo, fl, gui::GAP, i18n::lowercase_first_char}; + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Poweroff, + Halt, + Reboot, + RebootFirmware, + Suspend, + Hibernate, + HybridSleep, } #[derive(Deserialize, Debug, Clone)] @@ -77,14 +36,14 @@ pub struct SystemdPowerMenuConfig { /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. /// /// See [`Action`] for all available actions and what they do. - #[serde(default = "Action::all")] + #[serde(default = "Action::default_set")] pub actions: Vec, } impl Default for SystemdPowerMenuConfig { fn default() -> Self { Self { - actions: Action::all(), + actions: Action::default_set(), } } } @@ -123,7 +82,7 @@ impl AsyncComponent for SystemdPowerMenu { #[name = "menu"] #[transition(Crossfade)] match model { - Self::Menu => >k::Box::new(gtk::Orientation::Vertical, 15) { + Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { gtk::Label { set_markup: &format!("{} (Systemd)", fl!("power-menu-tooltip")), }, @@ -132,10 +91,7 @@ impl AsyncComponent for SystemdPowerMenu { append: &action_buttons(actions, sender.clone()), }, - Self::Confirm(action) => >k::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 15, - + Self::Confirm(action) => >k::Box::new(gtk::Orientation::Vertical, GAP) { gtk::Label { #[watch] set_markup: &format!("{}", @@ -146,11 +102,9 @@ impl AsyncComponent for SystemdPowerMenu { }, gtk::Box { - set_spacing: 15, - - gtk::Button { - set_label: &fl!("dialog-cancel"), + set_spacing:GAP, + gtk::Button::with_label(&fl!("dialog-cancel")) { connect_clicked => SystemdPowerMenuMsg::Cancel, }, @@ -251,7 +205,7 @@ fn action_buttons( let icon = gtk::Image::new(); icon.set_icon_name(Some(action.icon())); - let container = gtk::Box::new(gtk::Orientation::Horizontal, 15); + let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); container.append(&icon); container.append(&label); @@ -265,3 +219,49 @@ fn action_buttons( acc }) } + +impl Action { + /// Sensible default set of actions that most users would find sufficient. + pub fn default_set() -> Vec { + [Self::Poweroff, Self::Reboot, Self::Suspend].into() + } + + pub fn systemctl_args(&self) -> &'static [&'static str] { + match self { + Self::Poweroff => &["poweroff"], + Self::Halt => &["halt"], + Self::Reboot => &["reboot"], + Self::RebootFirmware => &["reboot", "--firmware-setup"], + Self::Suspend => &["suspend"], + Self::Hibernate => &["hibernate"], + Self::HybridSleep => &["hybrid-sleep"], + } + } + + /// Returns the icon name for this action + pub fn icon(&self) -> &'static str { + match self { + Self::Poweroff => crate::gui::icons::POWEROFF, + Self::Halt => crate::gui::icons::POWEROFF, + Self::Reboot => crate::gui::icons::REBOOT, + Self::RebootFirmware => crate::gui::icons::REBOOT_FIRMWARE, + Self::Suspend => crate::gui::icons::SUSPEND, + Self::Hibernate => crate::gui::icons::HIBERNATE, + Self::HybridSleep => crate::gui::icons::HIBERNATE, + } + } + + /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{kebab-case}")` + fn fl(&self) -> String { + use Action as A; + match self { + A::Poweroff => fl!("power-menu-poweroff"), + A::Halt => fl!("power-menu-halt"), + A::Reboot => fl!("power-menu-reboot"), + A::RebootFirmware => fl!("power-menu-reboot-firmware"), + A::Suspend => fl!("power-menu-suspend"), + A::Hibernate => fl!("power-menu-hibernate"), + A::HybridSleep => fl!("power-menu-hybrid-sleep"), + } + } +} diff --git a/src/gui/widget/power_menu/unix.rs b/src/gui/widget/power_menu/unix.rs new file mode 100644 index 0000000..f77d614 --- /dev/null +++ b/src/gui/widget/power_menu/unix.rs @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! A power menu widget that uses the generic `man 8 shutdown` linux command. +//! +//! See supported actions as variants of [`Action`]. + +use std::collections::HashSet; + +use relm4::{adw::glib::markup_escape_text, adw::prelude::*, prelude::*}; +use tokio::process::Command; + +use crate::{ + demo, fl, + gui::{icons, GAP}, + i18n::lowercase_first_char, +}; + +#[derive(Deserialize, Clone)] +pub struct UnixPowerMenuConfig { + #[serde(default = "Action::default_set")] + actions: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Poweroff, + Reboot, + Halt, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum UnixPowerMenu { + Menu, + Confirm(Action), +} + +#[derive(Debug)] +pub enum UnixPowerMenuMsg { + Request(Action), + Confirm, + Cancel, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for UnixPowerMenu { + type Init = UnixPowerMenuConfig; + type Input = UnixPowerMenuMsg; + type Output = (); + type CommandOutput = (); + + view! { + gtk::Box { + #[name = "menu"] + #[transition(Crossfade)] + match model { + Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { + gtk::Label { + set_markup: &format!("{} (Unix)", fl!("power-menu-tooltip")) + }, + + #[iterate] + append: &action_buttons(actions, sender.clone()), + }, + + Self::Confirm(action) => >k::Box::new(gtk::Orientation::Vertical, GAP) { + gtk::Label { + #[watch] + set_markup: &format!("{}", + markup_escape_text(&fl!("power-menu-confirm-dialog-heading", + what = lowercase_first_char(&action.fl())) + ) + ), + }, + + gtk::Box { + set_spacing: GAP, + + gtk::Button::with_label(&fl!("dialog-cancel")) { + connect_clicked => UnixPowerMenuMsg::Cancel, + }, + + gtk::Button { + add_css_class: "destructive-action", + + #[watch] + set_label: &action.fl(), + + connect_clicked => UnixPowerMenuMsg::Confirm, + } + } + }, + } + } + } + + async fn init( + UnixPowerMenuConfig { actions }: Self::Init, + root: Self::Root, + sender: relm4::AsyncComponentSender, + ) -> AsyncComponentParts { + let model = Self::Menu; + let widgets = view_output!(); + + widgets.menu.set_vhomogeneous(false); + widgets.menu.set_hhomogeneous(false); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _: &Self::Root, + ) { + use UnixPowerMenuMsg as M; + let action = match &message { + M::Request(action) => *action, + + M::Confirm => { + let Self::Confirm(what) = self else { return }; + *what + } + + M::Cancel => { + *self = Self::Menu; + return; + } + }; + + if *self == Self::Menu { + *self = Self::Confirm(action); + return; + } + + if demo() { + info!("Demo mode: not doing {action:?}"); + *self = Self::Menu; + + return; + } + + let shutdown = Command::new("shutdown") + .args(action.shutdown_args()) + .status(); + + sender.oneshot_command(async move { + let Err(why) = shutdown.await else { return }; + debug!("Failed to {action:?}: {why}"); + }); + } +} + +fn action_buttons( + activated: Vec, + sender: AsyncComponentSender, +) -> Vec { + let len = activated.len(); + let mut mentioned = HashSet::new(); + + activated + .into_iter() + .fold(Vec::with_capacity(len), |mut acc, action| { + if !mentioned.insert(action) { + return acc; + } + + let label = gtk::Label::new(Some(&action.fl())); + let icon = gtk::Image::new(); + icon.set_icon_name(Some(action.icon())); + + let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); + container.append(&icon); + container.append(&label); + + let button = gtk::Button::new(); + button.set_child(Some(&container)); + + let sender = sender.clone(); + button.connect_clicked(move |_| sender.input(UnixPowerMenuMsg::Request(action))); + + acc.push(button); + acc + }) +} + +impl Action { + /// Sensible default set of actions that most users would find sufficient. + pub fn default_set() -> Vec { + [Self::Poweroff, Self::Reboot].into() + } + + pub const fn shutdown_args(&self) -> &'static [&'static str] { + match self { + Self::Poweroff => &["--poweroff", "now"], + Self::Reboot => &["--reboot", "now"], + Self::Halt => &["--halt", "now"], + } + } + + /// Returns the icon name for this action + pub const fn icon(&self) -> &'static str { + match self { + Self::Poweroff => icons::POWEROFF, + Self::Reboot => icons::REBOOT, + Self::Halt => icons::POWEROFF, + } + } + + /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{kebab-case}")` + pub fn fl(&self) -> String { + match self { + Self::Poweroff => fl!("power-menu-poweroff"), + Self::Reboot => fl!("power-menu-reboot"), + Self::Halt => fl!("power-menu-halt"), + } + } +} diff --git a/src/i18n.rs b/src/i18n.rs index 57f5983..3da03ac 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -56,5 +56,11 @@ macro_rules! fl { /// Lowercase the first letter of the string. pub(crate) fn lowercase_first_char(string: &str) -> String { - string[0..1].to_lowercase() + &string[1..] + let mut chars = string.chars(); + let Some(uppercase) = chars.next() else { + return "".into(); + }; + + let lowercase = uppercase.to_lowercase().to_string(); + lowercase + chars.collect::().as_str() } From 9f1d33456426f69a72da53675bca7d080e8a0120 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sun, 16 Feb 2025 22:15:33 +0200 Subject: [PATCH 3/8] doc: Update power menu stuff in the readme --- README.md | 38 +++++++++++++++----------------------- regreet.sample.toml | 5 ++++- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 86def46..753474e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ These screenshots use the [Canta GTK theme](https://github.com/vinceliuice/Canta - Cursor theme - Cursor blink on/off - Font -* Allows changing reboot & poweroff commands for different init systems +* Power menu: supports `systemd` out-of-the-box, and generic linux `shutdown` command. Support for other init systems is easy to add. * Supports custom CSS files for further customizations * Respects `XDG_DATA_DIRS` environment variable * Respects fields `Hidden` and `NoDisplay` in session files @@ -51,7 +51,7 @@ These screenshots use the [Canta GTK theme](https://github.com/vinceliuice/Canta * Demo mode to run ReGreet without greetd for easier development. ## Requirements -* Rust 1.75.0+ (for compilation only) +* Rust 1.80.0+ (for compilation only) * greetd * GTK 4.0+ * A Wayland compositor (such as [Cage](https://www.hjdskes.nl/projects/cage/) or [Sway](https://swaywm.org/) or [Hyprland](https://hyprland.org/)) @@ -95,8 +95,6 @@ STATE\_DIR | `/var/lib/regreet` | The directory used to store the ReGreet state/ LOG\_DIR | `/var/log/regreet` | The directory used to store logs SESSION\_DIRS | `/usr/share/xsessions:/usr/share/wayland-sessions` | A colon (:) separated list of directories where the greeter looks for session files X11\_CMD\_PREFIX | `startx /usr/bin/env` | The default command prefix for X11 sessions to launch the X server (see [this explanation on Reddit](https://web.archive.org/web/20240803120131/https://old.reddit.com/r/linux/comments/1c8zdcw/using_x11_window_managers_with_greetd_login/)) -REBOOT\_CMD | `reboot` | The default command used to reboot the system -POWEROFF\_CMD | `poweroff` | The default command used to shut down the system LOGIN\_DEFS\_PATHS | `/etc/login.defs:/usr/etc/login.defs` | A colon (:) separated list of `login.defs` file paths. First found is loaded. LOGIN\_DEFS\_UID\_MIN | 1000 | Override the assumed default if `login.defs` doesnt specify `UID_MIN`. LOGIN\_DEFS\_UID\_MAX | 60000 | Override the assumed default if `login.defs` doesnt specify `UID_MAX`. @@ -213,7 +211,7 @@ env = GTK_USE_PORTAL,0 env = GDK_DEBUG,no-portals ``` -### Configuration +## Configuration The configuration file must be in the [TOML](https://toml.io/) format. By default, it is named `regreet.toml`, and located in the greetd configuration directory specified during compilation (`/etc/greetd/` by default). You can use a config file in a different location with the `--config` argument as follows: @@ -234,10 +232,18 @@ Currently, the following can be configured: * Cursor theme * Cursor blink on/off * Font -* Reboot command -* Shut down command +* Power menu * X11 command prefix (see [this explanation on Reddit](https://web.archive.org/web/20240803120131/https://old.reddit.com/r/linux/comments/1c8zdcw/using_x11_window_managers_with_greetd_login/)) +### Widgets + +Widgets are additional UI elements that do not interact with `greetd`. Their configuration is under the `[widget]` key. +See `regreet.sample.toml` for examples. + +- **Clock**: Located at the top of the screen and allows for `strftime`-like format speficiers. +- **Power menu**: Supports `systemd` and a generic linux `shutdown` command. You can configure the order and presence of + power menu actions. + **NOTE:** For configuring other essential features, such as the keyboard layout/mapping, the choice of monitor to use, etc., please check out the configuration options for the wayland compositor that you are using to run ReGreet. For example, if you use Cage, check out the [Cage wiki](https://github.com/cage-kiosk/cage/wiki/Configuration). If you use Sway, check out the [Sway wiki](https://github.com/swaywm/sway/wiki#configuration). @@ -258,22 +264,8 @@ For a general reference on CSS, please refer to the [MDN web docs](https://devel **Tip:** You might want to use [demo mode](#demo-mode) to test out your CSS before making it permanent. -### Changing Reboot/Shut Down Commands -The default reboot and shut down commands use the `reboot` and `poweroff` binaries, which are present on most Linux systems. -However, since the recommended way of using ReGreet is to avoid running it as root, the `reboot`/`poweroff` commands might not work on systems where superuser access is needed to run these commands. -In this case, if there is another command to reboot or shut down the system without superuser access, these commands can be set in the config file under the `[commands]` section. - -For example, to use `loginctl reboot` as the reboot command, use the following config: -```toml -[commands] -reboot = [ "loginctl", "reboot" ] -``` -Here, each command needs to be separated into a list containing the main command, followed by individual arguments. - -These commands can also be specified during compilation using the `REBOOT_CMD` and `POWEROFF_CMD` environment variables. - -### Logging and Caching -The state is are stored in `/var/lib/regreet/state.toml` (configurable during installation). +## Logging and Caching +The cache is are stored in `/var/cache/regreet/cache.toml` (configurable during installation). It contains the last authenticated user and the last used session per user, which are automatically selected on next login. If the greeter is unable to write to this file, then it reverts to the default behaviour. diff --git a/regreet.sample.toml b/regreet.sample.toml index d58a34e..685e2e1 100644 --- a/regreet.sample.toml +++ b/regreet.sample.toml @@ -71,7 +71,10 @@ label_width = 150 # The power menu widget [widget.power_menu.systemd] # systemd-aware -# [widget.power_menu.unix] # Uses the generic shutdown command # Selects what actions are displayed in the power menu actions = ["poweroff", "reboot"] + +# You can also use this power menu type: +#[widget.power_menu.unix] +#actions = ["poweroff", "reboot", "halt"] From 660788e674b3f843ce203951975aaa04a64529a1 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:58:08 +0200 Subject: [PATCH 4/8] feat: Add custom power menu type The custom power menu allows you to run arbitrary commands while integrating with the i18n layer, automatic icon selection and automatically infers the neccessity of the confirm dialog. --- src/gui/widget/power_menu/custom.rs | 283 +++++++++++++++++++++++++++ src/gui/widget/power_menu/mod.rs | 22 +++ src/gui/widget/power_menu/systemd.rs | 10 +- src/gui/widget/power_menu/unix.rs | 6 +- 4 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 src/gui/widget/power_menu/custom.rs diff --git a/src/gui/widget/power_menu/custom.rs b/src/gui/widget/power_menu/custom.rs new file mode 100644 index 0000000..4f7cd7d --- /dev/null +++ b/src/gui/widget/power_menu/custom.rs @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: 2025 max-ishere <47008271+max-ishere@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! A fully customizable power menu widget. + +use adw::glib::markup_escape_text; +use relm4::{adw::prelude::*, prelude::*}; +use tokio::process; + +use crate::{ + demo, fl, + gui::{widget::power_menu::header_label, GAP}, + i18n::lowercase_first_char, +}; + +#[derive(Deserialize, Clone)] + +pub struct CustomPowerMenuConfig { + commands: Vec, +} + +#[derive(Deserialize, Clone)] +pub struct Command { + /// The title of the action. + #[serde(flatten)] + title: Title, + + /// Command to be executed. + #[serde(alias = "cmd")] + command: Vec, + + /// If `true`, a confirmation will be shown before the [`Self::command`] is executed. + /// + /// If [`Some`], use the value. Otherwise, try to derive the value from the [`Title::Action`], falling back to + /// `false`. Actions that involve a poweroff are inferred to require confirmation. + confirm: Option, + + /// The icon name to set for this action. A list of installed icons can be looked up using the `icon-library` app. + icon: Option, +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Title { + /// One of the `i18n`-supported actions + Action(Action), + + /// Arbitrary name string + Name(String), +} + +#[derive(Deserialize, Clone, Copy)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Poweroff, + Halt, + Reboot, + RebootFirmware, + Suspend, + Hibernate, + HybridSleep, +} + +pub struct CustomPowerMenu { + state: MenuState, + commands: Vec, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum MenuState { + Menu, + Confirm(usize), +} + +#[derive(Debug)] +pub enum CustomPowerMenuMsg { + Request(usize), + Confirm, + Cancel, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for CustomPowerMenu { + type Init = CustomPowerMenuConfig; + type Input = CustomPowerMenuMsg; + type Output = (); + type CommandOutput = (); + + view! { + gtk::Box { + #[name="menu"] + #[transition(Crossfade)] + match model.state { + MenuState::Menu => gtk::Box::new(gtk::Orientation::Vertical, GAP) { + header_label("Custom") {}, + + #[iterate] + append: &action_buttons(&model.commands, sender.clone()), + }, + + MenuState::Confirm(index) =>>k::Box::new(gtk::Orientation::Vertical, GAP) { + gtk::Label { + #[watch] + set_markup: &format!("{}", + markup_escape_text(&fl!("power-menu-confirm-dialog-heading", + what = lowercase_first_char(&model.commands[index].fl())) + ) + ), + }, + + gtk::Box { + set_spacing:GAP, + + gtk::Button::with_label(&fl!("dialog-cancel")) { + connect_clicked => CustomPowerMenuMsg::Cancel, + }, + + gtk::Button { + add_css_class: "destructive-action", + + #[watch] + set_label: &model.commands[index].fl(), + + connect_clicked => CustomPowerMenuMsg::Confirm, + } + } + } + } + } + } + + async fn init( + CustomPowerMenuConfig { commands }: Self::Init, + root: Self::Root, + sender: relm4::AsyncComponentSender, + ) -> AsyncComponentParts { + let model = Self { + commands, + state: MenuState::Menu, + }; + let widgets = view_output!(); + + widgets.menu.set_hhomogeneous(false); + widgets.menu.set_vhomogeneous(false); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _: &Self::Root, + ) { + use CustomPowerMenuMsg as M; + let index = match message { + M::Request(index) => index, + + M::Confirm => { + let MenuState::Confirm(what) = self.state else { + return; + }; + what + } + + M::Cancel => { + self.state = MenuState::Menu; + return; + } + }; + + let command = &self.commands[index]; + + use Action as A; + let require_confirm = self.state == MenuState::Menu + && (command.confirm == Some(true) + || matches!( + command.title, + Title::Action(A::Poweroff | A::Reboot | A::RebootFirmware) + )); + + if require_confirm { + self.state = MenuState::Confirm(index); + return; + } + + let fl = command.fl(); + + if demo() { + info!("Demo mode: not doing {fl}"); + self.state = MenuState::Menu; + + return; + } + + let command = process::Command::new(&command.command[0]) + .args(&command.command[1..]) + .status(); + + sender.oneshot_command(async move { + let Err(why) = command.await else { return }; + debug!("Failed to {fl}: {why}"); + }); + } +} + +fn action_buttons( + commands: &[Command], + sender: AsyncComponentSender, +) -> Vec { + let len = commands.len(); + + commands + .iter() + .enumerate() + .fold(Vec::with_capacity(len), |mut acc, (index, command)| { + let button = gtk::Button::new(); + + if let Some(icon_name) = command.icon() { + let icon = gtk::Image::new(); + icon.set_icon_name(Some(icon_name)); + + let label = gtk::Label::new(Some(&command.fl())); + + let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); + container.append(&icon); + container.append(&label); + + button.set_child(Some(&container)); + } else { + button.set_label(&command.fl()); + } + + let sender = sender.clone(); + button.connect_clicked(move |_| sender.input(CustomPowerMenuMsg::Request(index))); + + acc.push(button); + acc + }) +} + +impl Command { + fn fl(&self) -> String { + let action = match self.title { + Title::Action(action) => action, + + Title::Name(ref name) => return name.clone(), + }; + + use Action as A; + match action { + A::Poweroff => fl!("power-menu-poweroff"), + A::Halt => fl!("power-menu-halt"), + A::Reboot => fl!("power-menu-reboot"), + A::RebootFirmware => fl!("power-menu-reboot-firmware"), + A::Suspend => fl!("power-menu-suspend"), + A::Hibernate => fl!("power-menu-hibernate"), + A::HybridSleep => fl!("power-menu-hybrid-sleep"), + } + } + + fn icon(&self) -> Option<&str> { + let None = self.icon else { + return self.icon.as_deref(); + }; + + let Title::Action(action) = self.title else { + return None; + }; + + use Action as A; + Some(match action { + A::Poweroff => crate::gui::icons::POWEROFF, + A::Halt => crate::gui::icons::POWEROFF, + A::Reboot => crate::gui::icons::REBOOT, + A::RebootFirmware => crate::gui::icons::REBOOT_FIRMWARE, + A::Suspend => crate::gui::icons::SUSPEND, + A::Hibernate => crate::gui::icons::HIBERNATE, + A::HybridSleep => crate::gui::icons::HIBERNATE, + }) + } +} diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs index 7793f1a..0a2e26d 100644 --- a/src/gui/widget/power_menu/mod.rs +++ b/src/gui/widget/power_menu/mod.rs @@ -4,6 +4,8 @@ //! A [serde-configurable][`PowerMenuConfig`] power menu. +use adw::glib::markup_escape_text; +use custom::{CustomPowerMenu, CustomPowerMenuConfig}; use relm4::prelude::*; use serde::Deserialize; use systemd::{SystemdPowerMenu, SystemdPowerMenuConfig}; @@ -11,6 +13,7 @@ use unix::{UnixPowerMenu, UnixPowerMenuConfig}; use crate::{fl, gui::icons}; +mod custom; mod systemd; mod unix; @@ -20,6 +23,7 @@ pub enum PowerMenuConfig { /// Systemd-aware widget Systemd(SystemdPowerMenuConfig), Unix(UnixPowerMenuConfig), + Custom(CustomPowerMenuConfig), } impl Default for PowerMenuConfig { @@ -31,6 +35,7 @@ impl Default for PowerMenuConfig { pub enum PowerMenu { Systemd(AsyncController), Unix(AsyncController), + Custom(AsyncController), } #[relm4::component(pub)] @@ -67,6 +72,12 @@ impl Component for PowerMenu { .launch(unix_power_menu_config) .detach(), ), + + Self::Init::Custom(custom_power_menu_config) => Self::Custom( + CustomPowerMenu::builder() + .launch(custom_power_menu_config) + .detach(), + ), }; let widgets = view_output!(); @@ -80,6 +91,17 @@ impl PowerMenu { match self { Self::Systemd(controller) => controller.widget(), Self::Unix(controller) => controller.widget(), + Self::Custom(controller) => controller.widget(), } } } + +fn header_label(backend: &str) -> gtk::Label { + let label = gtk::Label::new(None); + label.set_markup(&format!( + "{}\nBackend: {backend}", + markup_escape_text(&fl!("power-menu-tooltip")) + )); + + label +} diff --git a/src/gui/widget/power_menu/systemd.rs b/src/gui/widget/power_menu/systemd.rs index fec8c60..dfda05a 100644 --- a/src/gui/widget/power_menu/systemd.rs +++ b/src/gui/widget/power_menu/systemd.rs @@ -15,7 +15,11 @@ use relm4::{ }; use tokio::process::Command; -use crate::{demo, fl, gui::GAP, i18n::lowercase_first_char}; +use crate::{ + demo, fl, + gui::{widget::power_menu::header_label, GAP}, + i18n::lowercase_first_char, +}; #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] @@ -83,9 +87,7 @@ impl AsyncComponent for SystemdPowerMenu { #[transition(Crossfade)] match model { Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - set_markup: &format!("{} (Systemd)", fl!("power-menu-tooltip")), - }, + header_label("Systemd") {}, #[iterate] append: &action_buttons(actions, sender.clone()), diff --git a/src/gui/widget/power_menu/unix.rs b/src/gui/widget/power_menu/unix.rs index f77d614..168f1be 100644 --- a/src/gui/widget/power_menu/unix.rs +++ b/src/gui/widget/power_menu/unix.rs @@ -13,7 +13,7 @@ use tokio::process::Command; use crate::{ demo, fl, - gui::{icons, GAP}, + gui::{icons, widget::power_menu::header_label, GAP}, i18n::lowercase_first_char, }; @@ -57,9 +57,7 @@ impl AsyncComponent for UnixPowerMenu { #[transition(Crossfade)] match model { Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - set_markup: &format!("{} (Unix)", fl!("power-menu-tooltip")) - }, + header_label("Unix") {}, #[iterate] append: &action_buttons(actions, sender.clone()), From 92a453a11eb2a4077a6d7505e8732a98e123431e Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sat, 24 May 2025 03:18:17 +0300 Subject: [PATCH 5/8] rfct: Split the UI from config for power menu backends Instead of having copies of the UI and config code for each backend, put the UI code in a single Widget. Each backend implements a conversion from it's config to a common config that can represent any power menu. --- i18n/en/regreet.ftl | 6 +- i18n/uk/regreet.ftl | 6 +- src/gui/model.rs | 4 +- src/gui/widget/power_menu/custom.rs | 272 ++++------------------- src/gui/widget/power_menu/mod.rs | 311 +++++++++++++++++++++++---- src/gui/widget/power_menu/systemd.rs | 282 ++++-------------------- src/gui/widget/power_menu/unix.rs | 248 +++++---------------- 7 files changed, 416 insertions(+), 713 deletions(-) diff --git a/i18n/en/regreet.ftl b/i18n/en/regreet.ftl index 4ab1abb..a888692 100644 --- a/i18n/en/regreet.ftl +++ b/i18n/en/regreet.ftl @@ -14,4 +14,8 @@ power-menu-reboot-firmware = Reboot into firmware power-menu-suspend = Suspend power-menu-hibernate = Hibernate power-menu-hybrid-sleep = Hybrid sleep -power-menu-confirm-dialog-heading = Really { $what }? \ No newline at end of file +power-menu-confirm-dialog-heading = Really { $what }? + +power-menu-backend-systemd = Systemd +power-menu-backend-unix = Unix +power-menu-backend = Backend \ No newline at end of file diff --git a/i18n/uk/regreet.ftl b/i18n/uk/regreet.ftl index 01ac720..711fbb7 100644 --- a/i18n/uk/regreet.ftl +++ b/i18n/uk/regreet.ftl @@ -14,4 +14,8 @@ power-menu-reboot-firmware = Перезапустити до меню загру power-menu-suspend = Сон power-menu-hibernate = Гибернація power-menu-hybrid-sleep = Гібрідний сон -power-menu-confirm-dialog-heading = Точно { $what }? \ No newline at end of file +power-menu-confirm-dialog-heading = Точно { $what }? + +power-menu-backend-systemd = Systemd +power-menu-backend-unix = Юнікс +power-menu-backend = Реалізація \ No newline at end of file diff --git a/src/gui/model.rs b/src/gui/model.rs index 71f8dde..2a9a3e8 100644 --- a/src/gui/model.rs +++ b/src/gui/model.rs @@ -18,7 +18,7 @@ use relm4::{ gdk::{Display, Monitor}, prelude::*, }, - AsyncComponentSender, Component, Controller, + prelude::*, }; use tokio::{sync::Mutex, time::sleep}; @@ -95,7 +95,7 @@ pub struct Greeter { pub(super) demo: bool, pub(super) clock: Controller, - pub(super) power_menu: Controller, + pub(super) power_menu: AsyncController, } impl Greeter { diff --git a/src/gui/widget/power_menu/custom.rs b/src/gui/widget/power_menu/custom.rs index 4f7cd7d..2458d96 100644 --- a/src/gui/widget/power_menu/custom.rs +++ b/src/gui/widget/power_menu/custom.rs @@ -4,19 +4,14 @@ //! A fully customizable power menu widget. -use adw::glib::markup_escape_text; -use relm4::{adw::prelude::*, prelude::*}; -use tokio::process; - -use crate::{ - demo, fl, - gui::{widget::power_menu::header_label, GAP}, - i18n::lowercase_first_char, -}; +use super::{Action, PowerMenuInit}; #[derive(Deserialize, Clone)] +/// A fully-customizable power menu. You can run arbitrary commands and set labels for them. See [`Command`] for a list +/// of all properties that can be configured pub struct CustomPowerMenuConfig { + backend: String, commands: Vec, } @@ -47,237 +42,42 @@ pub enum Title { Action(Action), /// Arbitrary name string - Name(String), -} - -#[derive(Deserialize, Clone, Copy)] -#[serde(rename_all = "kebab-case")] -pub enum Action { - Poweroff, - Halt, - Reboot, - RebootFirmware, - Suspend, - Hibernate, - HybridSleep, + Label(String), } -pub struct CustomPowerMenu { - state: MenuState, - commands: Vec, -} - -#[derive(PartialEq, Eq, Clone, Copy)] -pub enum MenuState { - Menu, - Confirm(usize), -} - -#[derive(Debug)] -pub enum CustomPowerMenuMsg { - Request(usize), - Confirm, - Cancel, -} - -#[relm4::component(pub, async)] -impl AsyncComponent for CustomPowerMenu { - type Init = CustomPowerMenuConfig; - type Input = CustomPowerMenuMsg; - type Output = (); - type CommandOutput = (); - - view! { - gtk::Box { - #[name="menu"] - #[transition(Crossfade)] - match model.state { - MenuState::Menu => gtk::Box::new(gtk::Orientation::Vertical, GAP) { - header_label("Custom") {}, - - #[iterate] - append: &action_buttons(&model.commands, sender.clone()), - }, - - MenuState::Confirm(index) =>>k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - #[watch] - set_markup: &format!("{}", - markup_escape_text(&fl!("power-menu-confirm-dialog-heading", - what = lowercase_first_char(&model.commands[index].fl())) - ) - ), - }, - - gtk::Box { - set_spacing:GAP, - - gtk::Button::with_label(&fl!("dialog-cancel")) { - connect_clicked => CustomPowerMenuMsg::Cancel, - }, - - gtk::Button { - add_css_class: "destructive-action", - - #[watch] - set_label: &model.commands[index].fl(), - - connect_clicked => CustomPowerMenuMsg::Confirm, - } - } - } - } - } - } - - async fn init( - CustomPowerMenuConfig { commands }: Self::Init, - root: Self::Root, - sender: relm4::AsyncComponentSender, - ) -> AsyncComponentParts { - let model = Self { - commands, - state: MenuState::Menu, - }; - let widgets = view_output!(); - - widgets.menu.set_hhomogeneous(false); - widgets.menu.set_vhomogeneous(false); - - AsyncComponentParts { model, widgets } +impl PowerMenuInit for CustomPowerMenuConfig { + fn backend(&self) -> String { + self.backend.clone() } - - async fn update( - &mut self, - message: Self::Input, - sender: AsyncComponentSender, - _: &Self::Root, - ) { - use CustomPowerMenuMsg as M; - let index = match message { - M::Request(index) => index, - - M::Confirm => { - let MenuState::Confirm(what) = self.state else { - return; + fn commands(self) -> Vec { + let len = self.commands.len(); + self.commands.into_iter().fold( + Vec::with_capacity(len), + |mut acc, + Command { + title, + command, + confirm, + icon, + }| { + let icon = icon.unwrap_or(match title { + Title::Action(action) => action.icon().to_owned(), + Title::Label(_) => String::default(), + }); + + let label = match title { + Title::Action(action) => action.fl(), + Title::Label(label) => label, }; - what - } - - M::Cancel => { - self.state = MenuState::Menu; - return; - } - }; - - let command = &self.commands[index]; - - use Action as A; - let require_confirm = self.state == MenuState::Menu - && (command.confirm == Some(true) - || matches!( - command.title, - Title::Action(A::Poweroff | A::Reboot | A::RebootFirmware) - )); - - if require_confirm { - self.state = MenuState::Confirm(index); - return; - } - - let fl = command.fl(); - - if demo() { - info!("Demo mode: not doing {fl}"); - self.state = MenuState::Menu; - - return; - } - - let command = process::Command::new(&command.command[0]) - .args(&command.command[1..]) - .status(); - - sender.oneshot_command(async move { - let Err(why) = command.await else { return }; - debug!("Failed to {fl}: {why}"); - }); - } -} - -fn action_buttons( - commands: &[Command], - sender: AsyncComponentSender, -) -> Vec { - let len = commands.len(); - - commands - .iter() - .enumerate() - .fold(Vec::with_capacity(len), |mut acc, (index, command)| { - let button = gtk::Button::new(); - - if let Some(icon_name) = command.icon() { - let icon = gtk::Image::new(); - icon.set_icon_name(Some(icon_name)); - - let label = gtk::Label::new(Some(&command.fl())); - - let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); - container.append(&icon); - container.append(&label); - - button.set_child(Some(&container)); - } else { - button.set_label(&command.fl()); - } - - let sender = sender.clone(); - button.connect_clicked(move |_| sender.input(CustomPowerMenuMsg::Request(index))); - - acc.push(button); - acc - }) -} - -impl Command { - fn fl(&self) -> String { - let action = match self.title { - Title::Action(action) => action, - - Title::Name(ref name) => return name.clone(), - }; - - use Action as A; - match action { - A::Poweroff => fl!("power-menu-poweroff"), - A::Halt => fl!("power-menu-halt"), - A::Reboot => fl!("power-menu-reboot"), - A::RebootFirmware => fl!("power-menu-reboot-firmware"), - A::Suspend => fl!("power-menu-suspend"), - A::Hibernate => fl!("power-menu-hibernate"), - A::HybridSleep => fl!("power-menu-hybrid-sleep"), - } - } - - fn icon(&self) -> Option<&str> { - let None = self.icon else { - return self.icon.as_deref(); - }; - - let Title::Action(action) = self.title else { - return None; - }; - use Action as A; - Some(match action { - A::Poweroff => crate::gui::icons::POWEROFF, - A::Halt => crate::gui::icons::POWEROFF, - A::Reboot => crate::gui::icons::REBOOT, - A::RebootFirmware => crate::gui::icons::REBOOT_FIRMWARE, - A::Suspend => crate::gui::icons::SUSPEND, - A::Hibernate => crate::gui::icons::HIBERNATE, - A::HybridSleep => crate::gui::icons::HIBERNATE, - }) + acc.push(super::Command { + label, + command, + confirm: confirm.unwrap_or_default(), + icon, + }); + acc + }, + ) } } diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs index 0a2e26d..c8e5786 100644 --- a/src/gui/widget/power_menu/mod.rs +++ b/src/gui/widget/power_menu/mod.rs @@ -4,14 +4,21 @@ //! A [serde-configurable][`PowerMenuConfig`] power menu. -use adw::glib::markup_escape_text; -use custom::{CustomPowerMenu, CustomPowerMenuConfig}; -use relm4::prelude::*; +use custom::CustomPowerMenuConfig; +use relm4::{ + gtk::{glib::markup_escape_text, prelude::*}, + prelude::{AsyncComponentParts, *}, +}; use serde::Deserialize; -use systemd::{SystemdPowerMenu, SystemdPowerMenuConfig}; -use unix::{UnixPowerMenu, UnixPowerMenuConfig}; +use systemd::SystemdPowerMenuConfig; +use tokio::process; +use unix::UnixPowerMenuConfig; -use crate::{fl, gui::icons}; +use crate::{ + demo, fl, + gui::{icons, GAP}, + i18n::lowercase_first_char, +}; mod custom; mod systemd; @@ -32,16 +39,54 @@ impl Default for PowerMenuConfig { } } -pub enum PowerMenu { - Systemd(AsyncController), - Unix(AsyncController), - Custom(AsyncController), +pub struct PowerMenu { + state: MenuState, + commands: Vec, } -#[relm4::component(pub)] -impl Component for PowerMenu { +#[derive(Clone)] +pub struct Command { + /// The label of the action. + pub label: String, + + /// Command to be executed. + pub command: Vec, + + /// If `true`, a confirmation will be shown before the [`Self::command`] is executed. + pub confirm: bool, + + /// The icon name to set for this action. A list of installed icons can be looked up using the `icon-library` app. + /// + /// If empty, no icon will be displayed. + pub icon: String, +} + +#[derive(PartialEq, Eq)] +pub enum MenuState { + /// The default state with all buttons shown. + Menu, + + /// The confimation widget for a specific action. + Confirm(usize), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PowerMenuMsg { + /// Sent when the user selected a power menu button. If the action requires confirmation (involves a poweroff of the + /// system) a confirmation screen is shown ([`PowerMenu::Confirm`] state). + Request(usize), + + /// A confirmation of a [`PowerMenuMsg::Request`] was cancelled. Has no effect if no confirmation is requested. + Cancel, + + /// Confirms an action that was requested in [`PowerMenuMsg::Request`]. Has no effect if no confirmation is requested. + Confirm, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for PowerMenu { type Init = PowerMenuConfig; - type Input = (); + type Input = PowerMenuMsg; type Output = (); type CommandOutput = (); @@ -52,56 +97,240 @@ impl Component for PowerMenu { #[wrap(Some)] set_popover = >k::Popover { - model.widget() { }, + gtk::Box { + #[name = "menu"] + #[transition(Crossfade)] + match model.state { + MenuState::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { + gtk::Label { + set_markup: &format!( + "{}\n{}: {backend}", + markup_escape_text(&fl!("power-menu-tooltip")), + markup_escape_text(&fl!("power-menu-backend")), + ), + }, + + #[iterate] + append: &action_buttons(&model.commands, sender.clone()), + }, + + MenuState::Confirm(index) => >k::Box::new(gtk::Orientation::Vertical, GAP) { + gtk::Label { + #[watch] + set_markup: &format!("{}", + markup_escape_text(&fl!("power-menu-confirm-dialog-heading", + what = lowercase_first_char(&model.commands[index].label)) + ) + ), + }, + + gtk::Box { + set_spacing: GAP, + + gtk::Button::with_label(&fl!("dialog-cancel")) { + connect_clicked => PowerMenuMsg::Cancel, + }, + + gtk::Button { + add_css_class: "destructive-action", + + #[watch] + set_label: &model.commands[index].label, + + connect_clicked => PowerMenuMsg::Confirm, + } + } + }, + } + } + }, } } - fn init( + async fn init( init: Self::Init, root: Self::Root, - _sender: ComponentSender, - ) -> ComponentParts { - let model = match init { + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let (backend, commands) = match init { Self::Init::Systemd(systemd_config) => { - Self::Systemd(SystemdPowerMenu::builder().launch(systemd_config).detach()) + (systemd_config.backend(), systemd_config.commands()) } - Self::Init::Unix(unix_power_menu_config) => Self::Unix( - UnixPowerMenu::builder() - .launch(unix_power_menu_config) - .detach(), + Self::Init::Unix(unix_power_menu_config) => ( + unix_power_menu_config.backend(), + unix_power_menu_config.commands(), ), - Self::Init::Custom(custom_power_menu_config) => Self::Custom( - CustomPowerMenu::builder() - .launch(custom_power_menu_config) - .detach(), + Self::Init::Custom(custom_power_menu_config) => ( + custom_power_menu_config.backend(), + custom_power_menu_config.commands(), ), }; + let model = Self { + commands, + state: MenuState::Menu, + }; + let widgets = view_output!(); - ComponentParts { model, widgets } + widgets.menu.set_vhomogeneous(false); + widgets.menu.set_hhomogeneous(false); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _: &Self::Root, + ) { + use PowerMenuMsg as M; + let index = match message { + M::Request(index) => index, + + M::Confirm => { + let MenuState::Confirm(what) = self.state else { + return; + }; + what + } + + M::Cancel => { + self.state = MenuState::Menu; + return; + } + }; + + let Command { + confirm, + label, + command, + .. + } = &self.commands[index]; + + let require_confirm = self.state == MenuState::Menu && *confirm; + + if require_confirm { + self.state = MenuState::Confirm(index); + return; + } + + if demo() { + info!("Demo mode: not doing {label}"); + self.state = MenuState::Menu; + + return; + } + + let command = process::Command::new(&command[0]) + .args(&command[1..]) + .status(); + + let label = label.clone(); + sender.oneshot_command(async move { + let Err(why) = command.await else { return }; + debug!("Failed to {label}: {why}"); + }); } } -impl PowerMenu { - fn widget(&self) -> >k::Box { - match self { - Self::Systemd(controller) => controller.widget(), - Self::Unix(controller) => controller.widget(), - Self::Custom(controller) => controller.widget(), +fn action_buttons( + commands: &[Command], + sender: AsyncComponentSender, +) -> Vec { + commands.iter().enumerate().fold( + Vec::with_capacity(commands.len()), + |mut acc, (index, command)| { + let button = gtk::Button::new(); + + if command.icon.is_empty() { + button.set_label(&command.label); + } else { + let icon = gtk::Image::new(); + icon.set_icon_name(Some(&command.icon)); + + let label = gtk::Label::new(Some(&command.label)); + + let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); + container.append(&icon); + container.append(&label); + + button.set_child(Some(&container)); + } + + let sender = sender.clone(); + button.connect_clicked(move |_| sender.input(PowerMenuMsg::Request(index))); + + acc.push(button); + acc + }, + ) +} + +impl Command { + pub fn new(label: String, icon: String, confirm: bool, command: Vec) -> Self { + Self { + label, + command, + confirm, + icon, } } } -fn header_label(backend: &str) -> gtk::Label { - let label = gtk::Label::new(None); - label.set_markup(&format!( - "{}\nBackend: {backend}", - markup_escape_text(&fl!("power-menu-tooltip")) - )); +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Poweroff, + Halt, + Reboot, + RebootFirmware, + Suspend, + Hibernate, + HybridSleep, +} + +impl Action { + /// Returns the icon name for this action + pub const fn icon(&self) -> &'static str { + match self { + Self::Poweroff => crate::gui::icons::POWEROFF, + Self::Halt => crate::gui::icons::POWEROFF, + Self::Reboot => crate::gui::icons::REBOOT, + Self::RebootFirmware => crate::gui::icons::REBOOT_FIRMWARE, + Self::Suspend => crate::gui::icons::SUSPEND, + Self::Hibernate => crate::gui::icons::HIBERNATE, + Self::HybridSleep => crate::gui::icons::HIBERNATE, + } + } + + /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{kebab-case}")` + pub fn fl(&self) -> String { + use Action as A; + match self { + A::Poweroff => fl!("power-menu-poweroff"), + A::Halt => fl!("power-menu-halt"), + A::Reboot => fl!("power-menu-reboot"), + A::RebootFirmware => fl!("power-menu-reboot-firmware"), + A::Suspend => fl!("power-menu-suspend"), + A::Hibernate => fl!("power-menu-hibernate"), + A::HybridSleep => fl!("power-menu-hybrid-sleep"), + } + } + + pub const fn is_like_poweroff(&self) -> bool { + matches!( + self, + Self::Poweroff | Self::Halt | Self::Reboot | Self::RebootFirmware + ) + } +} - label +trait PowerMenuInit { + fn backend(&self) -> String; + fn commands(self) -> Vec; } diff --git a/src/gui/widget/power_menu/systemd.rs b/src/gui/widget/power_menu/systemd.rs index dfda05a..39b2d0e 100644 --- a/src/gui/widget/power_menu/systemd.rs +++ b/src/gui/widget/power_menu/systemd.rs @@ -8,262 +8,72 @@ use std::collections::HashSet; -use relm4::{ - gtk::{glib::markup_escape_text, prelude::*}, - prelude::{AsyncComponentParts, *}, - AsyncComponentSender, -}; -use tokio::process::Command; +use crate::fl; -use crate::{ - demo, fl, - gui::{widget::power_menu::header_label, GAP}, - i18n::lowercase_first_char, -}; - -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum Action { - Poweroff, - Halt, - Reboot, - RebootFirmware, - Suspend, - Hibernate, - HybridSleep, -} +use super::{Action, Command, PowerMenuInit}; #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case")] pub struct SystemdPowerMenuConfig { /// The list of actions to show. Order is preserved. The first unique occurance is used, with duplicates discarded. /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. - /// - /// See [`Action`] for all available actions and what they do. - #[serde(default = "Action::default_set")] + #[serde(default = "default_actions")] pub actions: Vec, } impl Default for SystemdPowerMenuConfig { fn default() -> Self { Self { - actions: Action::default_set(), - } - } -} - -#[derive(PartialEq, Eq)] -pub enum SystemdPowerMenu { - /// The default state with all buttons shown. - Menu, - - /// The confimation widget for a specific action. - Confirm(Action), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SystemdPowerMenuMsg { - /// Sent when the user selected a power menu button. If the action requires confirmation (involves a poweroff of the - /// system) a confirmation screen is shown ([`SystemdPowerMenu::Confirm`] state). - Request(Action), - - /// A confirmation of a [`Self::Request`] was cancelled. Has no effect if no confirmation is requested. - Cancel, - - /// Confirms an action that was requested in [`Self::Request`]. Has no effect if no confirmation is requested. - Confirm, -} - -#[relm4::component(pub, async)] -impl AsyncComponent for SystemdPowerMenu { - type Init = SystemdPowerMenuConfig; - type Input = SystemdPowerMenuMsg; - type Output = (); - type CommandOutput = (); - - view! { - gtk::Box { - #[name = "menu"] - #[transition(Crossfade)] - match model { - Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { - header_label("Systemd") {}, - - #[iterate] - append: &action_buttons(actions, sender.clone()), - }, - - Self::Confirm(action) => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - #[watch] - set_markup: &format!("{}", - markup_escape_text(&fl!("power-menu-confirm-dialog-heading", - what = lowercase_first_char(&action.fl())) - ) - ), - }, - - gtk::Box { - set_spacing:GAP, - - gtk::Button::with_label(&fl!("dialog-cancel")) { - connect_clicked => SystemdPowerMenuMsg::Cancel, - }, - - gtk::Button { - add_css_class: "destructive-action", - - #[watch] - set_label: &action.fl(), - - connect_clicked => SystemdPowerMenuMsg::Confirm, - } - } - }, - } - } - } - - async fn init( - SystemdPowerMenuConfig { actions }: Self::Init, - root: Self::Root, - sender: AsyncComponentSender, - ) -> AsyncComponentParts { - let model = Self::Menu; - let widgets = view_output!(); - - widgets.menu.set_vhomogeneous(false); - widgets.menu.set_hhomogeneous(false); - - AsyncComponentParts { model, widgets } - } - - async fn update( - &mut self, - message: Self::Input, - sender: AsyncComponentSender, - _: &Self::Root, - ) { - use SystemdPowerMenuMsg as M; - let action = match &message { - M::Request(action) => *action, - - M::Confirm => { - let Self::Confirm(what) = self else { return }; - *what - } - - M::Cancel => { - *self = Self::Menu; - return; - } - }; - - use Action as A; - let require_confirm = *self == Self::Menu - && matches!( - &message, - M::Request(A::Poweroff | A::Reboot | A::RebootFirmware) - ); - - if require_confirm { - *self = Self::Confirm(action); - return; + actions: default_actions(), } - - if demo() { - info!("Demo mode: not doing {action:?}"); - *self = Self::Menu; - - return; - } - - let systemctl = Command::new("systemctl") - .args(action.systemctl_args()) - .status(); - - sender.oneshot_command(async move { - let Err(why) = systemctl.await else { return }; - debug!("Failed to {action:?}: {why}"); - }); } } -fn action_buttons( - activated: Vec, - sender: AsyncComponentSender, -) -> Vec { - let len = activated.len(); - let mut mentioned = HashSet::new(); - - activated - .into_iter() - .fold(Vec::with_capacity(len), |mut acc, action| { - if !mentioned.insert(action) { - return acc; - } - - let label = gtk::Label::new(Some(&action.fl())); - let icon = gtk::Image::new(); - icon.set_icon_name(Some(action.icon())); - - let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); - container.append(&icon); - container.append(&label); - - let button = gtk::Button::new(); - button.set_child(Some(&container)); - - let sender = sender.clone(); - button.connect_clicked(move |_| sender.input(SystemdPowerMenuMsg::Request(action))); - - acc.push(button); - acc - }) +/// Sensible default set of actions that most users would find sufficient. +fn default_actions() -> Vec { + [Action::Poweroff, Action::Reboot, Action::Suspend].into() } -impl Action { - /// Sensible default set of actions that most users would find sufficient. - pub fn default_set() -> Vec { - [Self::Poweroff, Self::Reboot, Self::Suspend].into() +impl PowerMenuInit for SystemdPowerMenuConfig { + fn backend(&self) -> String { + fl!("power-menu-backend-systemd") } - pub fn systemctl_args(&self) -> &'static [&'static str] { - match self { - Self::Poweroff => &["poweroff"], - Self::Halt => &["halt"], - Self::Reboot => &["reboot"], - Self::RebootFirmware => &["reboot", "--firmware-setup"], - Self::Suspend => &["suspend"], - Self::Hibernate => &["hibernate"], - Self::HybridSleep => &["hybrid-sleep"], - } - } - - /// Returns the icon name for this action - pub fn icon(&self) -> &'static str { - match self { - Self::Poweroff => crate::gui::icons::POWEROFF, - Self::Halt => crate::gui::icons::POWEROFF, - Self::Reboot => crate::gui::icons::REBOOT, - Self::RebootFirmware => crate::gui::icons::REBOOT_FIRMWARE, - Self::Suspend => crate::gui::icons::SUSPEND, - Self::Hibernate => crate::gui::icons::HIBERNATE, - Self::HybridSleep => crate::gui::icons::HIBERNATE, - } - } - - /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{kebab-case}")` - fn fl(&self) -> String { - use Action as A; - match self { - A::Poweroff => fl!("power-menu-poweroff"), - A::Halt => fl!("power-menu-halt"), - A::Reboot => fl!("power-menu-reboot"), - A::RebootFirmware => fl!("power-menu-reboot-firmware"), - A::Suspend => fl!("power-menu-suspend"), - A::Hibernate => fl!("power-menu-hibernate"), - A::HybridSleep => fl!("power-menu-hybrid-sleep"), - } + fn commands(self) -> Vec { + let mut mentioned = HashSet::new(); + + let len = self.actions.len(); + + self.actions + .into_iter() + .fold(Vec::with_capacity(len), |mut acc, action| { + if !mentioned.insert(action) { + return acc; + } + + let command = match action { + Action::Poweroff => vec!["systemctl".to_string(), "poweroff".to_string()], + Action::Halt => vec!["systemctl".to_string(), "halt".to_string()], + Action::Reboot => vec!["systemctl".to_string(), "reboot".to_string()], + Action::RebootFirmware => vec![ + "systemctl".to_string(), + "reboot".to_string(), + "--firmware-setup".to_string(), + ], + Action::Suspend => vec!["systemctl".to_string(), "suspend".to_string()], + Action::Hibernate => vec!["systemctl".to_string(), "hibernate".to_string()], + Action::HybridSleep => { + vec!["systemctl".to_string(), "hybrid-sleep".to_string()] + } + }; + + acc.push(Command::new( + action.fl(), + action.icon().to_owned(), + action.is_like_poweroff(), + command, + )); + acc + }) } } diff --git a/src/gui/widget/power_menu/unix.rs b/src/gui/widget/power_menu/unix.rs index 168f1be..3ce811d 100644 --- a/src/gui/widget/power_menu/unix.rs +++ b/src/gui/widget/power_menu/unix.rs @@ -8,212 +8,68 @@ use std::collections::HashSet; -use relm4::{adw::glib::markup_escape_text, adw::prelude::*, prelude::*}; -use tokio::process::Command; +use crate::fl; -use crate::{ - demo, fl, - gui::{icons, widget::power_menu::header_label, GAP}, - i18n::lowercase_first_char, -}; +use super::{Action, Command, PowerMenuInit}; #[derive(Deserialize, Clone)] pub struct UnixPowerMenuConfig { - #[serde(default = "Action::default_set")] + /// The list of actions to show. Order is preserved. The first unique occurance is used, with duplicates discarded. + /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. + /// + /// Please note that only `poweroff`, `reboot`, and `halt` are supported by the unix `shutdown` command. + #[serde(default = "default_actions")] actions: Vec, } -#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum Action { - Poweroff, - Reboot, - Halt, +pub fn default_actions() -> Vec { + [Action::Poweroff, Action::Reboot].into() } -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum UnixPowerMenu { - Menu, - Confirm(Action), -} - -#[derive(Debug)] -pub enum UnixPowerMenuMsg { - Request(Action), - Confirm, - Cancel, -} - -#[relm4::component(pub, async)] -impl AsyncComponent for UnixPowerMenu { - type Init = UnixPowerMenuConfig; - type Input = UnixPowerMenuMsg; - type Output = (); - type CommandOutput = (); - - view! { - gtk::Box { - #[name = "menu"] - #[transition(Crossfade)] - match model { - Self::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { - header_label("Unix") {}, - - #[iterate] - append: &action_buttons(actions, sender.clone()), - }, - - Self::Confirm(action) => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - #[watch] - set_markup: &format!("{}", - markup_escape_text(&fl!("power-menu-confirm-dialog-heading", - what = lowercase_first_char(&action.fl())) - ) - ), - }, - - gtk::Box { - set_spacing: GAP, - - gtk::Button::with_label(&fl!("dialog-cancel")) { - connect_clicked => UnixPowerMenuMsg::Cancel, - }, - - gtk::Button { - add_css_class: "destructive-action", - - #[watch] - set_label: &action.fl(), - - connect_clicked => UnixPowerMenuMsg::Confirm, - } - } - }, - } - } - } - - async fn init( - UnixPowerMenuConfig { actions }: Self::Init, - root: Self::Root, - sender: relm4::AsyncComponentSender, - ) -> AsyncComponentParts { - let model = Self::Menu; - let widgets = view_output!(); - - widgets.menu.set_vhomogeneous(false); - widgets.menu.set_hhomogeneous(false); - - AsyncComponentParts { model, widgets } - } - - async fn update( - &mut self, - message: Self::Input, - sender: AsyncComponentSender, - _: &Self::Root, - ) { - use UnixPowerMenuMsg as M; - let action = match &message { - M::Request(action) => *action, - - M::Confirm => { - let Self::Confirm(what) = self else { return }; - *what - } - - M::Cancel => { - *self = Self::Menu; - return; - } - }; - - if *self == Self::Menu { - *self = Self::Confirm(action); - return; - } - - if demo() { - info!("Demo mode: not doing {action:?}"); - *self = Self::Menu; - - return; - } - - let shutdown = Command::new("shutdown") - .args(action.shutdown_args()) - .status(); - - sender.oneshot_command(async move { - let Err(why) = shutdown.await else { return }; - debug!("Failed to {action:?}: {why}"); - }); +impl PowerMenuInit for UnixPowerMenuConfig { + fn backend(&self) -> String { + fl!("power-menu-backend-unix") } -} - -fn action_buttons( - activated: Vec, - sender: AsyncComponentSender, -) -> Vec { - let len = activated.len(); - let mut mentioned = HashSet::new(); - - activated - .into_iter() - .fold(Vec::with_capacity(len), |mut acc, action| { - if !mentioned.insert(action) { - return acc; - } - - let label = gtk::Label::new(Some(&action.fl())); - let icon = gtk::Image::new(); - icon.set_icon_name(Some(action.icon())); - - let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); - container.append(&icon); - container.append(&label); - - let button = gtk::Button::new(); - button.set_child(Some(&container)); - let sender = sender.clone(); - button.connect_clicked(move |_| sender.input(UnixPowerMenuMsg::Request(action))); - - acc.push(button); - acc - }) -} - -impl Action { - /// Sensible default set of actions that most users would find sufficient. - pub fn default_set() -> Vec { - [Self::Poweroff, Self::Reboot].into() - } - - pub const fn shutdown_args(&self) -> &'static [&'static str] { - match self { - Self::Poweroff => &["--poweroff", "now"], - Self::Reboot => &["--reboot", "now"], - Self::Halt => &["--halt", "now"], - } - } - - /// Returns the icon name for this action - pub const fn icon(&self) -> &'static str { - match self { - Self::Poweroff => icons::POWEROFF, - Self::Reboot => icons::REBOOT, - Self::Halt => icons::POWEROFF, - } - } - - /// Returns the [`crate::fl!`] for the variant using this format: `fl!("power-menu-{kebab-case}")` - pub fn fl(&self) -> String { - match self { - Self::Poweroff => fl!("power-menu-poweroff"), - Self::Reboot => fl!("power-menu-reboot"), - Self::Halt => fl!("power-menu-halt"), - } + fn commands(self) -> Vec { + let mut mentioned = HashSet::new(); + + let len = self.actions.len(); + self.actions + .into_iter() + .fold(Vec::with_capacity(len), |mut acc, action| { + if !mentioned.insert(action) { + return acc; + } + + let command = match action { + Action::Poweroff => { + vec![ + "shutdown".to_string(), + "--poweroff".to_string(), + "now".to_string(), + ] + } + Action::Reboot => vec![ + "shutdown".to_string(), + "--reboot".to_string(), + "now".to_string(), + ], + Action::Halt => vec![ + "shutdown".to_string(), + "--halt".to_string(), + "now".to_string(), + ], + _ => return acc, + }; + + acc.push(Command::new( + action.fl(), + action.icon().to_owned(), + action.is_like_poweroff(), + command, + )); + acc + }) } } From 707f50a20861e67f4e36047e88f6e93b8e2aa790 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sun, 25 May 2025 00:55:49 +0300 Subject: [PATCH 6/8] doc: Document the power menu and the clock widgets --- README.md | 2 + docs/CONFIG.md | 133 +++++++++++++++++++++++++++ src/gui/widget/power_menu/custom.rs | 21 ++++- src/gui/widget/power_menu/mod.rs | 33 ++++++- src/gui/widget/power_menu/systemd.rs | 8 +- src/gui/widget/power_menu/unix.rs | 18 ++-- src/i18n.rs | 4 +- 7 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 docs/CONFIG.md diff --git a/README.md b/README.md index 753474e..5cc0713 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ Currently, the following can be configured: * Power menu * X11 command prefix (see [this explanation on Reddit](https://web.archive.org/web/20240803120131/https://old.reddit.com/r/linux/comments/1c8zdcw/using_x11_window_managers_with_greetd_login/)) +Refer to [the configuration documentation](./docs/CONFIG.md) for more detail. + ### Widgets Widgets are additional UI elements that do not interact with `greetd`. Their configuration is under the `[widget]` key. diff --git a/docs/CONFIG.md b/docs/CONFIG.md new file mode 100644 index 0000000..4768d37 --- /dev/null +++ b/docs/CONFIG.md @@ -0,0 +1,133 @@ + +# ReGreet configuration docs + +## Clock + +The clock widget can be customized to specify the +[time display format](https://docs.rs/jiff/0.1.14/jiff/fmt/strtime/index.html#conversion-specifications), +how often to update the clock, overrides to the system timezone and a custom fixed label width so that the non-monospace +font does not cause annoying width changes in the UI. + +```toml +[widget.clock] +# strftime format argument +format = "%a %H:%M" + +# How often to update the text +resolution = "500ms" + +# Override system timezone (IANA Time Zone Database name, aka /etc/zoneinfo path) +# Remove to use the system time zone. +timezone = "America/Chicago" + +# Ask GTK to make the label at least this wide. This helps keeps the parent element layout and width consistent. +# Experiment with different widths, the interpretation of this value is entirely up to GTK. +label_width = 150 +``` + +## Power menu + +The first step in configuring the power menu is to select the backend to use. The backend is specified as part of the +path under `widget.power-menu.`. Please note that only 1 backend can be active at a time. + +For your convenience, most backends allow you to specify an action that is automatically translated into the supported +languages. The correct icon is also selected. Actions that will poweroff your system will ask for confirmation before +they are executed. The actions are listed in the table below. + +| Config value | Confirmation required | +|-----------------|-----------------------| +| poweroff | Yes | +| halt | Yes | +| reboot | Yes | +| reboot-firmware | Yes | +| suspend | No | +| hibernate | No | +| hybrid-sleep | No | + +The convention most backends' configs follow is having a single `actions` key, which is an array of actions. This simple +structure allows you to easily add, remove, and reorder the buttons in the menu without having to verbosely specify all +the attributes like you can do with the [custom](#custom) backend. + +### Systemd + +Machines using Systemd as the init system can specify the following line to use the default configuration. + +```toml +[widget.power-menu.systemd] +``` + +The displayed actions can be changed using the `actions` key. [See above](#power-menu) for more details + +```toml +[widget.power-menu.systemd] +actions = ["suspend", "poweroff", "reboot-firmware"] +``` + +### Unix + +This backend should work on most systems as it does not use commands shipped with any specific init system. Instead, the +Linux `shutdown` command is used. + +```toml +[widget.power-menu.unix] +``` + +> [!NOTE] +> The only actions supported by this backend are poweroff, reboot, and halt. +> +> When other values are specified they are logged as warnings and silently skipped. + +```toml +[widget.power-menu.systemd] +actions = ["poweroff", "reboot", "halt"] +``` + +### Custom + +The custom backend allows you to fully customize the power menu. If none of the other backends suit your needs or +require additional customizations, consider using this backend. + +It is necesary to set the `backend` key to the backend that you are implementing. The backend string can have any value +and is only used as part of the label in the power menu. The actions can be specified in the `commands` key. + +The power menu button (as well as `commands`) has the following fields: + +- `String` Label +- `String` Icon (optional) +- `bool` Should this entry be confirmed? +- `Vec` Command to execute + +The label, icon and confirmation can be inferred by ReGreet automatically if you set the `action` field. + +```toml +[widget.power-menu.custom] +backend = "Custom" # Required value + +[[widget.power-menu.custom.commands]] +action = "poweroff" +command = ["shutdown", "--now"] +# `action` Infers: +# label = "..." ## NOTE: label and action are mutually exclusive +# icon = "..." +# confirm = ... +``` + +Overrides (that are different from the action defaults) can be specified for the icon and the confirm, but not the +label. The logic is that if you are specifying a custom label, you are likely also specifying a custom command that +ReGreet does not know how to interpret into the inferable fields. + +The icon can be hidden altogether by setting it to an empty string. + +```toml +[[widget.power-menu.custom.commands]] +action = "poweroff" +command = ["shutdown", "--now"] +icon = "" # no icon +confirm = false +# Infers: +# label = Poweroff" ## NOTE: label and action are mutually exclusive +``` diff --git a/src/gui/widget/power_menu/custom.rs b/src/gui/widget/power_menu/custom.rs index 2458d96..611c7d4 100644 --- a/src/gui/widget/power_menu/custom.rs +++ b/src/gui/widget/power_menu/custom.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -//! A fully customizable power menu widget. +//! A fully customizable power menu backend. use super::{Action, PowerMenuInit}; @@ -11,13 +11,24 @@ use super::{Action, PowerMenuInit}; /// A fully-customizable power menu. You can run arbitrary commands and set labels for them. See [`Command`] for a list /// of all properties that can be configured pub struct CustomPowerMenuConfig { + /// A string label in the UI that indicates the backend this menu uses. + /// + /// For example, if you implement the power menu using `runit` commands, set the backend to Runit. backend: String, commands: Vec, } +/// A set of customizations that can be done to the power menu item. +/// +/// Setting [`Self::title`] to [`Title::Action`] allows ReGreet to infer the translated label, the icon to use, and the +/// need to confirm the action. +/// +/// However, if the power menu item does not fit any of the known to ReGreet [`Action`]s, you can use the +/// [`Title::Label`] variant and optionally specify the [`Self::confirm`] and the [`Self::icon`]. #[derive(Deserialize, Clone)] pub struct Command { - /// The title of the action. + /// The title of the action. This field is flattened in the config, so instead of setting `*command*.title.action`, + /// set `*command*.action`. #[serde(flatten)] title: Title, @@ -27,11 +38,17 @@ pub struct Command { /// If `true`, a confirmation will be shown before the [`Self::command`] is executed. /// + /// The [`Option`] is handled according to these rules: + /// /// If [`Some`], use the value. Otherwise, try to derive the value from the [`Title::Action`], falling back to /// `false`. Actions that involve a poweroff are inferred to require confirmation. confirm: Option, /// The icon name to set for this action. A list of installed icons can be looked up using the `icon-library` app. + /// + /// If [`None`], the icon is inferred from the value of [`Title::Action`], falling back to no icon. + /// + /// An empty string can be used to disable the icon for this item. icon: Option, } diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs index c8e5786..be6f528 100644 --- a/src/gui/widget/power_menu/mod.rs +++ b/src/gui/widget/power_menu/mod.rs @@ -3,6 +3,32 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! A [serde-configurable][`PowerMenuConfig`] power menu. +//! +//! See submodules for information on how to configure each power menu backend. +//! +//! ## Implementing a custom power menu backend +//! +//! Users of ReGreet can use the [`custom`] backend to specify arbitrary commands you want to run. Although this is not +//! intended, nothing stops you from adding entries unrelated to power management. +//! +//! However, if you want to add support for another init system, for example, it is best to implement a custom backend. +//! Fortunately, this is easy to do. +//! +//! You will need to create a config for this backend. Most likely, only having a single `actions: Vec` field +//! will be sufficient. For forward compatibility, please do not skip creating a `struct {}` to hold the config (exactly +//! the `{}` struct format). Having this struct allows future maintainers to add or rename fields more easily without +//! having to deal with config structure migrations. +//! +//! Make sure to implement sensible defaults for the entire struct. In cases where a default value should be inferred +//! after the config is parsed, use an [`Option`] to detect that the user did not set the value and generate the default +//! in the [`PowerMenuInit`] implementation. +//! +//! Finally, implement a coversion from your custom config struct using the [`PowerMenuInit`] trait. The [`Action`] can +//! be used to infer a lot of information such as the [icon](`Action::icon`) and the [translated label](`Action::fl`). +//! The confirmation requirement logic defaults to checking if the action +//! [involves a poweroff](`Action::is_like_poweroff`). +//! +//! Lastly, add your config type as a variant to the [`PowerMenuConfig`]. use custom::CustomPowerMenuConfig; use relm4::{ @@ -72,8 +98,8 @@ pub enum MenuState { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PowerMenuMsg { - /// Sent when the user selected a power menu button. If the action requires confirmation (involves a poweroff of the - /// system) a confirmation screen is shown ([`PowerMenu::Confirm`] state). + /// Sent when the user selected a power menu button. If the action [requires confirmation](`Command::confirm`) + /// a confirmation screen is shown ([`MenuState::Confirm`] state). Request(usize), /// A confirmation of a [`PowerMenuMsg::Request`] was cancelled. Has no effect if no confirmation is requested. @@ -322,6 +348,8 @@ impl Action { } } + /// Returns `true` if executing this action would power off the system (meaning reboot counts). Putting the system to + /// sleep does not count as it can be easily undone by waking up the system. pub const fn is_like_poweroff(&self) -> bool { matches!( self, @@ -330,6 +358,7 @@ impl Action { } } +/// A convenience trait that converts any power menu config into a set of information that the generic UI can display. trait PowerMenuInit { fn backend(&self) -> String; fn commands(self) -> Vec; diff --git a/src/gui/widget/power_menu/systemd.rs b/src/gui/widget/power_menu/systemd.rs index 39b2d0e..e7a527b 100644 --- a/src/gui/widget/power_menu/systemd.rs +++ b/src/gui/widget/power_menu/systemd.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -//! A systemd-aware power menu widget. +//! The Systemd power menu backend. //! //! See supported actions as variants of [`Action`]. @@ -15,8 +15,12 @@ use super::{Action, Command, PowerMenuInit}; #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case")] pub struct SystemdPowerMenuConfig { - /// The list of actions to show. Order is preserved. The first unique occurance is used, with duplicates discarded. + /// The list of actions to show. Order is preserved. + /// + /// The first unique occurance is used, with duplicates discarded. /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. + /// + /// The button labels, icons and `systemctl` commands are automatically selected based on the action you specify. #[serde(default = "default_actions")] pub actions: Vec, } diff --git a/src/gui/widget/power_menu/unix.rs b/src/gui/widget/power_menu/unix.rs index 3ce811d..c1b819e 100644 --- a/src/gui/widget/power_menu/unix.rs +++ b/src/gui/widget/power_menu/unix.rs @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -//! A power menu widget that uses the generic `man 8 shutdown` linux command. -//! -//! See supported actions as variants of [`Action`]. +//! A power menu backend that uses the generic `man 8 shutdown` linux command. use std::collections::HashSet; @@ -14,10 +12,15 @@ use super::{Action, Command, PowerMenuInit}; #[derive(Deserialize, Clone)] pub struct UnixPowerMenuConfig { - /// The list of actions to show. Order is preserved. The first unique occurance is used, with duplicates discarded. + /// The list of actions to show. Order is preserved. + /// + /// The first unique occurance is used, with duplicates discarded. /// E.g. `["poweroff", "reboot", "poweroff"]` Results in this order of widgets: Poweroff, Reboot. /// - /// Please note that only `poweroff`, `reboot`, and `halt` are supported by the unix `shutdown` command. + /// The button labels, icons and `systemctl` commands are automatically selected based on the action you specify. + /// + /// Please note that only `poweroff`, `reboot`, and `halt` are supported by the unix `shutdown` command. Other + /// actions are silently filtered out. #[serde(default = "default_actions")] actions: Vec, } @@ -60,7 +63,10 @@ impl PowerMenuInit for UnixPowerMenuConfig { "--halt".to_string(), "now".to_string(), ], - _ => return acc, + unsupported => { + info!("{unsupported:?} is not supported in the Unix power menu backend."); + return acc; + } }; acc.push(Command::new( diff --git a/src/i18n.rs b/src/i18n.rs index 3da03ac..7238921 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -4,7 +4,9 @@ //! # Internationalization //! -//! Firstly Initialize the language selection with [`init`]. Then use the [`fl`] macro to request a string by id. +//! Firstly Initialize the language selection with [`init`]. Then use the [`fl!`] macro to request a string by id. +//! +//! [`fl!`]: crate::fl use std::sync::LazyLock; From 1c80edeb8059af2952f40b7fc6fdadfb60c4767a Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Sun, 25 May 2025 01:09:03 +0300 Subject: [PATCH 7/8] ci: Install Libadwaita for `cargo b` checks --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index eb3a06b..ed0c200 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install GTK4 - run: sudo apt update && sudo apt install libgtk-4-dev build-essential + run: sudo apt update && sudo apt install libgtk-4-dev build-essential libadwaita-1-dev - run: rustup toolchain install 1.75 --profile minimal - name: Restore build cache uses: Swatinem/rust-cache@v2.7.5 From 4a6d918002eccadfb464d0e4162c8f41362ea579 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:10:35 +0300 Subject: [PATCH 8/8] rfct: Use `adw::PreferencesGroup` in the power menu --- Cargo.lock | 107 ++++++++++++++++--------------- Cargo.toml | 11 ++-- src/gui/widget/power_menu/mod.rs | 82 +++++++++-------------- 3 files changed, 89 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d07122..35a635e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,9 +149,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cairo-rs" -version = "0.20.12" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +checksum = "dfe4354df4da648870e363387679081f8f9fc538ec8b55901e3740c6a0ef81b1" dependencies = [ "bitflags", "cairo-sys-rs", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +checksum = "47d6c3300c7103eb8e4de07591003511aa25664438f8c6fc317a3a9902c103f8" dependencies = [ "glib-sys", "libc", @@ -595,9 +595,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +checksum = "2a3c64459f569154f37616fc28923bfac490d4aaa134aaf5eca58a2c0c13050f" dependencies = [ "gdk-pixbuf-sys", "gio", @@ -607,9 +607,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +checksum = "3854ef7a6a8b8f3b4013a01d5f9cb0d1794ec4e810c6cb4e2cc6d980f1baf724" dependencies = [ "gio-sys", "glib-sys", @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +checksum = "c7e292649dc26e3440c508a00f42ab39156008320dd6e962d63eaf626ba4d7f0" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -635,9 +635,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +checksum = "f4f3174fa4f1e0bf2a7e04469b65db8f4d1db89a6f5cdc57727b14e97ce438cf" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.20.12" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +checksum = "ed68efc12b748a771be2dccc49480d8584004382967c98323245fc3c38b74a42" dependencies = [ "futures-channel", "futures-core", @@ -692,22 +692,22 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "glib" -version = "0.20.12" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" dependencies = [ "bitflags", "futures-channel", @@ -726,9 +726,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f" dependencies = [ "heck", "proc-macro-crate", @@ -739,9 +739,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" dependencies = [ "libc", "system-deps", @@ -755,9 +755,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" dependencies = [ "glib-sys", "libc", @@ -766,9 +766,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +checksum = "e7749aaf5d3b955bf3bfce39e3423705878a666b561384134da0e7786a45ddc3" dependencies = [ "glib", "graphene-sys", @@ -777,9 +777,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +checksum = "250abaee850a90a276509890a78029c356173f9573412bded5f155b0e41fa568" dependencies = [ "glib-sys", "libc", @@ -802,9 +802,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +checksum = "b6687e9f92ca89c000c376400cfaf7914d099413d72fdf4f84a25775a0b1fb2d" dependencies = [ "cairo-rs", "gdk4", @@ -817,9 +817,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +checksum = "5e76bcf64d9c4846f19651f45b400cc0c9c4c17b651849da520f3d77c6988c52" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -833,9 +833,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.9.7" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +checksum = "8f7887ee0ceeffedb25a418810a2c61497dacad51767fc13f9d60859b4023b8a" dependencies = [ "cairo-rs", "field-offset", @@ -854,9 +854,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "821160b4f17e7e4ed748818c23682d0a46bed04c287dbaac54dd4869d2c5e06a" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +checksum = "d274cbaf7d9aa55b7aff78cb21b43299d64e514e1300671469b66f691cc5a011" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1116,9 +1116,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libadwaita" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500135d29c16aabf67baafd3e7741d48e8b8978ca98bac39e589165c8dc78191" +checksum = "4df6715d1257bd8c093295b77a276ed129d73543b10304fec5829ced5d5b7c41" dependencies = [ "gdk4", "gio", @@ -1131,9 +1131,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680988058c2558baf3f548a370e4e78da3bf7f08469daa822ac414842c912db" +checksum = "fdf8950090cc180250cdb1ff859a39748feeda7a53a9f28ead3a17a14cc37ae2" dependencies = [ "gdk4-sys", "gio-sys", @@ -1318,9 +1318,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "pango" -version = "0.20.12" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +checksum = "e37b7a678e18c2e9f2485f7e39b7b2dac99590d5ddef08a7f56eae38a145402e" dependencies = [ "gio", "glib", @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +checksum = "f4f5daf21da43fba9f2a0092da0eebeb77637c23552bccaf58f791c518009c94" dependencies = [ "glib-sys", "gobject-sys", @@ -1515,6 +1515,7 @@ dependencies = [ "i18n-embed-fl", "jiff", "lazy_static", + "libadwaita", "lru", "pwd", "regex", @@ -1535,9 +1536,9 @@ dependencies = [ [[package]] name = "relm4" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30837553c1a8cfea1a404c83ec387c5c8ff9358e1060b057c274c5daa5035ad1" +checksum = "6bae902de22fd92e62641f047975abf228573425b9b8de175e8ab5b6cda10379" dependencies = [ "flume", "fragile", @@ -1553,15 +1554,15 @@ dependencies = [ [[package]] name = "relm4-css" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3b924557df1cddc687b60b313c4b76620fdbf0e463afa4b29f67193ccf37f9" +checksum = "37dbe7a114855a22618f0e13595ce6b3f165478c13c2dfc4f4f99614da105797" [[package]] name = "relm4-macros" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a895a7455441a857d100ca679bd24a92f91d28b5e3df63296792ac1af2eddde" +checksum = "175fce497fc6f11dde7ea56daa30ff7ad29a534bbc209d59d766659c880ba5f1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8abb4b7..ab683d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ rust-version = "1.80" description = "Clean and customizable greeter for greetd" repository = "https://github.com/rharish101/ReGreet/" license = "GPL-3.0-or-later" + +[features] +gtk4_8 = ["gtk4/v4_8"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -20,7 +23,7 @@ educe = "0.6" file-rotate = "0.7" glob = "0.3" greetd_ipc = { version = "0.10", features = ["tokio-codec"] } -gtk4 = "0.9" +gtk4 = "0.10" humantime-serde = "1.1.1" i18n-embed = { version = "0.15.2", features = [ "desktop-requester", @@ -29,10 +32,11 @@ i18n-embed = { version = "0.15.2", features = [ i18n-embed-fl = "0.9.2" jiff = "0.1.14" lazy_static = "1.5.0" +libadwaita = { version = "0.8", features = ["v1_6"] } lru = "0.12" pwd = "1.4.0" regex = "1.10" -relm4 = { version = "0.9", features = ["adw", "libadwaita"] } +relm4 = { version = "0.10", features = ["adw", "libadwaita"] } rust-embed = "8.5.0" serde = { version = "1.0", features = ["derive"] } shlex = "1.3" @@ -45,8 +49,5 @@ tracing-subscriber = { version = "0.3", features = ["local-time"] } tracker = "0.2" unic-langid = "0.9.5" -[features] -gtk4_8 = ["gtk4/v4_8"] - [dev-dependencies] test-case = "3.3.1" diff --git a/src/gui/widget/power_menu/mod.rs b/src/gui/widget/power_menu/mod.rs index be6f528..228c109 100644 --- a/src/gui/widget/power_menu/mod.rs +++ b/src/gui/widget/power_menu/mod.rs @@ -32,7 +32,8 @@ use custom::CustomPowerMenuConfig; use relm4::{ - gtk::{glib::markup_escape_text, prelude::*}, + adw::prelude::*, + gtk::glib::markup_escape_text, prelude::{AsyncComponentParts, *}, }; use serde::Deserialize; @@ -40,11 +41,7 @@ use systemd::SystemdPowerMenuConfig; use tokio::process; use unix::UnixPowerMenuConfig; -use crate::{ - demo, fl, - gui::{icons, GAP}, - i18n::lowercase_first_char, -}; +use crate::{demo, fl, gui::icons, i18n::lowercase_first_char}; mod custom; mod systemd; @@ -127,49 +124,38 @@ impl AsyncComponent for PowerMenu { #[name = "menu"] #[transition(Crossfade)] match model.state { - MenuState::Menu => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - set_markup: &format!( - "{}\n{}: {backend}", - markup_escape_text(&fl!("power-menu-tooltip")), - markup_escape_text(&fl!("power-menu-backend")), - ), - }, + MenuState::Menu => &adw::PreferencesGroup { + set_title: &markup_escape_text(&fl!("power-menu-tooltip")), + set_description: Some(&format!("{}: {backend}", markup_escape_text(&fl!("power-menu-backend")))), #[iterate] - append: &action_buttons(&model.commands, sender.clone()), + add: &action_buttons(&model.commands, sender.clone()), }, - MenuState::Confirm(index) => >k::Box::new(gtk::Orientation::Vertical, GAP) { - gtk::Label { - #[watch] - set_markup: &format!("{}", - markup_escape_text(&fl!("power-menu-confirm-dialog-heading", - what = lowercase_first_char(&model.commands[index].label)) - ) - ), - }, + MenuState::Confirm(index) => &adw::PreferencesGroup { + #[watch] + set_title: &markup_escape_text(&fl!("power-menu-confirm-dialog-heading", + what = lowercase_first_char(&model.commands[index].label)) + ), - gtk::Box { - set_spacing: GAP, + set_separate_rows: true, - gtk::Button::with_label(&fl!("dialog-cancel")) { - connect_clicked => PowerMenuMsg::Cancel, - }, + adw::ButtonRow { + set_title: &fl!("dialog-cancel"), - gtk::Button { - add_css_class: "destructive-action", + connect_activated => PowerMenuMsg::Cancel, + }, - #[watch] - set_label: &model.commands[index].label, + adw::ButtonRow { + #[watch] + set_title: &model.commands[index].label, + add_css_class: "destructive-action", - connect_clicked => PowerMenuMsg::Confirm, - } + connect_activated => PowerMenuMsg::Confirm, } - }, + } } } - }, } } @@ -267,29 +253,19 @@ impl AsyncComponent for PowerMenu { fn action_buttons( commands: &[Command], sender: AsyncComponentSender, -) -> Vec { +) -> Vec { commands.iter().enumerate().fold( Vec::with_capacity(commands.len()), |mut acc, (index, command)| { - let button = gtk::Button::new(); - - if command.icon.is_empty() { - button.set_label(&command.label); - } else { - let icon = gtk::Image::new(); - icon.set_icon_name(Some(&command.icon)); - - let label = gtk::Label::new(Some(&command.label)); - - let container = gtk::Box::new(gtk::Orientation::Horizontal, GAP); - container.append(&icon); - container.append(&label); + let button = adw::ButtonRow::new(); + button.set_title(&command.label); - button.set_child(Some(&container)); + if !command.icon.is_empty() { + button.set_start_icon_name(Some(&command.icon)); } let sender = sender.clone(); - button.connect_clicked(move |_| sender.input(PowerMenuMsg::Request(index))); + button.connect_activated(move |_| sender.input(PowerMenuMsg::Request(index))); acc.push(button); acc