Compare commits
1 Commits
c1b3d9134e
..
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f640f116ec |
@@ -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
+260
-1
@@ -169,6 +169,22 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fluxo-rs"
|
name = "fluxo-rs"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -180,11 +196,40 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -197,6 +242,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -204,7 +255,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -225,12 +278,24 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.183"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -334,6 +399,16 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -352,6 +427,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.3"
|
version = "1.12.3"
|
||||||
@@ -381,6 +462,25 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -479,6 +579,19 @@ dependencies = [
|
|||||||
"windows",
|
"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]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -594,6 +707,12 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -606,6 +725,58 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -753,6 +924,94 @@ version = "0.7.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
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]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -14,3 +14,6 @@ sysinfo = "0.38.4"
|
|||||||
toml = "1.0.6"
|
toml = "1.0.6"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
+147
-1
@@ -1,6 +1,8 @@
|
|||||||
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
@@ -224,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 {
|
pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||||
let config_path = custom_path.unwrap_or_else(|| {
|
let config_path = custom_path.unwrap_or_else(|| {
|
||||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||||
@@ -236,9 +312,10 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||||
match toml::from_str(&content) {
|
match toml::from_str::<Config>(&content) {
|
||||||
Ok(cfg) => {
|
Ok(cfg) => {
|
||||||
info!("Successfully loaded configuration from {:?}", config_path);
|
info!("Successfully loaded configuration from {:?}", config_path);
|
||||||
|
cfg.validate();
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -255,3 +332,72 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
|||||||
Config::default()
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-16
@@ -27,14 +27,15 @@ impl WaybarModule for BtModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(stdout) = run_command("bluetoothctl", &["show"])
|
if let Ok(stdout) = run_command("bluetoothctl", &["show"])
|
||||||
&& stdout.contains("Powered: no") {
|
&& stdout.contains("Powered: no")
|
||||||
return Ok(WaybarOutput {
|
{
|
||||||
text: config.bt.format_disabled.clone(),
|
return Ok(WaybarOutput {
|
||||||
tooltip: Some("Bluetooth Disabled".to_string()),
|
text: config.bt.format_disabled.clone(),
|
||||||
class: Some("disabled".to_string()),
|
tooltip: Some("Bluetooth Disabled".to_string()),
|
||||||
percentage: None,
|
class: Some("disabled".to_string()),
|
||||||
});
|
percentage: None,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(mac) = find_audio_device() {
|
if let Some(mac) = find_audio_device() {
|
||||||
let info = run_command("bluetoothctl", &["info", &mac])?;
|
let info = run_command("bluetoothctl", &["info", &mac])?;
|
||||||
@@ -90,12 +91,13 @@ impl WaybarModule for BtModule {
|
|||||||
|
|
||||||
fn find_audio_device() -> Option<String> {
|
fn find_audio_device() -> Option<String> {
|
||||||
if let Ok(sink) = run_command("pactl", &["get-default-sink"])
|
if let Ok(sink) = run_command("pactl", &["get-default-sink"])
|
||||||
&& sink.starts_with("bluez_output.") {
|
&& sink.starts_with("bluez_output.")
|
||||||
let parts: Vec<&str> = sink.split('.').collect();
|
{
|
||||||
if parts.len() >= 2 {
|
let parts: Vec<&str> = sink.split('.').collect();
|
||||||
return Some(parts[1].replace('_', ":"));
|
if parts.len() >= 2 {
|
||||||
}
|
return Some(parts[1].replace('_', ":"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) {
|
if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) {
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
@@ -104,9 +106,10 @@ fn find_audio_device() -> Option<String> {
|
|||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
let mac = parts[1];
|
let mac = parts[1];
|
||||||
if let Ok(info_str) = run_command("bluetoothctl", &["info", mac])
|
if let Ok(info_str) = run_command("bluetoothctl", &["info", mac])
|
||||||
&& info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") {
|
&& info_str.contains("0000110b-0000-1000-8000-00805f9b34fb")
|
||||||
return Some(mac.to_string());
|
{
|
||||||
}
|
return Some(mac.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-28
@@ -2,9 +2,8 @@ use crate::config::Config;
|
|||||||
use crate::modules::WaybarModule;
|
use crate::modules::WaybarModule;
|
||||||
use crate::output::WaybarOutput;
|
use crate::output::WaybarOutput;
|
||||||
use crate::state::SharedState;
|
use crate::state::SharedState;
|
||||||
use crate::utils::{TokenValue, format_template};
|
use crate::utils::{TokenValue, format_template, run_command};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
pub struct BudsModule;
|
pub struct BudsModule;
|
||||||
|
|
||||||
@@ -15,8 +14,7 @@ impl WaybarModule for BudsModule {
|
|||||||
|
|
||||||
match *action {
|
match *action {
|
||||||
"cycle_anc" => {
|
"cycle_anc" => {
|
||||||
let output = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
let current_mode = run_command("pbpctrl", &["get", "anc"])?;
|
||||||
let current_mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
|
|
||||||
let next_mode = match current_mode.as_str() {
|
let next_mode = match current_mode.as_str() {
|
||||||
"active" => "aware",
|
"active" => "aware",
|
||||||
@@ -24,9 +22,7 @@ impl WaybarModule for BudsModule {
|
|||||||
_ => "active",
|
_ => "active",
|
||||||
};
|
};
|
||||||
|
|
||||||
Command::new("pbpctrl")
|
let _ = run_command("pbpctrl", &["set", "anc", next_mode]);
|
||||||
.args(["set", "anc", next_mode])
|
|
||||||
.status()?;
|
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
@@ -35,9 +31,7 @@ impl WaybarModule for BudsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
"connect" => {
|
"connect" => {
|
||||||
Command::new("bluetoothctl")
|
let _ = run_command("bluetoothctl", &["connect", mac]);
|
||||||
.args(["connect", mac])
|
|
||||||
.status()?;
|
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
@@ -46,9 +40,7 @@ impl WaybarModule for BudsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
Command::new("bluetoothctl")
|
let _ = run_command("bluetoothctl", &["disconnect", mac]);
|
||||||
.args(["disconnect", mac])
|
|
||||||
.status()?;
|
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
@@ -62,8 +54,7 @@ impl WaybarModule for BudsModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bt_info = Command::new("bluetoothctl").args(["info", mac]).output()?;
|
let bt_str = run_command("bluetoothctl", &["info", mac])?;
|
||||||
let bt_str = String::from_utf8_lossy(&bt_info.stdout);
|
|
||||||
|
|
||||||
if !bt_str.contains("Connected: yes") {
|
if !bt_str.contains("Connected: yes") {
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
@@ -74,18 +65,18 @@ impl WaybarModule for BudsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let bat_cmd = Command::new("pbpctrl").args(["show", "battery"]).output();
|
let bat_output = match run_command("pbpctrl", &["show", "battery"]) {
|
||||||
if bat_cmd.is_err() || !bat_cmd.as_ref().unwrap().status.success() {
|
Ok(output) => output,
|
||||||
return Ok(WaybarOutput {
|
Err(_) => {
|
||||||
text: config.buds.format_disconnected.clone(),
|
return Ok(WaybarOutput {
|
||||||
tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()),
|
text: config.buds.format_disconnected.clone(),
|
||||||
class: Some("disconnected".to_string()),
|
tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()),
|
||||||
percentage: None,
|
class: Some("disconnected".to_string()),
|
||||||
});
|
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 left_bud = "unknown";
|
||||||
let mut right_bud = "unknown";
|
let mut right_bud = "unknown";
|
||||||
|
|
||||||
@@ -117,8 +108,7 @@ impl WaybarModule for BudsModule {
|
|||||||
format!("{}%", right_bud)
|
format!("{}%", right_bud)
|
||||||
};
|
};
|
||||||
|
|
||||||
let anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
let current_mode = run_command("pbpctrl", &["get", "anc"]).unwrap_or_default();
|
||||||
let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string();
|
|
||||||
|
|
||||||
let (anc_icon, class) = match current_mode.as_str() {
|
let (anc_icon, class) = match current_mode.as_str() {
|
||||||
"active" => ("ANC", "anc-active"),
|
"active" => ("ANC", "anc-active"),
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,3 +65,48 @@ impl WaybarModule for DiskModule {
|
|||||||
Err(anyhow::anyhow!("Mountpoint {} not found", mountpoint))
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+68
-10
@@ -2,7 +2,7 @@ use crate::config::Config;
|
|||||||
use crate::modules::WaybarModule;
|
use crate::modules::WaybarModule;
|
||||||
use crate::output::WaybarOutput;
|
use crate::output::WaybarOutput;
|
||||||
use crate::state::SharedState;
|
use crate::state::SharedState;
|
||||||
use crate::utils::{TokenValue, format_template};
|
use crate::utils::{TokenValue, format_template, run_command};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@@ -148,10 +148,7 @@ impl WaybarModule for NetworkModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_primary_interface() -> Result<String> {
|
fn get_primary_interface() -> Result<String> {
|
||||||
let output = std::process::Command::new("ip")
|
let stdout = run_command("ip", &["route", "list"])?;
|
||||||
.args(["route", "list"])
|
|
||||||
.output()?;
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
|
|
||||||
let mut defaults = Vec::new();
|
let mut defaults = Vec::new();
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
@@ -182,11 +179,7 @@ fn get_primary_interface() -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_ip_address(interface: &str) -> Option<String> {
|
fn get_ip_address(interface: &str) -> Option<String> {
|
||||||
let output = std::process::Command::new("ip")
|
let stdout = run_command("ip", &["-4", "addr", "show", interface]).ok()?;
|
||||||
.args(["-4", "addr", "show", interface])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
if line.trim().starts_with("inet ") {
|
if line.trim().starts_with("inet ") {
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
@@ -217,3 +210,68 @@ fn get_bytes(interface: &str) -> Result<(u64, u64)> {
|
|||||||
|
|
||||||
Ok((rx, tx))
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,3 +10,55 @@ pub struct WaybarOutput {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub percentage: Option<u8>,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,3 +84,8 @@ impl Default for GpuState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedState = Arc<RwLock<AppState>>;
|
pub type SharedState = Arc<RwLock<AppState>>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn mock_state(state: AppState) -> SharedState {
|
||||||
|
Arc::new(RwLock::new(state))
|
||||||
|
}
|
||||||
|
|||||||
+106
@@ -114,3 +114,109 @@ fn format_str(s: &str, align: &str, width: usize) -> String {
|
|||||||
_ => s.to_string(),
|
_ => 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