Compare commits
3 Commits
d2ebe0b800
..
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f640f116ec | |||
| c1b3d9134e | |||
| 78c004bcb4 |
@@ -0,0 +1,60 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
cache: false
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --tests -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
version-check:
|
||||
name: Version Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Compare versions
|
||||
shell: bash
|
||||
run: |
|
||||
NEW_VERSION=$(grep -m1 '^version =' Cargo.toml | cut -d '"' -f 2)
|
||||
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
OLD_VERSION=$(git show origin/${{ github.base_ref }}:Cargo.toml | grep -m1 '^version =' | cut -d '"' -f 2)
|
||||
|
||||
echo "Old version (main): $OLD_VERSION"
|
||||
echo "New version (PR): $NEW_VERSION"
|
||||
|
||||
if [ "$NEW_VERSION" = "$OLD_VERSION" ]; then
|
||||
echo "Error: Cargo.toml version has not been updated in this PR!"
|
||||
exit 1
|
||||
else
|
||||
echo "Success: Version updated from $OLD_VERSION to $NEW_VERSION"
|
||||
fi
|
||||
@@ -0,0 +1,109 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
- name: Install packaging tools
|
||||
run: apt-get update && apt-get install -y dpkg-dev
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(grep -m1 '^version =' Cargo.toml | cut -d '"' -f 2)
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "TAG=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "TAG=v$VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if Release Exists
|
||||
id: check_release
|
||||
shell: bash
|
||||
run: |
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/${{ steps.get_version.outputs.TAG }}")
|
||||
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "EXISTS=true" >> $GITHUB_OUTPUT
|
||||
echo "Release already exists for tag ${{ steps.get_version.outputs.TAG }}. Skipping."
|
||||
else
|
||||
echo "EXISTS=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
if: steps.check_release.outputs.EXISTS == 'false'
|
||||
run: cargo build --release
|
||||
|
||||
- name: Test
|
||||
if: steps.check_release.outputs.EXISTS == 'false'
|
||||
run: cargo test
|
||||
|
||||
- name: Package .deb
|
||||
if: steps.check_release.outputs.EXISTS == 'false'
|
||||
env:
|
||||
VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
TAG: ${{ steps.get_version.outputs.TAG }}
|
||||
run: |
|
||||
PKG="fluxo-rs_${VERSION}_amd64"
|
||||
|
||||
mkdir -p "${PKG}/DEBIAN"
|
||||
mkdir -p "${PKG}/usr/bin"
|
||||
|
||||
cp target/release/fluxo-rs "${PKG}/usr/bin/"
|
||||
strip "${PKG}/usr/bin/fluxo-rs"
|
||||
|
||||
printf '%s\n' \
|
||||
"Package: fluxo-rs" \
|
||||
"Version: ${VERSION}" \
|
||||
"Section: utils" \
|
||||
"Priority: optional" \
|
||||
"Architecture: amd64" \
|
||||
"Maintainer: fluxo-rs contributors" \
|
||||
"Description: High-performance daemon/client for Waybar custom modules" \
|
||||
" fluxo-rs is a compiled Rust daemon that polls system metrics and" \
|
||||
" serves formatted JSON output to Waybar custom modules over a Unix" \
|
||||
" domain socket. Replaces shell scripts with a single binary." \
|
||||
> "${PKG}/DEBIAN/control"
|
||||
|
||||
dpkg-deb --build "${PKG}"
|
||||
|
||||
mv "${PKG}.deb" "fluxo-rs-${TAG}-amd64.deb"
|
||||
echo "Built: fluxo-rs-${TAG}-amd64.deb"
|
||||
|
||||
- name: Create Release and Upload Assets
|
||||
if: steps.check_release.outputs.EXISTS == 'false'
|
||||
uses: https://github.com/softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.TAG }}
|
||||
name: Release ${{ steps.get_version.outputs.TAG }}
|
||||
body: |
|
||||
Automated release for version ${{ steps.get_version.outputs.VERSION }}
|
||||
Commit: ${{ github.sha }}
|
||||
Branch: ${{ github.ref_name }}
|
||||
files: |
|
||||
fluxo-rs-${{ steps.get_version.outputs.TAG }}-amd64.deb
|
||||
target/release/fluxo-rs
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
Generated
+298
-86
@@ -47,7 +47,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -73,12 +73,27 @@ version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
@@ -125,6 +140,29 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -138,33 +176,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fluxo-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"fs4",
|
||||
"ctrlc",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs4"
|
||||
version = "0.13.1"
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -179,6 +242,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
@@ -186,7 +255,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -207,6 +278,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -240,6 +317,18 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
@@ -255,7 +344,16 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -267,6 +365,12 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-kit"
|
||||
version = "0.3.2"
|
||||
@@ -295,6 +399,16 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -313,6 +427,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -352,9 +472,15 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -453,6 +579,19 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
@@ -568,6 +707,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -580,6 +725,58 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -703,15 +900,6 @@ 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"
|
||||
@@ -721,22 +909,6 @@ 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"
|
||||
@@ -746,60 +918,100 @@ 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 = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
+5
-2
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "fluxo-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
fs4 = "0.13.1"
|
||||
ctrlc = "3"
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
@@ -14,3 +14,6 @@ sysinfo = "0.38.4"
|
||||
toml = "1.0.6"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
+155
-4
@@ -1,6 +1,8 @@
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@@ -95,9 +97,11 @@ pub struct GpuConfig {
|
||||
impl Default for GpuConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format_amd: "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C".to_string(),
|
||||
format_amd: "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
|
||||
.to_string(),
|
||||
format_intel: "iGPU: {usage:>3.0}%".to_string(),
|
||||
format_nvidia: "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C".to_string(),
|
||||
format_nvidia: "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,6 +226,80 @@ impl Default for GameConfig {
|
||||
}
|
||||
}
|
||||
|
||||
static TOKEN_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)(?::([<>\^])?(\d+)?(?:\.(\d+))?)?\}").unwrap());
|
||||
|
||||
fn extract_tokens(format_str: &str) -> Vec<String> {
|
||||
TOKEN_RE
|
||||
.captures_iter(format_str)
|
||||
.map(|cap| cap[1].to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn validate_format(label: &str, format_str: &str, known_tokens: &[&str]) {
|
||||
for token in extract_tokens(format_str) {
|
||||
if !known_tokens.contains(&token.as_str()) {
|
||||
warn!(
|
||||
"Config [{}]: unknown token '{{{}}}' in format string. Known tokens: {:?}",
|
||||
label, token, known_tokens
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn validate(&self) {
|
||||
validate_format(
|
||||
"network",
|
||||
&self.network.format,
|
||||
&["interface", "ip", "rx", "tx"],
|
||||
);
|
||||
validate_format("cpu", &self.cpu.format, &["usage", "temp"]);
|
||||
validate_format("memory", &self.memory.format, &["used", "total"]);
|
||||
validate_format(
|
||||
"gpu.amd",
|
||||
&self.gpu.format_amd,
|
||||
&["usage", "vram_used", "vram_total", "temp"],
|
||||
);
|
||||
validate_format("gpu.intel", &self.gpu.format_intel, &["usage", "freq"]);
|
||||
validate_format(
|
||||
"gpu.nvidia",
|
||||
&self.gpu.format_nvidia,
|
||||
&["usage", "vram_used", "vram_total", "temp"],
|
||||
);
|
||||
validate_format(
|
||||
"sys",
|
||||
&self.sys.format,
|
||||
&["uptime", "load1", "load5", "load15", "procs"],
|
||||
);
|
||||
validate_format("disk", &self.disk.format, &["mount", "used", "total"]);
|
||||
validate_format("pool", &self.pool.format, &["used", "total"]);
|
||||
validate_format("power", &self.power.format, &["percentage", "icon"]);
|
||||
validate_format("buds", &self.buds.format, &["left", "right", "anc"]);
|
||||
validate_format(
|
||||
"audio.sink_unmuted",
|
||||
&self.audio.format_sink_unmuted,
|
||||
&["name", "icon", "volume"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.sink_muted",
|
||||
&self.audio.format_sink_muted,
|
||||
&["name", "icon"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.source_unmuted",
|
||||
&self.audio.format_source_unmuted,
|
||||
&["name", "icon", "volume"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.source_muted",
|
||||
&self.audio.format_source_muted,
|
||||
&["name", "icon"],
|
||||
);
|
||||
validate_format("bt.connected", &self.bt.format_connected, &["alias"]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
let config_path = custom_path.unwrap_or_else(|| {
|
||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||
@@ -234,9 +312,10 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
});
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
match toml::from_str(&content) {
|
||||
match toml::from_str::<Config>(&content) {
|
||||
Ok(cfg) => {
|
||||
info!("Successfully loaded configuration from {:?}", config_path);
|
||||
cfg.validate();
|
||||
cfg
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -246,7 +325,79 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("No config file found at {:?}, using default settings.", config_path);
|
||||
debug!(
|
||||
"No config file found at {:?}, using default settings.",
|
||||
config_path
|
||||
);
|
||||
Config::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
assert!(config.cpu.format.contains("temp"));
|
||||
assert!(config.memory.format.contains("used"));
|
||||
assert!(config.memory.format.contains("total"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_missing_config() {
|
||||
let config = load_config(Some(PathBuf::from("/nonexistent/config.toml")));
|
||||
// Should fallback to defaults without panicking
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_valid_partial_config() {
|
||||
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
// In TOML, braces have no special meaning in strings
|
||||
writeln!(tmpfile, "[cpu]").unwrap();
|
||||
writeln!(tmpfile, "format = \"custom: {{usage}}\"").unwrap();
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
// TOML treats {{ as literal {{ (no escape), so the value is "custom: {{usage}}"
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
// Other sections still have defaults
|
||||
assert!(config.memory.format.contains("used"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_invalid_toml() {
|
||||
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
write!(tmpfile, "this is not valid toml {{{{").unwrap();
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
// Should fallback to defaults
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_empty_config() {
|
||||
let tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Empty file is valid TOML, all sections default
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
}
|
||||
}
|
||||
|
||||
+75
-22
@@ -1,62 +1,95 @@
|
||||
use crate::config::Config;
|
||||
use crate::ipc::SOCKET_PATH;
|
||||
use crate::modules::network::NetworkDaemon;
|
||||
use crate::modules::hardware::HardwareDaemon;
|
||||
use crate::ipc::socket_path;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::modules::hardware::HardwareDaemon;
|
||||
use crate::modules::network::NetworkDaemon;
|
||||
use crate::state::{AppState, SharedState};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn, error, debug};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
struct SocketGuard {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Drop for SocketGuard {
|
||||
fn drop(&mut self) {
|
||||
debug!("Cleaning up socket file: {}", self.path);
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
|
||||
if fs::metadata(SOCKET_PATH).is_ok() {
|
||||
debug!("Removing stale socket file: {}", SOCKET_PATH);
|
||||
fs::remove_file(SOCKET_PATH)?;
|
||||
let sock_path = socket_path();
|
||||
|
||||
if fs::metadata(&sock_path).is_ok() {
|
||||
debug!("Removing stale socket file: {}", sock_path);
|
||||
fs::remove_file(&sock_path)?;
|
||||
}
|
||||
|
||||
let state: SharedState = Arc::new(RwLock::new(AppState::default()));
|
||||
let listener = UnixListener::bind(SOCKET_PATH)?;
|
||||
let listener = UnixListener::bind(&sock_path)?;
|
||||
let _guard = SocketGuard {
|
||||
path: sock_path.clone(),
|
||||
};
|
||||
|
||||
// Signal handling: set flag so main loop exits cleanly
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = Arc::clone(&running);
|
||||
ctrlc::set_handler(move || {
|
||||
info!("Received shutdown signal, exiting...");
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
})?;
|
||||
|
||||
// We store the original config_path to allow proper reloading later
|
||||
let config_path_clone = config_path.clone();
|
||||
let config = Arc::new(RwLock::new(crate::config::load_config(config_path)));
|
||||
|
||||
let poll_state = Arc::clone(&state);
|
||||
let poll_running = Arc::clone(&running);
|
||||
thread::spawn(move || {
|
||||
info!("Starting background polling thread");
|
||||
let mut network_daemon = NetworkDaemon::new();
|
||||
let mut hardware_daemon = HardwareDaemon::new();
|
||||
loop {
|
||||
while poll_running.load(Ordering::SeqCst) {
|
||||
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);
|
||||
info!("Fluxo daemon successfully bound to socket: {}", sock_path);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(mut stream) => {
|
||||
// Use non-blocking accept so we can check the running flag
|
||||
listener.set_nonblocking(true)?;
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
let state_clone = Arc::clone(&state);
|
||||
let config_clone = Arc::clone(&config);
|
||||
let cp_clone = config_path_clone.clone();
|
||||
thread::spawn(move || {
|
||||
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||
let mut reader = BufReader::new(&stream);
|
||||
let mut request = String::new();
|
||||
if let Err(e) = reader.read_line(&mut request) {
|
||||
error!("Failed to read from IPC stream: {}", e);
|
||||
return;
|
||||
}
|
||||
drop(reader);
|
||||
|
||||
let request = request.trim();
|
||||
if request.is_empty() { return; }
|
||||
if request.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = request.split_whitespace().collect();
|
||||
if let Some(module_name) = parts.first() {
|
||||
@@ -70,25 +103,37 @@ pub fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
|
||||
} else {
|
||||
error!("Failed to acquire write lock for configuration reload.");
|
||||
}
|
||||
let _ = stream.shutdown(Shutdown::Write);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(module = module_name, args = ?&parts[1..], "Handling IPC request");
|
||||
let response = handle_request(*module_name, &parts[1..], &state_clone, &config_clone);
|
||||
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);
|
||||
}
|
||||
let _ = stream.shutdown(Shutdown::Write);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(e) => error!("Failed to accept incoming connection: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
info!("Daemon shutting down gracefully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_lock: &Arc<RwLock<Config>>) -> String {
|
||||
fn handle_request(
|
||||
module_name: &str,
|
||||
args: &[&str],
|
||||
state: &SharedState,
|
||||
config_lock: &Arc<RwLock<Config>>,
|
||||
) -> String {
|
||||
let config = if let Ok(c) = config_lock.read() {
|
||||
c
|
||||
} else {
|
||||
@@ -102,8 +147,16 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_
|
||||
"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")]),
|
||||
"vol" => crate::modules::audio::AudioModule.run(
|
||||
&config,
|
||||
state,
|
||||
&["sink", args.first().unwrap_or(&"show")],
|
||||
),
|
||||
"mic" => crate::modules::audio::AudioModule.run(
|
||||
&config,
|
||||
state,
|
||||
&["source", args.first().unwrap_or(&"show")],
|
||||
),
|
||||
"gpu" => crate::modules::gpu::GpuModule.run(&config, state, args),
|
||||
"sys" => crate::modules::sys::SysModule.run(&config, state, args),
|
||||
"bt" | "bluetooth" => crate::modules::bt::BtModule.run(&config, state, args),
|
||||
@@ -113,7 +166,7 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_
|
||||
_ => {
|
||||
warn!("Received request for unknown module: '{}'", module_name);
|
||||
Err(anyhow::anyhow!("Unknown module: {}", module_name))
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -121,7 +174,7 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_
|
||||
Err(e) => {
|
||||
error!(module = module_name, error = %e, "Module execution failed");
|
||||
let err_out = crate::output::WaybarOutput {
|
||||
text: format!("\u{200B}Error\u{200B}"),
|
||||
text: "\u{200B}Error\u{200B}".to_string(),
|
||||
tooltip: Some(e.to_string()),
|
||||
class: Some("error".to_string()),
|
||||
percentage: None,
|
||||
|
||||
+13
-4
@@ -1,12 +1,21 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
pub const SOCKET_PATH: &str = "/tmp/fluxo.sock";
|
||||
pub fn socket_path() -> String {
|
||||
if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
format!("{}/fluxo.sock", dir)
|
||||
} else {
|
||||
"/tmp/fluxo.sock".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_data(module: &str, args: &[String]) -> anyhow::Result<String> {
|
||||
debug!(module, ?args, "Connecting to daemon socket: {}", SOCKET_PATH);
|
||||
let mut stream = UnixStream::connect(SOCKET_PATH)?;
|
||||
pub fn request_data(module: &str, args: &[&str]) -> anyhow::Result<String> {
|
||||
let sock = socket_path();
|
||||
debug!(module, ?args, "Connecting to daemon socket: {}", sock);
|
||||
let mut stream = UnixStream::connect(&sock)?;
|
||||
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
|
||||
|
||||
// Send module and args
|
||||
let mut request = module.to_string();
|
||||
|
||||
+24
-22
@@ -10,7 +10,7 @@ use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "fluxo")]
|
||||
@@ -98,27 +98,25 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Reload => {
|
||||
match ipc::request_data("reload", &[]) {
|
||||
Commands::Reload => match ipc::request_data("reload", &[]) {
|
||||
Ok(_) => info!("Reload signal sent to daemon"),
|
||||
Err(e) => {
|
||||
error!("Failed to send reload signal: {}", 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::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path])),
|
||||
Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind])),
|
||||
Commands::Vol { cycle } => {
|
||||
let action = if *cycle { "cycle" } else { "show" };
|
||||
handle_ipc_response(ipc::request_data("vol", &[action.to_string()]));
|
||||
handle_ipc_response(ipc::request_data("vol", &[action]));
|
||||
}
|
||||
Commands::Mic { cycle } => {
|
||||
let action = if *cycle { "cycle" } else { "show" };
|
||||
handle_ipc_response(ipc::request_data("mic", &[action.to_string()]));
|
||||
handle_ipc_response(ipc::request_data("mic", &[action]));
|
||||
}
|
||||
Commands::Gpu => handle_ipc_response(ipc::request_data("gpu", &[])),
|
||||
Commands::Sys => handle_ipc_response(ipc::request_data("sys", &[])),
|
||||
@@ -127,10 +125,16 @@ fn main() {
|
||||
// Client-side execution of the menu
|
||||
let config = config::load_config(None);
|
||||
|
||||
let devices_out = std::process::Command::new("bluetoothctl")
|
||||
let devices_out = match std::process::Command::new("bluetoothctl")
|
||||
.args(["devices"])
|
||||
.output()
|
||||
.expect("Failed to run bluetoothctl");
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(e) => {
|
||||
error!("bluetoothctl not found or failed: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let stdout = String::from_utf8_lossy(&devices_out.stdout);
|
||||
|
||||
let mut items = Vec::new();
|
||||
@@ -144,24 +148,24 @@ fn main() {
|
||||
}
|
||||
|
||||
if !items.is_empty() {
|
||||
if let Ok(selected) = utils::show_menu("Connect BT: ", &items, &config.general.menu_command) {
|
||||
if let Some(mac_start) = selected.rfind('(') {
|
||||
if let Some(mac_end) = selected.rfind(')') {
|
||||
if let Ok(selected) =
|
||||
utils::show_menu("Connect BT: ", &items, &config.general.menu_command)
|
||||
&& let Some(mac_start) = selected.rfind('(')
|
||||
&& let Some(mac_end) = selected.rfind(')')
|
||||
{
|
||||
let mac = &selected[mac_start + 1..mac_end];
|
||||
let _ = std::process::Command::new("bluetoothctl")
|
||||
.args(["connect", mac])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No paired Bluetooth devices found.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
handle_ipc_response(ipc::request_data("bt", &[action.clone()]));
|
||||
handle_ipc_response(ipc::request_data("bt", &[action]));
|
||||
}
|
||||
Commands::Buds { action } => handle_ipc_response(ipc::request_data("buds", &[action.clone()])),
|
||||
Commands::Buds { action } => handle_ipc_response(ipc::request_data("buds", &[action])),
|
||||
Commands::Power => handle_ipc_response(ipc::request_data("power", &[])),
|
||||
Commands::Game => handle_ipc_response(ipc::request_data("game", &[])),
|
||||
}
|
||||
@@ -169,8 +173,7 @@ fn main() {
|
||||
|
||||
fn handle_ipc_response(response: anyhow::Result<String>) {
|
||||
match response {
|
||||
Ok(json_str) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
Ok(json_str) => match serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
Ok(mut val) => {
|
||||
if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) {
|
||||
let processed_text = if text.contains('<') {
|
||||
@@ -185,8 +188,7 @@ fn handle_ipc_response(response: anyhow::Result<String>) {
|
||||
println!("{}", serde_json::to_string(&val).unwrap());
|
||||
}
|
||||
Err(_) => println!("{}", json_str),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let err_out = output::WaybarOutput {
|
||||
text: format!("\u{200B}Daemon offline ({})\u{200B}", e),
|
||||
|
||||
+69
-36
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -16,30 +16,30 @@ impl WaybarModule for AudioModule {
|
||||
match *action {
|
||||
"cycle" => {
|
||||
self.cycle_device(target_type)?;
|
||||
return Ok(WaybarOutput {
|
||||
Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
"show" | _ => {
|
||||
self.get_status(config, target_type)
|
||||
})
|
||||
}
|
||||
"show" => self.get_status(config, target_type),
|
||||
other => Err(anyhow!("Unknown audio action: '{}'", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioModule {
|
||||
fn get_status(&self, config: &Config, target_type: &str) -> Result<WaybarOutput> {
|
||||
let target = if target_type == "sink" { "@DEFAULT_AUDIO_SINK@" } else { "@DEFAULT_AUDIO_SOURCE@" };
|
||||
let target = if target_type == "sink" {
|
||||
"@DEFAULT_AUDIO_SINK@"
|
||||
} else {
|
||||
"@DEFAULT_AUDIO_SOURCE@"
|
||||
};
|
||||
|
||||
let output = Command::new("wpctl")
|
||||
.args(["get-volume", target])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stdout = run_command("wpctl", &["get-volume", target])?;
|
||||
|
||||
let parts: Vec<&str> = stdout.trim().split_whitespace().collect();
|
||||
let parts: Vec<&str> = stdout.split_whitespace().collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(anyhow!("Could not parse wpctl output: {}", stdout));
|
||||
}
|
||||
@@ -58,31 +58,43 @@ impl AudioModule {
|
||||
|
||||
let (text, class) = if muted {
|
||||
let icon = if target_type == "sink" { "" } else { "" };
|
||||
let format_str = if target_type == "sink" { &config.audio.format_sink_muted } else { &config.audio.format_source_muted };
|
||||
let format_str = if target_type == "sink" {
|
||||
&config.audio.format_sink_muted
|
||||
} else {
|
||||
&config.audio.format_source_muted
|
||||
};
|
||||
let t = format_template(
|
||||
format_str,
|
||||
&[
|
||||
("name", TokenValue::String(&name)),
|
||||
("icon", TokenValue::String(icon)),
|
||||
]
|
||||
],
|
||||
);
|
||||
(t, "muted")
|
||||
} else {
|
||||
let icon = if target_type == "sink" {
|
||||
if display_vol <= 30 { "" }
|
||||
else if display_vol <= 60 { "" }
|
||||
else { "" }
|
||||
if display_vol <= 30 {
|
||||
""
|
||||
} else if display_vol <= 60 {
|
||||
""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let format_str = if target_type == "sink" { &config.audio.format_sink_unmuted } else { &config.audio.format_source_unmuted };
|
||||
let format_str = if target_type == "sink" {
|
||||
&config.audio.format_sink_unmuted
|
||||
} else {
|
||||
&config.audio.format_source_unmuted
|
||||
};
|
||||
let t = format_template(
|
||||
format_str,
|
||||
&[
|
||||
("name", TokenValue::String(&name)),
|
||||
("icon", TokenValue::String(icon)),
|
||||
("volume", TokenValue::Int(display_vol as i64)),
|
||||
]
|
||||
],
|
||||
);
|
||||
(t, "unmuted")
|
||||
};
|
||||
@@ -96,19 +108,26 @@ impl AudioModule {
|
||||
}
|
||||
|
||||
fn get_description(&self, target_type: &str) -> Result<String> {
|
||||
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 info_stdout = run_command("pactl", &["info"])?;
|
||||
let search_key = if target_type == "sink" {
|
||||
"Default Sink:"
|
||||
} else {
|
||||
"Default Source:"
|
||||
};
|
||||
|
||||
let default_dev = info_stdout.lines()
|
||||
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))?;
|
||||
|
||||
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 list_cmd = if target_type == "sink" {
|
||||
"sinks"
|
||||
} else {
|
||||
"sources"
|
||||
};
|
||||
let list_stdout = run_command("pactl", &["list", list_cmd])?;
|
||||
|
||||
let mut current_name = String::new();
|
||||
for line in list_stdout.lines() {
|
||||
@@ -124,11 +143,15 @@ impl AudioModule {
|
||||
}
|
||||
|
||||
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 list_cmd = if target_type == "sink" {
|
||||
"sinks"
|
||||
} else {
|
||||
"sources"
|
||||
};
|
||||
let stdout = run_command("pactl", &["list", "short", list_cmd])?;
|
||||
|
||||
let devices: Vec<String> = stdout.lines()
|
||||
let devices: Vec<String> = stdout
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let parts: Vec<&str> = l.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
@@ -144,13 +167,19 @@ impl AudioModule {
|
||||
})
|
||||
.collect();
|
||||
|
||||
if devices.is_empty() { return Ok(()); }
|
||||
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 info_stdout = run_command("pactl", &["info"])?;
|
||||
let search_key = if target_type == "sink" {
|
||||
"Default Sink:"
|
||||
} else {
|
||||
"Default Source:"
|
||||
};
|
||||
|
||||
let current_dev = info_stdout.lines()
|
||||
let current_dev = info_stdout
|
||||
.lines()
|
||||
.find(|l| l.contains(search_key))
|
||||
.and_then(|l| l.split(':').nth(1))
|
||||
.map(|s| s.trim())
|
||||
@@ -160,7 +189,11 @@ impl AudioModule {
|
||||
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" };
|
||||
let set_cmd = if target_type == "sink" {
|
||||
"set-default-sink"
|
||||
} else {
|
||||
"set-default-source"
|
||||
};
|
||||
Command::new("pactl").args([set_cmd, next_dev]).status()?;
|
||||
|
||||
Ok(())
|
||||
|
||||
+21
-21
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
@@ -14,7 +14,9 @@ impl WaybarModule for BtModule {
|
||||
|
||||
if *action == "disconnect" {
|
||||
if let Some(mac) = find_audio_device() {
|
||||
let _ = Command::new("bluetoothctl").args(["disconnect", &mac]).output();
|
||||
let _ = Command::new("bluetoothctl")
|
||||
.args(["disconnect", &mac])
|
||||
.output();
|
||||
}
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
@@ -24,9 +26,9 @@ impl WaybarModule for BtModule {
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(output) = Command::new("bluetoothctl").arg("show").output() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if stdout.contains("Powered: no") {
|
||||
if let Ok(stdout) = run_command("bluetoothctl", &["show"])
|
||||
&& stdout.contains("Powered: no")
|
||||
{
|
||||
return Ok(WaybarOutput {
|
||||
text: config.bt.format_disabled.clone(),
|
||||
tooltip: Some("Bluetooth Disabled".to_string()),
|
||||
@@ -34,11 +36,9 @@ impl WaybarModule for BtModule {
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mac) = find_audio_device() {
|
||||
let info_output = Command::new("bluetoothctl").args(["info", &mac]).output()?;
|
||||
let info = String::from_utf8_lossy(&info_output.stdout);
|
||||
let info = run_command("bluetoothctl", &["info", &mac])?;
|
||||
|
||||
let mut alias = mac.clone();
|
||||
let mut battery = None;
|
||||
@@ -48,7 +48,8 @@ impl WaybarModule for BtModule {
|
||||
if line.contains("Alias:") {
|
||||
alias = line.split("Alias:").nth(1).unwrap_or("").trim().to_string();
|
||||
} else if line.contains("Battery Percentage:") {
|
||||
if let Some(bat_str) = line.split('(').nth(1).and_then(|s| s.split(')').next()) {
|
||||
if let Some(bat_str) = line.split('(').nth(1).and_then(|s| s.split(')').next())
|
||||
{
|
||||
battery = bat_str.parse::<u8>().ok();
|
||||
}
|
||||
} else if line.contains("Trusted: yes") {
|
||||
@@ -61,12 +62,14 @@ impl WaybarModule for BtModule {
|
||||
alias,
|
||||
mac,
|
||||
trusted,
|
||||
battery.map(|b| format!("{}%", b)).unwrap_or_else(|| "N/A".to_string())
|
||||
battery
|
||||
.map(|b| format!("{}%", b))
|
||||
.unwrap_or_else(|| "N/A".to_string())
|
||||
);
|
||||
|
||||
let text = format_template(
|
||||
&config.bt.format_connected,
|
||||
&[("alias", TokenValue::String(&alias))]
|
||||
&[("alias", TokenValue::String(&alias))],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
@@ -87,33 +90,30 @@ impl WaybarModule for BtModule {
|
||||
}
|
||||
|
||||
fn find_audio_device() -> Option<String> {
|
||||
if let Ok(output) = Command::new("pactl").arg("get-default-sink").output() {
|
||||
let sink = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if sink.starts_with("bluez_output.") {
|
||||
if let Ok(sink) = run_command("pactl", &["get-default-sink"])
|
||||
&& sink.starts_with("bluez_output.")
|
||||
{
|
||||
let parts: Vec<&str> = sink.split('.').collect();
|
||||
if parts.len() >= 2 {
|
||||
return Some(parts[1].replace('_', ":"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(output) = Command::new("bluetoothctl").args(["devices", "Connected"]).output() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) {
|
||||
for line in stdout.lines() {
|
||||
if line.starts_with("Device ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let mac = parts[1];
|
||||
if let Ok(info) = Command::new("bluetoothctl").args(["info", mac]).output() {
|
||||
let info_str = String::from_utf8_lossy(&info.stdout);
|
||||
if info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") {
|
||||
if let Ok(info_str) = run_command("bluetoothctl", &["info", mac])
|
||||
&& info_str.contains("0000110b-0000-1000-8000-00805f9b34fb")
|
||||
{
|
||||
return Some(mac.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
+12
-8
@@ -2,22 +2,26 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
use sysinfo::Disks;
|
||||
|
||||
pub struct BtrfsModule;
|
||||
|
||||
impl WaybarModule for BtrfsModule {
|
||||
fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let disks = if let Ok(s) = state.read() {
|
||||
s.disks.clone()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Failed to read state"));
|
||||
};
|
||||
|
||||
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;
|
||||
if disk.filesystem.contains("btrfs") {
|
||||
let size = disk.total_bytes as f64;
|
||||
let available = disk.available_bytes as f64;
|
||||
total_size += size;
|
||||
total_used += size - available;
|
||||
}
|
||||
@@ -49,7 +53,7 @@ impl WaybarModule for BtrfsModule {
|
||||
&[
|
||||
("used", TokenValue::Float(used_gb)),
|
||||
("total", TokenValue::Float(size_gb)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
|
||||
+38
-23
@@ -2,9 +2,8 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct BudsModule;
|
||||
|
||||
@@ -15,8 +14,7 @@ impl WaybarModule for BudsModule {
|
||||
|
||||
match *action {
|
||||
"cycle_anc" => {
|
||||
let output = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
||||
let current_mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let current_mode = run_command("pbpctrl", &["get", "anc"])?;
|
||||
|
||||
let next_mode = match current_mode.as_str() {
|
||||
"active" => "aware",
|
||||
@@ -24,31 +22,39 @@ impl WaybarModule for BudsModule {
|
||||
_ => "active",
|
||||
};
|
||||
|
||||
Command::new("pbpctrl").args(["set", "anc", next_mode]).status()?;
|
||||
let _ = run_command("pbpctrl", &["set", "anc", next_mode]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None, class: None, percentage: None,
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
"connect" => {
|
||||
Command::new("bluetoothctl").args(["connect", mac]).status()?;
|
||||
let _ = run_command("bluetoothctl", &["connect", mac]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None, class: None, percentage: None,
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
"disconnect" => {
|
||||
Command::new("bluetoothctl").args(["disconnect", mac]).status()?;
|
||||
let _ = run_command("bluetoothctl", &["disconnect", mac]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None, class: None, percentage: None,
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
"show" | _ => {}
|
||||
"show" => {}
|
||||
other => {
|
||||
return Err(anyhow::anyhow!("Unknown buds action: '{}'", other));
|
||||
}
|
||||
}
|
||||
|
||||
let bt_info = Command::new("bluetoothctl").args(["info", mac]).output()?;
|
||||
let bt_str = String::from_utf8_lossy(&bt_info.stdout);
|
||||
let bt_str = run_command("bluetoothctl", &["info", mac])?;
|
||||
|
||||
if !bt_str.contains("Connected: yes") {
|
||||
return Ok(WaybarOutput {
|
||||
@@ -59,8 +65,9 @@ impl WaybarModule for BudsModule {
|
||||
});
|
||||
}
|
||||
|
||||
let bat_cmd = Command::new("pbpctrl").args(["show", "battery"]).output();
|
||||
if bat_cmd.is_err() || !bat_cmd.as_ref().unwrap().status.success() {
|
||||
let bat_output = match run_command("pbpctrl", &["show", "battery"]) {
|
||||
Ok(output) => output,
|
||||
Err(_) => {
|
||||
return Ok(WaybarOutput {
|
||||
text: config.buds.format_disconnected.clone(),
|
||||
tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()),
|
||||
@@ -68,9 +75,8 @@ impl WaybarModule for BudsModule {
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let bat_result = bat_cmd.unwrap();
|
||||
let bat_output = String::from_utf8_lossy(&bat_result.stdout);
|
||||
let mut left_bud = "unknown";
|
||||
let mut right_bud = "unknown";
|
||||
|
||||
@@ -85,15 +91,24 @@ impl WaybarModule for BudsModule {
|
||||
if left_bud == "unknown" && right_bud == "unknown" {
|
||||
return Ok(WaybarOutput {
|
||||
text: "{}".to_string(),
|
||||
tooltip: None, class: None, percentage: None,
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
|
||||
let left_display = if left_bud == "unknown" { "---".to_string() } else { format!("{}%", left_bud) };
|
||||
let right_display = if right_bud == "unknown" { "---".to_string() } else { format!("{}%", right_bud) };
|
||||
let left_display = if left_bud == "unknown" {
|
||||
"---".to_string()
|
||||
} else {
|
||||
format!("{}%", left_bud)
|
||||
};
|
||||
let right_display = if right_bud == "unknown" {
|
||||
"---".to_string()
|
||||
} else {
|
||||
format!("{}%", right_bud)
|
||||
};
|
||||
|
||||
let anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
||||
let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string();
|
||||
let current_mode = run_command("pbpctrl", &["get", "anc"]).unwrap_or_default();
|
||||
|
||||
let (anc_icon, class) = match current_mode.as_str() {
|
||||
"active" => ("ANC", "anc-active"),
|
||||
@@ -108,7 +123,7 @@ impl WaybarModule for BudsModule {
|
||||
("left", TokenValue::String(&left_display)),
|
||||
("right", TokenValue::String(&right_display)),
|
||||
("anc", TokenValue::String(anc_icon)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
|
||||
+57
-2
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct CpuModule;
|
||||
@@ -26,7 +26,7 @@ impl WaybarModule for CpuModule {
|
||||
&[
|
||||
("usage", TokenValue::Float(usage)),
|
||||
("temp", TokenValue::Float(temp)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
let class = if usage > 95.0 {
|
||||
@@ -45,3 +45,58 @@ impl WaybarModule for CpuModule {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, CpuState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_cpu_normal() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 25.0,
|
||||
temp: 45.0,
|
||||
model: "Test CPU".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("25.0"));
|
||||
assert!(output.text.contains("45.0"));
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(25));
|
||||
assert_eq!(output.tooltip.as_deref(), Some("Test CPU"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cpu_high() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 80.0,
|
||||
temp: 70.0,
|
||||
model: "Test".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cpu_max() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 99.0,
|
||||
temp: 95.0,
|
||||
model: "Test".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("max"));
|
||||
}
|
||||
}
|
||||
|
||||
+66
-10
@@ -2,28 +2,36 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
use sysinfo::Disks;
|
||||
|
||||
pub struct DiskModule;
|
||||
|
||||
impl WaybarModule for DiskModule {
|
||||
fn run(&self, config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
||||
fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
||||
let mountpoint = args.first().unwrap_or(&"/");
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let disks = if let Ok(s) = state.read() {
|
||||
s.disks.clone()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Failed to read state"));
|
||||
};
|
||||
|
||||
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;
|
||||
if disk.mount_point == *mountpoint {
|
||||
let total = disk.total_bytes as f64;
|
||||
let available = disk.available_bytes 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 percentage = if total > 0.0 {
|
||||
(used / total) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let class = if percentage > 95.0 {
|
||||
"max"
|
||||
@@ -39,12 +47,15 @@ impl WaybarModule for DiskModule {
|
||||
("mount", TokenValue::String(mountpoint)),
|
||||
("used", TokenValue::Float(used_gb)),
|
||||
("total", TokenValue::Float(total_gb)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return Ok(WaybarOutput {
|
||||
text,
|
||||
tooltip: Some(format!("Used: {:.1}G\nTotal: {:.1}G\nFree: {:.1}G", used_gb, total_gb, free_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),
|
||||
});
|
||||
@@ -54,3 +65,48 @@ impl WaybarModule for DiskModule {
|
||||
Err(anyhow::anyhow!("Mountpoint {} not found", mountpoint))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, DiskInfo, mock_state};
|
||||
|
||||
fn state_with_disk(mount: &str, total: u64, available: u64) -> crate::state::SharedState {
|
||||
mock_state(AppState {
|
||||
disks: vec![DiskInfo {
|
||||
mount_point: mount.to_string(),
|
||||
filesystem: "ext4".to_string(),
|
||||
total_bytes: total,
|
||||
available_bytes: available,
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_found() {
|
||||
let gb = 1024 * 1024 * 1024;
|
||||
let state = state_with_disk("/", 100 * gb, 60 * gb);
|
||||
let config = Config::default();
|
||||
let output = DiskModule.run(&config, &state, &["/"]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(40)); // 40% used
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_high() {
|
||||
let gb = 1024 * 1024 * 1024;
|
||||
let state = state_with_disk("/", 100 * gb, 15 * gb);
|
||||
let config = Config::default();
|
||||
let output = DiskModule.run(&config, &state, &["/"]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high")); // 85% used
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_not_found() {
|
||||
let state = mock_state(AppState::default());
|
||||
let config = Config::default();
|
||||
let result = DiskModule.run(&config, &state, &["/nonexistent"]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+4
-13
@@ -2,25 +2,16 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::run_command;
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct GameModule;
|
||||
|
||||
impl WaybarModule for GameModule {
|
||||
fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let output = Command::new("hyprctl")
|
||||
.args(["getoption", "animations:enabled", "-j"])
|
||||
.output();
|
||||
|
||||
let mut is_gamemode = false;
|
||||
|
||||
if let Ok(out) = output {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
if stdout.contains("\"int\": 0") {
|
||||
is_gamemode = true;
|
||||
}
|
||||
}
|
||||
let is_gamemode = run_command("hyprctl", &["getoption", "animations:enabled", "-j"])
|
||||
.map(|stdout| stdout.contains("\"int\": 0"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_gamemode {
|
||||
Ok(WaybarOutput {
|
||||
|
||||
+15
-4
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct GpuModule;
|
||||
@@ -21,7 +21,15 @@ impl WaybarModule for GpuModule {
|
||||
state_lock.gpu.model.clone(),
|
||||
)
|
||||
} else {
|
||||
(false, String::from("Unknown"), 0.0, 0.0, 0.0, 0.0, String::from("Unknown"))
|
||||
(
|
||||
false,
|
||||
String::from("Unknown"),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
String::from("Unknown"),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,13 +63,16 @@ impl WaybarModule for GpuModule {
|
||||
("vram_used", TokenValue::Float(vram_used)),
|
||||
("vram_total", TokenValue::Float(vram_total)),
|
||||
("temp", TokenValue::Float(temp)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
let tooltip = if vendor == "Intel" {
|
||||
format!("Model: {}\nApprox Usage: {:.0}%", model, usage)
|
||||
} else {
|
||||
format!("Model: {}\nUsage: {:.0}%\nVRAM: {:.1}/{:.1}GB\nTemp: {:.1}°C", model, usage, vram_used, vram_total, temp)
|
||||
format!(
|
||||
"Model: {}\nUsage: {:.0}%\nVRAM: {:.1}/{:.1}GB\nTemp: {:.1}°C",
|
||||
model, usage, vram_used, vram_total, temp
|
||||
)
|
||||
};
|
||||
|
||||
Ok(WaybarOutput {
|
||||
|
||||
+95
-33
@@ -1,18 +1,27 @@
|
||||
use crate::state::SharedState;
|
||||
use sysinfo::{Components, System};
|
||||
use crate::state::{DiskInfo, SharedState};
|
||||
use sysinfo::{Components, Disks, System};
|
||||
|
||||
pub struct HardwareDaemon {
|
||||
sys: System,
|
||||
components: Components,
|
||||
gpu_vendor: Option<String>,
|
||||
gpu_poll_counter: u8,
|
||||
disk_poll_counter: u8,
|
||||
}
|
||||
|
||||
impl HardwareDaemon {
|
||||
pub fn new() -> Self {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
let mut sys = System::new();
|
||||
sys.refresh_cpu_usage();
|
||||
sys.refresh_memory();
|
||||
let components = Components::new_with_refreshed_list();
|
||||
Self { sys, components, gpu_vendor: None }
|
||||
Self {
|
||||
sys,
|
||||
components,
|
||||
gpu_vendor: None,
|
||||
gpu_poll_counter: 0,
|
||||
disk_poll_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, state: SharedState) {
|
||||
@@ -21,15 +30,25 @@ impl HardwareDaemon {
|
||||
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());
|
||||
let cpu_model = self
|
||||
.sys
|
||||
.cpus()
|
||||
.first()
|
||||
.map(|c| c.brand().to_string())
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
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() {
|
||||
if (label.contains("tctl")
|
||||
|| label.contains("cpu")
|
||||
|| label.contains("package")
|
||||
|| label.contains("temp1"))
|
||||
&& let Some(temp) = component.temperature()
|
||||
{
|
||||
cpu_temp = temp as f64;
|
||||
if cpu_temp > 0.0 { break; }
|
||||
if cpu_temp > 0.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,16 +63,16 @@ impl HardwareDaemon {
|
||||
let mut process_count = 0;
|
||||
if let Ok(loadavg_str) = std::fs::read_to_string("/proc/loadavg") {
|
||||
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
||||
if parts.len() >= 4 {
|
||||
if let Some(total_procs) = parts[3].split('/').nth(1) {
|
||||
if parts.len() >= 4
|
||||
&& let Some(total_procs) = parts[3].split('/').nth(1)
|
||||
{
|
||||
process_count = total_procs.parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.temp = cpu_temp;
|
||||
state_lock.cpu.model = cpu_model;
|
||||
|
||||
state_lock.memory.total_gb = total_mem;
|
||||
@@ -65,19 +84,40 @@ impl HardwareDaemon {
|
||||
state_lock.sys.uptime = uptime;
|
||||
state_lock.sys.process_count = process_count;
|
||||
|
||||
// Poll GPU every 5 seconds to avoid expensive nvidia-smi calls
|
||||
self.gpu_poll_counter = (self.gpu_poll_counter + 1) % 5;
|
||||
if self.gpu_poll_counter == 0 {
|
||||
self.poll_gpu(&mut state_lock.gpu);
|
||||
}
|
||||
|
||||
// Poll disks every 10 seconds
|
||||
self.disk_poll_counter = (self.disk_poll_counter + 1) % 10;
|
||||
if self.disk_poll_counter == 0 {
|
||||
state_lock.disks = Disks::new_with_refreshed_list()
|
||||
.iter()
|
||||
.map(|d| DiskInfo {
|
||||
mount_point: d.mount_point().to_string_lossy().into_owned(),
|
||||
filesystem: d.file_system().to_string_lossy().to_lowercase(),
|
||||
total_bytes: d.total_space(),
|
||||
available_bytes: d.available_space(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_gpu(&mut self, gpu: &mut crate::state::GpuState) {
|
||||
gpu.active = false;
|
||||
|
||||
if self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none() {
|
||||
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||
.args(["--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name", "--format=csv,noheader,nounits"])
|
||||
if (self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none())
|
||||
&& let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||
.args([
|
||||
"--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name",
|
||||
"--format=csv,noheader,nounits",
|
||||
])
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = stdout.lines().next() {
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
@@ -94,24 +134,37 @@ impl HardwareDaemon {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() {
|
||||
if self.gpu_vendor.as_deref() == Some("AMD")
|
||||
|| self.gpu_vendor.as_deref() == Some("Intel")
|
||||
|| self.gpu_vendor.is_none()
|
||||
{
|
||||
for i in 0..=3 {
|
||||
let base = format!("/sys/class/drm/card{}/device", i);
|
||||
|
||||
if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.is_none() {
|
||||
if let Ok(usage_str) = std::fs::read_to_string(format!("{}/gpu_busy_percent", base)) {
|
||||
if (self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.is_none())
|
||||
&& let Ok(usage_str) =
|
||||
std::fs::read_to_string(format!("{}/gpu_busy_percent", base))
|
||||
{
|
||||
gpu.active = true;
|
||||
gpu.vendor = "AMD".to_string();
|
||||
gpu.usage = usage_str.trim().parse().unwrap_or(0.0);
|
||||
|
||||
if let Ok(mem_used) = std::fs::read_to_string(format!("{}/mem_info_vram_used", base)) {
|
||||
gpu.vram_used = mem_used.trim().parse::<f64>().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0;
|
||||
if let Ok(mem_used) =
|
||||
std::fs::read_to_string(format!("{}/mem_info_vram_used", base))
|
||||
{
|
||||
gpu.vram_used = mem_used.trim().parse::<f64>().unwrap_or(0.0)
|
||||
/ 1024.0
|
||||
/ 1024.0
|
||||
/ 1024.0;
|
||||
}
|
||||
if let Ok(mem_total) = std::fs::read_to_string(format!("{}/mem_info_vram_total", base)) {
|
||||
gpu.vram_total = mem_total.trim().parse::<f64>().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0;
|
||||
if let Ok(mem_total) =
|
||||
std::fs::read_to_string(format!("{}/mem_info_vram_total", base))
|
||||
{
|
||||
gpu.vram_total = mem_total.trim().parse::<f64>().unwrap_or(0.0)
|
||||
/ 1024.0
|
||||
/ 1024.0
|
||||
/ 1024.0;
|
||||
}
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(format!("{}/hwmon", base)) {
|
||||
@@ -127,19 +180,25 @@ impl HardwareDaemon {
|
||||
self.gpu_vendor = Some("AMD".to_string());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() {
|
||||
let freq_path = if std::path::Path::new(&format!("{}/gt_cur_freq_mhz", base)).exists() {
|
||||
let freq_path =
|
||||
if std::path::Path::new(&format!("{}/gt_cur_freq_mhz", base)).exists() {
|
||||
Some(format!("{}/gt_cur_freq_mhz", base))
|
||||
} else if std::path::Path::new(&format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)).exists() {
|
||||
} else if std::path::Path::new(&format!(
|
||||
"/sys/class/drm/card{}/gt_cur_freq_mhz",
|
||||
i
|
||||
))
|
||||
.exists()
|
||||
{
|
||||
Some(format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(path) = freq_path {
|
||||
if let Ok(freq_str) = std::fs::read_to_string(&path) {
|
||||
if let Some(path) = freq_path
|
||||
&& let Ok(freq_str) = std::fs::read_to_string(&path)
|
||||
{
|
||||
gpu.active = true;
|
||||
gpu.vendor = "Intel".to_string();
|
||||
|
||||
@@ -151,7 +210,11 @@ impl HardwareDaemon {
|
||||
max_freq = max_str.trim().parse::<f64>().unwrap_or(0.0);
|
||||
}
|
||||
|
||||
gpu.usage = if max_freq > 0.0 { (cur_freq / max_freq) * 100.0 } else { 0.0 };
|
||||
gpu.usage = if max_freq > 0.0 {
|
||||
(cur_freq / max_freq) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
gpu.temp = 0.0;
|
||||
gpu.vram_used = 0.0;
|
||||
gpu.vram_total = 0.0;
|
||||
@@ -164,4 +227,3 @@ impl HardwareDaemon {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-7
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct MemoryModule;
|
||||
@@ -11,23 +11,24 @@ impl WaybarModule for MemoryModule {
|
||||
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let (used_gb, total_gb) = {
|
||||
if let Ok(state_lock) = state.read() {
|
||||
(
|
||||
state_lock.memory.used_gb,
|
||||
state_lock.memory.total_gb,
|
||||
)
|
||||
(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 ratio = if total_gb > 0.0 {
|
||||
(used_gb / total_gb) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let text = format_template(
|
||||
&config.memory.format,
|
||||
&[
|
||||
("used", TokenValue::Float(used_gb)),
|
||||
("total", TokenValue::Float(total_gb)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
let class = if ratio > 95.0 {
|
||||
@@ -46,3 +47,55 @@ impl WaybarModule for MemoryModule {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, MemoryState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_memory_normal() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 8.0,
|
||||
total_gb: 32.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("8.00"));
|
||||
assert!(output.text.contains("32.00"));
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(25)); // 8/32 = 25%
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_high() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 26.0,
|
||||
total_gb: 32.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high")); // 81%
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_zero_total() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 0.0,
|
||||
total_gb: 0.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -1,16 +1,16 @@
|
||||
pub mod network;
|
||||
pub mod cpu;
|
||||
pub mod memory;
|
||||
pub mod hardware;
|
||||
pub mod disk;
|
||||
pub mod btrfs;
|
||||
pub mod audio;
|
||||
pub mod gpu;
|
||||
pub mod sys;
|
||||
pub mod bt;
|
||||
pub mod btrfs;
|
||||
pub mod buds;
|
||||
pub mod power;
|
||||
pub mod cpu;
|
||||
pub mod disk;
|
||||
pub mod game;
|
||||
pub mod gpu;
|
||||
pub mod hardware;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod power;
|
||||
pub mod sys;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::output::WaybarOutput;
|
||||
@@ -20,4 +20,3 @@ use anyhow::Result;
|
||||
pub trait WaybarModule {
|
||||
fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput>;
|
||||
}
|
||||
|
||||
|
||||
+117
-40
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -30,21 +30,20 @@ impl NetworkDaemon {
|
||||
|
||||
pub fn poll(&mut self, state: SharedState) {
|
||||
// Cache invalidation: if the interface directory doesn't exist, clear cache
|
||||
if let Some(ref iface) = self.cached_interface {
|
||||
if !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists() {
|
||||
if let Some(ref iface) = self.cached_interface
|
||||
&& !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists()
|
||||
{
|
||||
self.cached_interface = None;
|
||||
self.cached_ip = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-detect interface if needed
|
||||
if self.cached_interface.is_none() {
|
||||
if let Ok(iface) = get_primary_interface() {
|
||||
if !iface.is_empty() {
|
||||
self.cached_interface = Some(iface.clone());
|
||||
if self.cached_interface.is_none()
|
||||
&& let Ok(iface) = get_primary_interface()
|
||||
&& !iface.is_empty()
|
||||
{
|
||||
self.cached_ip = get_ip_address(&iface);
|
||||
}
|
||||
}
|
||||
self.cached_interface = Some(iface);
|
||||
}
|
||||
|
||||
if let Some(ref interface) = self.cached_interface {
|
||||
@@ -55,17 +54,26 @@ impl NetworkDaemon {
|
||||
|
||||
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;
|
||||
let time_diff = (time_now - self.last_time) as f64;
|
||||
let rx_mbps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) as f64
|
||||
/ time_diff
|
||||
/ 1024.0
|
||||
/ 1024.0;
|
||||
let tx_mbps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) as f64
|
||||
/ time_diff
|
||||
/ 1024.0
|
||||
/ 1024.0;
|
||||
|
||||
if let Ok(mut state_lock) = state.write() {
|
||||
state_lock.network.rx_mbps = rx_mbps;
|
||||
state_lock.network.tx_mbps = tx_mbps;
|
||||
state_lock.network.interface = interface.clone();
|
||||
state_lock.network.ip = self.cached_ip.clone().unwrap_or_default();
|
||||
}
|
||||
} else if let Ok(mut state_lock) = state.write() {
|
||||
// First poll: no speed data yet, but update interface/ip
|
||||
state_lock.network.interface = interface.clone();
|
||||
state_lock.network.ip = self.cached_ip.clone().unwrap_or_default();
|
||||
}
|
||||
|
||||
self.last_time = time_now;
|
||||
@@ -75,13 +83,32 @@ impl NetworkDaemon {
|
||||
// Read failed, might be down
|
||||
self.cached_interface = None;
|
||||
}
|
||||
} else if let Ok(mut state_lock) = state.write() {
|
||||
// No interface detected
|
||||
state_lock.network.interface.clear();
|
||||
state_lock.network.ip.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaybarModule for NetworkModule {
|
||||
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let interface = get_primary_interface()?;
|
||||
let (interface, ip, rx_mbps, tx_mbps) = if let Ok(s) = state.read() {
|
||||
(
|
||||
s.network.interface.clone(),
|
||||
s.network.ip.clone(),
|
||||
s.network.rx_mbps,
|
||||
s.network.tx_mbps,
|
||||
)
|
||||
} else {
|
||||
return Ok(WaybarOutput {
|
||||
text: "No connection".to_string(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
};
|
||||
|
||||
if interface.is_empty() {
|
||||
return Ok(WaybarOutput {
|
||||
text: "No connection".to_string(),
|
||||
@@ -91,24 +118,16 @@ impl WaybarModule for NetworkModule {
|
||||
});
|
||||
}
|
||||
|
||||
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 ip_display = if ip.is_empty() { "No IP" } else { &ip };
|
||||
|
||||
let mut output_text = format_template(
|
||||
&config.network.format,
|
||||
&[
|
||||
("interface", TokenValue::String(&interface)),
|
||||
("ip", TokenValue::String(&ip)),
|
||||
("ip", TokenValue::String(ip_display)),
|
||||
("rx", TokenValue::Float(rx_mbps)),
|
||||
("tx", TokenValue::Float(tx_mbps)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
if interface.starts_with("tun")
|
||||
@@ -116,12 +135,12 @@ impl WaybarModule for NetworkModule {
|
||||
|| interface.starts_with("ppp")
|
||||
|| interface.starts_with("pvpn")
|
||||
{
|
||||
output_text = format!(" {}", output_text);
|
||||
output_text = format!(" {}", output_text);
|
||||
}
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text: output_text,
|
||||
tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip)),
|
||||
tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip_display)),
|
||||
class: Some(interface),
|
||||
percentage: None,
|
||||
})
|
||||
@@ -129,10 +148,7 @@ impl WaybarModule for NetworkModule {
|
||||
}
|
||||
|
||||
fn get_primary_interface() -> Result<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["route", "list"])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stdout = run_command("ip", &["route", "list"])?;
|
||||
|
||||
let mut defaults = Vec::new();
|
||||
for line in stdout.lines() {
|
||||
@@ -163,14 +179,10 @@ fn get_primary_interface() -> Result<String> {
|
||||
}
|
||||
|
||||
fn get_ip_address(interface: &str) -> Option<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["-4", "addr", "show", interface])
|
||||
.output()
|
||||
.ok()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stdout = run_command("ip", &["-4", "addr", "show", interface]).ok()?;
|
||||
for line in stdout.lines() {
|
||||
if line.trim().starts_with("inet ") {
|
||||
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() > 1 {
|
||||
let ip_cidr = parts[1];
|
||||
let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr);
|
||||
@@ -198,3 +210,68 @@ fn get_bytes(interface: &str) -> Result<(u64, u64)> {
|
||||
|
||||
Ok((rx, tx))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, NetworkState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_network_no_connection() {
|
||||
let state = mock_state(AppState::default());
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.text, "No connection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_connected() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 1.5,
|
||||
tx_mbps: 0.3,
|
||||
interface: "eth0".to_string(),
|
||||
ip: "192.168.1.100".to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("eth0"));
|
||||
assert!(output.text.contains("192.168.1.100"));
|
||||
assert!(output.text.contains("1.50"));
|
||||
assert_eq!(output.class.as_deref(), Some("eth0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_vpn_prefix() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 0.0,
|
||||
tx_mbps: 0.0,
|
||||
interface: "wg0".to_string(),
|
||||
ip: "10.0.0.1".to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.starts_with(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_no_ip() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 0.0,
|
||||
tx_mbps: 0.0,
|
||||
interface: "eth0".to_string(),
|
||||
ip: String::new(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("No IP"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
|
||||
@@ -32,15 +32,15 @@ impl WaybarModule for PowerModule {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("AC") || name.starts_with("ADP") {
|
||||
let online_path = entry.path().join("online");
|
||||
if let Ok(online_str) = fs::read_to_string(online_path) {
|
||||
if online_str.trim() == "1" {
|
||||
if let Ok(online_str) = fs::read_to_string(online_path)
|
||||
&& online_str.trim() == "1"
|
||||
{
|
||||
ac_online = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(bat_path) = battery_path else {
|
||||
if ac_online {
|
||||
@@ -60,9 +60,11 @@ impl WaybarModule for PowerModule {
|
||||
}
|
||||
};
|
||||
|
||||
let capacity_str = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "0".to_string());
|
||||
let capacity_str =
|
||||
fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "0".to_string());
|
||||
let percentage: u8 = capacity_str.trim().parse().unwrap_or(0);
|
||||
let status_str = fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".to_string());
|
||||
let status_str =
|
||||
fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".to_string());
|
||||
let state = status_str.trim().to_lowercase();
|
||||
|
||||
let (icon, class, tooltip) = if state == "charging" || ac_online {
|
||||
@@ -95,7 +97,7 @@ impl WaybarModule for PowerModule {
|
||||
&[
|
||||
("percentage", TokenValue::Int(percentage as i64)),
|
||||
("icon", TokenValue::String(icon)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{format_template, TokenValue};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct SysModule;
|
||||
@@ -38,7 +38,7 @@ impl WaybarModule for SysModule {
|
||||
("load1", TokenValue::Float(load1)),
|
||||
("load5", TokenValue::Float(load5)),
|
||||
("load15", TokenValue::Float(load15)),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
|
||||
@@ -10,3 +10,55 @@ pub struct WaybarOutput {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub percentage: Option<u8>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_full_output_serialization() {
|
||||
let output = WaybarOutput {
|
||||
text: "CPU: 50%".to_string(),
|
||||
tooltip: Some("Details".to_string()),
|
||||
class: Some("normal".to_string()),
|
||||
percentage: Some(50),
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["text"], "CPU: 50%");
|
||||
assert_eq!(val["tooltip"], "Details");
|
||||
assert_eq!(val["class"], "normal");
|
||||
assert_eq!(val["percentage"], 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_fields_omitted() {
|
||||
let output = WaybarOutput {
|
||||
text: "test".to_string(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["text"], "test");
|
||||
assert!(val.get("tooltip").is_none());
|
||||
assert!(val.get("class").is_none());
|
||||
assert!(val.get("percentage").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_optional_fields() {
|
||||
let output = WaybarOutput {
|
||||
text: "test".to_string(),
|
||||
tooltip: Some("tip".to_string()),
|
||||
class: None,
|
||||
percentage: Some(75),
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["tooltip"], "tip");
|
||||
assert!(val.get("class").is_none());
|
||||
assert_eq!(val["percentage"], 75);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,23 @@ pub struct AppState {
|
||||
pub memory: MemoryState,
|
||||
pub sys: SysState,
|
||||
pub gpu: GpuState,
|
||||
pub disks: Vec<DiskInfo>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DiskInfo {
|
||||
pub mount_point: String,
|
||||
pub filesystem: String,
|
||||
pub total_bytes: u64,
|
||||
pub available_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct NetworkState {
|
||||
pub rx_mbps: f64,
|
||||
pub tx_mbps: f64,
|
||||
pub interface: String,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -73,3 +84,8 @@ impl Default for GpuState {
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<RwLock<AppState>>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn mock_state(state: AppState) -> SharedState {
|
||||
Arc::new(RwLock::new(state))
|
||||
}
|
||||
|
||||
+133
-5
@@ -2,6 +2,20 @@ use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Run an external command and return its stdout as a trimmed String.
|
||||
/// Provides clear error messages when the command is not found or fails.
|
||||
pub fn run_command(cmd: &str, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new(cmd)
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("'{}' not found or failed to execute. Is it installed?", cmd))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("'{}' exited with {}: {}", cmd, output.status, stderr.trim());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result<String> {
|
||||
let cmd_str = menu_cmd.replace("{prompt}", prompt);
|
||||
let mut child = Command::new("sh")
|
||||
@@ -14,7 +28,9 @@ pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result<Strin
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let input = items.join("\n");
|
||||
stdin.write_all(input.as_bytes()).context("Failed to write to menu stdin")?;
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.context("Failed to write to menu stdin")?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().context("Failed to wait on menu")?;
|
||||
@@ -49,8 +65,13 @@ pub fn format_template(template: &str, values: &[(&str, TokenValue)]) -> String
|
||||
let name = &caps[1];
|
||||
if let Some((_, val)) = values.iter().find(|(k, _)| *k == name) {
|
||||
let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">");
|
||||
let width = caps.get(3).map(|m| m.as_str().parse::<usize>().unwrap_or(0)).unwrap_or(0);
|
||||
let precision = caps.get(4).map(|m| m.as_str().parse::<usize>().unwrap_or(0));
|
||||
let width = caps
|
||||
.get(3)
|
||||
.map(|m| m.as_str().parse::<usize>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let precision = caps
|
||||
.get(4)
|
||||
.map(|m| m.as_str().parse::<usize>().unwrap_or(0));
|
||||
|
||||
match val {
|
||||
TokenValue::Float(f) => format_float(*f, align, width, precision),
|
||||
@@ -60,7 +81,8 @@ pub fn format_template(template: &str, values: &[(&str, TokenValue)]) -> String
|
||||
} else {
|
||||
caps[0].to_string()
|
||||
}
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn format_float(f: f64, align: &str, width: usize, precision: Option<usize>) -> String {
|
||||
@@ -89,6 +111,112 @@ fn format_str(s: &str, align: &str, width: usize) -> String {
|
||||
"<" => format!("{:<width$}", s, width = width),
|
||||
"^" => format!("{:^width$}", s, width = width),
|
||||
">" => format!("{:>width$}", s, width = width),
|
||||
_ => format!("{}", s),
|
||||
_ => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_string_token() {
|
||||
let result = format_template("{name}", &[("name", TokenValue::String("hello"))]);
|
||||
assert_eq!(result, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_float_token() {
|
||||
let result = format_template("{val}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, "3.15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_int_token() {
|
||||
let result = format_template("{count}", &[("count", TokenValue::Int(42))]);
|
||||
assert_eq!(result, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_right_align_with_precision() {
|
||||
let result = format_template("{val:>8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, " 3.15");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_left_align_with_precision() {
|
||||
let result = format_template("{val:<8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, "3.15 ");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_center_align_with_precision() {
|
||||
let result = format_template("{val:^8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, " 3.15 ");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_right_align() {
|
||||
let result = format_template("{val:>5}", &[("val", TokenValue::Int(42))]);
|
||||
assert_eq!(result, " 42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_left_align() {
|
||||
let result = format_template("{val:<10}", &[("val", TokenValue::String("hi"))]);
|
||||
assert_eq!(result, "hi ");
|
||||
assert_eq!(result.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_token_preserved() {
|
||||
let result = format_template("{unknown}", &[("name", TokenValue::String("test"))]);
|
||||
assert_eq!(result, "{unknown}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_tokens() {
|
||||
let result = format_template(
|
||||
"CPU: {usage:>4.1}% {temp:>4.1}C",
|
||||
&[
|
||||
("usage", TokenValue::Float(55.3)),
|
||||
("temp", TokenValue::Float(65.0)),
|
||||
],
|
||||
);
|
||||
assert_eq!(result, "CPU: 55.3% 65.0C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_tokens() {
|
||||
let result = format_template("plain text", &[]);
|
||||
assert_eq!(result, "plain text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_template() {
|
||||
let result = format_template("", &[("x", TokenValue::Int(1))]);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_token_types() {
|
||||
let result = format_template(
|
||||
"{name} ({ip}): {rx:>5.2} MB/s",
|
||||
&[
|
||||
("name", TokenValue::String("eth0")),
|
||||
("ip", TokenValue::String("10.0.0.1")),
|
||||
("rx", TokenValue::Float(1.5)),
|
||||
],
|
||||
);
|
||||
assert_eq!(result, "eth0 (10.0.0.1): 1.50 MB/s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_precision_zero() {
|
||||
let result = format_template("{val:>3.0}", &[("val", TokenValue::Float(99.7))]);
|
||||
assert_eq!(result, "100");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user