commit 311f517f671aac0465e9c0a6adace91a75e2699c Author: Nils Pukropp Date: Fri Mar 13 15:32:43 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91a2647 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# This file should only ignore things that are generated during a `x.py` build, +# generated by common IDEs, and optional files controlled by the user that +# affect the build (such as bootstrap.toml). +# In particular, things like `mir_dump` should not be listed here; they are only +# created during manual debugging and many people like to clean up instead of +# having git ignore such leftovers. You can use `.git/info/exclude` to +# configure your local ignore list. + +## File system +.DS_Store +desktop.ini + +## Editor +*.swp +*.swo +Session.vim +.cproject +.idea +*.iml +.vscode +.project +.vim/ +.helix/ +.zed/ +.favorites.json +.settings/ +.vs/ +.dir-locals.el + +## Tool +.valgrindrc +.cargo +# Included because it is part of the test case +!/tests/run-make/thumb-none-qemu/example/.cargo + +## Configuration +/bootstrap.toml +/config.toml +/Makefile +config.mk +config.stamp +no_llvm_build + +## Build +/dl/ +/doc/ +/inst/ +/llvm/ +/mingw-build/ +/build +/build-rust-analyzer +/dist/ +/unicode-downloads +/target +/library/target +/src/bootstrap/target +/src/ci/citool/target +/src/tools/x/target +# Created by `x vendor` +/vendor +# Created by default with `src/ci/docker/run.sh` +/obj/ +# Created by nix dev shell / .envrc +src/tools/nix-dev-shell/flake.lock + +## ICE reports +rustc-ice-*.txt + +## Temporary files +*~ +\#* +\#*\# +.#* + +## Tags +tags +tags.* +TAGS +TAGS.* + +## Python +__pycache__/ +*.py[cod] +*$py.class + +## Node +node_modules +/src/doc/rustc-dev-guide/mermaid.min.js + +## Rustdoc GUI tests +tests/rustdoc-gui/src/**.lock + +## Test dashboard +.citool-cache/ +test-dashboard/ + +## direnv +/.envrc +/.direnv/ + +## nix +/flake.nix +flake.lock +/default.nix + +# Before adding new lines, see the comment at the top. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a3f5329 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,794 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fluxo-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "fs4", + "serde", + "serde_json", + "sysinfo", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6303db7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fluxo-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.0", features = ["derive"] } +fs4 = "0.13.1" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +sysinfo = "0.38.4" +toml = "1.0.6" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..97bf934 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,38 @@ +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +#[derive(Deserialize, Default)] +pub struct Config { + #[serde(default)] + pub network: NetworkConfig, +} + +#[derive(Deserialize)] +pub struct NetworkConfig { + pub format: String, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + format: "{interface} ({ip}):  {rx} MB/s  {tx} MB/s".to_string(), + } + } +} + +pub fn load_config() -> Config { + let config_dir = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/")); + PathBuf::from(home).join(".config") + }); + let config_path = config_dir.join("fluxo/config.toml"); + + if let Ok(content) = fs::read_to_string(config_path) { + toml::from_str(&content).unwrap_or_default() + } else { + Config::default() + } +} diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..ea70257 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,104 @@ +use crate::config::Config; +use crate::ipc::SOCKET_PATH; +use crate::modules::network::NetworkDaemon; +use crate::modules::hardware::HardwareDaemon; +use crate::modules::WaybarModule; +use crate::state::{AppState, SharedState}; +use anyhow::Result; +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::UnixListener; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; +use tracing::{info, warn, error, debug}; + +pub fn run_daemon() -> Result<()> { + if fs::metadata(SOCKET_PATH).is_ok() { + debug!("Removing stale socket file: {}", SOCKET_PATH); + fs::remove_file(SOCKET_PATH)?; + } + + let state: SharedState = Arc::new(RwLock::new(AppState::default())); + let listener = UnixListener::bind(SOCKET_PATH)?; + let config = crate::config::load_config(); + let config = Arc::new(config); + + // Spawn the background polling thread + let poll_state = Arc::clone(&state); + thread::spawn(move || { + info!("Starting background polling thread"); + let mut network_daemon = NetworkDaemon::new(); + let mut hardware_daemon = HardwareDaemon::new(); + loop { + network_daemon.poll(Arc::clone(&poll_state)); + hardware_daemon.poll(Arc::clone(&poll_state)); + thread::sleep(Duration::from_secs(1)); + } + }); + + info!("Fluxo daemon successfully bound to socket: {}", SOCKET_PATH); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let state_clone = Arc::clone(&state); + let config_clone = Arc::clone(&config); + thread::spawn(move || { + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut request = String::new(); + if let Err(e) = reader.read_line(&mut request) { + error!("Failed to read from IPC stream: {}", e); + return; + } + + let request = request.trim(); + if request.is_empty() { return; } + + let parts: Vec<&str> = request.split_whitespace().collect(); + if let Some(module_name) = parts.first() { + debug!(module = module_name, args = ?&parts[1..], "Handling IPC request"); + let response = handle_request(*module_name, &parts[1..], &state_clone, &config_clone); + if let Err(e) = stream.write_all(response.as_bytes()) { + error!("Failed to write IPC response: {}", e); + } + } + }); + } + Err(e) => error!("Failed to accept incoming connection: {}", e), + } + } + + Ok(()) +} + +fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config: &Config) -> String { + debug!(module = module_name, args = ?args, "Handling request"); + + let result = match module_name { + "net" | "network" => crate::modules::network::NetworkModule.run(config, state, args), + "cpu" => crate::modules::cpu::CpuModule.run(config, state, args), + "mem" | "memory" => crate::modules::memory::MemoryModule.run(config, state, args), + "disk" => crate::modules::disk::DiskModule.run(config, state, args), + "pool" | "btrfs" => crate::modules::btrfs::BtrfsModule.run(config, state, args), + "vol" => crate::modules::audio::AudioModule.run(config, state, &["sink", args.get(0).unwrap_or(&"show")]), + "mic" => crate::modules::audio::AudioModule.run(config, state, &["source", args.get(0).unwrap_or(&"show")]), + _ => { + warn!("Received request for unknown module: '{}'", module_name); + Err(anyhow::anyhow!("Unknown module: {}", module_name)) + }, + }; + + match result { + Ok(output) => serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string()), + Err(e) => { + let err_out = crate::output::WaybarOutput { + text: "Error".to_string(), + tooltip: Some(e.to_string()), + class: Some("error".to_string()), + percentage: None, + }; + serde_json::to_string(&err_out).unwrap_or_else(|_| "{}".to_string()) + } + } +} diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..1ac41cf --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,27 @@ +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use tracing::debug; + +pub const SOCKET_PATH: &str = "/tmp/fluxo.sock"; + +pub fn request_data(module: &str, args: &[String]) -> anyhow::Result { + debug!(module, ?args, "Connecting to daemon socket: {}", SOCKET_PATH); + let mut stream = UnixStream::connect(SOCKET_PATH)?; + + // Send module and args + let mut request = module.to_string(); + for arg in args { + request.push(' '); + request.push_str(arg); + } + request.push('\n'); + + debug!("Sending IPC request: {}", request.trim()); + stream.write_all(request.as_bytes())?; + + let mut response = String::new(); + stream.read_to_string(&mut response)?; + debug!("Received IPC response: {}", response); + + Ok(response) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7bf5280 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,118 @@ +mod config; +mod daemon; +mod ipc; +mod modules; +mod output; +mod state; + +use clap::{Parser, Subcommand}; +use std::process; +use tracing::{error, info}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +#[derive(Parser)] +#[command(name = "fluxo")] +#[command(about = "A high-performance daemon/client for Waybar custom modules", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Start the background polling daemon + Daemon, + /// Network speed module + #[command(alias = "network")] + Net, + /// CPU usage and temp module + Cpu, + /// Memory usage module + #[command(alias = "memory")] + Mem, + /// Disk usage module (path defaults to /) + Disk { + #[arg(default_value = "/")] + path: String, + }, + /// Storage pool aggregate module (e.g., btrfs) + #[command(alias = "btrfs")] + Pool { + #[arg(default_value = "btrfs")] + kind: String, + }, + /// Audio volume (sink) control + Vol { + /// Cycle to the next available output device + #[arg(short, long)] + cycle: bool, + }, + /// Microphone (source) control + Mic { + /// Cycle to the next available input device + #[arg(short, long)] + cycle: bool, + }, +} + +fn main() { + // Initialize professional logging + tracing_subscriber::registry() + .with(fmt::layer().with_target(false).pretty()) + .with(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) + .init(); + + let cli = Cli::parse(); + + match &cli.command { + Commands::Daemon => { + info!("Starting Fluxo daemon..."); + if let Err(e) = daemon::run_daemon() { + error!("Daemon failed: {}", e); + process::exit(1); + } + } + Commands::Net => { + handle_ipc_response(ipc::request_data("net", &[])); + } + Commands::Cpu => { + handle_ipc_response(ipc::request_data("cpu", &[])); + } + Commands::Mem => { + handle_ipc_response(ipc::request_data("mem", &[])); + } + Commands::Disk { path } => { + handle_ipc_response(ipc::request_data("disk", &[path.clone()])); + } + Commands::Pool { kind } => { + handle_ipc_response(ipc::request_data("pool", &[kind.clone()])); + } + Commands::Vol { cycle } => { + let action = if *cycle { "cycle" } else { "show" }; + handle_ipc_response(ipc::request_data("vol", &[action.to_string()])); + } + Commands::Mic { cycle } => { + let action = if *cycle { "cycle" } else { "show" }; + handle_ipc_response(ipc::request_data("mic", &[action.to_string()])); + } + } +} + +fn handle_ipc_response(response: anyhow::Result) { + match response { + Ok(json_str) => { + println!("{}", json_str); + } + Err(e) => { + // Provide a graceful fallback JSON if the daemon isn't running + let err_out = output::WaybarOutput { + text: "Daemon offline".to_string(), + tooltip: Some(e.to_string()), + class: Some("error".to_string()), + percentage: None, + }; + println!("{}", serde_json::to_string(&err_out).unwrap()); + process::exit(1); + } + } +} diff --git a/src/modules/audio.rs b/src/modules/audio.rs new file mode 100644 index 0000000..7e2d880 --- /dev/null +++ b/src/modules/audio.rs @@ -0,0 +1,154 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::{Result, anyhow}; +use std::process::Command; + +pub struct AudioModule; + +impl WaybarModule for AudioModule { + fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result { + let target_type = args.first().unwrap_or(&"sink"); + let action = args.get(1).unwrap_or(&"show"); + + match *action { + "cycle" => { + self.cycle_device(target_type)?; + return Ok(WaybarOutput { + text: String::new(), + tooltip: None, + class: None, + percentage: None, + }); + } + "show" | _ => { + self.get_status(target_type) + } + } + } +} + +impl AudioModule { + fn get_status(&self, target_type: &str) -> Result { + let target = if target_type == "sink" { "@DEFAULT_AUDIO_SINK@" } else { "@DEFAULT_AUDIO_SOURCE@" }; + + // Get volume and mute status via wpctl (faster than pactl for this) + let output = Command::new("wpctl") + .args(["get-volume", target]) + .output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + + // Output format: "Volume: 0.50" or "Volume: 0.50 [MUTED]" + let parts: Vec<&str> = stdout.trim().split_whitespace().collect(); + if parts.len() < 2 { + return Err(anyhow!("Could not parse wpctl output: {}", stdout)); + } + + let vol_val: f64 = parts[1].parse().unwrap_or(0.0); + let vol = (vol_val * 100.0).round() as u8; + let display_vol = std::cmp::min(vol, 100); + let muted = stdout.contains("[MUTED]"); + + let description = self.get_description(target_type)?; + let name = if description.len() > 20 { + format!("{}...", &description[..17]) + } else { + description.clone() + }; + + let (text, class) = if muted { + let icon = if target_type == "sink" { "" } else { "" }; + (format!("{} {}", name, icon), "muted") + } else { + let icon = if target_type == "sink" { + if display_vol <= 30 { "" } + else if display_vol <= 60 { "" } + else { "" } + } else { + "" + }; + (format!("{} {}% {}", name, display_vol, icon), "unmuted") + }; + + Ok(WaybarOutput { + text, + tooltip: Some(description), + class: Some(class.to_string()), + percentage: Some(display_vol), + }) + } + + fn get_description(&self, target_type: &str) -> Result { + // Get the default device name + let info_output = Command::new("pactl").arg("info").output()?; + let info_stdout = String::from_utf8_lossy(&info_output.stdout); + let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; + + let default_dev = info_stdout.lines() + .find(|l| l.contains(search_key)) + .and_then(|l| l.split(':').nth(1)) + .map(|s| s.trim()) + .ok_or_else(|| anyhow!("Default {} not found", target_type))?; + + // Get the description of that device + let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; + let list_output = Command::new("pactl").args(["list", list_cmd]).output()?; + let list_stdout = String::from_utf8_lossy(&list_output.stdout); + + let mut current_name = String::new(); + for line in list_stdout.lines() { + if line.trim().starts_with("Name: ") { + current_name = line.split(':').nth(1).unwrap_or("").trim().to_string(); + } + if current_name == default_dev && line.trim().starts_with("Description: ") { + return Ok(line.split(':').nth(1).unwrap_or("").trim().to_string()); + } + } + + Ok(default_dev.to_string()) + } + + fn cycle_device(&self, target_type: &str) -> Result<()> { + let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; + let output = Command::new("pactl").args(["list", "short", list_cmd]).output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + + let mut devices: Vec = stdout.lines() + .filter_map(|l| { + let parts: Vec<&str> = l.split_whitespace().collect(); + if parts.len() >= 2 { + let name = parts[1].to_string(); + if target_type == "source" && name.contains(".monitor") { + None + } else { + Some(name) + } + } else { + None + } + }) + .collect(); + + if devices.is_empty() { return Ok(()); } + + let info_output = Command::new("pactl").arg("info").output()?; + let info_stdout = String::from_utf8_lossy(&info_output.stdout); + let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; + + let current_dev = info_stdout.lines() + .find(|l| l.contains(search_key)) + .and_then(|l| l.split(':').nth(1)) + .map(|s| s.trim()) + .unwrap_or(""); + + let current_index = devices.iter().position(|d| d == current_dev).unwrap_or(0); + let next_index = (current_index + 1) % devices.len(); + let next_dev = &devices[next_index]; + + let set_cmd = if target_type == "sink" { "set-default-sink" } else { "set-default-source" }; + Command::new("pactl").args([set_cmd, next_dev]).status()?; + + Ok(()) + } +} diff --git a/src/modules/btrfs.rs b/src/modules/btrfs.rs new file mode 100644 index 0000000..242bfbc --- /dev/null +++ b/src/modules/btrfs.rs @@ -0,0 +1,53 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use sysinfo::Disks; + +pub struct BtrfsModule; + +impl WaybarModule for BtrfsModule { + fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result { + let disks = Disks::new_with_refreshed_list(); + let mut total_used: f64 = 0.0; + let mut total_size: f64 = 0.0; + + for disk in &disks { + if disk.file_system().to_string_lossy().to_lowercase().contains("btrfs") { + let size = disk.total_space() as f64; + let available = disk.available_space() as f64; + total_size += size; + total_used += size - available; + } + } + + if total_size == 0.0 { + return Ok(WaybarOutput { + text: "No BTRFS".to_string(), + tooltip: None, + class: Some("normal".to_string()), + percentage: None, + }); + } + + let used_gb = total_used / 1024.0 / 1024.0 / 1024.0; + let size_gb = total_size / 1024.0 / 1024.0 / 1024.0; + let percentage = (total_used / total_size) * 100.0; + + let class = if percentage > 95.0 { + "max" + } else if percentage > 80.0 { + "high" + } else { + "normal" + }; + + Ok(WaybarOutput { + text: format!("{:.0}G / {:.0}G", used_gb, size_gb), + tooltip: Some(format!("BTRFS Usage: {:.1}%", percentage)), + class: Some(class.to_string()), + percentage: Some(percentage as u8), + }) + } +} diff --git a/src/modules/cpu.rs b/src/modules/cpu.rs new file mode 100644 index 0000000..a885303 --- /dev/null +++ b/src/modules/cpu.rs @@ -0,0 +1,40 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; + +pub struct CpuModule; + +impl WaybarModule for CpuModule { + fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + let (usage, temp, model) = { + if let Ok(state_lock) = state.read() { + ( + state_lock.cpu.usage, + state_lock.cpu.temp, + state_lock.cpu.model.clone(), + ) + } else { + (0.0, 0.0, String::from("Unknown")) + } + }; + + let text = format!("{:.1}% {:.1}C", usage, temp); + + let class = if usage > 95.0 { + "max" + } else if usage > 75.0 { + "high" + } else { + "normal" + }; + + Ok(WaybarOutput { + text: format!("CPU: {}", text), + tooltip: Some(model), + class: Some(class.to_string()), + percentage: Some(usage as u8), + }) + } +} diff --git a/src/modules/disk.rs b/src/modules/disk.rs new file mode 100644 index 0000000..8664e4c --- /dev/null +++ b/src/modules/disk.rs @@ -0,0 +1,46 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use sysinfo::Disks; + +pub struct DiskModule; + +impl WaybarModule for DiskModule { + fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result { + let mountpoint = args.first().unwrap_or(&"/"); + + let disks = Disks::new_with_refreshed_list(); + for disk in &disks { + if disk.mount_point().to_string_lossy() == *mountpoint { + let total = disk.total_space() as f64; + let available = disk.available_space() as f64; + let used = total - available; + + let used_gb = used / 1024.0 / 1024.0 / 1024.0; + let total_gb = total / 1024.0 / 1024.0 / 1024.0; + let free_gb = available / 1024.0 / 1024.0 / 1024.0; + + let percentage = if total > 0.0 { (used / total) * 100.0 } else { 0.0 }; + + let class = if percentage > 95.0 { + "max" + } else if percentage > 80.0 { + "high" + } else { + "normal" + }; + + return Ok(WaybarOutput { + text: format!("{} {:.1}G/{:.1}G", mountpoint, used_gb, total_gb), + tooltip: Some(format!("Used: {:.1}G\nTotal: {:.1}G\nFree: {:.1}G", used_gb, total_gb, free_gb)), + class: Some(class.to_string()), + percentage: Some(percentage as u8), + }); + } + } + + Err(anyhow::anyhow!("Mountpoint {} not found", mountpoint)) + } +} diff --git a/src/modules/hardware.rs b/src/modules/hardware.rs new file mode 100644 index 0000000..ad6be6f --- /dev/null +++ b/src/modules/hardware.rs @@ -0,0 +1,52 @@ +use crate::state::SharedState; +use sysinfo::{Components, System}; + +pub struct HardwareDaemon { + sys: System, + components: Components, +} + +impl HardwareDaemon { + pub fn new() -> Self { + let mut sys = System::new_all(); + sys.refresh_all(); + let components = Components::new_with_refreshed_list(); + Self { sys, components } + } + + pub fn poll(&mut self, state: SharedState) { + self.sys.refresh_cpu_usage(); + self.sys.refresh_memory(); + self.components.refresh(true); + + let cpu_usage = self.sys.global_cpu_usage(); + let cpu_model = self.sys.cpus().first().map(|c| c.brand().to_string()).unwrap_or_else(|| "Unknown".to_string()); + + // Try to find a reasonable CPU temperature + // Often 'coretemp' or 'k10temp' depending on AMD/Intel + let mut cpu_temp = 0.0; + for component in &self.components { + let label = component.label().to_lowercase(); + if label.contains("tctl") || label.contains("cpu") || label.contains("package") || label.contains("temp1") { + if let Some(temp) = component.temperature() { + cpu_temp = temp as f64; + if cpu_temp > 0.0 { break; } + } + } + } + + let total_mem = self.sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + // Accurate used memory matching htop/free (Total - Available) + let available_mem = self.sys.available_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + let used_mem = total_mem - available_mem; + + if let Ok(mut state_lock) = state.write() { + state_lock.cpu.usage = cpu_usage as f64; + state_lock.cpu.temp = cpu_temp as f64; + state_lock.cpu.model = cpu_model; + + state_lock.memory.total_gb = total_mem; + state_lock.memory.used_gb = used_mem; + } + } +} diff --git a/src/modules/memory.rs b/src/modules/memory.rs new file mode 100644 index 0000000..c949aa8 --- /dev/null +++ b/src/modules/memory.rs @@ -0,0 +1,39 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; + +pub struct MemoryModule; + +impl WaybarModule for MemoryModule { + fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + let (used_gb, total_gb) = { + if let Ok(state_lock) = state.read() { + ( + state_lock.memory.used_gb, + state_lock.memory.total_gb, + ) + } else { + (0.0, 0.0) + } + }; + + let ratio = if total_gb > 0.0 { (used_gb / total_gb) * 100.0 } else { 0.0 }; + + let class = if ratio > 95.0 { + "max" + } else if ratio > 75.0 { + "high" + } else { + "normal" + }; + + Ok(WaybarOutput { + text: format!("{:.2}/{:.2}GB", used_gb, total_gb), + tooltip: None, + class: Some(class.to_string()), + percentage: Some(ratio as u8), + }) + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..687f27a --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,17 @@ +pub mod network; +pub mod cpu; +pub mod memory; +pub mod hardware; +pub mod disk; +pub mod btrfs; +pub mod audio; + +use crate::config::Config; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; + +pub trait WaybarModule { + fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result; +} + diff --git a/src/modules/network.rs b/src/modules/network.rs new file mode 100644 index 0000000..e66d2fe --- /dev/null +++ b/src/modules/network.rs @@ -0,0 +1,179 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::{debug, warn}; + +pub struct NetworkModule; + +pub struct NetworkDaemon { + last_time: u64, + last_rx_bytes: u64, + last_tx_bytes: u64, +} + +impl NetworkDaemon { + pub fn new() -> Self { + Self { + last_time: 0, + last_rx_bytes: 0, + last_tx_bytes: 0, + } + } + + pub fn poll(&mut self, state: SharedState) { + if let Ok(interface) = get_primary_interface() { + if !interface.is_empty() { + let time_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if let Ok((rx_bytes_now, tx_bytes_now)) = get_bytes(&interface) { + if self.last_time > 0 && time_now > self.last_time { + let time_diff = time_now - self.last_time; + let rx_bps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) / time_diff; + let tx_bps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) / time_diff; + + let rx_mbps = (rx_bps as f64) / 1024.0 / 1024.0; + let tx_mbps = (tx_bps as f64) / 1024.0 / 1024.0; + + debug!(interface, rx = rx_mbps, tx = tx_mbps, "Network stats updated"); + + if let Ok(mut state_lock) = state.write() { + state_lock.network.rx_mbps = rx_mbps; + state_lock.network.tx_mbps = tx_mbps; + } + } + + self.last_time = time_now; + self.last_rx_bytes = rx_bytes_now; + self.last_tx_bytes = tx_bytes_now; + } + } else { + warn!("No primary network interface found during poll"); + } + } + } +} + +impl WaybarModule for NetworkModule { + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { + let interface = get_primary_interface()?; + if interface.is_empty() { + return Ok(WaybarOutput { + text: "No connection".to_string(), + tooltip: None, + class: None, + percentage: None, + }); + } + + let ip = get_ip_address(&interface).unwrap_or_else(|| String::from("No IP")); + + let (rx_mbps, tx_mbps) = { + if let Ok(state_lock) = state.read() { + (state_lock.network.rx_mbps, state_lock.network.tx_mbps) + } else { + (0.0, 0.0) + } + }; + + let mut output_text = config + .network + .format + .replace("{interface}", &interface) + .replace("{ip}", &ip) + .replace("{rx}", &format!("{:.2}", rx_mbps)) + .replace("{tx}", &format!("{:.2}", tx_mbps)); + + if interface.starts_with("tun") + || interface.starts_with("wg") + || interface.starts_with("ppp") + || interface.starts_with("pvpn") + { + output_text = format!(" {}", output_text); + } + + Ok(WaybarOutput { + text: output_text, + tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip)), + class: Some(interface), + percentage: None, + }) + } +} + +fn get_primary_interface() -> Result { + let output = std::process::Command::new("ip") + .args(["route", "list"]) + .output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + + let mut defaults = Vec::new(); + for line in stdout.lines() { + if line.starts_with("default") { + let parts: Vec<&str> = line.split_whitespace().collect(); + let mut dev = ""; + let mut metric = 0; + for i in 0..parts.len() { + if parts[i] == "dev" && i + 1 < parts.len() { + dev = parts[i + 1]; + } + if parts[i] == "metric" && i + 1 < parts.len() { + metric = parts[i + 1].parse::().unwrap_or(0); + } + } + if !dev.is_empty() { + defaults.push((metric, dev.to_string())); + } + } + } + + defaults.sort_by_key(|k| k.0); + if let Some((_, dev)) = defaults.first() { + Ok(dev.clone()) + } else { + Ok(String::new()) + } +} + +fn get_ip_address(interface: &str) -> Option { + let output = std::process::Command::new("ip") + .args(["-4", "addr", "show", interface]) + .output() + .ok()?; + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if line.trim().starts_with("inet ") { + let parts: Vec<&str> = line.trim().split_whitespace().collect(); + if parts.len() > 1 { + let ip_cidr = parts[1]; + let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr); + return Some(ip.to_string()); + } + } + } + None +} + +fn get_bytes(interface: &str) -> Result<(u64, u64)> { + let rx_path = format!("/sys/class/net/{}/statistics/rx_bytes", interface); + let tx_path = format!("/sys/class/net/{}/statistics/tx_bytes", interface); + + let rx = fs::read_to_string(&rx_path) + .unwrap_or_else(|_| "0".to_string()) + .trim() + .parse() + .unwrap_or(0); + let tx = fs::read_to_string(&tx_path) + .unwrap_or_else(|_| "0".to_string()) + .trim() + .parse() + .unwrap_or(0); + + Ok((rx, tx)) +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..97c7c02 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,12 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct WaybarOutput { + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tooltip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub class: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..c49a571 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,39 @@ +use std::sync::{Arc, RwLock}; + +#[derive(Default, Clone)] +pub struct AppState { + pub network: NetworkState, + pub cpu: CpuState, + pub memory: MemoryState, +} + +#[derive(Default, Clone)] +pub struct NetworkState { + pub rx_mbps: f64, + pub tx_mbps: f64, +} + +#[derive(Clone)] +pub struct CpuState { + pub usage: f64, + pub temp: f64, + pub model: String, +} + +impl Default for CpuState { + fn default() -> Self { + Self { + usage: 0.0, + temp: 0.0, + model: String::from("Unknown"), + } + } +} + +#[derive(Default, Clone)] +pub struct MemoryState { + pub used_gb: f64, + pub total_gb: f64, +} + +pub type SharedState = Arc>;