1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-04-19 19:34:24 +02:00

Merge branch 'master' into feat/networkmanager

This commit is contained in:
Reinout Meliesie 2024-04-18 01:38:16 +02:00
commit b860f5b603
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
57 changed files with 2450 additions and 858 deletions

View file

@ -17,4 +17,5 @@ $SUDO apt-get update && $SUDO apt-get install --assume-yes \
libssl-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-layer-shell-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}
libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libluajit-5.1-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}

View file

@ -1,15 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used" />
<option name="command" value="clippy -- &#10;-W clippy::pedantic &#10;-W clippy::unwrap_used&#10;-A clippy::cast_possible_wrap&#10;-A clippy::cast_possible_truncation&#10;-A clippy::cast_sign_loss&#10;-A clippy::cast_precision_loss&#10;-A clippy::cast_lossless&#10;-A clippy::module_name_repetitions" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="false" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">

View file

@ -2,18 +2,17 @@
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package ironbar --bin ironbar" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs>
<env name="IRONBAR_LOG" value="info" />
<env name="IRONBAR_CONFIG" value="examples/test.corn" />
</envs>
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs>
<env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="debug" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">

404
Cargo.lock generated
View file

@ -283,18 +283,18 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -306,6 +306,9 @@ name = "bitflags"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
dependencies = [
"serde",
]
[[package]]
name = "block-buffer"
@ -331,6 +334,16 @@ dependencies = [
"log",
]
[[package]]
name = "bstr"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
@ -351,9 +364,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cairo-rs"
version = "0.18.3"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1"
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [
"bitflags 2.4.0",
"cairo-sys-rs",
@ -423,24 +436,22 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.35"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"pure-rust-locales",
"wasm-bindgen",
"windows-targets 0.52.0",
]
[[package]]
name = "clap"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
@ -460,9 +471,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -476,16 +487,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "color-eyre"
version = "0.6.3"
@ -628,50 +629,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "cxx"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote 1.0.35",
"scratch",
"syn 2.0.48",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
[[package]]
name = "cxxbridge-macro"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote 1.0.35",
"syn 2.0.48",
]
[[package]]
name = "darling"
version = "0.14.4"
@ -817,15 +774,6 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "encoding_rs"
version = "0.8.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-kinds"
version = "0.5.1"
@ -1270,7 +1218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5"
dependencies = [
"heck 0.4.1",
"proc-macro-crate 2.0.1",
"proc-macro-crate 2.0.0",
"proc-macro-error",
"proc-macro2",
"quote 1.0.35",
@ -1380,9 +1328,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
dependencies = [
"bytes",
"fnv",
@ -1390,19 +1338,13 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.2.6",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.1"
@ -1582,26 +1524,25 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.56"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cxx",
"cxx-build",
"cc",
]
[[package]]
@ -1626,16 +1567,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -1643,7 +1574,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.1",
"hashbrown",
"serde",
]
[[package]]
@ -1696,6 +1628,7 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
name = "ironbar"
version = "0.15.0-pre"
dependencies = [
"cairo-rs",
"cfg-if",
"chrono",
"clap",
@ -1704,13 +1637,14 @@ dependencies = [
"dirs",
"futures-lite 2.3.0",
"futures-signals",
"futures-util",
"glib",
"gtk",
"gtk-layer-shell",
"hyprland",
"indexmap 2.2.6",
"indexmap",
"libpulse-binding",
"lua-src",
"mlua",
"mpd-utils",
"mpris",
"nix 0.27.1",
@ -1733,7 +1667,6 @@ dependencies = [
"upower_dbus",
"walkdir",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
"zbus",
]
@ -1787,11 +1720,12 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libcorn"
version = "0.9.0"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07283e36c1e48784b5e24e27fd4f6deabd83ebfa05648316dcead505ecbab40f"
checksum = "48ada41708c215eb095c72db1ca41913c347f75d382f2fc9e8d1017a22b7671e"
dependencies = [
"cfg-if",
"indexmap",
"pest",
"pest_derive",
"serde",
@ -1844,15 +1778,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "link-cplusplus"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
dependencies = [
"cc",
]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@ -1867,9 +1792,9 @@ checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
name = "lock_api"
version = "0.4.9"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
@ -1884,6 +1809,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "lua-src"
version = "546.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2"
dependencies = [
"cc",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -1960,10 +1894,34 @@ dependencies = [
]
[[package]]
name = "mpd-utils"
version = "0.2.0"
name = "mlua"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7081a86d39a604868a671b0166febc1f31a4c4801d9436ab733f2664baabf8a4"
checksum = "868d02cb5eb97761bbf6bd6922c1c7a88b8ea252bbf43bd8350a0bf8497a1fc0"
dependencies = [
"bstr",
"mlua-sys",
"num-traits",
"once_cell",
"rustc-hash",
]
[[package]]
name = "mlua-sys"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2847b42764435201d8cbee1f517edb79c4cca4181877b90047587c89e1b7bce4"
dependencies = [
"cc",
"cfg-if",
"pkg-config",
]
[[package]]
name = "mpd-utils"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe50e71b3206a46eff95e96549aa60953dc072baffaa04b71415024f8f254d2"
dependencies = [
"futures",
"mpd_client",
@ -2257,15 +2215,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.7"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.2.16",
"redox_syscall 0.4.1",
"smallvec",
"windows-sys 0.45.0",
"windows-targets 0.48.0",
]
[[package]]
@ -2410,11 +2368,10 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
dependencies = [
"toml_datetime",
"toml_edit 0.20.2",
]
@ -2551,6 +2508,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@ -2608,13 +2574,12 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.12.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
dependencies = [
"base64 0.21.0",
"base64 0.22.0",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
@ -2637,7 +2602,6 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
@ -2650,13 +2614,14 @@ dependencies = [
[[package]]
name = "ron"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
"base64 0.21.0",
"bitflags 2.4.0",
"serde",
"serde_derive",
]
[[package]]
@ -2665,6 +2630,12 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -2703,13 +2674,20 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.21.0",
"base64 0.22.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustversion"
version = "1.0.12"
@ -2752,12 +2730,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "security-framework"
version = "2.8.2"
@ -2809,9 +2781,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
@ -2831,9 +2803,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
@ -2852,11 +2824,11 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.9.21"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 1.9.3",
"indexmap",
"itoa",
"ryu",
"serde",
@ -3110,27 +3082,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.0.4"
@ -3140,7 +3091,7 @@ dependencies = [
"cfg-expr",
"heck 0.4.1",
"pkg-config",
"toml",
"toml 0.7.6",
"version-compare",
]
@ -3170,15 +3121,6 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.58"
@ -3253,9 +3195,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@ -3267,6 +3209,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.5.5",
"tokio-macros",
"tracing",
"windows-sys 0.48.0",
]
@ -3318,10 +3261,22 @@ dependencies = [
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
name = "toml"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.9",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
@ -3332,11 +3287,11 @@ version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.2.6",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.5.10",
]
[[package]]
@ -3345,9 +3300,22 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.2.6",
"indexmap",
"toml_datetime",
"winnow",
"winnow 0.5.10",
]
[[package]]
name = "toml_edit"
version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.5",
]
[[package]]
@ -3517,12 +3485,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.0.4"
@ -3531,9 +3493,9 @@ checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
[[package]]
name = "universal-config"
version = "0.4.3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e41ae277cedae5c3a4d6dbe5d7ad5068cdbcd9e7005de28996cada89c28c776"
checksum = "6b01452126ed3a29e5d54cc9f036c541c2ceeb152fb9234fef8a1fc3054b26ca"
dependencies = [
"dirs",
"libcorn",
@ -3542,15 +3504,15 @@ dependencies = [
"serde_json",
"serde_yaml",
"thiserror",
"toml",
"toml 0.8.12",
"tracing",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "upower_dbus"
@ -3868,12 +3830,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.48.0",
"windows-targets 0.52.0",
]
[[package]]
@ -4099,10 +4061,19 @@ dependencies = [
]
[[package]]
name = "winreg"
version = "0.50.0"
name = "winnow"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
@ -4165,6 +4136,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"winapi",

View file

@ -11,6 +11,7 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features]
default = [
"cli",
"cairo",
"clipboard",
"clock",
"config+all",
@ -46,6 +47,8 @@ http = ["dep:reqwest"]
"config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["nix"]
clock = ["chrono"]
@ -71,7 +74,7 @@ upower = ["upower_dbus", "zbus", "futures-lite"]
volume = ["libpulse-binding"]
workspaces = ["futures-util"]
workspaces = ["futures-lite"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
@ -81,7 +84,7 @@ workspaces = ["futures-util"]
gtk = "0.18.1"
gtk-layer-shell = "0.8.0"
glib = "0.18.5"
tokio = { version = "1.36.0", features = [
tokio = { version = "1.37.0", features = [
"macros",
"rt-multi-thread",
"time",
@ -92,7 +95,7 @@ tokio = { version = "1.36.0", features = [
] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-error = { version = "0.2.0" , default-features = false }
tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.3"
@ -102,32 +105,36 @@ dirs = "5.0.1"
walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false }
wayland-client = "0.31.1"
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [
"calloop",
] }
universal-config = { version = "0.4.3", default_features = false }
universal-config = { version = "0.5.0", default_features = false }
ctrlc = "3.4.2"
cfg-if = "1.0.0"
# cli
clap = { version = "4.5.3", optional = true, features = ["derive"] }
clap = { version = "4.5.4", optional = true, features = ["derive"] }
# ipc
serde_json = { version = "1.0.114", optional = true }
# http
reqwest = { version = "0.12.2", optional = true }
reqwest = { version = "0.12.3", default_features = false, features = ["default-tls", "http2"], optional = true }
# cairo
lua-src = { version = "546.0.2", optional = true }
mlua = { version = "0.9.6", optional = true, features = ["luajit"] }
cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
# clock
chrono = { version = "0.4.35", optional = true, features = ["unstable-locales"] }
chrono = { version = "0.4.38", optional = true, default_features = false, features = ["clock", "unstable-locales"] }
# music
mpd-utils = { version = "0.2.0", optional = true }
mpd-utils = { version = "0.2.1", optional = true }
mpris = { version = "2.0.1", optional = true }
# networkmanager
@ -137,10 +144,9 @@ futures-signals = { version = "0.3.33", optional = true }
sysinfo = { version = "0.29.11", optional = true }
# tray
system-tray = { version = "0.2.0", optional = true }
system-tray = { version = "0.2.0", optional = true }
# upower
futures-lite = { version = "2.3.0", optional = true }
upower_dbus = { version = "0.3.2", optional = true }
# volume
@ -148,11 +154,11 @@ libpulse-binding = { version = "2.28.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.13", features = ["silent"], optional = true }
futures-util = { version = "0.3.30", optional = true }
hyprland = { version = "0.3.13", default_features = false, features = ["listener", "tokio", "silent"], optional = true }
# shared
futures-lite = { version = "2.3.0", optional = true } # workspaces, upower
regex = { version = "1.10.4", default-features = false, features = [
"std",
], optional = true } # music, sys_info
zbus = { version = "3.15.2", optional = true } # networkmanager, notifications, upower
zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # networkmanager, notifications, upower

View file

@ -49,8 +49,8 @@ dnf install libpulseaudio-devel
By default, all features are enabled for convenience. This can result in a significant compile time.
If you know you are not going to need all the features, you can compile with only the features you need.
As of `v0.10.0`, compiling with no features is about 33% faster.
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
As of `v0.15.0`, compiling with no features is about 50% faster.
On a 3800X, it takes about 45 seconds for no features and 90 seconds for all.
This difference is expected to increase as the bar develops.
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
@ -77,6 +77,7 @@ cargo build --release --no-default-features \
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | |
| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| focused | Enables the `focused` module. |
@ -84,6 +85,7 @@ cargo build --release --no-default-features \
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |
| music+mpd | Enables the `music` module with MPD support. |
| notifications | Enables the `notiications` module. |
| sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |

View file

@ -332,6 +332,7 @@ For information on the `Script` type, and embedding scripts in strings, see [her
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
| `disable_popup` | `boolean` | `false` | Prevents the popup from opening on-click for this widget. |
#### Appearance

View file

@ -79,7 +79,7 @@ Responds with `ok`.
### `get`
Gets an [ironvar](ironvars) value.
Gets an [ironvar](ironvars) value.
Responds with `ok_value` if the value exists, otherwise `error`.
@ -104,6 +104,20 @@ Responds with `ok`.
}
```
### list
Gets a list of all [ironvar](ironvars) values.
Responds with `ok_value`.
Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `.
```json
{
"type": "list"
}
```
### `load_css`
Loads an additional CSS stylesheet, with hot-reloading enabled.

View file

@ -20,9 +20,11 @@
## Custom
- [Power Menu](power-menu)
- [Weather](weather)
# Modules
- [Cairo](cairo)
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)

View file

@ -0,0 +1,468 @@
Creates a button on the bar which displays the current weather condition and temperature.
Clicking the button opens a popup with forecast information for the next few days.
Weather information is fetched from [wttr.in](https://wttr.in) via an external script.
You will need to set up the script to be run as a service.
![custom weather widget, with popup open](https://f.jstanger.dev/github/ironbar/custom-weather.png)
## Configuration
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "custom",
"class": "weather",
"bar": [
{
"type": "button",
"label": "#weather_current",
"on_click": "popup:toggle"
}
],
"popup": [
{
"type": "box",
"orientation": "vertical",
"widgets": [
{
"type": "label",
"name": "header",
"label": "Forecast"
},
{
"type": "box",
"widgets": [
{
"type": "box",
"name": "dates",
"orientation": "vertical",
"widgets": [
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_0"
},
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_1"
},
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_2"
}
]
},
{
"type": "box",
"name": "temps",
"orientation": "vertical",
"widgets": [
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_0"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_0"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_0"
}
]
},
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_1"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_1"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_1"
}
]
},
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_2"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_2"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_2"
}
]
}
]
}
]
}
]
}
]
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "custom"
class = "weather"
[[end.bar]]
type = "button"
label = "#weather_current"
on_click = "popup:toggle"
[[end.popup]]
type = "box"
orientation = "vertical"
[[end.popup.widgets]]
type = "label"
name = "header"
label = "Forecast"
[[end.popup.widgets]]
type = "box"
[[end.popup.widgets.widgets]]
type = "box"
name = "dates"
orientation = "vertical"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_0"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_1"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_2"
[[end.popup.widgets.widgets]]
type = "box"
name = "temps"
orientation = "vertical"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_0"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_0"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_0"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_1"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_1"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_1"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_2"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_2"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_2"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: custom
class: weather
bar:
- type: button
label: '#weather_current'
on_click: popup:toggle
popup:
- type: box
orientation: vertical
widgets:
- type: label
name: header
label: Forecast
- type: box
widgets:
- type: box
name: dates
orientation: vertical
widgets:
- type: label
class: weather-date
label: '#weather_date_0'
- type: label
class: weather-date
label: '#weather_date_1'
- type: label
class: weather-date
label: '#weather_date_2'
- type: box
name: temps
orientation: vertical
widgets:
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_0'
- type: label
class: weather-avg
label: ' #weather_avg_0'
- type: label
class: weather-low
label: ' #weather_low_0'
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_1'
- type: label
class: weather-avg
label: ' #weather_avg_1'
- type: label
class: weather-low
label: ' #weather_low_1'
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_2'
- type: label
class: weather-avg
label: ' #weather_avg_2'
- type: label
class: weather-low
label: ' #weather_low_2'
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$weather = {
type = "custom"
class = "weather"
bar = [ { type = "button" label = "#weather_current" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Forecast" }
{
type = "box"
widgets = [
{ type = "box" name="dates" orientation = "vertical" widgets = [
{ type = "label" class="weather-date" label = "#weather_date_0" }
{ type = "label" class="weather-date" label = "#weather_date_1" }
{ type = "label" class="weather-date" label = "#weather_date_2" }
]}
{ type = "box" name="temps" orientation = "vertical" widgets = [
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_0" }
{ type = "label" class="weather-avg" label = " #weather_avg_0" }
{ type = "label" class="weather-low" label = " #weather_low_0" }
]
}
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_1" }
{ type = "label" class="weather-avg" label = " #weather_avg_1" }
{ type = "label" class="weather-low" label = " #weather_low_1" }
]
}
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_2" }
{ type = "label" class="weather-avg" label = " #weather_avg_2" }
{ type = "label" class="weather-low" label = " #weather_low_2" }
]
}
] }
]
}
]
} ]
}
} in {
end = [ $weather ]
}
```
</details>
## Script
Run the following script on a timer. Ensure to fill out your city name.
```js
#!/usr/bin/env zx
const location = "Canterbury";
// JS uses Sunday as first day
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
// bar logic
const data = await fetch(`https://wttr.in/${location}?format=%c %t|%m %t|%S|%s`)
.then(r => r.text());
const [day, night, sunrise, sunset] = data.replaceAll("+", "").split("|");
const [sunriseH, sunriseM, sunriseS] = sunrise.split(":");
const [sunsetH, sunsetM, sunsetS] = sunset.split(":");
const currentTime = new Date();
const sunriseTime = new Date(currentTime);
sunriseTime.setHours(sunriseH);
sunriseTime.setMinutes(sunriseM);
sunriseTime.setSeconds(sunriseS);
const sunsetTime = new Date(currentTime);
sunsetTime.setHours(sunsetH);
sunsetTime.setMinutes(sunsetM);
sunsetTime.setSeconds(sunsetS);
let value = day;
if(currentTime < sunriseTime || currentTime > sunsetTime) value = night;
await $`ironbar set weather_current ${value}`;
// popup logic
const forecast = await fetch(`https://wttr.in/${location}?format=j1`).then(r => r.json());
for (const i in forecast.weather) {
const report = forecast.weather[i];
const day = weekday[new Date(report.date).getDay()];
await $`ironbar set weather_date_${i} ${day}`;
await $`ironbar set weather_avg_${i} ${report.avgtempC.padStart(2, "0")}`;
await $`ironbar set weather_high_${i} ${report.maxtempC.padStart(2, "0")}`;
await $`ironbar set weather_low_${i} ${report.mintempC.padStart(2, "0")}`;
}
```
## Styling
```css
.popup-weather #header {
font-size: 1.8em;
padding-bottom: 0.4em;
margin-bottom: 0.6em;
border-bottom: 1px solid @color-border;
}
.popup-weather .weather-date {
font-size: 1.5em;
padding-right: 1em;
}
.popup-weather .weather-avg {
margin-left: 0.5em;
margin-right: 0.5em;
}
/*
this is a hack to align the different font sizes on left/right
you may need to adjust for different fonts
*/
.popup-weather #temps label {
padding-top: 0.2em;
margin-bottom: 0.7em;
}
```

215
docs/modules/Cairo.md Normal file
View file

@ -0,0 +1,215 @@
Allows you to render custom content using the Lua and the Cairo drawing library.
This is an advanced feature which provides a powerful escape hatch, allowing you to fetch data and render anything
using an embedded scripting environment.
Scripts are automatically hot-reloaded.
> [!NOTE]
> The Lua engine uses LuaJIT 5.1, and requires the use of a library called `lgi`.
> Ensure you have the correct lua-lgi package installed.
![Circle clock](https://f.jstanger.dev/github/ironbar/cairo-clock.png)
## Configuration
> Type: `cairo`
| Name | Type | Default | Description |
|--------------------|-----------|---------|----------------------------------------------------|
| `path` | `string` | `null` | The path to the Lua script to load. |
| `frequency` | `float` | `200` | The number of milliseconds between each draw call. |
| `width` | `integer` | `42` | The canvas width in pixels. |
| `height` | `integer` | `42` | The canvas height in pixels. |
<details>
<summary>JSON</summary>
```json
{
"center": [
{
"type": "cairo",
"path": ".config/ironbar/clock.lua",
"frequency": 100,
"width": 300,
"height": 300
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[center]]
type = "cairo"
path = ".config/ironbar/clock.lua"
frequency = 100
width = 300
height = 300
```
</details>
<details>
<summary>YAML</summary>
```yaml
center:
- type: cairo
path: .config/ironbar/clock.lua
frequency: 100
width: 300
height: 300
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$config_dir = ".config/ironbar"
$cairo = {
type = "cairo"
path = "$config_dir/clock.lua"
frequency = 100
width = 300
height = 300
}
} in {
center = [ $cairo ]
}
```
</details>
### Script
Every script must contain a function called `draw`.
This takes a single parameter, which is the Cairo context.
Outside of this, you can do whatever you like.
The full lua `stdlib` is available, and you can load in additional system packages as desired.
The most basic example, which draws a red square, can be seen below:
```lua
function draw(cr)
cr:set_source_rgb(1.0, 0.0, 0.0)
cr:paint()
end
```
A longer example, used to create the clock in the image at the top of the page, is shown below:
<details>
<summary>Circle clock</summary>
```lua
function get_ms()
local ms = tostring(io.popen('date +%s%3N'):read('a')):sub(-4, 9999)
return tonumber(ms) / 1000
end
function draw(cr)
local center_x = 150
local center_y = 150
local radius = 130
local date_table = os.date("*t")
local hours = date_table["hour"]
local minutes = date_table["min"]
local seconds = date_table["sec"]
local ms = get_ms()
local label_seconds = seconds
seconds = seconds + ms
local hours_str = tostring(hours)
if string.len(hours_str) == 1 then
hours_str = "0" .. hours_str
end
local minutes_str = tostring(minutes)
if string.len(minutes_str) == 1 then
minutes_str = "0" .. minutes_str
end
local seconds_str = tostring(label_seconds)
if string.len(seconds_str) == 1 then
seconds_str = "0" .. seconds_str
end
local font_size = radius / 5.5
cr:set_source_rgb(1.0, 1.0, 1.0)
cr:move_to(center_x - font_size * 2.5 + 10, center_y + font_size / 2.5)
cr:set_font_size(font_size)
cr:show_text(hours_str .. ':' .. minutes_str .. ':' .. seconds_str)
cr:stroke()
if hours > 12 then
hours = hours - 12
end
local line_width = radius / 8
local start_angle = -math.pi / 2
local end_angle = start_angle + ((hours + minutes / 60 + seconds / 3600) / 12) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius, start_angle, end_angle)
cr:stroke()
end_angle = start_angle + ((minutes + seconds / 60) / 60) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius * 0.8, start_angle, end_angle)
cr:stroke()
if seconds == 0 then
seconds = 60
end
end_angle = start_angle + (seconds / 60) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius * 0.6, start_angle, end_angle)
cr:stroke()
return 0
end
```
</details>
> [!TIP]
> The C documentation for the Cairo context interface can be found [here](https://www.cairographics.org/manual/cairo-cairo-t.html).
> The Lua interface provides a slightly friendlier API which restructures things slightly.
> The `cairo_` prefix is dropped, and the `cairo_t *cr` parameters are replaced with a namespaced call.
> For example, `cairo_paint (cairo_t *cr)` becomes `cr:paint()`
> [!TIP]
> Ironbar's Cairo module has similar functionality to the popular Conky program.
> You can often re-use scripts with little work.
### Initialization
You can optionally create an `init.lua` file in your config directory.
Any code in here will be executed once, on bar startup.
As variables and functions are global by default in Lua,
this provides a mechanism for sharing code between multiple modules.
## Styling
| Selector | Description |
|----------|-------------------------|
| `.cairo` | Cairo widget container. |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -13,6 +13,7 @@ Clicking on the widget opens a popup with the time and a calendar.
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Pango markup is supported. |
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. Pango markup is supported. |
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the time on the clock button. |
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>

View file

@ -1,6 +1,11 @@
Allows you to compose custom modules consisting of multiple widgets, including popups.
Allows you to compose custom modules consisting of multiple modules and widgets, including popups.
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
The module provides a set of utility widgets, such as containers, labels and buttons.
In addition to these, you can also add any native module.
Paired with the other custom modules such as Cairo,
this provides a powerful declarative interface for constructing your own interfaces.
If you only intend to run a single script, prefer the [script](script) module,
or [label](label) if you only need a single text label.
@ -13,6 +18,11 @@ or [label](label) if you only need a single text label.
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
It is well worth looking at the examples.
| Name | Type | Default | Description |
|---------|------------------------|------------|------------------------------------------|
| `bar` | `(Module or Widget)[]` | `[]` | Modules and widgets to add to the bar. |
| `popup` | `(Module or Widget)[]` | `null` | Modules and widgets to add to the popup. |
### `Widget`
There are many widget types, each with their own config options.
@ -36,7 +46,7 @@ A container to place nested widgets inside.
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this box. |
#### Label
@ -47,6 +57,7 @@ A text label. Pango markup is supported.
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the label. |
#### Button
@ -54,10 +65,12 @@ A clickable button, which can run a command when clicked.
> Type `button`
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|--------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. Ignored if `widgets` is set. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this button. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the button. |
#### Image
@ -197,6 +210,7 @@ to help get your head around what's going on:
<button class="power-btn" label="" on_click="!reboot" />
</box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
<clock disable_popup="true" />
</box>
</popup>
</custom>
@ -252,6 +266,10 @@ to help get your head around what's going on:
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
},
{
"type": "clock",
"disable_popup": true
}
]
}
@ -309,6 +327,10 @@ type = 'button'
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
[[end.popup.widgets]]
type = 'clock'
disable_popup = true
```
</details>
@ -345,6 +367,8 @@ end:
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
- type: clock
disable_popup: true
type: custom
```
@ -370,6 +394,7 @@ let {
]
}
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
{ type = "clock" disable_popup = true }
]
}

View file

@ -19,6 +19,8 @@ Pango markup is supported.
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. |
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). |
<details>
<summary>JSON</summary>

24
flake.lock generated
View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1708794349,
"narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=",
"lastModified": 1711681752,
"narHash": "sha256-LEg6/dmEFxx6Ygti5DO9MOhGNpyB7zdxdWtzv/FCTXk=",
"owner": "ipetkov",
"repo": "crane",
"rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa",
"rev": "ada0fb4dcce4561acb1eb17c59b7306d9d4a95f3",
"type": "github"
},
"original": {
@ -58,11 +58,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709200309,
"narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=",
"lastModified": 1711715736,
"narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811",
"rev": "807c549feabce7eddbf259dbdcec9e0600a0660d",
"type": "github"
},
"original": {
@ -72,11 +72,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1709150264,
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
@ -102,11 +102,11 @@
]
},
"locked": {
"lastModified": 1709172595,
"narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=",
"lastModified": 1711851236,
"narHash": "sha256-EJ03x3N9ihhonAttkaCrqxb0djDq3URCuDpmVPbNZhA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f",
"rev": "f258266af947599e8069df1c2e933189270f143a",
"type": "github"
},
"original": {

374
flake.nix
View file

@ -1,208 +1,222 @@
{
description = "Nix Flake for ironbar";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
};
outputs = {
self,
nixpkgs,
rust-overlay,
crane,
naersk,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-linux"
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
self.overlays.default
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
in {
overlays.default = final: prev: let
rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
outputs = { self, nixpkgs, rust-overlay, crane, naersk, ... }:
let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [ self.overlays.default rust-overlay.overlays.default ];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
};
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in
if builder == "crane"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
overlays.default = final: prev:
let
rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
};
packages = genSystems (
system: let
pkgs = pkgsFor system;
in
(self.overlays.default pkgs pkgs)
// {
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date="
+ (mkDate (self.lastModifiedDate or "19700101")) + "_"
+ (self.shortRev or "dirty");
in if builder == "crane" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
};
};
packages = genSystems (system:
let pkgs = pkgsFor system;
in (self.overlays.default pkgs pkgs) // {
default = self.packages.${system}.ironbar;
}
);
apps = genSystems (system: let
pkgs = pkgsFor system;
in {
default = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
});
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
];
});
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = {
config,
lib,
pkgs,
...
}: let
cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json {};
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [];
description = "The features to be used.";
};
};
config = let
pkg = cfg.package.override {features = cfg.features;};
in
lib.mkIf cfg.enable {
home.packages = [pkg];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
};
apps = genSystems (system:
let pkgs = pkgsFor system;
in rec {
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
default = ironbar;
});
devShells = genSystems (system:
let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
luajit
luajitPackages.lgi
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = { config, lib, pkgs, ... }:
let
cfg = config.programs.ironbar;
defaultIronbarPackage =
self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json { };
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ ];
description = "The features to be used.";
};
};
config = let pkg = cfg.package.override { features = cfg.features; };
in lib.mkIf cfg.enable {
home.packages = [ pkg ];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" =
lib.mkIf (cfg.style != "") { text = cfg.style; };
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = [ "graphical-session.target" ];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = with config.wayland.windowManager; [
(lib.mkIf hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf sway.systemd.enable "sway-session.target")
(lib.mkIf river.systemd.enable "river-session.target")
];
};
};
};
};
};
nixConfig = {
extra-substituters = ["https://jakestanger.cachix.org"];
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
extra-substituters = [ "https://cache.garnix.io" ];
extra-trusted-public-keys =
[ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ];
};
}

4
lua/draw.lua Normal file
View file

@ -0,0 +1,4 @@
function(id, ptr)
local cr = __lgi_core.record.new(cairo.Context, ptr)
_G['__draw_' .. id](cr)
end

4
lua/init.lua Normal file
View file

@ -0,0 +1,4 @@
local lgi = require('lgi')
cairo = lgi.cairo
__lgi_core = require('lgi.core')

View file

@ -14,6 +14,8 @@
libxkbcommon,
libpulseaudio,
openssl,
luajit,
luajitPackages,
pkg-config,
hicolor-icon-theme,
rustPlatform,
@ -23,74 +25,105 @@
builderName ? "nix",
builder ? {},
}: let
hasFeature = f: features == [ ] || builtins.elem f features;
basePkg = rec {
inherit version;
pname = "ironbar";
src = builtins.path {
name = "ironbar";
path = lib.cleanSource ../.;
};
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon libpulseaudio openssl];
propagatedBuildInputs = [
gtk3
nativeBuildInputs = [
pkg-config
wrapGAppsHook
gobject-introspection
];
buildInputs = [
gtk3
gdk-pixbuf
glib
gtk-layer-shell
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon ]
++ (if hasFeature "http" then [ openssl ] else [])
++ (if hasFeature "volume" then [ libpulseaudio ] else [])
++ (if hasFeature "cairo" then [ luajit ] else []);
propagatedBuildInputs = [ gtk3 ];
lgi = luajitPackages.lgi;
gappsWrapperArgs = ''
# Thumbnailers
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
--prefix XDG_DATA_DIRS : "${librsvg}/share"
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
# gtk-launch
--suffix PATH : "${lib.makeBinPath [ gtk3 ]}"
''
+ (if hasFeature "cairo" then ''
--prefix LUA_PATH : "./?.lua;${lgi}/share/lua/5.1/?.lua;${lgi}/share/lua/5.1/?/init.lua;${luajit}/share/lua/5.1/\?.lua;${luajit}/share/lua/5.1/?/init.lua"
--prefix LUA_CPATH : "./?.so;${lgi}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/loadall.so"
'' else "");
preFixup = ''
gappsWrapperArgs+=(
# Thumbnailers
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
--prefix XDG_DATA_DIRS : "${librsvg}/share"
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
${gappsWrapperArgs}
)
'';
passthru = {
updateScript = gnome.updateScript {
packageName = pname;
attrPath = "gnome.${pname}";
};
};
meta = with lib; {
homepage = "https://github.com/JakeStanger/ironbar";
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
description =
"Customisable gtk-layer-shell wlroots/sway bar written in rust.";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "ironbar";
};
};
flags = let
noDefault =
if features == []
then ""
else "--no-default-features";
featuresStr =
if features == []
then ""
else ''-F "${builtins.concatStringsSep "," features}"'';
in [noDefault featuresStr];
in
if builderName == "naersk"
then
builder.buildPackage (basePkg
// {
cargoOptions = old: old ++ flags;
})
else if builderName == "crane"
then
builder.buildPackage (basePkg
// {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg
// {
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})
noDefault = if features == [ ] then "" else "--no-default-features";
featuresStr = if features == [ ] then
""
else
''-F "${builtins.concatStringsSep "," features}"'';
in [ noDefault featuresStr ];
in if builderName == "naersk" then
builder.buildPackage (basePkg // { cargoBuildOptions = old: old ++ flags; })
else if builderName == "crane" then
builder.buildPackage (basePkg // {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg // {
buildNoDefaultFeatures = features != [ ];
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock { lockFile = ../Cargo.lock; };
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" =
"sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})

View file

@ -9,6 +9,9 @@ pkgs.mkShell {
gtk-layer-shell
gcc
openssl
libpulseaudio
luajit
luajitPackages.lgi
];
nativeBuildInputs = with pkgs; [

View file

@ -1,7 +1,5 @@
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
use crate::popup::Popup;
use crate::Ironbar;
use color_eyre::Result;
@ -350,57 +348,10 @@ fn add_modules(
ironbar: &Rc<Ironbar>,
popup: &Rc<Popup>,
) -> Result<()> {
let orientation = info.bar_position.orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("common config to exist");
let widget_parts = create_module(
*$module,
$id,
ironbar.clone(),
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);
content.add(&container);
}};
}
let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
for config in modules {
let id = Ironbar::unique_id();
match config {
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
#[cfg(feature = "focused")]
ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Label(mut module) => add_module!(module, id),
#[cfg(feature = "launcher")]
ModuleConfig::Launcher(mut module) => add_module!(module, id),
#[cfg(feature = "music")]
ModuleConfig::Music(mut module) => add_module!(module, id),
#[cfg(feature = "networkmanager")]
ModuleConfig::NetworkManager(mut module) => add_module!(module, id),
#[cfg(feature = "notifications")]
ModuleConfig::Notifications(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
#[cfg(feature = "tray")]
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "volume")]
ModuleConfig::Volume(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}
config.create(&module_factory, content, info)?;
}
Ok(())

View file

@ -1,4 +1,4 @@
use crate::{await_sync, register_client};
use crate::{await_sync, register_fallible_client};
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use std::fmt::{Debug, Display, Formatter};
@ -141,4 +141,4 @@ pub trait WorkspaceClient: Debug + Send + Sync {
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}
register_client!(dyn WorkspaceClient, workspaces);
register_fallible_client!(dyn WorkspaceClient, workspaces);

View file

@ -1,7 +1,7 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn};
use color_eyre::{Report, Result};
use futures_util::StreamExt;
use futures_lite::StreamExt;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender};

41
src/clients/lua.rs Normal file
View file

@ -0,0 +1,41 @@
use mlua::Lua;
use std::ops::Deref;
use std::path::Path;
use tracing::{debug, error};
/// Wrapper around Lua instance
/// to create a singleton and handle initialization.
#[derive(Debug)]
pub struct LuaEngine {
lua: Lua,
}
impl LuaEngine {
pub fn new(config_dir: &Path) -> Self {
let lua = unsafe { Lua::unsafe_new() };
let user_init = config_dir.join("init.lua");
if user_init.exists() {
debug!("loading user init script");
if let Err(err) = lua.load(user_init).exec() {
error!("{err:?}");
}
}
debug!("loading internal init script");
if let Err(err) = lua.load(include_str!("../../lua/init.lua")).exec() {
error!("{err:?}");
}
Self { lua }
}
}
impl Deref for LuaEngine {
type Target = Lua;
fn deref(&self) -> &Self::Target {
&self.lua
}
}

View file

@ -1,10 +1,15 @@
use crate::Ironbar;
use crate::{await_sync, Ironbar};
use color_eyre::Result;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "networkmanager")]
@ -28,6 +33,8 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "networkmanager")]
@ -42,6 +49,8 @@ pub struct Clients {
volume: Option<Arc<volume::Client>>,
}
pub type ClientResult<T> = Result<Arc<T>>;
impl Clients {
pub(crate) fn new() -> Self {
Self::default()
@ -63,12 +72,23 @@ impl Clients {
}
#[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> Arc<dyn compositor::WorkspaceClient> {
// TODO: Error handling here isn't great - should throw a user-friendly error & exit
self.workspaces
.get_or_insert_with(|| {
compositor::Compositor::create_workspace_client().expect("to be valid compositor")
})
pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
let client = match &self.workspaces {
Some(workspaces) => workspaces.clone(),
None => {
let client = compositor::Compositor::create_workspace_client()?;
self.workspaces.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
.get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
.clone()
}
@ -88,29 +108,35 @@ impl Clients {
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> Arc<swaync::Client> {
self.notifications
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async { swaync::Client::new().await }))
})
.clone()
pub fn notifications(&mut self) -> ClientResult<swaync::Client> {
let client = match &self.notifications {
Some(client) => client.clone(),
None => {
let client = await_sync(async { swaync::Client::new().await })?;
let client = Arc::new(client);
self.notifications.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<tray::Client> {
// TODO: Error handling here isn't great - should throw a user-friendly error
self.tray
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async {
let service_name =
format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
pub fn tray(&mut self) -> ClientResult<tray::Client> {
let client = match &self.tray {
Some(client) => client.clone(),
None => {
let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
tray::Client::new(&service_name)
.await
.expect("to be able to start client")
}))
})
.clone()
let client = await_sync(async { tray::Client::new(&service_name).await })?;
let client = Arc::new(client);
self.tray.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "upower")]
@ -137,6 +163,14 @@ pub trait ProvidesClient<T: ?Sized> {
fn provide(&self) -> Arc<T>;
}
/// Types implementing this trait
/// indicate that they provide a singleton client instance of type `T`,
/// which may fail to be created.
pub trait ProvidesFallibleClient<T: ?Sized> {
/// Returns a singleton client instance of type `T`.
fn try_provide(&self) -> ClientResult<T>;
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
@ -159,3 +193,26 @@ macro_rules! register_client {
}
};
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
/// The implementation calls `$method` (second argument)
/// on the `Clients` struct to obtain the client instance.
///
/// # Example
/// `register_client!(Client, clipboard);`
#[macro_export]
macro_rules! register_fallible_client {
($ty:ty, $method:ident) => {
impl<TSend, TReceive> $crate::clients::ProvidesFallibleClient<$ty>
for $crate::modules::WidgetContext<TSend, TReceive>
where
TSend: Clone,
{
fn try_provide(&self) -> color_eyre::Result<std::sync::Arc<$ty>> {
self.ironbar.clients.borrow_mut().$method()
}
}
};
}

View file

@ -1,6 +1,6 @@
mod dbus;
use crate::{register_client, send, spawn};
use crate::{register_fallible_client, send, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
@ -24,9 +24,9 @@ type GetSubscribeData = (bool, bool, u32, bool);
impl From<GetSubscribeData> for Event {
fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
Self {
count,
dnd,
cc_open,
count,
inhibited,
}
}
@ -40,15 +40,13 @@ pub struct Client {
}
impl Client {
pub async fn new() -> Self {
let dbus = Box::pin(zbus::Connection::session())
.await
.expect("failed to create connection to system bus");
pub async fn new() -> Result<Self> {
let dbus = Box::pin(zbus::Connection::session()).await?;
let proxy = SwayNcProxy::new(&dbus).await.unwrap();
let proxy = SwayNcProxy::new(&dbus).await?;
let (tx, rx) = broadcast::channel(8);
let mut stream = proxy.receive_subscribe_v2().await.unwrap();
let mut stream = proxy.receive_subscribe_v2().await?;
{
let tx = tx.clone();
@ -62,7 +60,7 @@ impl Client {
});
}
Self { proxy, tx, _rx: rx }
Ok(Self { proxy, tx, _rx: rx })
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
@ -85,4 +83,4 @@ impl Client {
}
}
register_client!(Client, notifications);
register_fallible_client!(Client, notifications);

View file

@ -1,4 +1,4 @@
use crate::register_client;
use crate::register_fallible_client;
pub use system_tray::client::Client;
register_client!(Client, tray);
register_fallible_client!(Client, tray);

View file

@ -27,6 +27,8 @@ pub struct CommonConfig {
pub on_mouse_exit: Option<ScriptInput>,
pub tooltip: Option<String>,
#[serde(default)]
pub disable_popup: bool,
}
#[derive(Debug, Deserialize, Clone)]
@ -38,6 +40,34 @@ pub enum TransitionType {
SlideEnd,
}
#[derive(Debug, Default, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ModuleOrientation {
#[default]
#[serde(alias = "h")]
Horizontal,
#[serde(alias = "v")]
Vertical,
}
impl ModuleOrientation {
pub const fn to_angle(self) -> f64 {
match self {
Self::Horizontal => 0.0,
Self::Vertical => 90.0,
}
}
}
impl From<ModuleOrientation> for Orientation {
fn from(o: ModuleOrientation) -> Self {
match o {
ModuleOrientation::Horizontal => Self::Horizontal,
ModuleOrientation::Vertical => Self::Vertical,
}
}
}
impl TransitionType {
pub const fn to_revealer_transition_type(
&self,

View file

@ -2,6 +2,8 @@ mod common;
mod r#impl;
mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
@ -29,16 +31,21 @@ use crate::modules::upower::UpowerModule;
use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
use cfg_if::cfg_if;
use color_eyre::Result;
use serde::Deserialize;
use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType};
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
@ -68,6 +75,51 @@ pub enum ModuleConfig {
Workspaces(Box<WorkspacesModule>),
}
impl ModuleConfig {
pub fn create(
self,
module_factory: &AnyModuleFactory,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()> {
macro_rules! create {
($module:expr) => {
module_factory.create(*$module, container, info)
};
}
match self {
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]
Self::Clock(module) => create!(module),
Self::Custom(module) => create!(module),
#[cfg(feature = "focused")]
Self::Focused(module) => create!(module),
Self::Label(module) => create!(module),
#[cfg(feature = "launcher")]
Self::Launcher(module) => create!(module),
#[cfg(feature = "music")]
Self::Music(module) => create!(module),
#[cfg(feature = "notifications")]
Self::Notifications(module) => create!(module),
Self::Script(module) => create!(module),
#[cfg(feature = "sys_info")]
Self::SysInfo(module) => create!(module),
#[cfg(feature = "tray")]
Self::Tray(module) => create!(module),
#[cfg(feature = "upower")]
Self::Upower(module) => create!(module),
#[cfg(feature = "volume")]
Self::Volume(module) => create!(module),
#[cfg(feature = "workspaces")]
Self::Workspaces(module) => create!(module),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub enum BarEntryConfig {
Single(BarConfig),

View file

@ -32,6 +32,9 @@ pub enum Command {
key: Box<str>,
},
/// Gets the current value of all `ironvar`s.
List,
/// Load an additional CSS stylesheet.
/// The sheet is automatically hot-reloaded.
LoadCss {

View file

@ -129,7 +129,7 @@ impl Ipc {
ironbar.reload_config();
for output in outputs {
match crate::load_output_bars(ironbar, application, output) {
match crate::load_output_bars(ironbar, application, &output) {
Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"),
}
@ -153,6 +153,20 @@ impl Ipc {
None => Response::error("Variable not found"),
}
}
Command::List => {
let variable_manager = Ironbar::variable_manager();
let mut values = read_lock!(variable_manager)
.get_all()
.iter()
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
.collect::<Vec<_>>();
values.sort();
let value = values.join("\n");
Response::OkValue { value }
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
@ -172,7 +186,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
@ -209,7 +223,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)

View file

@ -46,6 +46,10 @@ impl VariableManager {
self.variables.get(key).and_then(IronVar::get)
}
pub fn get_all(&self) -> &HashMap<Box<str>, IronVar> {
&self.variables
}
/// Subscribes to an `ironvar`, creating it if it does not exist.
/// Any time the var is set, its value is sent on the channel.
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
@ -66,7 +70,7 @@ impl VariableManager {
/// Ironbar dynamic variable representation.
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
#[derive(Debug)]
struct IronVar {
pub struct IronVar {
value: Option<String>,
tx: broadcast::Sender<Option<String>>,
_rx: broadcast::Receiver<Option<String>>,
@ -82,7 +86,7 @@ impl IronVar {
/// Gets the current variable value.
/// Prefer to subscribe to changes where possible.
fn get(&self) -> Option<String> {
pub fn get(&self) -> Option<String> {
self.value.clone()
}

View file

@ -1,3 +1,31 @@
/// Provides implementations of methods required by the `Module` trait
/// which cannot be included as part of the trait.
///
/// This removes the need to add the same boilerplate method definitions
/// to every module implementation.
///
/// # Usage:
///
/// ```rs
/// impl Module for ClockModule {
/// type SendMessage = DateTime<Local>;
/// type ReceiveMessage = ();
///
/// module_impl!("clock");
/// }
#[macro_export]
macro_rules! module_impl {
($name:literal) => {
fn name() -> &'static str {
$name
}
fn take_common(&mut self) -> $crate::config::CommonConfig {
self.common.take().unwrap_or_default()
}
};
}
/// Sends a message on an asynchronous `Sender` using `send()`
/// Panics if the message cannot be sent.
///

View file

@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>,
config_dir: PathBuf,
}
impl Ironbar {
fn new() -> Self {
let (config, config_dir) = load_config();
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
config: Rc::new(RefCell::new(load_config())),
config: Rc::new(RefCell::new(config)),
config_dir,
}
}
@ -192,7 +196,7 @@ impl Ironbar {
while let Ok(event) = rx_outputs.recv().await {
match event.event_type {
OutputEventType::New => {
match load_output_bars(&instance, &app, event.output) {
match load_output_bars(&instance, &app, &event.output) {
Ok(mut new_bars) => {
instance.bars.borrow_mut().append(&mut new_bars);
}
@ -260,7 +264,7 @@ impl Ironbar {
/// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")]
fn reload_config(&self) {
self.config.replace(load_config());
self.config.replace(load_config().0);
}
}
@ -270,20 +274,37 @@ fn start_ironbar() {
}
/// Loads the config file from disk.
fn load_config() -> Config {
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
fn load_config() -> (Config, PathBuf) {
let config_path = env::var("IRONBAR_CONFIG");
Config::default()
});
let (config, directory) = if let Ok(config_path) = config_path {
let path = PathBuf::from(config_path);
(
ConfigLoader::load(&path),
path.parent()
.map(PathBuf::from)
.ok_or_else(|| Report::msg("Specified path has no parent")),
)
} else {
let config_loader = ConfigLoader::new("ironbar");
(
config_loader.find_and_load(),
config_loader.config_dir().map_err(Report::new),
)
};
let mut config = config.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
let directory = directory
.and_then(|dir| dir.canonicalize().map_err(Report::new))
.unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
debug!("Loaded config file");
@ -297,7 +318,7 @@ fn load_config() -> Config {
}
}
config
(config, directory)
}
/// Gets the GDK `Display` instance.
@ -316,7 +337,7 @@ fn get_display() -> Display {
fn load_output_bars(
ironbar: &Rc<Ironbar>,
app: &Application,
output: OutputInfo,
output: &OutputInfo,
) -> Result<Vec<Bar>> {
let Some(monitor_name) = &output.name else {
return Err(Report::msg("Output missing monitor name"));

198
src/modules/cairo.rs Normal file
View file

@ -0,0 +1,198 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, spawn, try_send};
use cairo::{Format, ImageSurface};
use glib::translate::IntoGlibPtr;
use glib::Propagation;
use gtk::prelude::*;
use gtk::DrawingArea;
use mlua::{Error, Function, LightUserData};
use notify::event::ModifyKind;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::time::sleep;
use tracing::{debug, error};
#[derive(Debug, Clone, Deserialize)]
pub struct CairoModule {
path: PathBuf,
#[serde(default = "default_frequency")]
frequency: u64,
#[serde(default = "default_size")]
width: u32,
#[serde(default = "default_size")]
height: u32,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_size() -> u32 {
42
}
const fn default_frequency() -> u64 {
200
}
impl Module<gtk::Box> for CairoModule {
type SendMessage = ();
type ReceiveMessage = ();
module_impl!("cairo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let path = self.path.to_path_buf();
let tx = context.tx.clone();
spawn(async move {
let parent = path.parent().expect("to have parent path");
let mut watcher = recommended_watcher({
let path = path.clone();
move |res: notify::Result<Event>| match res {
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
debug!("{event:?}");
if event.paths.first().is_some_and(|p| p == &path) {
try_send!(tx, ModuleUpdateEvent::Update(()));
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
_ => {}
}
})
.expect("Failed to create lua file watcher");
watcher
.watch(parent, RecursiveMode::NonRecursive)
.expect("Failed to start lua file watcher");
// avoid watcher from dropping
loop {
sleep(Duration::from_secs(1)).await;
}
});
// Lua needs to run synchronously with the GTK updates,
// so the controller does not handle the script engine.
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<gtk::Box>>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let id = context.id.to_string();
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let surface = ImageSurface::create(Format::ARgb32, self.width as i32, self.height as i32)?;
let area = DrawingArea::new();
let lua = context
.ironbar
.clients
.borrow_mut()
.lua(&context.ironbar.config_dir);
// this feels kinda dirty,
// but it keeps draw functions separate in the global scope
let script = fs::read_to_string(&self.path)?
.replace("function draw", format!("function __draw_{id}").as_str());
lua.load(&script).exec()?;
{
let lua = lua.clone();
let id = id.clone();
let path = self.path.clone();
area.connect_draw(move |_, cr| {
let function: Function = lua
.load(include_str!("../../lua/draw.lua"))
.eval()
.expect("to be valid");
if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
error!("{err}");
return Propagation::Stop;
}
let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
// mlua needs a valid return type, even if we don't return anything
if let Err(err) =
function.call::<_, Option<bool>>((id.as_str(), LightUserData(ptr)))
{
match err {
Error::RuntimeError(message) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua runtime error] {}:{message}", path.display())
}
_ => error!("{err}"),
}
return Propagation::Stop;
}
Propagation::Proceed
});
}
area.set_size_request(self.width as i32, self.height as i32);
container.add(&area);
glib::spawn_future_local(async move {
loop {
area.queue_draw();
glib::timeout_future(Duration::from_millis(self.frequency)).await;
}
});
glib_recv!(context.subscribe(), _ev => {
let res = fs::read_to_string(&self.path)
.map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
match res {
Ok(script) => {
match lua.load(&script).exec() {
Ok(_) => {},
Err(Error::SyntaxError { message, ..}) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua syntax error] {}:{message}", self.path.display())
},
Err(err) => error!("lua error: {err:?}")
}
},
Err(err) => error!("{err:?}")
}
});
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View file

@ -5,7 +5,7 @@ use crate::image::new_icon_button;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use glib::Propagation;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
@ -13,7 +13,6 @@ use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error};
@ -65,9 +64,7 @@ impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent;
fn name() -> &'static str {
"clipboard"
}
module_impl!("clipboard");
fn spawn_controller(
&self,
@ -78,7 +75,7 @@ impl Module<Button> for ClipboardModule {
let max_items = self.max_items;
let tx = context.tx.clone();
let client: Arc<clipboard::Client> = context.client();
let client = context.client::<clipboard::Client>();
// listen to clipboard events
spawn(async move {
@ -137,7 +134,7 @@ impl Module<Button> for ClipboardModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -147,6 +144,7 @@ impl Module<Button> for ClipboardModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -8,12 +8,12 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc};
use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
#[derive(Debug, Deserialize, Clone)]
pub struct ClockModule {
@ -31,6 +31,9 @@ pub struct ClockModule {
#[serde(default = "default_locale")]
locale: String,
#[serde(default)]
orientation: ModuleOrientation,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -41,6 +44,7 @@ impl Default for ClockModule {
format: default_format(),
format_popup: default_popup_format(),
locale: default_locale(),
orientation: ModuleOrientation::Horizontal,
common: Some(CommonConfig::default()),
}
}
@ -71,9 +75,7 @@ impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>;
type ReceiveMessage = ();
fn name() -> &'static str {
"clock"
}
module_impl!("clock");
fn spawn_controller(
&self,
@ -100,7 +102,7 @@ impl Module<Button> for ClockModule {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let label = Label::builder()
.angle(info.bar_position.get_angle())
.angle(self.orientation.to_angle())
.use_markup(true)
.build();
button.add(&label);
@ -120,7 +122,12 @@ impl Module<Button> for ClockModule {
});
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -130,6 +137,7 @@ impl Module<Button> for ClockModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0);

View file

@ -1,15 +1,15 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::config::ModuleOrientation;
use crate::modules::custom::WidgetConfig;
use gtk::prelude::*;
use gtk::Orientation;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct BoxWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
orientation: Option<ModuleOrientation>,
widgets: Option<Vec<WidgetConfig>>,
}
@ -20,9 +20,7 @@ impl CustomWidget for BoxWidget {
let container = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
container.set_orientation(
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
);
container.set_orientation(orientation.into());
}
if let Some(widgets) = self.widgets {

View file

@ -1,12 +1,13 @@
use gtk::prelude::*;
use gtk::{Button, Label};
use gtk::{Button, Label, Orientation};
use serde::Deserialize;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
@ -14,6 +15,9 @@ pub struct ButtonWidget {
class: Option<String>,
label: Option<String>,
on_click: Option<String>,
widgets: Option<Vec<WidgetConfig>>,
#[serde(default)]
orientation: ModuleOrientation,
}
impl CustomWidget for ButtonWidget {
@ -23,9 +27,20 @@ impl CustomWidget for ButtonWidget {
let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label {
if let Some(widgets) = self.widgets {
let container = gtk::Box::new(Orientation::Horizontal, 0);
for widget in widgets {
widget.widget.add_to(&container, &context, widget.common);
}
button.add(&container);
} else if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
label.set_angle(self.orientation.to_angle());
button.add(&label);
dynamic_string(&text, move |string| {

View file

@ -3,6 +3,7 @@ use gtk::Label;
use serde::Deserialize;
use crate::build;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
@ -12,6 +13,8 @@ pub struct LabelWidget {
name: Option<String>,
class: Option<String>,
label: String,
#[serde(default)]
orientation: ModuleOrientation,
}
impl CustomWidget for LabelWidget {
@ -20,6 +23,8 @@ impl CustomWidget for LabelWidget {
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
let label = build!(self, Self::Widget);
label.set_angle(self.orientation.to_angle());
label.set_use_markup(true);
{

View file

@ -9,15 +9,16 @@ use self::image::ImageWidget;
use self::label::LabelWidget;
use self::r#box::BoxWidget;
use self::slider::SliderWidget;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleConfig};
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
wrap_widget, AnyModuleFactory, BarModuleFactory, Module, ModuleInfo, ModuleParts, ModulePopup,
ModuleUpdateEvent, PopupButton, PopupModuleFactory, WidgetContext,
};
use crate::script::Script;
use crate::{send_async, spawn};
use color_eyre::{Report, Result};
use crate::{module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use serde::Deserialize;
@ -40,11 +41,18 @@ pub struct CustomModule {
#[derive(Debug, Deserialize, Clone)]
pub struct WidgetConfig {
#[serde(flatten)]
widget: Widget,
widget: WidgetOrModule,
#[serde(flatten)]
common: CommonConfig,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum WidgetOrModule {
Widget(Widget),
Module(ModuleConfig),
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Widget {
@ -58,10 +66,12 @@ pub enum Widget {
#[derive(Clone)]
struct CustomWidgetContext<'a> {
info: &'a ModuleInfo<'a>,
tx: &'a mpsc::Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
module_factory: AnyModuleFactory,
}
trait CustomWidget {
@ -103,14 +113,16 @@ pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orient
};
}
/// Attempts to parse an `Orientation` from `String`.
/// Will accept `horizontal`, `vertical`, `h` or `v`.
/// Ignores case.
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
impl WidgetOrModule {
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) {
match self {
WidgetOrModule::Widget(widget) => widget.add_to(parent, context, common),
WidgetOrModule::Module(config) => {
if let Err(err) = config.create(&context.module_factory, parent, context.info) {
error!("{err:?}");
}
}
}
}
}
@ -151,9 +163,7 @@ impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn name() -> &'static str {
"custom"
}
module_impl!("custom");
fn spawn_controller(
&self,
@ -191,7 +201,7 @@ impl Module<gtk::Box> for CustomModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.orientation();
@ -200,10 +210,13 @@ impl Module<gtk::Box> for CustomModule {
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext {
info,
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
module_factory: BarModuleFactory::new(context.ironbar.clone(), context.popup.clone())
.into(),
};
self.bar.clone().into_iter().for_each(|widget| {
@ -212,8 +225,22 @@ impl Module<gtk::Box> for CustomModule {
.add_to(&container, &custom_context, widget.common);
});
for button in popup_buttons.borrow().iter() {
button.ensure_popup_id();
}
context.button_id = popup_buttons
.borrow()
.first()
.map_or(usize::MAX, PopupButton::popup_id);
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleParts {
@ -226,6 +253,7 @@ impl Module<gtk::Box> for CustomModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
@ -235,10 +263,17 @@ impl Module<gtk::Box> for CustomModule {
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
info,
tx: &tx,
bar_orientation: info.bar_position.orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
module_factory: PopupModuleFactory::new(
context.ironbar,
context.popup,
context.button_id,
)
.into(),
};
for widget in popup {

View file

@ -4,18 +4,20 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
#[serde(default)]
orientation: ModuleOrientation,
label: Option<String>,
value: Option<ScriptInput>,
#[serde(default = "default_max")]
@ -33,11 +35,7 @@ impl CustomWidget for ProgressWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let progress = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
progress.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
progress.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&progress, length, context.bar_orientation);

View file

@ -8,17 +8,19 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
#[serde(default)]
orientation: ModuleOrientation,
value: Option<ScriptInput>,
on_change: Option<String>,
#[serde(default = "default_min")]
@ -45,11 +47,7 @@ impl CustomWidget for SliderWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let scale = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
scale.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
scale.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&scale, length, context.bar_orientation);

View file

@ -3,7 +3,7 @@ use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -50,9 +50,7 @@ impl Module<gtk::Box> for FocusedModule {
type SendMessage = Option<(String, String)>;
type ReceiveMessage = ();
fn name() -> &'static str {
"focused"
}
module_impl!("focused");
fn spawn_controller(
&self,

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, try_send};
use crate::{glib_recv, module_impl, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -29,9 +29,7 @@ impl Module<Label> for LabelModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"label"
}
module_impl!("label");
fn spawn_controller(
&self,

View file

@ -7,7 +7,7 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report};
use gtk::prelude::*;
use gtk::{Button, Orientation};
@ -80,9 +80,7 @@ impl Module<gtk::Box> for LauncherModule {
type SendMessage = LauncherUpdate;
type ReceiveMessage = ItemEvent;
fn name() -> &'static str {
"launcher"
}
module_impl!("launcher");
fn spawn_controller(
&self,
@ -401,7 +399,7 @@ impl Module<gtk::Box> for LauncherModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
@ -414,6 +412,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250;

View file

@ -10,12 +10,14 @@ use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widge
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use crate::clients::ProvidesClient;
use crate::clients::{ClientResult, ProvidesClient, ProvidesFallibleClient};
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
@ -56,6 +58,8 @@ pub enum ModuleLocation {
Center,
Right,
}
#[derive(Clone)]
pub struct ModuleInfo<'a> {
pub app: &'a Application,
pub location: ModuleLocation,
@ -87,10 +91,16 @@ where
{
pub id: usize,
pub ironbar: Rc<Ironbar>,
pub popup: Rc<Popup>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>,
// TODO: Don't like this - need some serious refactoring to deal with it
// This is a hack to be able to pass data from module -> popup creation
// for custom widget only.
pub button_id: usize,
_update_rx: broadcast::Receiver<TSend>,
}
@ -109,6 +119,13 @@ where
ProvidesClient::provide(self)
}
pub fn try_client<T: ?Sized>(&self) -> ClientResult<T>
where
WidgetContext<TSend, TReceive>: ProvidesFallibleClient<T>,
{
ProvidesFallibleClient::try_provide(self)
}
/// Subscribes to events sent from this widget.
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe()
@ -124,6 +141,32 @@ impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
pub fn setup_identifiers(&self, common: &CommonConfig) {
if let Some(ref name) = common.name {
self.widget.set_widget_name(name);
if let Some(ref popup) = self.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
self.widget.style_context().add_class(part);
}
if let Some(ref popup) = self.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
}
}
}
}
#[derive(Debug, Clone)]
@ -152,11 +195,24 @@ impl ModulePopup for Option<gtk::Box> {
}
pub trait PopupButton {
fn ensure_popup_id(&self) -> usize;
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button,
/// or creates a new one if it does not exist.
fn ensure_popup_id(&self) -> usize {
if let Some(id) = self.try_popup_id() {
id
} else {
let id = Ironbar::unique_id();
self.set_tag("popup-id", id);
id
}
}
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
@ -203,165 +259,290 @@ where
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
<Self as Module<W>>::SendMessage: Clone,
{
None
}
fn take_common(&mut self) -> CommonConfig;
}
/// Creates a module and sets it up.
/// This setup includes widget/popup content and event channels.
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo,
popup: &Rc<Popup>,
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
pub trait ModuleFactory {
fn create<TModule, TWidget, TSend, TRev>(
&self,
mut module: TModule,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRev>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let id = Ironbar::unique_id();
let common = module.take_common();
let (tx, rx) = broadcast::channel(64);
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRev>(64);
let context = WidgetContext {
id,
ironbar,
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
};
let (tx, rx) = broadcast::channel(64);
module.spawn_controller(info, &context, controller_rx)?;
let context = WidgetContext {
id,
ironbar: self.ironbar().clone(),
popup: self.popup().clone(),
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
button_id: usize::MAX, // hack :(
};
let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
module.spawn_controller(info, &context, controller_rx)?;
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
let module_name = TModule::name();
let instance_name = common
.name
.clone()
.unwrap_or_else(|| module_name.to_string());
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
popup.register_content(id, instance_name, popup_content);
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
self.popup()
.register_content(id, instance_name, popup_content);
}
self.setup_receiver(tx, ui_rx, module_name, id, common.disable_popup);
module_parts.setup_identifiers(&common);
let ev_container = wrap_widget(
&module_parts.widget,
common,
info.bar_position.orientation(),
);
container.add(&ev_container);
Ok(())
}
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static;
Ok(module_parts)
fn ironbar(&self) -> &Rc<Ironbar>;
fn popup(&self) -> &Rc<Popup>;
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
/// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>(
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
#[derive(Clone)]
pub struct BarModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
name: &'static str,
id: usize,
) where
TSend: Debug + Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
}
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
if popup.is_visible() {
popup.hide();
} else {
popup.show(id, button_id);
impl BarModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>) -> Self {
Self { ironbar, popup }
}
}
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
impl ModuleFactory for BarModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
has_popup_opened = true;
}
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.hide();
popup.show_at(id, geometry);
has_popup_opened = true;
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
}
}
});
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleParts<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
#[derive(Clone)]
pub struct PopupModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
button_id: usize,
}
if let Some(ref popup) = widget_parts.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
impl PopupModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>, button_id: usize) -> Self {
Self {
ironbar,
popup,
button_id,
}
}
}
impl ModuleFactory for PopupModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
let button_id = self.button_id;
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
}
}
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
#[derive(Clone)]
pub enum AnyModuleFactory {
Bar(BarModuleFactory),
Popup(PopupModuleFactory),
}
impl ModuleFactory for AnyModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
match self {
AnyModuleFactory::Bar(bar) => bar.setup_receiver(tx, rx, name, id, disable_popup),
AnyModuleFactory::Popup(popup) => popup.setup_receiver(tx, rx, name, id, disable_popup),
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
widget_parts.widget.style_context().add_class(part);
fn ironbar(&self) -> &Rc<Ironbar> {
match self {
AnyModuleFactory::Bar(bar) => bar.ironbar(),
AnyModuleFactory::Popup(popup) => popup.ironbar(),
}
}
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
fn popup(&self) -> &Rc<Popup> {
match self {
AnyModuleFactory::Bar(bar) => bar.popup(),
AnyModuleFactory::Popup(popup) => popup.popup(),
}
}
}
impl From<BarModuleFactory> for AnyModuleFactory {
fn from(value: BarModuleFactory) -> Self {
Self::Bar(value)
}
}
impl From<PopupModuleFactory> for AnyModuleFactory {
fn from(value: PopupModuleFactory) -> Self {
Self::Popup(value)
}
}
/// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned.
pub fn wrap_widget<W: IsA<Widget>>(

View file

@ -22,7 +22,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
pub use self::config::MusicModule;
use self::config::PlayerType;
@ -87,9 +87,7 @@ impl Module<Button> for MusicModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = PlayerCommand;
fn name() -> &'static str {
"music"
}
module_impl!("music");
fn spawn_controller(
&self,
@ -255,7 +253,7 @@ impl Module<Button> for MusicModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -265,6 +263,7 @@ impl Module<Button> for MusicModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;

View file

@ -2,7 +2,7 @@ use crate::clients::swaync;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use gtk::prelude::*;
use gtk::{Align, Button, Label, Overlay};
use serde::Deserialize;
@ -75,7 +75,7 @@ fn default_icon_open_dnd() -> String {
}
impl Icons {
fn icon(&self, value: &swaync::Event) -> &str {
fn icon(&self, value: swaync::Event) -> &str {
match (value.cc_open, value.count > 0, value.dnd) {
(true, _, true) => &self.open_dnd,
(true, true, false) => &self.open_some,
@ -97,9 +97,7 @@ impl Module<Overlay> for NotificationsModule {
type SendMessage = swaync::Event;
type ReceiveMessage = UiEvent;
fn name() -> &'static str {
"notifications"
}
module_impl!("notifications");
fn spawn_controller(
&self,
@ -110,7 +108,7 @@ impl Module<Overlay> for NotificationsModule {
where
<Self as Module<Overlay>>::SendMessage: Clone,
{
let client = context.client::<swaync::Client>();
let client = context.try_client::<swaync::Client>()?;
{
let client = client.clone();
@ -174,7 +172,7 @@ impl Module<Overlay> for NotificationsModule {
let button = button.clone();
glib_recv!(context.subscribe(), ev => {
let icon = self.icons.icon(&ev);
let icon = self.icons.icon(ev);
button.set_label(icon);
label.set_label(&ev.count.to_string());

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
@ -48,9 +48,7 @@ impl Module<Label> for ScriptModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"script"
}
module_impl!("script");
fn spawn_controller(
&self,

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn};
use crate::{glib_recv, module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -21,6 +21,11 @@ pub struct SysInfoModule {
#[serde(default = "Interval::default")]
interval: Interval,
#[serde(default)]
orientation: ModuleOrientation,
direction: Option<ModuleOrientation>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -116,9 +121,7 @@ impl Module<gtk::Box> for SysInfoModule {
type SendMessage = HashMap<String, String>;
type ReceiveMessage = ();
fn name() -> &'static str {
"sysinfo"
}
module_impl!("sysinfo");
fn spawn_controller(
&self,
@ -184,11 +187,16 @@ impl Module<gtk::Box> for SysInfoModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
_info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.orientation(), 10);
let layout = match self.direction {
Some(orientation) => orientation,
None => self.orientation,
};
let container = gtk::Box::new(layout.into(), 10);
let mut labels = Vec::new();
@ -196,7 +204,7 @@ impl Module<gtk::Box> for SysInfoModule {
let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item");
label.set_angle(info.bar_position.get_angle());
label.set_angle(self.orientation.to_angle());
container.add(&label);
labels.push(label);

View file

@ -6,7 +6,7 @@ use crate::clients::tray;
use crate::config::CommonConfig;
use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, lock, send_async, spawn};
use crate::{glib_recv, lock, module_impl, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
@ -57,9 +57,7 @@ impl Module<MenuBar> for TrayModule {
type SendMessage = Event;
type ReceiveMessage = ActivateRequest;
fn name() -> &'static str {
"tray"
}
module_impl!("tray");
fn spawn_controller(
&self,
@ -69,14 +67,14 @@ impl Module<MenuBar> for TrayModule {
) -> Result<()> {
let tx = context.tx.clone();
let client = context.client::<tray::Client>();
let client = context.try_client::<tray::Client>()?;
let mut tray_rx = client.subscribe();
let initial_items = lock!(client.items()).clone();
// listen to tray updates
spawn(async move {
for (key, (item, menu)) in initial_items.into_iter() {
for (key, (item, menu)) in initial_items {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
@ -91,7 +89,7 @@ impl Module<MenuBar> for TrayModule {
}
while let Ok(message) = tray_rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(message))
send_async!(tx, ModuleUpdateEvent::Update(message));
}
});
@ -161,12 +159,11 @@ fn on_update(
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
container.add(&menu_item.widget);
match icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label)
}
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
menu_item.set_image(&image);
} else {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label);
};
menu_item.widget.show();

View file

@ -15,7 +15,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
@ -54,9 +54,7 @@ impl Module<gtk::Button> for UpowerModule {
type SendMessage = UpowerProperties;
type ReceiveMessage = ();
fn name() -> &'static str {
"upower"
}
module_impl!("upower");
fn spawn_controller(
&self,
@ -183,7 +181,6 @@ impl Module<gtk::Button> for UpowerModule {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
label.set_angle(info.bar_position.get_angle());
let format = self.format.clone();
let rx = context.subscribe();
@ -211,7 +208,7 @@ impl Module<gtk::Button> for UpowerModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -221,6 +218,7 @@ impl Module<gtk::Button> for UpowerModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -4,7 +4,7 @@ use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, lock, send_async, spawn, try_send};
use crate::{glib_recv, lock, module_impl, send_async, spawn, try_send};
use glib::Propagation;
use gtk::pango::EllipsizeMode;
use gtk::prelude::*;
@ -99,9 +99,7 @@ impl Module<Button> for VolumeModule {
type SendMessage = Event;
type ReceiveMessage = Update;
fn name() -> &'static str {
"volume"
}
module_impl!("volume");
fn spawn_controller(
&self,
@ -208,7 +206,12 @@ impl Module<Button> for VolumeModule {
}
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -218,6 +221,7 @@ impl Module<Button> for VolumeModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -2,7 +2,7 @@ use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, Workspa
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme};
@ -144,9 +144,7 @@ impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
fn name() -> &'static str {
"workspaces"
}
module_impl!("workspaces");
fn spawn_controller(
&self,
@ -155,7 +153,7 @@ impl Module<gtk::Box> for WorkspacesModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces();
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
@ -168,7 +166,7 @@ impl Module<gtk::Box> for WorkspacesModule {
}
});
let client = context.client::<dyn WorkspaceClient>();
let client = context.try_client::<dyn WorkspaceClient>()?;
// Change workspace focus
spawn(async move {

View file

@ -12,7 +12,7 @@ use tracing::{debug, trace};
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::Ironbar;
use crate::rc_mut;
#[derive(Debug, Clone)]
pub struct PopupCacheValue {
@ -23,7 +23,8 @@ pub struct PopupCacheValue {
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub container_cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub button_cache: Rc<RefCell<Vec<Button>>>,
monitor: Monitor,
pos: BarPosition,
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
@ -106,10 +107,11 @@ impl Popup {
Self {
window: win,
cache: Rc::new(RefCell::new(HashMap::new())),
container_cache: rc_mut!(HashMap::new()),
button_cache: rc_mut!(vec![]),
monitor: module_info.monitor.clone(),
pos,
current_widget: Rc::new(RefCell::new(None)),
current_widget: rc_mut!(None),
}
}
@ -117,8 +119,7 @@ impl Popup {
debug!("Registered popup content for #{}", key);
for button in &content.buttons {
let id = Ironbar::unique_id();
button.set_tag("popup-id", id);
button.ensure_popup_id();
}
let orientation = self.pos.orientation();
@ -126,7 +127,8 @@ impl Popup {
let window = self.window.clone();
let current_widget = self.current_widget.clone();
let cache = self.cache.clone();
let cache = self.container_cache.clone();
let button_cache = self.button_cache.clone();
content
.container
@ -135,11 +137,9 @@ impl Popup {
trace!("Resized: {}x{}", rect.width(), rect.height());
if let Some((widget_id, button_id)) = *current_widget.borrow() {
if let Some(PopupCacheValue { content, .. }) =
cache.borrow().get(&widget_id)
{
if let Some(PopupCacheValue { .. }) = cache.borrow().get(&widget_id) {
Self::set_position(
&content.buttons,
&button_cache.borrow(),
button_id,
orientation,
&monitor,
@ -150,7 +150,11 @@ impl Popup {
}
});
self.cache
self.button_cache
.borrow_mut()
.append(&mut content.buttons.clone());
self.container_cache
.borrow_mut()
.insert(key, PopupCacheValue { name, content });
}
@ -158,16 +162,17 @@ impl Popup {
pub fn show(&self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
*self.current_widget.borrow_mut() = Some((widget_id, button_id));
content.container.style_context().add_class("popup");
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
Self::set_position(
&content.buttons,
&self.button_cache.borrow(),
button_id,
self.pos.orientation(),
&self.monitor,
@ -179,8 +184,9 @@ impl Popup {
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
content.container.style_context().add_class("popup");
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
@ -216,7 +222,7 @@ impl Popup {
}
}
/// Hides the popover
/// Hides the popup
pub fn hide(&self) {
*self.current_widget.borrow_mut() = None;
self.window.hide();

View file

@ -0,0 +1,192 @@
{
position = "bottom"
start = [
{
type = "clock"
format = "%H:%M"
}
{
type = "clock"
format = "%H:%M"
orientation = "horizontal"
}
{
type = "clock"
format = "%H:%M"
orientation = "h"
}
{
type = "clock"
format = "%H:%M"
orientation = "vertical"
}
{
type = "clock"
format = "%H:%M"
orientation = "v"
}
{
type = "custom"
bar = [
{
type = "label"
label = "label"
}
{
type = "label"
label = "label"
orientation = "horizontal"
}
{
type = "label"
label = "label"
orientation = "h"
}
{
type = "label"
label = "label"
orientation = "vertical"
}
{
type = "label"
label = "label"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "button"
label = "label"
}
{
type = "button"
label = "label"
orientation = "horizontal"
}
{
type = "button"
label = "label"
orientation = "h"
}
{
type = "button"
label = "label"
orientation = "vertical"
}
{
type = "button"
label = "label"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "progress"
value = "echo 50"
}
{
type = "progress"
value = "echo 50"
orientation = "horizontal"
}
{
type = "progress"
value = "echo 50"
orientation = "h"
}
{
type = "progress"
value = "echo 50"
orientation = "vertical"
}
{
type = "progress"
value = "echo 50"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "slider"
value = "echo 50"
length = 100
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "horizontal"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "h"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "vertical"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "v"
}
]
}
{
type = "sys_info"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
{
type = "sys_info"
orientation = "vertical"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
{
type = "sys_info"
layout = "vertical"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
]
}