diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..e2452d1 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,18 @@ +on: + pull_request: + branches: [ master, dev ] + push: + branches: [ master, dev ] + +jobs: + build: + name: Build Nix targets + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install Nix + uses: DeterminateSystems/determinate-nix-action@v3 + with: + fail-mode: true + - name: Build default package + run: nix build diff --git a/Cargo.lock b/Cargo.lock index b9b1153..54d0609 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,34 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -23,18 +51,105 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "battery" version = "0.7.8" @@ -90,6 +205,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "calloop" version = "0.13.0" @@ -118,13 +239,15 @@ dependencies = [ [[package]] name = "capybar" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "battery", "chrono", + "clap", "fontconfig", "fontdue", + "hyprland", "serde", "smithay-client-toolkit", "sysinfo", @@ -163,6 +286,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -206,6 +375,27 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "dlib" version = "0.5.2" @@ -221,6 +411,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -234,7 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -262,6 +458,28 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" version = "0.15.3" @@ -273,12 +491,51 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +[[package]] +name = "hyprland" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9c1413b6f0fd10b2e4463479490e30b2497ae4449f044da16053f5f2cb03b8" +dependencies = [ + "ahash", + "async-stream", + "derive_more", + "either", + "futures-lite", + "hyprland-macros", + "num-traits", + "once_cell", + "paste", + "phf", + "serde", + "serde_json", + "serde_repr", + "tokio", +] + +[[package]] +name = "hyprland-macros" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -313,6 +570,29 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -396,6 +676,26 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + [[package]] name = "nix" version = "0.19.1" @@ -445,12 +745,75 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -475,7 +838,7 @@ dependencies = [ "pin-project-lite", "rustix 1.0.7", "tracing", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -505,6 +868,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + [[package]] name = "rustix" version = "0.38.44" @@ -515,7 +899,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -528,7 +912,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -537,6 +921,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -557,6 +947,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -572,6 +985,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -615,6 +1034,22 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.101" @@ -680,6 +1115,35 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.23" @@ -755,6 +1219,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uom" version = "0.30.0" @@ -765,6 +1235,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1039,6 +1527,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1230,3 +1727,23 @@ dependencies = [ "once_cell", "pkg-config", ] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2bbee13..4ce023d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,21 @@ [package] name = "capybar" -version = "0.1.1" +description = "Wayland native toolbar" +version = "0.2.0" edition = "2021" license = "MIT" +repository = "https://github.com/CapyCore/capybar" + +[features] +default = [ + "keyboard+all" +] + +hyprland = [] + +keyboard = [] +"keyboard+hyprland" = ["keyboard", "hyprland"] +"keyboard+all" = ["keyboard", "keyboard+hyprland"] [dependencies] #Wayland handling @@ -18,6 +31,9 @@ anyhow = "1.0.98" toml = "0.8.23" serde = {version = "1.0.219", features = [ "derive" ] } +#CLI arguments +clap = {version = "4.5.41", features = [ "derive" ]} + ### Widget dependencies #Fonts fontconfig = "0.9.0" @@ -28,3 +44,5 @@ chrono = "0.4.41" battery = "0.7.8" #CPU sysinfo = "0.35.1" + +hyprland = "0.4.0-beta" diff --git a/README.md b/README.md index 09d6116..6c67fd9 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,48 @@ Simple customizable bar applications that aims to have as little external depend - Clock - Battery - CPU usage + - Keyboard layout - Row container (WIP) - Bar container ## Instalation -Currently bar needs to be build manually. To do so clone the repo and write main file. Bulding the bar is done with cargo. The example is located in examples folder. To use the basic example run: + +### Nix + +Capybar can be installed on nix using home manager. +- Extend your inputs with: +```nix + inputs = { + # ... + capybar.url = "github:CapyCore/capybar"; + }; ``` -cargo build --release --example basic + +- Extend your imports with: +```nix +imports = [ inputs.capybar.homeManagerModules.default ]; +``` + +- Enable capybar: +```nix +programs.capybar = { + enable = true; +} +``` + +### Others +Currently bar needs to be build manually. To do so clone the repo and write main file. Bulding the bar is done with cargo. The example is located in examples folder. +``` +cargo build --release ``` -## Usage After building the bar the executable will be located in `./target/release/` -The basic example exetutable is `./target/release/examples/basic` +## Usage +Capybar can be run using `capybar` command in a terminal of your choice. You can change configuration path via flag +`--cfg_path` (default path is `$HOME/.config/capybar`) and config extention via `--cfg_type` (default is toml, no other types are +currently supported). More info could be accesed wit `--help` flag. ## License diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 0a31898..690c3c3 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -1,4 +1,5 @@ use capybar::{ + root::Root, util::Color, widgets::{ battery::{Battery, BatterySettings}, @@ -8,7 +9,6 @@ use capybar::{ text::TextSettings, Style, WidgetData, WidgetNew, }, - Root, }; use wayland_client::{globals::registry_queue_init, Connection}; @@ -60,6 +60,7 @@ fn main() -> Result<(), Box> { bar.create_child_left( CPU::new, CPUSettings { + update_rate: 1000, text_settings: TextSettings { font_color: catpuccin_mocha.font, size: 25.0, diff --git a/examples/toml_config/README.md b/examples/toml_config/README.md new file mode 100644 index 0000000..92a15ff --- /dev/null +++ b/examples/toml_config/README.md @@ -0,0 +1,21 @@ +# Toml config +Contains toml config bar implementaition. +The config is located in [config.toml](https://github.com/CapyCore/capybar/blob/master/examples/toml_config/config.toml) file + +Example uses system's mono font for text and JetBrainsNerd font for emoji like battery symbol + +## The toml config example looks like this: +![Screenshot of the toml_config example bar](./bar.png) +## Usage +Build the example with +``` +cargo build --release --example toml_config +``` +Then find executable at ```capybar/target/release/example/toml_config``` + +Or run the example with +``` +cargo run --release --example toml_config +``` + + diff --git a/examples/toml_config/config.toml b/examples/toml_config/config.toml index 5fa651d..f8d02d6 100644 --- a/examples/toml_config/config.toml +++ b/examples/toml_config/config.toml @@ -12,13 +12,22 @@ width = 1920 background = 0x1e1e2eff border = [1, 0x74c7ecff] +[[bar.left]] + widget = "keyboard" + [[bar.left.settings]] + size = 24 + font_color = 0xf5e0dcff + margin = [10,0,0,0] + layout_mappings = {"Russian" = "RU", "English (US)" = "EN"} + [[bar.left.settings]] + update_rate = 0 + [[bar.left]] widget = "cpu" [bar.left.settings] size = 24 font_color = 0xf5e0dcff margin = [10,0,0,0] - update_rate = 1000 [[bar.center]] widget = "clock" diff --git a/examples/toml_config/main.rs b/examples/toml_config/main.rs index 5fe862d..6979e10 100644 --- a/examples/toml_config/main.rs +++ b/examples/toml_config/main.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use capybar::{config::Config, Root}; +use capybar::{config::Config, root::Root}; use wayland_client::{globals::registry_queue_init, Connection}; fn main() -> Result<()> { diff --git a/nix/flake.lock b/flake.lock similarity index 77% rename from nix/flake.lock rename to flake.lock index de82eae..66b4127 100644 --- a/nix/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1746810718, - "narHash": "sha256-VljtYzyttmvkWUKTVJVW93qAsJsrBbgAzy7DdnJaQfI=", + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0c0bf9c057382d5f6f63d54fd61f1abd5e1c2f63", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", "type": "github" }, "original": { @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1746931022, - "narHash": "sha256-cXn1RsYZjS23n0+YP3TiH7XBlEvy8FA2mG54MdAL6x0=", + "lastModified": 1752461263, + "narHash": "sha256-f4XVgqkWF1vSzPbOG5xvi4aAd/n1GwSNsji3mLMFwYQ=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c46d2764319f962b20ce9c03ce6644dd0de87bc9", + "rev": "9cc51d100d24fb7ea13a0bee1480ee84fa12a0ad", "type": "github" }, "original": { diff --git a/nix/flake.nix b/flake.nix similarity index 74% rename from nix/flake.nix rename to flake.nix index e44cc8b..c95be5a 100644 --- a/nix/flake.nix +++ b/flake.nix @@ -1,8 +1,8 @@ { - description = "kapibar"; + description = "capybar"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; rust-overlay.url = "github:oxalica/rust-overlay"; }; @@ -25,30 +25,35 @@ rustc = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); }; - manifest = (pkgs.lib.importTOML ./crates/kapibar/Cargo.toml).package; + manifest = (pkgs.lib.importTOML ./Cargo.toml).package; in rustPlatform.buildRustPackage { pname = manifest.name; inherit (manifest) version; buildInputs = with pkgs; [ + fontconfig libxkbcommon - cairo - libpulseaudio ]; nativeBuildInputs = with pkgs; [ pkg-config ]; - + cargoLock = { lockFile = ./Cargo.lock; allowBuiltinFetchGit = true; }; src = pkgs.lib.cleanSource ./.; - - RUSTFLAGS = "--cfg tokio_unstable"; + + meta = { + description = "Native wayland toolbar"; + homepage = "https://github.com/CapyCore/capybar"; + platforms = nixpkgs.lib.platforms.linux; + license = nixpkgs.lib.licenses.mit; + mainProgram = "capybar"; + }; }; # Function to build dev shell @@ -66,23 +71,24 @@ rustfmt clippy pkg-config + fontconfig libxkbcommon - cairo - libpulseaudio ]; - - RUSTFLAGS = "--cfg tokio_unstable"; }; in { # Generate per-system outputs packages = forAllSystems (system: { default = packageFor system; - kapibar = packageFor system; + capybar = packageFor system; }); devShells = forAllSystems (system: { default = devShellFor system; }); + + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-tree); + + homeManagerModules.default = import ./nix/module.nix self; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..a887707 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,22 @@ +self: { + lib, + pkgs, + config, + ... +}: let + cfg = config.programs.capybar; +in with lib; { + options.programs.capybar = { + enable = mkEnableOption "capybar"; + + package = mkOption { + type = types.package; + description = "The capybar package to use."; + default = self.packages.${pkgs.system}.capybar; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + }; +} diff --git a/src/lib.rs b/src/lib.rs index b193ec6..5ce7486 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod config; +pub mod processes; +pub mod root; pub mod util; pub mod widgets; - -pub mod root; -pub use root::Root; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5f999ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +use std::{fmt::Display, path::PathBuf}; + +use anyhow::Result; +use capybar::{config::Config, root::Root}; +use clap::{Args, Parser, ValueEnum}; +use std::env::var; +use thiserror::Error; +use wayland_client::{globals::registry_queue_init, Connection}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(flatten)] + args: Arguments, +} + +#[derive(Debug, Args)] +struct Arguments { + /// What config type to use + #[arg(long, value_enum, default_value_t = ConfigTypes::Toml, value_name = "TYPE")] + cfg_type: ConfigTypes, + + #[arg(long, value_name = "FILE")] + /// Directory where the config is located + cfg_path: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum ConfigTypes { + Toml, +} + +impl Display for ConfigTypes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigTypes::Toml => write!(f, "toml"), + } + } +} + +#[derive(Debug, Error)] +enum Errors { + #[error( + "Configuration file does not exist! + Make sure you are passing `--cfg_type ` with correct type if it is not TOML. + Make sure you provide '--cfg_path ' with your config file or \ + place it config at `~/.config/capybar/config." + )] + ConfigNotExist, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + let mut cfg_path; + match cli.args.cfg_path { + None => { + if let Ok(config_home) = var("XDG_CONFIG_HOME") + .or_else(|_| var("HOME").map(|home| format!("{home}/.config"))) + { + cfg_path = config_home.into(); + } else { + return Err(Errors::ConfigNotExist.into()); + } + } + Some(value) => cfg_path = value, + } + + if cfg_path.is_dir() { + cfg_path.push("capybar"); + let file_name = "config.".to_string() + &cli.args.cfg_type.to_string(); + cfg_path.push(file_name); + } + + if !cfg_path.exists() { + return Err(Errors::ConfigNotExist.into()); + } + + let config = match cli.args.cfg_type { + ConfigTypes::Toml => Config::parse_toml(cfg_path)?, + }; + + let conn = Connection::connect_to_env()?; + let (globals, mut event_queue) = registry_queue_init(&conn)?; + + let mut capybar = Root::new(&globals, &mut event_queue)?; + capybar.apply_config(config)?; + + capybar.init(&mut event_queue)?.run(&mut event_queue)?; + + Ok(()) +} diff --git a/src/processes/clients/hyprland/keyboard.rs b/src/processes/clients/hyprland/keyboard.rs new file mode 100644 index 0000000..2244edb --- /dev/null +++ b/src/processes/clients/hyprland/keyboard.rs @@ -0,0 +1,116 @@ +use std::{cell::RefCell, rc::Rc}; + +use anyhow::anyhow; +use chrono::{DateTime, Duration, Local}; +use hyprland::{data::Devices, shared::HyprData}; + +use crate::{ + processes::{clients::KeyboardTrait, Process, ProcessError, ProcessNew, ProcessSettings}, + root::Environment, + util::signals::Signal, +}; + +/// Process that tracks current keyboard layout +pub struct Keyboard { + settings: ProcessSettings, + + last_layout: RefCell, + last_update: RefCell>, + + env: Option>, +} + +impl Keyboard { + fn get_main_keyboard() -> Result { + let devices = Devices::get(); + if let Err(err) = devices { + return Err(ProcessError::Custom("Keyboard".to_string(), err.into())); + } + + let keyboards = devices.unwrap().keyboards; + + if keyboards.is_empty() { + return Err(ProcessError::Custom( + "Keyboard".to_string(), + anyhow!("No Keyboard connected"), + )); + } + + for keyboard in keyboards { + if keyboard.main { + return Ok(keyboard); + } + } + + Err(ProcessError::Custom( + "Keyboard".to_string(), + anyhow!("No main keyboard found"), + )) + } +} + +impl Process for Keyboard { + fn bind(&mut self, env: std::rc::Rc) -> Result<(), ProcessError> { + self.env = Some(Rc::clone(&env)); + Ok(()) + } + + fn init(&self) -> Result<(), ProcessError> { + if self.env.is_none() { + return Err(ProcessError::RunWithNoEnv("Keyboard".to_string())); + } + + let mut signals = self.env.as_ref().unwrap().signals.borrow_mut(); + if !signals.contains_key("keyboard") { + signals.insert("keyboard".to_string(), Signal::new()); + } + + *self.last_layout.borrow_mut() = Keyboard::get_main_keyboard()?.active_keymap; + signals["keyboard"].emit(&self.last_layout.clone()); + + Ok(()) + } + + fn run(&self) -> Result<(), ProcessError> { + if self.env.is_none() { + return Err(ProcessError::RunWithNoEnv("Keyboard".to_string())); + } + + let mut last_update = self.last_update.borrow_mut(); + if Local::now() - *last_update < Duration::milliseconds(self.settings.update_rate) { + return Ok(()); + } + *last_update = Local::now(); + + let signals = self.env.as_ref().unwrap().signals.borrow_mut(); + let mut last_layout = self.last_layout.borrow_mut(); + let current_layout = Keyboard::get_main_keyboard()?.active_keymap; + if *last_layout != current_layout { + *last_layout = current_layout; + signals["keyboard"].emit(&last_layout.clone()); + } + + Ok(()) + } +} + +impl ProcessNew for Keyboard { + type Settings = ProcessSettings; + + fn new( + env: Option>, + settings: Self::Settings, + ) -> Result + where + Self: Sized, + { + Ok(Keyboard { + settings, + last_update: RefCell::new(DateTime::default()), + last_layout: RefCell::new(String::new()), + env, + }) + } +} + +impl KeyboardTrait for Keyboard {} diff --git a/src/processes/clients/hyprland/mod.rs b/src/processes/clients/hyprland/mod.rs new file mode 100644 index 0000000..9123b57 --- /dev/null +++ b/src/processes/clients/hyprland/mod.rs @@ -0,0 +1,3 @@ +//! Current module describes all of the hyprland communication + +pub mod keyboard; diff --git a/src/processes/clients/mod.rs b/src/processes/clients/mod.rs new file mode 100644 index 0000000..d92221b --- /dev/null +++ b/src/processes/clients/mod.rs @@ -0,0 +1,14 @@ +//! Current module describes all of capybars clients. Different compositors handle some stuff +//! differently. All of the unique behaviours is described here. + +use super::Process; + +#[cfg(feature = "hyprland")] +pub mod hyprland; + +#[allow(dead_code)] +#[cfg(feature = "keyboard")] +trait KeyboardTrait: Process {} + +#[cfg(feature = "keyboard+hyprland")] +pub use hyprland::keyboard::Keyboard; diff --git a/src/processes/mod.rs b/src/processes/mod.rs new file mode 100644 index 0000000..153b018 --- /dev/null +++ b/src/processes/mod.rs @@ -0,0 +1,57 @@ +//! Current module describes all of capybars included processes as well as their common behaviour. +//! +//! Process can be treated as a backend component. +//! To communicate with frontend you can use [Signal](crate::util::signals::Signal) + +pub mod clients; + +use std::rc::Rc; + +use serde::Deserialize; +use thiserror::Error; + +use crate::root::Environment; + +fn default_update_rate() -> i64 { + 1000 +} + +#[derive(Debug, Deserialize, Clone, Copy)] +pub struct ProcessSettings { + #[serde(default = "default_update_rate")] + pub update_rate: i64, +} + +/// A **data structure** that can be used as a widget inside a capybar. +pub trait Process { + /// Bind a widget to a new environment. + fn bind(&mut self, env: Rc) -> Result<(), ProcessError>; + + /// Prepare `Process` for a first run + fn init(&self) -> Result<(), ProcessError>; + + /// Run the process + fn run(&self) -> Result<(), ProcessError>; +} + +/// A `Process` that can be unifiedly created. +/// +/// Implementing this trait allows creating `Process` and binding the environment without +/// intermidiate steps. Simplifies process creation inside of scripts. +pub trait ProcessNew { + type Settings; + + fn new(env: Option>, settings: Self::Settings) -> Result + where + Self: Sized; +} + +#[derive(Debug, Error)] +pub enum ProcessError { + /// Argument is a name of a widget + #[error("Trying to run a procces \"{0}\" not bound to any environment")] + RunWithNoEnv(String), + + #[error("Custom error occured in widget \"{0}\": \n \"{1}\"")] + Custom(String, anyhow::Error), +} diff --git a/src/root.rs b/src/root.rs index e8e1145..e305daf 100644 --- a/src/root.rs +++ b/src/root.rs @@ -1,8 +1,11 @@ use std::{ cell::RefCell, cmp::{max, min}, + collections::HashMap, num::NonZeroU32, rc::Rc, + thread, + time::Duration, }; use anyhow::Result; @@ -34,24 +37,27 @@ use wayland_client::{ Connection, EventQueue, QueueHandle, }; -/// Structure containing things all the widgets in capybar needs access to -pub struct Environment { - pub config: Config, - pub drawer: RefCell, -} - use crate::{ config::Config, + processes::{clients, Process, ProcessError, ProcessNew}, util::{ fonts::{self, FontsError}, + signals::Signal, Drawer, }, widgets::{ - battery::Battery, clock::Clock, containers::bar::Bar, cpu::CPU, text::Text, Widget, - WidgetNew, WidgetsList, + self, battery::Battery, clock::Clock, containers::bar::Bar, cpu::CPU, keyboard::Keyboard, + text::Text, Widget, WidgetError, WidgetNew, WidgetsList, }, }; +/// Structure containing things all the widgets in capybar needs access to +pub struct Environment { + pub config: Config, + pub drawer: RefCell, + pub signals: RefCell>, +} + #[derive(Error, Debug)] pub enum RootError { #[error("Environment is not initialised before drawing")] @@ -66,7 +72,6 @@ pub struct Root { output_state: OutputState, shm: Shm, - exit: bool, first_configure: bool, width: u32, height: u32, @@ -77,6 +82,7 @@ pub struct Root { pointer: Option, widgets: Vec>, + processes: Vec>, env: Option>, } @@ -165,9 +171,7 @@ impl OutputHandler for Root { } impl LayerShellHandler for Root { - fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, _layer: &LayerSurface) { - self.exit = true; - } + fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, _layer: &LayerSurface) {} fn configure( &mut self, @@ -275,11 +279,8 @@ impl KeyboardHandler for Root { _qh: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, - event: KeyEvent, + _: KeyEvent, ) { - if event.keysym == Keysym::Escape { - self.exit = true; - } } fn release_key( @@ -365,7 +366,6 @@ impl Root { output_state: OutputState::new(globals, &qh), shm, - exit: false, first_configure: true, width: 16, height: 16, @@ -376,6 +376,7 @@ impl Root { pointer: None, widgets: Vec::new(), + processes: Vec::new(), env: None, }; @@ -391,6 +392,10 @@ impl Root { WidgetsList::Clock(settings) => bar.create_child_left(Clock::new, settings)?, WidgetsList::Battery(settings) => bar.create_child_left(Battery::new, settings)?, WidgetsList::CPU(settings) => bar.create_child_left(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_left(Keyboard::new, wsettings)? + } } } @@ -402,6 +407,10 @@ impl Root { bar.create_child_center(Battery::new, settings)? } WidgetsList::CPU(settings) => bar.create_child_center(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_center(widgets::keyboard::Keyboard::new, wsettings)? + } } } @@ -411,6 +420,10 @@ impl Root { WidgetsList::Clock(settings) => bar.create_child_right(Clock::new, settings)?, WidgetsList::Battery(settings) => bar.create_child_right(Battery::new, settings)?, WidgetsList::CPU(settings) => bar.create_child_right(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_right(widgets::keyboard::Keyboard::new, wsettings)? + } } } @@ -422,19 +435,24 @@ impl Root { self.layer.set_anchor(Anchor::TOP); self.layer .set_keyboard_interactivity(KeyboardInteractivity::OnDemand); - self.width = 0; - self.height = 0; + self.width = 1; + self.height = 1; self.env = Some(Rc::new(Environment { config: Config::default(), drawer: RefCell::new(Drawer::new(&mut self.shm, 1, 1)), + signals: RefCell::new(HashMap::new()), })); - for widget in &mut self.widgets { - widget.bind(Rc::clone(self.env.as_ref().unwrap()))?; + for process in &mut self.processes { + process.bind(Rc::clone(self.env.as_ref().unwrap()))?; + + process.init()?; } for widget in &mut self.widgets { + widget.bind(Rc::clone(self.env.as_ref().unwrap()))?; + widget.init()?; let data = widget.data().borrow_mut(); self.height = max( @@ -476,13 +494,10 @@ impl Root { loop { event_queue.blocking_dispatch(self)?; - - if self.exit { - break; - } + thread::sleep(Duration::from_millis(100)); } - Ok(self) + //Ok(self) } pub fn add_font_by_name(&mut self, name: &'static str) -> Result<(), FontsError> { @@ -503,17 +518,31 @@ impl Root { pub fn create_widget(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.widgets.push(Box::new(f(self.env.clone(), settings)?)); Ok(()) } + pub fn create_process(&mut self, f: F, settings: W::Settings) -> Result<()> + where + W: ProcessNew + Process + 'static, + F: FnOnce(Option>, W::Settings) -> Result, + { + self.processes + .push(Box::new(f(self.env.clone(), settings)?)); + Ok(()) + } + fn draw(&mut self, qh: &QueueHandle) -> Result<()> { if self.env.is_none() { return Err(RootError::EnvironmentNotInit.into()); } + for process in &mut self.processes { + process.run()?; + } + self.layer .wl_surface() .damage_buffer(0, 0, self.width as i32, self.height as i32); diff --git a/src/util/color.rs b/src/util/color.rs index 59b7e31..e029551 100644 --- a/src/util/color.rs +++ b/src/util/color.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use serde::Deserialize; /// Color structure used in capy. Color is stored as an rgba value. -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] pub struct Color(u32); impl Display for Color { @@ -41,6 +41,23 @@ impl Color { Self(u32::from_le_bytes(*bytes)) } + pub fn from_rgba_f32(r: f32, g: f32, b: f32, a: f32) -> Option { + if !(0.0..=1.0).contains(&r) + || !(0.0..=1.0).contains(&g) + || !(0.0..=1.0).contains(&b) + || !(0.0..=1.0).contains(&a) + { + return None; + } + + Some(Self::from_rgba( + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + (a * 255.0).round() as u8, + )) + } + pub const fn to_be_bytes(self) -> [u8; 4] { self.0.to_be_bytes() } @@ -49,6 +66,37 @@ impl Color { self.0.to_le_bytes() } + pub fn r(&self) -> u8 { + ((self.0 & 0xFF000000) >> 24) as u8 + } + + pub fn g(&self) -> u8 { + ((self.0 & 0x00FF0000) >> 16) as u8 + } + + pub fn b(&self) -> u8 { + ((self.0 & 0x0000FF00) >> 8) as u8 + } + + pub fn a(&self) -> u8 { + (self.0 & 0x000000FF) as u8 + } + + pub fn set_r(&mut self, a: u8) { + self.0 &= 0x00FFFFFF; + self.0 |= (a as u32) << 24; + } + + pub fn set_g(&mut self, a: u8) { + self.0 &= 0xFF00FFFF; + self.0 |= (a as u32) << 16; + } + + pub fn set_b(&mut self, a: u8) { + self.0 &= 0xFFFF00FF; + self.0 |= (a as u32) << 8; + } + pub fn set_a(&mut self, a: u8) { self.0 &= 0xFFFFFF00; self.0 |= a as u32; @@ -58,27 +106,32 @@ impl Color { let bg = background.to_be_bytes(); let fg = foreground.to_be_bytes(); - //TODO check if checking for a == 0 improves speed - - let bg = [ - bg[0] as f32 * bg[3] as f32, - bg[1] as f32 * bg[3] as f32, - bg[2] as f32 * bg[3] as f32, - bg[3] as f32, - ]; - let fg = [ - fg[0] as f32 * fg[3] as f32, - fg[1] as f32 * fg[3] as f32, - fg[2] as f32 * fg[3] as f32, - fg[3] as f32, - ]; - - let coef = 1.0 - fg[3] / 255.0; - let a = fg[3] + bg[3] * coef; + if fg[3] == 0 { + return *background; + } + if fg[3] == 255 { + return *foreground; + } + if bg[3] == 0 { + return *foreground; + } + + let bg_alpha = bg[3] as f32 / 255.0; + let fg_alpha = fg[3] as f32 / 255.0; + + let a = fg_alpha + bg_alpha * (1.0 - fg_alpha); + + let blend_channel = |fg_c: u8, bg_c: u8| -> u8 { + let fg_norm = fg_c as f32 / 255.0; + let bg_norm = bg_c as f32 / 255.0; + let blended = (fg_norm * fg_alpha) + (bg_norm * bg_alpha * (1.0 - fg_alpha)); + (blended / a * 255.0).round() as u8 + }; + Color::from_rgba( - ((fg[0] + bg[0] * coef) / a).floor() as u8, - ((fg[1] + bg[1] * coef) / a).floor() as u8, - ((fg[2] + bg[2] * coef) / a).floor() as u8, + blend_channel(fg[0], bg[0]), + blend_channel(fg[1], bg[1]), + blend_channel(fg[2], bg[2]), (a * 255.0).floor() as u8, ) } diff --git a/src/util/mod.rs b/src/util/mod.rs index df6aad7..3d1556a 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,3 +5,5 @@ pub mod drawer; pub use drawer::Drawer; pub mod fonts; + +pub mod signals; diff --git a/src/util/signals.rs b/src/util/signals.rs new file mode 100644 index 0000000..bf41de7 --- /dev/null +++ b/src/util/signals.rs @@ -0,0 +1,175 @@ +use std::{ + any::Any, + cell::{Ref, RefCell}, +}; + +type Callback = Box; + +/// Reactive communication channel for decoupled component interaction +/// +/// Signals implement a publish-subscribe pattern where: +/// - Publishers emit values through [emit](Signal::emit) or [emit_unclonable](Signal::emit_unclonable) +/// - Subscribers register callbacks via [connect](Signal::connect) +/// +/// ### Core Features +/// - **Type-erased values**: All emitted values are passed as `&dyn Any` +/// - **Value history**: Optionally stores last emitted value (see [emit](Signal::emit)) +/// - **Immediate callback invocation**: New connections receive current value immediately +/// +/// ### Behavior Details +/// - **Downcasting responsibility**: Receivers must validate and downcast values +/// - **Callback persistence**: Handlers remain registered until signal destruction +/// +/// ### Usage Notes +/// - Prefer `emit` for cloneable types requiring history +/// - Use `emit_unclonable` for non-cloneable types or when history isn't needed +/// - In capybar, signals are stored in an [Environment](crate::root::Environment)'s +/// `signals` [HashMap](std::collections::HashMap) +/// +/// # Examples +/// ``` +/// use capybar::util::signals::Signal; +/// use std::{cell::RefCell, rc::Rc}; +/// +/// let signal = Signal::new(); +/// let tracker = Rc::new(RefCell::new(0)); +/// +/// // Connect callback that processes i32 values +/// let track = Rc::clone(&tracker); +/// signal.connect(move |data| { +/// if let Some(num) = data.downcast_ref::() { +/// *track.borrow_mut() = *num; +/// } +/// }); +/// +/// // Emit value to all connected callbacks +/// signal.emit(&42i32); +/// assert_eq!(*tracker.borrow(), 42); +/// ``` +#[derive(Default)] +pub struct Signal { + listeners: RefCell>, + last_value: RefCell>>, +} + +impl Signal { + /// Creates a new, empty Signal instance + pub fn new() -> Self { + Signal { + listeners: RefCell::new(Vec::new()), + last_value: RefCell::new(None), + } + } + /// Registers a callback to be invoked on signal emissions + /// + /// The callback will be immediately invoked with the current `last_value` + /// if one exists. All registered callbacks are invoked when [emit](Signal::emit) + /// is called. + /// + /// # Arguments + /// * `callback` - Handler function that receives emitted data as `&dyn Any` + /// + /// Note: Callbacks persist until the Signal is dropped + pub fn connect(&self, callback: F) + where + F: Fn(&dyn Any) + 'static, + { + if let Some(value) = &*self.last_value.borrow() { + callback(&**value); + } + + self.listeners.borrow_mut().push(Box::new(callback)); + } + + /// Emits a value to all connected callbacks + /// + /// This operation: + /// 1. Clones the value (must implement [Any] + [Clone]) + /// 2. Stores the cloned value as the new `last_value` + /// 3. Invokes all callbacks with a reference to the original value + /// + /// Prefer this over [emit_unclonable](Signal::emit_unclonable) when: + /// - You need value history tracking + /// - Your type is cheap to clone + pub fn emit(&self, value: &T) { + let cloned = (*value).clone(); + *self.last_value.borrow_mut() = Some(Box::new(cloned)); + for callback in &*self.listeners.borrow_mut() { + callback(value); + } + } + + /// Emits a value without storing or cloning it + /// + /// Unlike [emit](Signal::emit): + /// - Doesn't update `last_value` + /// - Doesn't require [Clone] implementation + /// - Slightly more efficient for non-cloneable types + /// + /// Use when: + /// - You don't need value history + /// - The value can't be cloned + /// - Callbacks don't need persistent access to the value + pub fn emit_unclonable(&self, value: &T) { + for callback in &*self.listeners.borrow_mut() { + callback(value); + } + } + + /// Returns a read-only reference to the internal last_value storage + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// if let Some(value) = &*signal.last_value_ref() { + /// if let Some(num) = value.downcast_ref::() { + /// assert_eq!(*num, 42); + /// } + /// }; + /// ``` + pub fn last_value_ref(&self) -> Ref<'_, Option>> { + self.last_value.borrow() + } + + /// Processes the last value through a callback function + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// signal.with_last_value(|any| { + /// if let Some(num) = any.and_then(|a| a.downcast_ref::()) { + /// assert_eq!(*num, 42); + /// } + /// }); + /// ``` + pub fn with_last_value(&self, f: F) -> R + where + F: FnOnce(Option<&dyn Any>) -> R, + { + let last_value = self.last_value_ref(); + let any_ref = last_value.as_ref().map(|boxed| &**boxed as &dyn Any); + f(any_ref) + } + + /// Retrieves a cloned copy of the last value if available and of type T + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// if let Some(num) = signal.get_last_value_cloned::() { + /// assert_eq!(num, 42); + /// } + /// ``` + pub fn get_last_value_cloned(&self) -> Option { + self.with_last_value(|opt| opt.and_then(|any| any.downcast_ref::().cloned())) + } +} diff --git a/src/widgets/battery.rs b/src/widgets/battery.rs index 692e35b..ca02bb2 100644 --- a/src/widgets/battery.rs +++ b/src/widgets/battery.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use super::{ text::{Text, TextSettings}, - Style, Widget, WidgetData, WidgetNew, WidgetStyled, + Style, Widget, WidgetData, WidgetError, WidgetNew, WidgetStyled, }; const fn battery_not_charging_default() -> [char; 11] { @@ -171,12 +171,15 @@ impl Widget for Battery { &self.data } - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.percent.borrow_mut().bind(env.clone())?; self.icon.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.icon.borrow_mut().init()?; self.percent.borrow_mut().init()?; @@ -186,7 +189,7 @@ impl Widget for Battery { Ok(()) } - fn draw(&self) -> anyhow::Result<()> { + fn draw(&self) -> anyhow::Result<(), WidgetError> { let info = self.get_info(); let mut prev_charge = self.prev_charge.borrow_mut(); @@ -233,12 +236,18 @@ impl WidgetNew for Battery { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { + let manager = Manager::new(); + if let Err(err) = manager { + return Err(WidgetError::Custom(err.into())); + } + + let manager = manager.unwrap(); Ok(Self { - manager: Manager::new()?, + manager, icon: RefCell::new(Text::new( env.clone(), diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index a90215c..ed5ccc9 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -10,7 +10,7 @@ use crate::{ widgets::{text::Text, Widget}, }; -use super::{text::TextSettings, WidgetData, WidgetNew}; +use super::{text::TextSettings, WidgetData, WidgetError, WidgetNew}; fn default_format() -> String { "%H:%M".to_string() @@ -67,11 +67,11 @@ impl Clock { } impl Widget for Clock { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.text.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { let text = self.text.borrow_mut(); text.init()?; @@ -85,7 +85,7 @@ impl Widget for Clock { Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { self.update(); self.text.borrow_mut().draw() } @@ -98,7 +98,7 @@ impl Widget for Clock { impl WidgetNew for Clock { type Settings = ClockSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/src/widgets/containers/bar.rs b/src/widgets/containers/bar.rs index 55dad65..2e30d41 100644 --- a/src/widgets/containers/bar.rs +++ b/src/widgets/containers/bar.rs @@ -59,7 +59,7 @@ impl Bar { pub fn create_child_left(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.left .borrow_mut() @@ -70,7 +70,7 @@ impl Bar { pub fn create_child_center(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.center .borrow_mut() @@ -81,7 +81,7 @@ impl Bar { pub fn create_child_right(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.right .borrow_mut() @@ -91,7 +91,10 @@ impl Bar { } impl Widget for Bar { - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.left.borrow_mut().bind(Rc::clone(&env))?; self.center.borrow_mut().bind(Rc::clone(&env))?; self.right.borrow_mut().bind(Rc::clone(&env))?; @@ -99,9 +102,9 @@ impl Widget for Bar { Ok(()) } - fn draw(&self) -> anyhow::Result<()> { + fn draw(&self) -> anyhow::Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Bar".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Bar".to_string())); } let data = self.data.borrow_mut(); @@ -168,7 +171,7 @@ impl Widget for Bar { Ok(()) } - fn init(&self) -> anyhow::Result<()> { + fn init(&self) -> Result<(), WidgetError> { let left = self.left.borrow_mut(); let center = self.center.borrow_mut(); let right = self.right.borrow_mut(); @@ -206,7 +209,7 @@ impl WidgetNew for Bar { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { diff --git a/src/widgets/containers/row.rs b/src/widgets/containers/row.rs index a282a22..1578848 100644 --- a/src/widgets/containers/row.rs +++ b/src/widgets/containers/row.rs @@ -60,7 +60,7 @@ pub struct Row { } impl Widget for Row { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.env = Some(Rc::clone(&env)); let mut children = self.children.borrow_mut(); @@ -72,7 +72,7 @@ impl Widget for Row { Ok(()) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { let mut data = self.data.borrow_mut(); let mut children = self.children.borrow_mut(); let border = match self.settings.border { @@ -92,9 +92,9 @@ impl Widget for Row { Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Row".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Row".to_string())); } self.align_children()?; @@ -320,7 +320,7 @@ impl Row { impl WidgetNew for Row { type Settings = RowSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/src/widgets/cpu.rs b/src/widgets/cpu.rs index 94dc75b..944d0b0 100644 --- a/src/widgets/cpu.rs +++ b/src/widgets/cpu.rs @@ -7,7 +7,7 @@ use sysinfo::{CpuRefreshKind, RefreshKind, System}; use super::{ text::{Text, TextSettings}, - Style, Widget, WidgetData, WidgetNew, + Style, Widget, WidgetData, WidgetError, WidgetNew, }; /// Settings of a [CPU] widget @@ -38,7 +38,7 @@ pub struct CPU { sys: RefCell, last_update: RefCell>, - upadte_rate: TimeDelta, + update_rate: TimeDelta, } impl CPU { @@ -48,7 +48,7 @@ impl CPU { sys.global_cpu_usage().round() as usize } - fn align(&self) -> Result<()> { + fn align(&self) { let icon = self.icon.borrow_mut(); let text = self.percent.borrow_mut(); @@ -73,28 +73,31 @@ impl CPU { + text_data.margin.0 + text_data.margin.1 + text_data.width; - - Ok(()) } } impl Widget for CPU { - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.percent.borrow_mut().bind(env.clone())?; self.icon.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.icon.borrow_mut().init()?; self.percent.borrow_mut().init()?; - self.align() + self.align(); + + Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { let mut last_update = self.last_update.borrow_mut(); - if Local::now() - *last_update >= self.upadte_rate { + if Local::now() - *last_update >= self.update_rate { let info = self.get_info(); { @@ -107,7 +110,7 @@ impl Widget for CPU { } } - self.align()?; + self.align(); *last_update = Local::now(); } @@ -126,7 +129,7 @@ impl WidgetNew for CPU { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { @@ -161,7 +164,7 @@ impl WidgetNew for CPU { RefreshKind::nothing().with_cpu(CpuRefreshKind::nothing().with_cpu_usage()), )), - upadte_rate: TimeDelta::milliseconds(settings.update_rate as i64), + update_rate: TimeDelta::milliseconds(settings.update_rate as i64), last_update: RefCell::new( chrono::Local::now() - TimeDelta::milliseconds(settings.update_rate as i64), ), diff --git a/src/widgets/keyboard.rs b/src/widgets/keyboard.rs new file mode 100644 index 0000000..57070b7 --- /dev/null +++ b/src/widgets/keyboard.rs @@ -0,0 +1,166 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use anyhow::Result; +use serde::Deserialize; + +use crate::{ + root::Environment, + widgets::{text::Text, Widget}, +}; + +use super::{text::TextSettings, Style, WidgetData, WidgetError, WidgetNew}; + +/// Settings of a [Keyboard] widget +#[derive(Deserialize, Default, Debug, Clone)] +pub struct KeyboardSettings { + #[serde(default, flatten)] + pub default_data: WidgetData, + + /// Settings for underlying [Text] widget + #[serde(default, flatten)] + pub text_settings: TextSettings, + + #[serde(default, flatten)] + pub style: Style, + + /// Map from underlying layout name to display name + #[serde(default)] + pub layout_mappings: HashMap, +} + +/// Widget displaying current keyboard layout. +pub struct Keyboard { + data: RefCell, + layout_mappings: Rc>, + + icon: RefCell, + text: Rc>, + + env: Option>, +} + +impl Keyboard { + fn align(&self) { + let icon = self.icon.borrow_mut(); + let text = self.text.borrow_mut(); + + let mut icon_data = icon.data().borrow_mut(); + let mut text_data = text.data().borrow_mut(); + let data = &mut self.data.borrow_mut(); + + icon_data.position.0 = data.position.0 + icon_data.margin.0; + icon_data.position.1 = data.position.1 + icon_data.margin.2; + text_data.position.0 = + icon_data.position.0 + icon_data.width + icon_data.margin.1 + text_data.margin.0; + text_data.position.1 = data.position.1 + text_data.margin.2; + + data.height = usize::max( + text_data.position.1 + text_data.height + text_data.margin.3, + icon_data.position.1 + icon_data.height + icon_data.margin.3, + ); + + data.width = icon_data.margin.0 + + icon_data.margin.1 + + icon_data.width + + text_data.margin.0 + + text_data.margin.1 + + text_data.width; + } +} + +impl Widget for Keyboard { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { + self.env = Some(env.clone()); + self.text.borrow_mut().bind(env.clone())?; + self.icon.borrow_mut().bind(env) + } + + fn init(&self) -> Result<(), WidgetError> { + if self.env.is_none() { + return Err(WidgetError::InitWithNoEnv("Keyboard".to_string())); + } + + let signals = self.env.as_ref().unwrap().signals.borrow_mut(); + + if !signals.contains_key("keyboard") { + return Err(WidgetError::NoCorespondingSignal( + "Keyboard".to_string(), + "Keyboard".to_string(), + )); + } + + let signal_text = Rc::clone(&self.text); + let layout_mappings = Rc::clone(&self.layout_mappings); + + signals["keyboard"].connect(move |data| { + if let Some(text) = data.downcast_ref::() { + let layout = if layout_mappings.contains_key(text) { + layout_mappings.get(text).unwrap() + } else { + &text.to_string() + }; + + signal_text.borrow_mut().change_text(layout); + } + }); + + self.icon.borrow_mut().init()?; + self.text.borrow_mut().init()?; + + self.align(); + + Ok(()) + } + + fn draw(&self) -> Result<(), WidgetError> { + self.align(); + + self.text.borrow_mut().draw()?; + self.icon.borrow_mut().draw() + } + + fn data(&self) -> &RefCell { + &self.data + } +} + +impl WidgetNew for Keyboard { + type Settings = KeyboardSettings; + + fn new(env: Option>, settings: Self::Settings) -> Result + where + Self: Sized, + { + Ok(Keyboard { + data: RefCell::new(settings.default_data), + layout_mappings: Rc::new(settings.layout_mappings), + + icon: RefCell::new(Text::new( + env.clone(), + TextSettings { + text: "󰌌".to_string(), + default_data: WidgetData { + margin: (0, 0, 0, 0), + ..WidgetData::default() + }, + fontid: 1, + ..settings.text_settings.clone() + }, + )?), + text: Rc::new(RefCell::new(Text::new( + env, + TextSettings { + text: String::new(), + + default_data: WidgetData { + margin: (5, 0, 2, 0), + ..WidgetData::default() + }, + ..settings.text_settings.clone() + }, + )?)), + + env: None, + }) + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index ad23519..662937f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,28 +3,28 @@ pub mod containers; pub mod battery; pub mod clock; pub mod cpu; +pub mod keyboard; pub mod text; use std::{cell::RefCell, rc::Rc}; -use anyhow::Result; use serde::Deserialize; use thiserror::Error; -use crate::{root::Environment, util::Color}; +use crate::{processes::ProcessSettings, root::Environment, util::Color}; use {battery::BatterySettings, clock::ClockSettings, cpu::CPUSettings, text::TextSettings}; /// A **data structure** that can be used as a widget inside a capybar. pub trait Widget { /// Bind a widget to a new environment. - fn bind(&mut self, env: Rc) -> Result<()>; + fn bind(&mut self, env: Rc) -> Result<(), WidgetError>; - /// Draw an entire widget to a `Drawer` - fn draw(&self) -> Result<()>; + /// Draw an entire widget to a current environment's `Drawer` + fn draw(&self) -> Result<(), WidgetError>; /// Prepare `Widget` for a first draw - fn init(&self) -> Result<()>; + fn init(&self) -> Result<(), WidgetError>; /// Return `WidgetData` associated to the widget fn data(&self) -> &RefCell; @@ -37,7 +37,7 @@ pub trait Widget { pub trait WidgetNew: Widget { type Settings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized; } @@ -47,8 +47,23 @@ pub enum WidgetError { #[error("Invalid widget bounds")] InvalidBounds, + /// Argument is a name of a widget #[error("Trying to draw a widget \"{0}\" not bound to any environment")] DrawWithNoEnv(String), + + /// Argument is a name of a widget + #[error("Trying to initialise a widget \"{0}\" not bound to any environment")] + InitWithNoEnv(String), + + /// Arguments are a name of a widget and a name of coresponding process + #[error( + "When initialising widget \"{0}\" no coresponding signal was found. + Maybe process \"{1}\" was not created?" + )] + NoCorespondingSignal(String, String), + + #[error(transparent)] + Custom(#[from] anyhow::Error), } /// Global common data used by `Widget` data structure. @@ -104,7 +119,7 @@ pub trait WidgetStyled: Widget { fn style_mut(&mut self) -> &mut Style; - fn apply_style(&self) -> Result<()> { + fn apply_style(&self) -> Result<(), WidgetError> { let mut data = self.data().borrow_mut(); let style = self.style(); @@ -128,4 +143,5 @@ pub enum WidgetsList { Battery(BatterySettings), #[serde(rename = "cpu")] CPU(CPUSettings), + Keyboard(keyboard::KeyboardSettings, ProcessSettings), } diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 96a7602..474ea90 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -91,7 +91,7 @@ impl Text { } impl Widget for Text { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.env = Some(env); let _env = self.env.as_mut().unwrap(); @@ -107,16 +107,16 @@ impl Widget for Text { Ok(()) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.update_width(); self.data.borrow_mut().height = self.layout.height() as usize; Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Text".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Text".to_string())); } let font = &fonts::fonts_vec()[self.settings.fontid]; @@ -146,7 +146,7 @@ impl Widget for Text { impl WidgetNew for Text { type Settings = TextSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000..83c8c0a --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1 @@ +mod util; diff --git a/tests/util/color.rs b/tests/util/color.rs new file mode 100644 index 0000000..02acef3 --- /dev/null +++ b/tests/util/color.rs @@ -0,0 +1,205 @@ +#[cfg(test)] +mod tests { + use capybar::util::Color; + + #[test] + fn test_from_rgba() { + let c = Color::from_rgba(0x12, 0x34, 0x56, 0x78); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_from_hex() { + let c = Color::from_hex(0x12345678); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_from_be_bytes() { + let bytes = [0x12, 0x34, 0x56, 0x78]; + let c = Color::from_be_bytes(&bytes); + assert_eq!(c.to_be_bytes(), bytes); + } + + #[test] + fn test_from_le_bytes() { + let bytes = [0x78, 0x56, 0x34, 0x12]; + let c = Color::from_le_bytes(&bytes); + assert_eq!(c.to_le_bytes(), bytes); + } + + #[test] + fn test_from_rgba_f32_valid() { + let c = Color::from_rgba_f32(0.0, 0.5, 1.0, 0.0).unwrap(); + assert_eq!(c.r(), 0); + assert_eq!(c.g(), 128); + assert_eq!(c.b(), 255); + assert_eq!(c.a(), 0); + } + + #[test] + fn test_from_rgba_f32_out_of_range() { + assert!(Color::from_rgba_f32(-0.1, 0.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(1.1, 0.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, -1.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 2.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 0.0, -0.5).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 0.0, 1.5).is_none()); + } + + #[test] + fn test_setters() { + let mut c = Color::from_rgba(0, 0, 0, 0); + + c.set_r(0x12); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + c.set_g(0x34); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + c.set_b(0x56); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0); + + c.set_a(0x78); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_blending_edge_cases() { + let bg = Color::from_rgba(100, 150, 200, 255); + let fg = Color::from_rgba(0, 0, 0, 0); + assert_eq!(Color::blend_colors(&bg, &fg), bg); + + let fg = Color::from_rgba(50, 100, 150, 255); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + + let bg = Color::from_rgba(0, 0, 0, 0); + let fg = Color::from_rgba(75, 125, 175, 128); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + + let fg = Color::from_rgba(0, 0, 0, 0); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + } + + #[test] + fn test_blending_accuracy() { + let white = Color::from_rgba(255, 255, 255, 255); + let gray = Color::from_rgba(128, 128, 128, 128); + let blended = Color::blend_colors(&white, &gray); + assert_eq!(blended, Color::from_rgba(191, 191, 191, 255)); + + let blue = Color::from_rgba(0, 0, 255, 255); + let red = Color::from_rgba(255, 0, 0, 128); + let blended = Color::blend_colors(&blue, &red); + assert_eq!(blended, Color::from_rgba(128, 0, 127, 255)); + + let bg = Color::from_rgba(100, 100, 100, 128); + let fg = Color::from_rgba(200, 200, 200, 128); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(167, 167, 167, 191)); + } + + #[test] + fn test_blend_semi_transparent() { + let bg = Color::from_rgba(100, 100, 100, 255); + let fg = Color::from_rgba(200, 200, 200, 128); + let blended = Color::blend_colors(&bg, &fg); + + assert!(blended.r() > 100 && blended.r() < 200); + assert!(blended.g() > 100 && blended.g() < 200); + assert!(blended.b() > 100 && blended.b() < 200); + } + + #[test] + fn test_blending_alpha_boundaries() { + let bg = Color::from_rgba(0, 0, 0, 255); + let fg = Color::from_rgba(255, 255, 255, 1); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended.a(), 255); + + let fg = Color::from_rgba(255, 0, 0, 254); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(254, 0, 0, 255)); + } + + #[test] + fn test_blending_extreme_values() { + let white = Color::from_rgba(255, 255, 255, 255); + let black = Color::from_rgba(0, 0, 0, 255); + assert_eq!(Color::blend_colors(&white, &black), black); + + let transparent = Color::from_rgba(0, 0, 0, 0); + let visible = Color::from_rgba(10, 20, 30, 40); + assert_eq!(Color::blend_colors(&transparent, &visible), visible); + + let bg = Color::from_rgba(255, 255, 255, 255); + let fg = Color::from_rgba(255, 255, 255, 255); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(255, 255, 255, 255)); + } + #[test] + fn test_no_overflow() { + let c = Color::from_rgba(255, 255, 255, 255); + assert_eq!(c.r(), 255); + assert_eq!(c.g(), 255); + assert_eq!(c.b(), 255); + assert_eq!(c.a(), 255); + + let c = Color::from_rgba(0, 0, 0, 0); + assert_eq!(c.r(), 0); + assert_eq!(c.g(), 0); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + let bg = Color::from_rgba(255, 255, 255, 255); + let fg = Color::from_rgba(0, 0, 0, 0); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, bg); + } + + #[test] + fn test_display() { + assert_eq!( + format!("{}", Color::from_rgba(0x12, 0x34, 0x56, 0x78)), + "0x12345678" + ); + assert_eq!(format!("{}", Color::from_rgba(0, 0, 0, 0)), "0x00000000"); + assert_eq!( + format!("{}", Color::from_rgba(0xFF, 0xFF, 0xFF, 0xFF)), + "0xffffffff" + ); + assert_eq!( + format!("{}", Color::from_rgba(0xAB, 0xCD, 0xEF, 0x12)), + "0xabcdef12" + ); + } + + #[test] + fn test_byte_conversion_roundtrip() { + let original = Color::from_rgba(0x12, 0x34, 0x56, 0x78); + + let be_bytes = original.to_be_bytes(); + assert_eq!(Color::from_be_bytes(&be_bytes), original); + + let le_bytes = original.to_le_bytes(); + assert_eq!(Color::from_le_bytes(&le_bytes), original); + } +} diff --git a/tests/util/mod.rs b/tests/util/mod.rs new file mode 100644 index 0000000..2c57d70 --- /dev/null +++ b/tests/util/mod.rs @@ -0,0 +1,2 @@ +mod color; +mod signals; diff --git a/tests/util/signals.rs b/tests/util/signals.rs new file mode 100644 index 0000000..124d9b1 --- /dev/null +++ b/tests/util/signals.rs @@ -0,0 +1,264 @@ +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::rc::Rc; + + use capybar::util::signals::Signal; + + #[test] + fn initial_state() { + let signal = Signal::new(); + assert!(signal.last_value_ref().is_none()); + } + + #[test] + fn emit_store_last_value() { + let signal = Signal::new(); + signal.emit(&42i32); + assert_eq!(signal.get_last_value_cloned::(), Some(42)); + } + + #[test] + fn emit_unclonable_does_not_store() { + let signal = Signal::new(); + signal.emit(&100u8); + signal.emit_unclonable(&200u8); + assert_eq!(signal.get_last_value_cloned::(), Some(100)); + } + #[test] + + fn connect_triggers_immediately() { + let signal = Signal::new(); + signal.emit(&"initial"); + let triggered = Rc::new(Cell::new(false)); + let trigger_clone = Rc::clone(&triggered); + signal.connect(move |data| { + if data.downcast_ref::<&str>().is_some() { + trigger_clone.set(true); + } + }); + assert!(triggered.get()); + } + + #[test] + fn no_immediate_trigger_without_last_value() { + let signal = Signal::new(); + let triggered = Rc::new(Cell::new(false)); + let trigger_clone = Rc::clone(&triggered); + signal.connect(move |_| trigger_clone.set(true)); + assert!(!triggered.get()); + } + + #[test] + fn basic_usage() { + let signal = Signal::new(); + let last_value = Rc::new(RefCell::new(None)); + let last_value_clone = Rc::clone(&last_value); + + signal.connect(move |data| { + if let Some(value) = data.downcast_ref::() { + *last_value_clone.borrow_mut() = Some(*value); + } + }); + + assert!(last_value.borrow().is_none()); + + signal.emit(&42i32); + assert_eq!(*last_value.borrow(), Some(42)); + + signal.emit(&100i32); + assert_eq!(*last_value.borrow(), Some(100)); + + signal.emit_unclonable(&42i32); + assert_eq!(*last_value.borrow(), Some(42)); + + signal.emit_unclonable(&100i32); + assert_eq!(*last_value.borrow(), Some(100)); + } + + #[test] + #[should_panic(expected = "already borrowed")] + fn recursive_emit_panics() { + let signal = Signal::new(); + let signal_clone = Rc::new(signal); + let weak = Rc::downgrade(&signal_clone); + signal_clone.connect(move |_| { + if let Some(s) = weak.upgrade() { + s.emit(&"recursive"); + } + }); + signal_clone.emit(&"trigger"); + } + + #[test] + fn type_erasure_handling() { + let signal = Signal::new(); + signal.emit(&1i32); + signal.emit(&"string"); + let result = signal.get_last_value_cloned::<&str>(); + assert_eq!(result, Some("string")); + } + + #[test] + fn with_last_value_helper() { + let signal = Signal::new(); + signal.emit(&999u64); + signal.with_last_value(|opt| { + assert_eq!( + opt.and_then(|v| v.downcast_ref::()).copied(), + Some(999) + ); + }); + } + + #[test] + fn non_clone_type_emission() { + struct NonClone(i32); + let signal = Signal::new(); + signal.emit_unclonable(&NonClone(42)); + // Verify emission occurred via callback + let received = Rc::new(Cell::new(None)); + let recv_clone = Rc::clone(&received); + signal.connect(move |data| { + if let Some(nc) = data.downcast_ref::() { + recv_clone.set(Some(nc.0)); + } + }); + signal.emit_unclonable(&NonClone(100)); + assert_eq!(received.get(), Some(100)); + } + + #[test] + fn get_last_value_wrong_type() { + let signal = Signal::new(); + signal.emit(&5i16); + assert!(signal.get_last_value_cloned::().is_none()); + } + + #[test] + fn reacts_to_correct_type() { + let signal = Signal::new(); + let state = Rc::new(RefCell::new(0)); + let state_clone = Rc::clone(&state); + + signal.connect(move |data| { + if let Some(value) = data.downcast_ref::() { + *state_clone.borrow_mut() += value; + } + }); + + signal.emit(&42i32); + signal.emit(&"ignore"); + assert_eq!(*state.borrow(), 42); + + signal.emit_unclonable(&42i32); + signal.emit_unclonable(&"ignore"); + assert_eq!(*state.borrow(), 84); + } + + #[test] + fn ignores_wrong_type() { + let signal = Signal::new(); + let called = Rc::new(RefCell::new(true)); + let called_clone = Rc::clone(&called); + + signal.connect(move |data| { + if data.downcast_ref::().is_some() { + *called_clone.borrow_mut() = false; + } + }); + + signal.emit(&"not a bool"); + signal.emit_unclonable(&"not a bool"); + assert!(*called.borrow()); + } + + #[test] + fn latest_emit_applied() { + let signal = Signal::new(); + let value = Rc::new(RefCell::new(0)); + let value_clone = Rc::clone(&value); + + signal.connect(move |data| { + if let Some(v) = data.downcast_ref::() { + *value_clone.borrow_mut() = *v; + } + }); + + signal.emit(&1i32); + signal.emit(&2i32); + signal.emit(&3i32); + + assert_eq!(*value.borrow(), 3); + + signal.emit_unclonable(&1i32); + signal.emit_unclonable(&2i32); + signal.emit_unclonable(&3i32); + + assert_eq!(*value.borrow(), 3); + } + + #[test] + fn no_panic_on_no_listeners() { + let signal = Signal::new(); + signal.emit(&"test"); + signal.emit_unclonable(&42i32); + } + + #[test] + fn emit_many_listeners() { + let signal = Signal::new(); + let count = Rc::new(RefCell::new(0)); + + // Add 1000 listeners + for _ in 0..1000 { + let count_clone = Rc::clone(&count); + signal.connect(move |data| { + if data.downcast_ref::().is_some() { + *count_clone.borrow_mut() += 1; + } + }); + } + + signal.emit(&1); + assert_eq!(*count.borrow(), 1000); + + signal.emit_unclonable(&1); + assert_eq!(*count.borrow(), 2000); + } + + #[test] + fn mixed_types_in_callbacks() { + let signal = Signal::new(); + let int_state = Rc::new(RefCell::new(0)); + let string_state = Rc::new(RefCell::new(String::new())); + + let int_clone = Rc::clone(&int_state); + signal.connect(move |data| { + if let Some(v) = data.downcast_ref::() { + *int_clone.borrow_mut() += v; + } + }); + + let str_clone = Rc::clone(&string_state); + signal.connect(move |data| { + if let Some(s) = data.downcast_ref::<&str>() { + *str_clone.borrow_mut() = s.to_string(); + } + }); + + signal.emit(&10); + signal.emit(&"text"); + signal.emit(&20); + + assert_eq!(*int_state.borrow(), 30); + assert_eq!(*string_state.borrow(), "text"); + + signal.emit_unclonable(&10); + signal.emit_unclonable(&"test"); + signal.emit_unclonable(&20); + + assert_eq!(*int_state.borrow(), 60); + assert_eq!(*string_state.borrow(), "test"); + } +}