From a8fe99b34536fed788ecaa85f546e5f9545e5a2a Mon Sep 17 00:00:00 2001 From: jmb05 Date: Sun, 1 Nov 2026 16:21:46 +0100 Subject: [PATCH] Inline some clones to make code more readable, add config, implement dimming -> locking -> sleeping into idle daemon, fix waiting on network thread, implement run menu with application launching and math evaluation, --- Cargo.lock | 285 ++++++++++++++++++------ Cargo.toml | 5 + jmb-bar.conf | 4 + src/backend/idle.rs | 247 ++++++++++++--------- src/backend/network.rs | 28 ++- src/backend/window_manager_hypr.rs | 88 +++++--- src/config.rs | 59 +++++ src/main.rs | 2 + src/ui.rs | 12 +- src/ui/action_centre.rs | 161 +++++++------- src/ui/run_menu.rs | 201 +++++++++++++++-- src/ui/utilities_panel.rs | 338 +++++++++++++++-------------- style/bar.scss | 8 +- style/run_menu.scss | 21 ++ 14 files changed, 975 insertions(+), 484 deletions(-) create mode 100644 jmb-bar.conf create mode 100644 src/config.rs diff --git a/Cargo.lock b/Cargo.lock index ec3389d..9d535a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,9 +377,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" dependencies = [ "cc", "cmake", @@ -550,6 +550,12 @@ dependencies = [ "digest", ] +[[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.12.1" @@ -607,6 +613,17 @@ dependencies = [ "zbus", ] +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -1029,9 +1046,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc907c45bb7e9ef8fc3778defd0f3cd8c81288f1b4e748eba064948f20c47122" +checksum = "9c035cb64cba03dad2fb921f3e218785e8cf9a41790da5eb3a1400f62fa2be11" dependencies = [ "cpubits", "ctutils", @@ -1076,9 +1093,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" +checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a" dependencies = [ "cmov", "subtle", @@ -1242,9 +1259,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +checksum = "6a996a108feb8b427a6e12fcf6af4aa83409b79d72c15337d7741368131ee004" dependencies = [ "const-oid", "pem-rfc7468", @@ -1417,7 +1434,7 @@ dependencies = [ [[package]] name = "edid" version = "0.4.0" -source = "git+https://github.com/jmb05/rust-edid#509d792617f521ea3797b70b547ac64a5fa35935" +source = "git+https://github.com/jmb05/rust-edid#ab42e7fa8139b35ace725dad55ef8e698d4feb52" dependencies = [ "nom 8.0.0", ] @@ -1430,9 +1447,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "elliptic-curve" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4496221fcfc21015fd8c0e26fcfab00504832dc0604f52acdd7bad2a8dc81" +checksum = "900f37e7e18d74a2347389af6cd69e3a30d5cc9e697f40004865679aa75e9c7c" dependencies = [ "base16ct", "crypto-bigint", @@ -1458,9 +1475,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.42" +version = "0.8.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5" dependencies = [ "cfg-if", "core_detect", @@ -1678,6 +1695,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freedesktop-desktop-entry" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a69ad6111a2b572cb41e0f3542d546386e841013cee7c45d705b61a15a87dd" +dependencies = [ + "bstr", + "gettext-rs", + "log", + "memchr", + "thiserror 2.0.21", + "unicase", + "xdg", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1897,6 +1929,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gettext-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df5e3fa8c077b15fdfa8ce130e2bc73d663b8aaa6d51b71421b8659980b1d80" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa7e4460b196107cc1bb2629488e8b6dbb7f34f6683ea6753c4ac22f590785" +dependencies = [ + "cc", + "temp-dir", +] + [[package]] name = "ghash" version = "0.6.0" @@ -2415,8 +2467,8 @@ dependencies = [ [[package]] name = "hyprland" -version = "0.4.0" -source = "git+https://github.com/romanstingler/hyprland-rs-next?branch=master#80c1fc217c77873dc9f76c64419d1ed568410c06" +version = "0.7.0" +source = "git+https://github.com/romanstingler/hyprland-rs-next?branch=master#7c2f69f8be78d89b58720bce7cad03b82e99baa3" dependencies = [ "async-stream", "chardetng", @@ -2434,8 +2486,8 @@ dependencies = [ [[package]] name = "hyprland-macros" -version = "0.4.0" -source = "git+https://github.com/romanstingler/hyprland-rs-next?branch=master#80c1fc217c77873dc9f76c64419d1ed568410c06" +version = "0.7.0" +source = "git+https://github.com/romanstingler/hyprland-rs-next?branch=master#7c2f69f8be78d89b58720bce7cad03b82e99baa3" dependencies = [ "proc-macro2", "quote", @@ -2487,7 +2539,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -2709,6 +2761,7 @@ dependencies = [ "crossbeam-channel", "ddc-hi", "eventfd", + "freedesktop-desktop-entry", "futures-util", "glib", "glib-build-tools", @@ -2723,15 +2776,19 @@ dependencies = [ "jmb-logger-rs", "libc", "libpulse-binding", + "meval", "mpris", "nmrs", "notify", + "nucleo-matcher", "rustsni", + "serde", "serde_json", - "sysinfo 0.40.0", + "sysinfo 0.40.1", "thiserror 2.0.21", "time-format", "tokio", + "toml", "wayland-client", "wayland-protocols", "zbus", @@ -2807,9 +2864,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.106" +version = "0.3.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" dependencies = [ "cfg-if", "futures-util", @@ -2982,6 +3039,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" +[[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.14" @@ -2993,9 +3063,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" [[package]] name = "lru-slab" @@ -3079,6 +3149,15 @@ dependencies = [ "libc", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3179,6 +3258,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "meval" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" +dependencies = [ + "fnv", + "nom 1.2.4", +] + [[package]] name = "mime" version = "0.3.17" @@ -3352,6 +3441,12 @@ dependencies = [ "zvariant", ] +[[package]] +name = "nom" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" + [[package]] name = "nom" version = "7.1.3" @@ -3417,6 +3512,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + [[package]] name = "num-bigint" version = "0.5.1" @@ -3525,6 +3630,26 @@ dependencies = [ "libloading 0.8.9", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "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 = "objc2" version = "0.6.5" @@ -3601,6 +3726,15 @@ dependencies = [ "objc2-core-foundation", ] +[[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.21.4" @@ -3615,9 +3749,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" [[package]] name = "option-ext" @@ -3678,9 +3812,9 @@ dependencies = [ [[package]] name = "pageant" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0786409b5aaa884655c9b4065681db0835db0cc185a6fb19fb17a5d38656866" +checksum = "e0bc922d9aa8cfcf598de27b3c6122cb2eeda9d6fe71527a22ebcb7f633540ff" dependencies = [ "base16ct", "byteorder", @@ -4242,9 +4376,9 @@ checksum = "263a3f48f01e7309e857138bd47f785585b4a005e8e56c6d2824ce91195999c3" [[package]] name = "pulldown-cmark" -version = "0.13.5" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb" +checksum = "3db2be46835f013dd3f4fa7f5f7402c537787b54ebb6fe2b94bbdd7e2669bb7b" dependencies = [ "bitflags 2.13.2", "memchr", @@ -4707,9 +4841,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags 2.13.2", "errno", @@ -5353,9 +5487,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110e37d98880fd804cd47509b90a8444f7aaaab655ae625e83cd442a8bc2ebf2" +checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8" dependencies = [ "libc", "memchr", @@ -5430,6 +5564,12 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[package]] +name = "temp-dir" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" + [[package]] name = "tempfile" version = "3.27.0" @@ -5660,9 +5800,9 @@ checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tonic" -version = "0.14.6" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +checksum = "e2cdfc673aabd30fe8fee92e65837b644c37c5d47f0f97ae6d5e0f1237d15e1b" dependencies = [ "async-trait", "axum", @@ -5681,6 +5821,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-stream", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -5689,9 +5830,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.6" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +checksum = "ac6d916d206ed7bdda1778ddff9bda43303789c258877e7bc22fb44ee5c984b4" dependencies = [ "prettyplease", "proc-macro2", @@ -5701,9 +5842,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.6" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +checksum = "eba464054e2226f182a34489b8b079bf4c6e3e7ab5fd7fc30e024f24aa92a484" dependencies = [ "bytes", "prost 0.14.4", @@ -5712,9 +5853,9 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.6" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +checksum = "2ea216ddc448e08e7839ab09e20e60b06b6a1c32745ccb212c0ebd161c199f1b" dependencies = [ "prettyplease", "proc-macro2", @@ -5920,9 +6061,9 @@ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" [[package]] name = "unicode-xid" @@ -5988,9 +6129,12 @@ dependencies = [ [[package]] name = "utf8_iter" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] [[package]] name = "utf8parse" @@ -5998,6 +6142,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + [[package]] name = "uuid" version = "1.29.0" @@ -6089,12 +6239,11 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" dependencies = [ "cfg-if", - "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", @@ -6102,9 +6251,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.79" +version = "0.4.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" dependencies = [ "js-sys", "tokio", @@ -6113,9 +6262,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6123,9 +6272,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" dependencies = [ "bumpalo", "proc-macro2", @@ -6136,9 +6285,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.129" +version = "0.2.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" dependencies = [ "unicode-ident", ] @@ -6202,9 +6351,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.106" +version = "0.3.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" dependencies = [ "js-sys", "wasm-bindgen", @@ -6684,7 +6833,7 @@ dependencies = [ "serde", "thiserror 2.0.21", "windows 0.62.2", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -6719,14 +6868,20 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xattr" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +checksum = "963d6fab92d5504b971dc8bd90ca743c2575e590bc8071ea5bc571598667ae08" dependencies = [ "libc", "rustix", ] +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "xml-rs" version = "0.8.29" @@ -6819,9 +6974,9 @@ dependencies = [ [[package]] name = "zcheapstr" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +checksum = "939f186d9ecebe044315a5cb6d6f78568bbc21152adc954d5a0c2f58bdbc9920" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index d683c95..ef0e956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,11 @@ ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" } wayland-protocols = { version = "0.32.13", features = ["staging", "client"] } wayland-client = "0.31.15" zbus = "5.19.0" +serde = { version = "1.0.229", features = ["derive"] } +freedesktop-desktop-entry = "0.8.4" +nucleo-matcher = "0.3.1" +meval = "0.2.0" +toml = "1.1.8" #zbus = "6.1.0" [build-dependencies] diff --git a/jmb-bar.conf b/jmb-bar.conf new file mode 100644 index 0000000..1eeb279 --- /dev/null +++ b/jmb-bar.conf @@ -0,0 +1,4 @@ +idle_timeout = 20 +dim_timeout = 60 +lock_timeout = 300 +sleep_timeout = 320 \ No newline at end of file diff --git a/src/backend/idle.rs b/src/backend/idle.rs index 36ba353..df9830c 100644 --- a/src/backend/idle.rs +++ b/src/backend/idle.rs @@ -1,12 +1,13 @@ use crate::backend::{BackendThread, Registrator}; +use crate::config::Config; use crate::{APP_NAME, LOGGER}; use crossbeam_channel::{Receiver, Sender}; use jmb_logger_rs::*; -use nmrs::raw::zbus::interface; use std::collections::HashMap; use std::error::Error; +use std::process::Command; use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; +use std::time::Duration; use tokio::sync::watch; use tokio::time::sleep; use wayland_client::globals::registry_queue_init; @@ -17,12 +18,11 @@ use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{ Event, ExtIdleNotificationV1, }; use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1; -use zbus::conn::Builder; +use zbus::interface; use zbus::message::Header; const SERVICE: &str = "org.freedesktop.ScreenSaver"; const OBJECT_PATH: &str = "/org/freedesktop/ScreenSaver"; -const LEGACY_OBJECT_PATH: &str = "/ScreenSaver"; struct WlState { idle: Option, @@ -53,14 +53,8 @@ impl Dispatch for WlState { _: &QueueHandle, ) { match event { - Event::Idled => { - println!("Idled: {:?}", Instant::now()); - state.idle = Some(true); - } - Event::Resumed => { - println!("Resumed: {:?}", Instant::now()); - state.idle = Some(false); - } + Event::Idled => state.idle = Some(true), + Event::Resumed => state.idle = Some(false), _ => {} } if let Some(idle) = state.idle { @@ -85,15 +79,15 @@ impl Dispatch for WlState { struct Inhibitor { owner: String, application: String, - _reason: String, + reason: String, } #[derive(Debug, Clone)] pub enum IdleState { Init(Sender), Dimmed, - Inhibited, - Caffeine, + Inhibited(bool), + Caffeine(bool), Active, } @@ -128,73 +122,92 @@ impl BackendThread> for IdleDaemon { } fn init(registration_rx: Receiver>) { + let config = Config::load(); let (tx, rx) = crossbeam_channel::unbounded::(); - let tx_clone = tx.clone(); let instance = Arc::new(Mutex::new(Self { subscribers: Vec::new(), })); - let instance_clone = instance.clone(); - std::thread::spawn(move || { - while let Ok(sender) = registration_rx.recv() { - if sender.send(IdleState::Init(tx_clone.clone())).is_ok() { - trace!(&LOGGER, "Added IdleDaemon Subscriber"); - instance_clone - .lock() - .expect("Couldn't get lock") - .subscribers - .push(sender); + std::thread::spawn({ + let instance = instance.clone(); + let tx = tx.clone(); + move || { + while let Ok(sender) = registration_rx.recv() { + if sender.send(IdleState::Init(tx.clone())).is_ok() { + trace!(&LOGGER, "Added IdleDaemon Subscriber"); + instance + .lock() + .expect("Couldn't get lock") + .subscribers + .push(sender); + } } } }); let (wl_tx, wl_rx) = watch::channel(false); - let (dbus_tx, dbus_rx) = watch::channel((false, false)); - std::thread::spawn(move || { - if let Err(err) = Self::start_idle_notifier(wl_tx) { - error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}"); + let (dbus_tx, dbus_rx) = watch::channel(false); + std::thread::spawn({ + let config = config.clone(); + move || { + if let Err(err) = Self::start_idle_notifier(wl_tx, &config) { + error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}"); + } } }); - std::thread::spawn(move || { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to create Tokio runtime"); - runtime.block_on(async move { - let mut idle_inhibitor = None; - while let Ok(msg) = rx.recv() { - match msg { - IdleMessage::Inhibit => { - if idle_inhibitor.is_none() - && let Ok(ii) = IdleInhibitor::inhibit_idle().await - { - idle_inhibitor = Some(ii); + std::thread::spawn({ + let instance = instance.clone(); + move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + runtime.block_on(async move { + let mut idle_inhibitor = None; + while let Ok(msg) = rx.recv() { + match msg { + IdleMessage::Inhibit => { + if idle_inhibitor.is_none() + && let Ok(ii) = IdleInhibitor::inhibit_idle().await + { + instance.lock().unwrap().publish(IdleState::Caffeine(true)); + idle_inhibitor = Some(ii); + } } - } - IdleMessage::Release => { - if let Some(ii) = idle_inhibitor.take() - && let Err(e) = ii.uninhibit_idle().await - { - warn!(&LOGGER, "Error uninhibiting {e}"); + IdleMessage::Release => { + if let Some(ii) = idle_inhibitor.take() { + match ii.uninhibit_idle().await { + Ok(_) => { + instance + .lock() + .unwrap() + .publish(IdleState::Caffeine(false)); + } + Err(e) => warn!(&LOGGER, "Error uninhibiting {e}"), + } + } } } } - } - }); + }); + } }); - std::thread::spawn(move || { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to create Tokio runtime"); + std::thread::spawn({ + let instance_clone = instance.clone(); + move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); - runtime.block_on(async move { - let res = IdleDaemon::start_dbus(dbus_tx).await; - match res { - Ok((_, _)) => { - Self::run_idle_timer(instance, wl_rx, dbus_rx).await; + runtime.block_on(async move { + let res = IdleDaemon::start_dbus(dbus_tx, instance_clone).await; + match res { + Ok((_, _)) => { + Self::run_idle_timer(instance, wl_rx, dbus_rx, &config).await; + } + Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"), } - Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"), - } - }); + }); + } }); } } @@ -215,20 +228,21 @@ impl IdleStage { } } - fn timeout(self) -> Duration { + fn timeout(self, cfg: &Config) -> Duration { match self { - Self::Dim => Duration::from_secs(20), - Self::TurnOff => Duration::from_secs(20), - Self::Sleep => Duration::from_secs(20), + Self::Dim => Duration::from_secs(cfg.dim_timeout_delta()), + Self::TurnOff => Duration::from_secs(cfg.lock_timeout_delta()), + Self::Sleep => Duration::from_secs(cfg.sleep_timeout_delta()), } } } impl IdleDaemon { async fn run_idle_timer( - _instance: Arc>, + instance: Arc>, mut wl_rx: watch::Receiver, - mut dbus_rx: watch::Receiver<(bool, bool)>, + mut dbus_rx: watch::Receiver, + config: &Config, ) { loop { if let Err(e) = wl_rx.changed().await { @@ -237,22 +251,26 @@ impl IdleDaemon { } let wl_idle = *wl_rx.borrow_and_update(); let dbus_state = *dbus_rx.borrow_and_update(); - println!("Wayland idle state changed {wl_idle} (DBus state: {dbus_state:?})"); - if wl_idle && !dbus_state.0 { - println!("Going into idle cycle"); + if wl_idle && !dbus_state { let mut stage = IdleStage::Dim; loop { tokio::select! { - _ = sleep(stage.timeout()) => { + _ = sleep(stage.timeout(config)) => { + match stage { + IdleStage::Dim => instance.lock().unwrap().publish(IdleState::Dimmed), + IdleStage::TurnOff => _ = Command::new("hyprlock").spawn(), + IdleStage::Sleep => _ = Command::new("systemctl").arg("sleep").spawn(), + } stage = stage.next(); - println!("Reached next stage {stage:?}"); } res = wl_rx.changed() => { if let Err(e) = res { warn!(&LOGGER, "Error getting Wayland idle state {e}"); return; } - println!("WL state change"); + if !*wl_rx.borrow_and_update() { + instance.lock().unwrap().publish(IdleState::Active); + } break; } res = dbus_rx.changed() => { @@ -260,7 +278,6 @@ impl IdleDaemon { warn!(&LOGGER, "Error getting D-Bus idle state {e}"); return; } - println!("D-Bus state change"); break; } } @@ -269,7 +286,7 @@ impl IdleDaemon { } } - fn start_idle_notifier(tx: watch::Sender) -> Result<(), Box> { + fn start_idle_notifier(tx: watch::Sender, config: &Config) -> Result<(), Box> { let conn = Connection::connect_to_env()?; let (globals, mut event_queue) = registry_queue_init::(&conn)?; let queue_handle = event_queue.handle(); @@ -280,7 +297,12 @@ impl IdleDaemon { let mut state = WlState { idle: None, tx }; - let _notification = notifier.get_idle_notification(10_000, &seat, &queue_handle, ()); + let _notification = notifier.get_idle_notification( + (config.idle_timeout() * 1000) as u32, + &seat, + &queue_handle, + (), + ); loop { event_queue.blocking_dispatch(&mut state)?; @@ -288,16 +310,18 @@ impl IdleDaemon { } async fn start_dbus( - tx: watch::Sender<(bool, bool)>, + tx: watch::Sender, + daemon: Arc>, ) -> zbus::Result<(ScreenSaver, zbus::connection::Connection)> { - let service = ScreenSaver::new(tx); + let service = ScreenSaver::new(tx, daemon); - let connection = Builder::session()? - .name(SERVICE)? - .serve_at(OBJECT_PATH, service.clone())? - .serve_at(LEGACY_OBJECT_PATH, service.clone())? - .build() + let connection = zbus::Connection::session().await?; + + connection + .object_server() + .at(OBJECT_PATH, service.clone()) .await?; + connection.request_name(SERVICE).await?; info!(&LOGGER, "Providing {SERVICE}"); @@ -347,22 +371,24 @@ impl IdleInhibitor { #[derive(Clone)] struct ScreenSaver { inner: Arc>, + daemon: Arc>, } struct Inner { next_cookie: u32, inhibitors: HashMap, - tx: watch::Sender<(bool, bool)>, + tx: watch::Sender, } impl ScreenSaver { - fn new(tx: watch::Sender<(bool, bool)>) -> Self { + fn new(tx: watch::Sender, daemon: Arc>) -> Self { Self { inner: Arc::new(Mutex::new(Inner { next_cookie: 1, inhibitors: HashMap::new(), tx, })), + daemon, } } } @@ -399,21 +425,26 @@ impl ScreenSaver { } }; - let caffeine_flag = application_name.eq(APP_NAME); - - debug!(&LOGGER, "Inhibitor added: {application_name} {cookie}"); + debug!( + &LOGGER, + "Inhibitor added - Name: '{application_name}' Reason: '{reason_for_inhibit}' Value: {cookie}" + ); inner.inhibitors.insert( cookie, Inhibitor { owner, application: application_name, - _reason: reason_for_inhibit, + reason: reason_for_inhibit, }, ); + self.daemon + .lock() + .unwrap() + .publish(IdleState::Inhibited(true)); + inner.tx.send_modify(|state| { - state.0 = !inner.inhibitors.is_empty(); - state.1 = caffeine_flag; + *state = !inner.inhibitors.is_empty(); }); Ok(cookie) } @@ -432,25 +463,25 @@ impl ScreenSaver { if let Some(removed) = inner.inhibitors.remove(&cookie) { debug!( &LOGGER, - "Inhibitor removed: {} {cookie}", removed.application + "Inhibitor removed - Name: '{}' Reason: '{}' Value: {cookie}", + removed.application, + removed.reason ); } - let mut caffeine_flag = false; - for inhibitor in inner.inhibitors.values() { - if inhibitor.application.eq(APP_NAME) { - caffeine_flag = true; - break; - } - } + + self.daemon + .lock() + .unwrap() + .publish(IdleState::Inhibited(!inner.inhibitors.is_empty())); + inner.tx.send_modify(|state| { - state.0 = !inner.inhibitors.is_empty(); - state.1 = caffeine_flag; + *state = !inner.inhibitors.is_empty(); }); Ok(()) } - Some(_) => Err(zbus::fdo::Error::AccessDenied( - "Cookie belongs to another D-Bus client".into(), - )), + Some(_) => Err(zbus::fdo::Error::AccessDenied(format!( + "Cookie {cookie} belongs to another D-Bus client" + ))), None => Err(zbus::fdo::Error::Failed("Unknown inhibition cookie".into())), } } diff --git a/src/backend/network.rs b/src/backend/network.rs index 3de7668..ac996a0 100644 --- a/src/backend/network.rs +++ b/src/backend/network.rs @@ -161,6 +161,26 @@ impl BackendThread> for NetworkBac } }; + std::thread::spawn({ + let nm = nm.clone(); + let mut instance = instance.clone(); + move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + runtime.block_on(async move { + while let Ok(tx) = registration_rx.recv() { + if tx.send(NetworkState::Init(instance.command_tx.clone())).is_ok() { + instance.subscribers.push(tx); + let _ = instance.send_all_networks(&nm).await; + let _ = instance.send_active_connection(&nm).await; + } + } + }); + }}); + let mut events = match nm.network_events().await { Ok(events) => events, Err(err) => { @@ -169,7 +189,6 @@ impl BackendThread> for NetworkBac } }; - let mut debouncer = EventDebouncer { duration: Duration::from_millis(200), deadline: None, @@ -177,13 +196,6 @@ impl BackendThread> for NetworkBac }; debouncer.sleep.as_mut().reset(Instant::now() + Duration::from_secs(86400)); loop { - while let Ok(tx) = registration_rx.try_recv() { - if tx.send(NetworkState::Init(instance.command_tx.clone())).is_ok() { - instance.subscribers.push(tx); - let _ = instance.send_all_networks(&nm).await; - let _ = instance.send_active_connection(&nm).await; - } - } tokio::select! { command = command_rx.recv() => { let Some(command) = command else { diff --git a/src/backend/window_manager_hypr.rs b/src/backend/window_manager_hypr.rs index 43f7625..195934b 100644 --- a/src/backend/window_manager_hypr.rs +++ b/src/backend/window_manager_hypr.rs @@ -1,10 +1,10 @@ use crate::LOGGER; use crate::backend::{BackendThread, SimpleBackendThread}; use crossbeam_channel::{Receiver, Sender}; -use hyprland::data::{Client, Monitor, Workspaces}; +use hyprland::data::{Client, CursorPosition, Monitor, Monitors, Workspaces}; use hyprland::event_listener::EventListener; use hyprland::prelude::*; -use hyprland::shared::{MonitorId, WorkspaceId, WorkspaceType}; +use hyprland::shared::{MonitorId, WorkspaceId}; use jmb_logger_rs::*; use std::io; use std::process::{Command, Output}; @@ -24,7 +24,6 @@ pub struct HyprlandComms { } impl SimpleBackendThread for HyprlandComms { - fn subscribers(&self) -> &Vec> { &self.subscribers } @@ -38,9 +37,7 @@ impl SimpleBackendThread for HyprlandComms { let inst_clone = instance.clone(); listener.add_workspace_changed_handler(move |workspace| { - if let WorkspaceType::Regular(name) = workspace.name - && let Ok(id) = name.parse::() - { + if let Ok(id) = workspace.name.parse::() { inst_clone .lock() .expect("Couldn't get Subscribers") @@ -50,26 +47,24 @@ impl SimpleBackendThread for HyprlandComms { let inst_clone = instance.clone(); listener.add_workspace_added_handler(move |workspace| { - if let WorkspaceType::Regular(name) = workspace.name { - //unpacks the name of the workspace - if let Ok(wss) = Workspaces::get() { - //gets all workspaces - if let Some(ws) = wss - .to_vec() - .iter() - .find(|ws| ws.id().unwrap() == workspace.id) - { - //find the current workspace Object from its id - if let Ok(id) = name.parse::() { - // parse the id as an int - inst_clone - .lock() - .expect("Couldn't get Subscribers") - .publish(WindowManagerEvent::WorkspaceAdded( - id, - ws.monitor_id.unwrap(), - )); - } + //unpacks the name of the workspace + if let Ok(wss) = Workspaces::get() { + //gets all workspaces + if let Some(ws) = wss + .to_vec() + .iter() + .find(|ws| ws.id().unwrap() == workspace.id().unwrap()) + { + //find the current workspace Object from its id + if let Ok(id) = workspace.name.parse::() { + // parse the id as an int + inst_clone + .lock() + .expect("Couldn't get Subscribers") + .publish(WindowManagerEvent::WorkspaceAdded( + id, + ws.monitor_id.unwrap(), + )); } } } @@ -77,9 +72,7 @@ impl SimpleBackendThread for HyprlandComms { let inst_clone = instance.clone(); listener.add_workspace_deleted_handler(move |workspace| { - if let WorkspaceType::Regular(name) = workspace.name - && let Ok(id) = name.parse::() - { + if let Ok(id) = workspace.name.parse::() { inst_clone .lock() .expect("Couldn't get Subscribers") @@ -92,7 +85,9 @@ impl SimpleBackendThread for HyprlandComms { inst_clone .lock() .expect("Couldn't get Subscribers") - .publish(WindowManagerEvent::WorkspaceChanged(evt.workspace_id)); + .publish(WindowManagerEvent::WorkspaceChanged( + evt.workspace.id().unwrap(), + )); }); let inst_clone = instance.clone(); @@ -106,15 +101,22 @@ impl SimpleBackendThread for HyprlandComms { let inst_clone = instance.clone(); listener.add_workspace_moved_handler(move |event| { - if let Some(monitor_id) = Self::monitor_id_from_workspace_id(event.id) { + if let Some(monitor_id) = + Self::monitor_id_from_workspace_id(event.workspace.id().unwrap()) + { inst_clone .lock() .expect("Couldn't get Subscribers") - .publish(WindowManagerEvent::WorkspaceRemoved(event.id)); + .publish(WindowManagerEvent::WorkspaceRemoved( + event.workspace.id().unwrap(), + )); inst_clone .lock() .expect("Couldn't get Subscribers") - .publish(WindowManagerEvent::WorkspaceAdded(event.id, monitor_id)); + .publish(WindowManagerEvent::WorkspaceAdded( + event.workspace.id().unwrap(), + monitor_id, + )); } }); @@ -160,7 +162,7 @@ impl HyprlandComms { None } - pub fn _current_monitor() -> MonitorId { + pub fn current_monitor() -> MonitorId { if let Ok(monitor) = Monitor::get_active() { return monitor.id; } @@ -197,4 +199,22 @@ impl HyprlandComms { .arg(format!("hyprctl dispatch \"{dispatcher}\"")) .output() } + + pub fn cursor_monitor() -> Option { + if let Ok(pos) = CursorPosition::get() + && let Ok(monitors) = Monitors::get() + { + for monitor in monitors { + if pos.x > monitor.x as i64 + && pos.x < (monitor.x as i64 + monitor.width as i64) + && pos.y > monitor.y as i64 + && pos.y < (monitor.y as i64 + monitor.height as i64) + { + return Some(monitor.id); + } + } + } + + None + } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..394c4be --- /dev/null +++ b/src/config.rs @@ -0,0 +1,59 @@ +use serde::Deserialize; +use std::fs; + +#[derive(Deserialize, Debug, Clone)] +pub(crate) struct Config { + idle_timeout: u64, + dim_timeout: u64, + lock_timeout: u64, + sleep_timeout: u64, +} + +impl Default for Config { + fn default() -> Self { + Self { + idle_timeout: 20, + dim_timeout: 60, + lock_timeout: 300, + sleep_timeout: 320, + } + } +} + +impl Config { + pub fn load() -> Config { + fs::read_to_string("jmb-bar.conf") + .ok() + .map(|s| toml::from_str::(&s).ok()) + .flatten() + .unwrap_or_default() + } + + pub fn idle_timeout(&self) -> u64 { + self.idle_timeout + } + + pub fn dim_timeout(&self) -> u64 { + self.dim_timeout + } + + pub fn lock_timeout(&self) -> u64 { + self.lock_timeout + } + + pub fn sleep_timeout(&self) -> u64 { + self.sleep_timeout + } + + pub fn dim_timeout_delta(&self) -> u64 { + self.dim_timeout - self.idle_timeout + } + + pub fn lock_timeout_delta(&self) -> u64 { + self.lock_timeout - self.dim_timeout_delta() + } + + pub fn sleep_timeout_delta(&self) -> u64 { + self.sleep_timeout - self.lock_timeout_delta() + } +} diff --git a/src/main.rs b/src/main.rs index 5d7cad7..cf9ec24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use crate::backend::Backend; mod backend; +mod config; mod css; mod ctl; mod ui; +use crate::config::Config; use glib::clone; use gtk::prelude::*; use gtk::{Application, CssProvider, gdk, gio}; diff --git a/src/ui.rs b/src/ui.rs index 691af0f..54e19e0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -39,7 +39,7 @@ pub fn create_bars(app: &Application, backend: Backend) { monitor, create_and_show_bar( app_clone.clone(), - &display, + display.clone(), monitor, action_centre.clone(), channels, @@ -61,11 +61,11 @@ pub fn create_bars(app: &Application, backend: Backend) { let action_centre = action_centre.clone(); let channels = backend_clone.subscribe_all(); let bars = bars.clone(); - let display = display.clone(); glib::idle_add_local_once(move || { + let display = Display::default().expect("Couldn't find GDK Display"); bars.lock().expect("Couldn't get active bars").insert( pos, - create_and_show_bar(app_clone, &display, pos, action_centre, channels), + create_and_show_bar(app_clone, display, pos, action_centre, channels), ); }); } @@ -74,7 +74,7 @@ pub fn create_bars(app: &Application, backend: Backend) { let mut ctl_channel = backend.subscribe(&[ChannelType::Control]); let ctl_rx = ctl_channel.take_ctl_rx().unwrap(); let app_clone = app.clone(); - let menu = RunMenu::create(app_clone); + let mut menu = RunMenu::create(app_clone); glib::timeout_add_local(Duration::from_millis(100), move || { while let Ok(cmd) = ctl_rx.try_recv() { match cmd { @@ -89,7 +89,7 @@ pub fn create_bars(app: &Application, backend: Backend) { fn create_and_show_bar( app: Application, - display: &Display, + display: Display, monitor_idx: u32, action_centre: ActionCentre, mut channels: BackendChannels, @@ -138,7 +138,7 @@ fn create_and_show_bar( let title_button: Option