├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .pkg └── aur │ ├── PKGBUILD │ └── update.sh ├── Cargo.lock ├── Cargo.toml ├── README.md ├── baru.yaml ├── build.rs ├── lib ├── audio │ ├── CMakeLists.txt │ ├── include │ │ └── audio.h │ └── src │ │ └── audio.c └── netlink │ ├── CMakeLists.txt │ ├── include │ └── netlink.h │ └── src │ ├── common.c │ ├── wired.c │ └── wireless.c ├── public └── baru.png └── src ├── cli.rs ├── error.rs ├── http.rs ├── lib.rs ├── main.rs ├── module.rs ├── modules ├── battery.rs ├── brightness.rs ├── cpu_freq.rs ├── cpu_usage.rs ├── date_time.rs ├── memory.rs ├── mic.rs ├── mod.rs ├── sound.rs ├── temperature.rs ├── weather.rs ├── wired.rs └── wireless.rs ├── netlink.rs ├── pulse.rs ├── signal.rs ├── trace.rs └── util.rs /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | test: 10 | uses: ./.github/workflows/test.yml 11 | 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | needs: test 16 | env: 17 | ARTIFACT_DIR: artifact 18 | outputs: 19 | sha256: ${{ steps.gen_sha256.outputs.sha256 }} 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | - name: Install dependencies 26 | run: sudo apt-get install libnl-3-dev libnl-genl-3-dev libnl-route-3-dev libpulse-dev 27 | - name: Build 28 | run: cargo build --release --locked 29 | - name: Prepare artifact 30 | run: | 31 | mkdir $ARTIFACT_DIR 32 | mv -f target/release/baru $ARTIFACT_DIR/ 33 | - name: Gen sha256 34 | id: gen_sha256 35 | working-directory: ${{ env.ARTIFACT_DIR }} 36 | run: | 37 | sha256=$(sha256sum baru) 38 | echo "$sha256" 39 | echo "sha256=$sha256" >> "$GITHUB_OUTPUT" 40 | echo "$sha256" > baru.sha256sum 41 | - name: Upload artifact 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: baru 45 | path: ${{ env.ARTIFACT_DIR }} 46 | retention-days: 2 47 | 48 | release: 49 | name: Release 50 | needs: build 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: write 54 | env: 55 | SHA256: ${{ needs.build.outputs.sha256 }} 56 | steps: 57 | - name: Download binary artifact 58 | uses: actions/download-artifact@v4 59 | with: 60 | name: baru 61 | - name: Integrity check 62 | run: | 63 | sha256sum -c baru.sha256sum 64 | [ "$SHA256" == "$(cat baru.sha256sum)" ] && echo "sha256 OK" 65 | - name: Create release 66 | uses: softprops/action-gh-release@v2 67 | with: 68 | files: | 69 | baru 70 | baru.sha256sum 71 | 72 | aur-packaging: 73 | name: Publish AUR package 74 | needs: release 75 | runs-on: ubuntu-latest 76 | env: 77 | PKGNAME: baru 78 | PKGBUILD: ./.pkg/aur/PKGBUILD 79 | RELEASE_TAG: ${{ github.ref_name }} 80 | REPOSITORY: ${{ github.repository }} 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@v4 84 | - name: Download sources 85 | run: curl -LfsSo "$PKGNAME-$RELEASE_TAG".tar.gz "https://github.com/$REPOSITORY/archive/refs/tags/$RELEASE_TAG.tar.gz" 86 | - name: Update PKGBUILD 87 | run: ./.pkg/aur/update.sh 88 | - name: Show PKGBUILD 89 | run: cat "$PKGBUILD" 90 | - name: Publish 91 | uses: KSXGitHub/github-actions-deploy-aur@v2.7.2 92 | with: 93 | pkgname: ${{ env.PKGNAME }} 94 | pkgbuild: ${{ env.PKGBUILD }} 95 | commit_username: ${{ secrets.AUR_USERNAME }} 96 | commit_email: ${{ secrets.AUR_EMAIL }} 97 | ssh_private_key: ${{ secrets.AUR_SSH_KEY }} 98 | commit_message: ${{ github.ref_name }} 99 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_call: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Install Rust toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | components: clippy 24 | - name: Install dependencies 25 | run: sudo apt-get install libnl-3-dev libnl-genl-3-dev libnl-route-3-dev libpulse-dev 26 | - name: Lint 27 | run: cargo clippy 28 | - name: Check 29 | run: cargo check 30 | - name: Test 31 | run: cargo test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .clangd 4 | compile_commands.json 5 | cmake-build-debug 6 | -------------------------------------------------------------------------------- /.pkg/aur/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Pierre Dommerc 2 | 3 | pkgname=baru 4 | pkgver=0.1.0 5 | pkgrel=1 6 | pkgdesc='A simple system monitor for WM statusbar' 7 | arch=('x86_64') 8 | url='https://github.com/doums/baru' 9 | license=('MPL-2.0') 10 | depends=('libnl' 'libpulse') 11 | makedepends=('rust' 'cargo') 12 | provides=('baru') 13 | conflicts=('baru') 14 | options=(!debug) 15 | source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz") 16 | sha256sums=('xxx') 17 | 18 | build() { 19 | cd "$pkgname-$pkgver" 20 | cargo build --release --locked 21 | } 22 | 23 | package() { 24 | cd "$pkgname-$pkgver" 25 | install -Dvm 755 "target/release/baru" "$pkgdir/usr/bin/baru" 26 | install -Dvm 644 "baru.yaml" "$pkgdir/usr/share/baru/baru.yaml" 27 | } 28 | 29 | -------------------------------------------------------------------------------- /.pkg/aur/update.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # script to bump version and update sources hash of a PKGBUILD 4 | 5 | set -e 6 | 7 | red="\e[38;5;1m" 8 | green="\e[38;5;2m" 9 | bold="\e[1m" 10 | reset="\e[0m" 11 | 12 | if [ -z "$PKGBUILD" ]; then 13 | >&2 printf " %b%b✕%b PKGBUILD not set\n" "$red" "$bold" "$reset" 14 | exit 1 15 | fi 16 | 17 | if [ -z "$PKGNAME" ]; then 18 | >&2 printf " %b%b✕%b PKGNAME not set\n" "$red" "$bold" "$reset" 19 | exit 1 20 | fi 21 | 22 | if [ -z "$RELEASE_TAG" ]; then 23 | >&2 printf " %b%b✕%b RELEASE_TAG not set\n" "$red" "$bold" "$reset" 24 | exit 1 25 | fi 26 | 27 | if ! [ -a "$PKGBUILD" ]; then 28 | >&2 printf " %b%b✕%b no such file $PKGBUILD\n" "$red" "$bold" "$reset" 29 | exit 1 30 | fi 31 | 32 | if ! [[ "$RELEASE_TAG" =~ ^v.*? ]]; then 33 | >&2 printf " %b%b✕%b invalid tag $RELEASE_TAG\n" "$red" "$bold" "$reset" 34 | exit 1 35 | fi 36 | 37 | pkgver="${RELEASE_TAG#v}" 38 | tarball="$PKGNAME-$RELEASE_TAG".tar.gz 39 | 40 | if ! [ -a "$tarball" ]; then 41 | >&2 printf " %b%b✕%b no such file $tarball\n" "$red" "$bold" "$reset" 42 | exit 1 43 | fi 44 | 45 | # bump package version 46 | sed -i "s/pkgver=.*/pkgver=$pkgver/" "$PKGBUILD" 47 | printf " %b%b✓%b bump version to $RELEASE_TAG\n" "$green" "$bold" "$reset" 48 | 49 | # generate new checksum 50 | sum=$(set -o pipefail && sha256sum "$tarball" | awk '{print $1}') 51 | sed -i "s/sha256sums=('.*')/sha256sums=('$sum')/" "$PKGBUILD" 52 | printf " %b%b✓%b generated checksum $sum\n" "$green" "$bold" "$reset" 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baru" 3 | version = "0.4.3" 4 | description = "A simple system monitor for WM statusbar" 5 | authors = ["pierre "] 6 | edition = "2021" 7 | links = "netlink,audio" 8 | build = "build.rs" 9 | 10 | [dependencies] 11 | anyhow = "1.0.86" 12 | clap = { version = "4.5", features = ["derive"] } 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_yaml = "0.9" 15 | tracing = "0.1" 16 | tracing-subscriber = { version = "0.3.1", features = [ 17 | "tracing-log", 18 | "env-filter", 19 | ] } 20 | tracing-appender = "0.2" 21 | once_cell = "1.19.0" 22 | chrono = "0.4" 23 | regex = "1" 24 | reqwest = { version = "0.12.6", features = ["blocking", "json"] } 25 | signal-hook = "0.3.17" 26 | 27 | [build-dependencies] 28 | cmake = "0.1" 29 | 30 | [profile.release] 31 | codegen-units = 1 32 | strip = true 33 | opt-level = "s" 34 | lto = true 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![baru](https://img.shields.io/github/actions/workflow/status/doums/baru/test.yml?color=0D0D0D&logoColor=BFBFBF&labelColor=404040&logo=github&style=for-the-badge)](https://github.com/doums/baru/actions?query=workflow%3ATest) 2 | [![baru](https://img.shields.io/aur/version/baru?color=0D0D0D&logoColor=BFBFBF&labelColor=404040&logo=arch-linux&style=for-the-badge)](https://aur.archlinux.org/packages/baru/) 3 | 4 | ## baru 5 | 6 | A simple system monitor for WM statusbar 7 | 8 | ![baru](https://raw.githubusercontent.com/doums/baru/master/public/baru.png) 9 | 10 | Baru is a lightweight system monitor for WM status-bar.\ 11 | It can be used as a provider with any status-bar that can read from `stdout`.\ 12 | Like [xmobar](https://codeberg.org/xmobar/xmobar), 13 | [lemonbar](https://github.com/LemonBoy/bar), 14 | [dwm](https://dwm.suckless.org/status_monitor/) etc… 15 | 16 | --- 17 | 18 | [features](#features) ∎ [prerequisite](#prerequisite) ∎ [install](#install) ∎ [configuration](#configuration) ∎ [usage](#usage) ∎ [credits](#credits) ∎ [license](#license) 19 | 20 | ### Features 21 | 22 | * date and time 23 | * battery (level, status, design level based) 24 | * wireless (state, essid, signal strength) 25 | * wired (state) 26 | * audio sink and source (level, muted) 27 | * brightness 28 | * cpu usage, frequency and temperature 29 | * memory (percent or used/total in gigabyte/gibibyte) 30 | * weather current condition and 31 | temperature ([OpenWeatherMap](https://openweathermap.org/)) 32 | * dynamic and customizable labels, play nicely with icons and [nerd-fonts](https://www.nerdfonts.com/) 33 | * customizable format output 34 | * configuration in YAML 35 | 36 | ### Prerequisite 37 | 38 | The following system libraries are required: 39 | 40 | - libnl (for wired and wireless modules) 41 | - libpulse (for sound and mic modules) 42 | 43 | ### Install 44 | 45 | - Arch Linux (AUR) [package](https://aur.archlinux.org/packages/baru) 46 | - latest [release](https://github.com/doums/baru/releases) 47 | 48 | ### Configuration 49 | 50 | The binary looks for the config file `baru.yaml` located 51 | in `$XDG_CONFIG_HOME/baru/` (default to `$HOME/.config/baru/`).\ 52 | If the config file is not found, baru prints an error and exits. 53 | 54 | You can find the full config details [here](https://github.com/doums/baru/blob/master/baru.yaml). 55 | 56 | TIPS: To test and debug your config run baru from the terminal like this: 57 | 58 | ```shell 59 | RUST_LOG=debug baru -l stdout 60 | ``` 61 | 62 | Use the root `format` option to customize baru output.\ 63 | You can pick which modules you want to display. Using the special markup `%x` 64 | where `x` is the letter of the module. 65 | These markups are replaced by the output of the corresponding modules. 66 | 67 | Modules available: 68 | - `a` battery 69 | - `b` brightness 70 | - `c` cpu usage 71 | - `d` datetime 72 | - `e` wired 73 | - `f` cpu frequency 74 | - `m` memory 75 | - `i` mic 76 | - `r` weather 77 | - `s` sound 78 | - `t` temperature 79 | - `w` wireless 80 | 81 | Module output:\ 82 | Each module takes a `format` option.\ 83 | `%l` and `%v` are respectively the label and the current value of the module. 84 | 85 | #### Config example 86 | 87 | ```yaml 88 | format: '%m %f %c %t %b %i %s %w%e %a %d' 89 | tick: 50 90 | 91 | # modules configuration 92 | battery: 93 | full_design: true 94 | low_level: 30 95 | full_label: '*' 96 | charging_label: '^' 97 | discharging_label: 'b' 98 | low_label: '!' 99 | unknown_label: '?' 100 | format: '%l %v' # display label and value 101 | brightness: 102 | label: 'l' 103 | format: '%l %v' 104 | cpu_usage: 105 | label: 'c' 106 | high_label: '!' 107 | format: '%v %l' 108 | cpu_freq: 109 | tick: 100 110 | high_level: 60 111 | label: 'f' 112 | high_label: '!' 113 | format: '%v %l' 114 | memory: 115 | label: 'm' 116 | high_label: '!' 117 | format: '%v %l' 118 | mic: 119 | label: 'i' 120 | mute_label: '.' 121 | format: '%v %l' 122 | sound: 123 | label: 's' 124 | mute_label: '.' 125 | format: '%v %l' 126 | temperature: 127 | core_inputs: 2..5 128 | label: 't' 129 | high_label: '!' 130 | format: '%v %l' 131 | wired: 132 | discrete: true 133 | label: 'e' 134 | disconnected_label: '\' 135 | format: '%l' # display label only 136 | wireless: 137 | interface: wlan0 138 | display: Essid 139 | max_essid_len: 5 140 | label: 'w' 141 | disconnected_label: '\' 142 | format: '%v %l' 143 | weather: 144 | tick: 300 # seconds 145 | # your openweathermap api key 146 | api_key: 1234567890 147 | location: 'Metz' 148 | unit: metric 149 | icons: 150 | clear_sky: [ '󰖙', '󰖔' ] # day, night 151 | partly_cloudy: [ '󰖕', '󰼱' ] 152 | cloudy: '󰖐' 153 | very_cloudy: '󰖐' 154 | shower_rain: '󰖖' 155 | rain: '󰖖' 156 | thunderstorm: '󰖓' 157 | snow: '󰖘' 158 | mist: '󰖑' 159 | format: '%v' 160 | ``` 161 | 162 | ### Usage 163 | 164 | ```shell 165 | baru -h 166 | ``` 167 | 168 | When spawning baru from your WM/status-bar you can pass the `-l file` flag\ 169 | if you want baru to log into a file (useful for debugging).\ 170 | Logs are written to the directory `$XDG_CACHE_HOME/baru/` (default 171 | to `$HOME/.cache/baru/`). 172 | 173 | ```shell 174 | baru -l file 175 | ``` 176 | 177 | ### Implementation details 178 | 179 | Baru gathers the information from `/sys` and `/proc` filesystems (filled by the 180 | kernel).\ 181 | Except audio and network modules which use C libraries.\ 182 | All modules are threaded and loaded on-demand.\ 183 | Thanks to this modular design (as well Rust and C), baru is lightweight and 184 | efficient.\ 185 | It can run at high refresh rate with a minimal cpu footprint. 186 | 187 | The audio module communicates with 188 | the [PipeWire](https://pipewire.org/)/[PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)\ 189 | server 190 | through [client API](https://freedesktop.org/software/pulseaudio/doxygen/) to 191 | retrieve its data. Wireless and wired\ 192 | modules use the netlink interface with the help 193 | of [libnl](https://www.infradead.org/~tgr/libnl/) to talk directly\ 194 | to kernel and retrieve their data.\ 195 | In addition, wireless module uses 196 | the [802.11](https://github.com/torvalds/linux/blob/master/include/uapi/linux/nl80211.h) 197 | API. 198 | 199 | ### Dev 200 | 201 | #### Prerequisites 202 | 203 | - [Rust](https://www.rust-lang.org/tools/install) 204 | - CMake 205 | - libnl and libpulse present on the system 206 | 207 | ```shell 208 | RUST_LOG=trace cargo run -- -l stdout 209 | ``` 210 | 211 | ### Credits 212 | 213 | Clément Dommerc for providing me with the C code for the lib `netlink`, wireless 214 | part. 215 | 216 | ### License 217 | 218 | Mozilla Public License 2.0 219 | -------------------------------------------------------------------------------- /baru.yaml: -------------------------------------------------------------------------------- 1 | # List of all options 2 | 3 | # If the word "required" is mentioned, the corresponding option is required. 4 | # Otherwise, it is optional. 5 | 6 | 7 | # # # # # 8 | # Root # 9 | # # # # # 10 | 11 | # format: String, required 12 | # 13 | # The global output. 14 | # 15 | # You can pick which modules you want to display. Using the special markup `%x` 16 | # where `x` is the letter of the module. 17 | # These markups are replaced by the output of the corresponding modules. 18 | # 19 | # Modules available: 20 | # a → battery 21 | # b → brightness 22 | # c → cpu usage 23 | # d → datetime 24 | # e → wired 25 | # f → cpu frequency 26 | # m → memory 27 | # i → mic 28 | # r → weather 29 | # s → sound 30 | # t → temperature 31 | # w → wireless 32 | # 33 | # The character "%" can be escaped by prepending a backslash: `\%` 34 | # 35 | format: '%c %t %b %s %w%e %a %d' 36 | 37 | # tick: u32, default: 50 38 | # 39 | # The main refresh rate in millisecond. 40 | # 41 | tick: 100 42 | 43 | # pulse_tick: u32, default: 50 44 | # 45 | # The refresh rate in millisecond of the PulseAudio thread. 46 | # 47 | pulse_tick: 100 48 | 49 | # failed_icon: String, default: ✗ 50 | # 51 | # The icon printed when a module has failed. 52 | # 53 | failed_icon: '✗' 54 | 55 | # Module output: 56 | # Each module takes a `format` string option. 57 | # `%l` and `%v` are respectively the label and the current value of the module. 58 | # Note: for some module, the label can be dynamic. See below. 59 | 60 | 61 | # # # # # # # # # # 62 | # Battery module # 63 | # # # # # # # # # # 64 | 65 | battery: 66 | # Takes the following options: 67 | 68 | # tick: u32, default: 500 69 | # 70 | # The refresh rate in millisecond of the module thread. 71 | # 72 | tick: 1000 73 | 74 | # placeholder: String, default: - 75 | # 76 | # Value to display when there is no data available yet. 77 | # 78 | placeholder: '-' 79 | 80 | # name: String, default: BAT0 81 | # 82 | # The directory name under /sys/class/power_supply/ 83 | # 84 | name: BAT1 85 | 86 | # low_level: u32, default: 10 87 | # 88 | # The level below which the battery level is considered low. 89 | # 90 | low_level: 20 91 | 92 | # full_design: bool, default: false 93 | # 94 | # Whether or not the current level is calculated based on the full design value. 95 | # 96 | full_design: true 97 | 98 | # full_label: String, default: *ba 99 | # 100 | # The label printed when the battery is full. 101 | # 102 | full_label: '*ba' 103 | 104 | # charging_label: String, default: ^ba 105 | # 106 | # The label printed when the battery is charging. 107 | # 108 | charging_label: '^ba' 109 | 110 | # discharging_label: String, default: bat 111 | # 112 | # The label printed when the battery is discharging. 113 | # 114 | discharging_label: bat 115 | 116 | # low_label: String, default: !ba 117 | # 118 | # The label printed when the battery is discharging and the level is low. 119 | # 120 | low_label: '!ba' 121 | 122 | # unknown_label: String, default: .ba 123 | # 124 | # The label printed when the battery state is unknown. 125 | # 126 | unknown_label: '.ba' 127 | 128 | # format: String, default: %l:%v 129 | # 130 | # The module format. 131 | # 132 | format: '%l:%v' 133 | 134 | 135 | # # # # # # # # # # # 136 | # Brightness module # 137 | # # # # # # # # # # # 138 | 139 | brightness: 140 | # Takes the following option: 141 | 142 | # tick: u32, default: 50 143 | # 144 | # The refresh rate in millisecond of the module thread. 145 | # 146 | tick: 100 147 | 148 | # placeholder: String, default: - 149 | # 150 | # Value to display when there is no data available yet. 151 | # 152 | placeholder: '-' 153 | 154 | # sys_path: String, default: /sys/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight 155 | # 156 | # /sys/devices path 157 | # 158 | sys_path: /sys/devices/something/intel_backlight 159 | 160 | # label: String, default: bri 161 | # 162 | # The label printed. 163 | # 164 | label: bri 165 | 166 | # format: String, default: %l:%v 167 | # 168 | # The module format. 169 | # 170 | format: '%l:%v' 171 | 172 | 173 | # # # # # # # # # # # 174 | # Cpu usage module # 175 | # # # # # # # # # # # 176 | 177 | cpu_usage: 178 | # Takes the following options: 179 | 180 | # tick: u32, default: 500 181 | # 182 | # The refresh rate in millisecond of the module thread. 183 | # 184 | tick: 1000 185 | 186 | # placeholder: String, default: - 187 | # 188 | # Value to display when there is no data available yet. 189 | # 190 | placeholder: '-' 191 | 192 | # high_level: u32, default: 90 193 | # 194 | # The percentage above which the cpu usage is considered high. 195 | # 196 | high_level: 95 197 | 198 | # label: String, default: cpu 199 | # 200 | # The label printed when the cpu usage is below high level. 201 | # 202 | label: cpu 203 | 204 | # high_label: String, default: !cp 205 | # 206 | # The label printed when the cpu usage is above high level. 207 | # 208 | high_label: '!cp' 209 | 210 | # format: String, default: %l:%v 211 | # 212 | # The module format. 213 | # 214 | format: '%l:%v' 215 | 216 | 217 | # # # # # # # # # # # # # 218 | # Cpu frequency module # 219 | # # # # # # # # # # # # # 220 | 221 | cpu_freq: 222 | # Takes the following options: 223 | 224 | # tick: u32, default: 100 225 | # 226 | # The refresh rate in millisecond of the module thread. 227 | # 228 | tick: 500 229 | 230 | # placeholder: String, default: - 231 | # 232 | # Value to display when there is no data available yet. 233 | # 234 | placeholder: '-' 235 | 236 | # show_max_freq: bool, default: false 237 | # 238 | # Whether or not the maximum cpu frequency is printed. 239 | # 240 | max_freq: true 241 | 242 | # unit: Unit, default: Smart 243 | # 244 | # enum Display { MHz, GHz, Smart } 245 | # 246 | # The unit of the frequency. 247 | # Smart means if the frequency is less than 1000MHz it will be printed in MHz. 248 | # Otherwise it will be printed in GHz. 249 | # 250 | unit: MHz 251 | 252 | # high_level: u32, default: 80 253 | # 254 | # The percentage above which the cpu frequency is considered high. 255 | # 256 | high_level: 90 257 | 258 | # label: String, default: fre 259 | # 260 | # The label printed when the cpu frequency is below high level. 261 | # 262 | label: fre 263 | 264 | # high_label: String, default: !fr 265 | # 266 | # The label printed when the cpu frequency is above high level. 267 | # 268 | high_label: '!fr' 269 | 270 | # format: String, default: %l:%v 271 | # 272 | # The module format. 273 | # 274 | format: '%l:%v' 275 | 276 | 277 | # # # # # # # # # # 278 | # DateTime module # 279 | # # # # # # # # # # 280 | 281 | date_time: 282 | # Takes the following option: 283 | 284 | # tick: u32, default: 500 285 | # 286 | # The refresh rate in millisecond of the module thread. 287 | # 288 | tick: 1000 289 | 290 | # placeholder: String, default: - 291 | # 292 | # Value to display when there is no data available yet. 293 | # 294 | placeholder: '-' 295 | 296 | # date_format: String, default: %a. %-e %B %Y, %-kh%M 297 | # 298 | # The format. For the syntax see https://docs.rs/chrono/*/chrono/format/strftime/index.html 299 | # 300 | date_format: '%-kh%M' 301 | 302 | # label: String, default: None 303 | # 304 | # The label printed. 305 | # 306 | label: dat 307 | 308 | # format: String, default: %v 309 | # 310 | # The module format. 311 | # 312 | format: '%v' 313 | 314 | 315 | # # # # # # # # # 316 | # Memory module # 317 | # # # # # # # # # 318 | 319 | memory: 320 | # Takes the following options: 321 | 322 | # tick: u32, default: 500 323 | # 324 | # The refresh rate in millisecond of the module thread. 325 | # 326 | tick: 1000 327 | 328 | # placeholder: String, default: - 329 | # 330 | # Value to display when there is no data available yet. 331 | # 332 | placeholder: '-' 333 | 334 | # display: Display, default: GiB 335 | # 336 | # enum Display { GB, GiB, Percentage } 337 | # 338 | # Display as used/total in mega/gigabyte, as used/total in mebi/gibibyte or as a percentage. 339 | # 340 | display: GB 341 | 342 | # high_level: u32, default: 90 343 | # 344 | # The percentage above which the memory usage is considered high. 345 | # 346 | high_level: 95 347 | 348 | # label: String, default: mem 349 | # 350 | # The label printed when the memory usage is below high level. 351 | # 352 | label: mem 353 | 354 | # high_label: String, default: !me 355 | # 356 | # The label printed when the memory usage is above high level. 357 | # 358 | high_label: '!me' 359 | 360 | # format: String, default: %l:%v 361 | # 362 | # The module format. 363 | # 364 | format: '%l:%v' 365 | 366 | 367 | # # # # # # # # 368 | # Mic module # 369 | # # # # # # # # 370 | 371 | mic: 372 | # Takes the following options: 373 | 374 | # tick: u32, default: 50 375 | # 376 | # The refresh rate in millisecond of the module thread. 377 | # 378 | tick: 100 379 | 380 | # placeholder: String, default: - 381 | # 382 | # Value to display when there is no data available yet. 383 | # 384 | placeholder: '-' 385 | 386 | # source_name: String, default: None 387 | # 388 | # If not provided, the default source will be used automatically. 389 | # You can get it with `pactl list sources` command. 390 | # 391 | source_name: "some_name" 392 | 393 | # label: String, default: mic 394 | # 395 | # The label printed when the mic is unmute. 396 | # 397 | label: mic 398 | 399 | # mute_label: String, default: .mi 400 | # 401 | # The label printed when the mic is mute. 402 | # 403 | mute_label: '.mi' 404 | 405 | # format: String, default: %l:%v 406 | # 407 | # The module format. 408 | # 409 | format: '%l_%v' 410 | 411 | 412 | # # # # # # # # # # 413 | # Weather module # 414 | # # # # # # # # # # 415 | 416 | weather: 417 | # Module to display the current weather condition and temperature. 418 | # The weather condition is either shown as an icon or as a text. 419 | # The data is fetched from https://openweathermap.org/current API. 420 | # It takes the following options: 421 | 422 | # tick: u32, default: 120 423 | # 424 | # The refresh rate in **seconds** of the weather data. 425 | # 426 | tick: 120 427 | 428 | # location: { lat: f32, lon: f32 } | String, required 429 | # 430 | # The location of the weather data. It can be either coordinates 431 | # (latitude and longitude) or a city name/zip-code. 432 | # ⚠ city name/zip-code is deprecated by the openweather API. 433 | # 434 | location: 435 | lat: 42.38 436 | lon: 8.94 437 | 438 | # api_key: String, required 439 | # 440 | # Your openweathermap API key. 441 | # see https://home.openweathermap.org/api_keys 442 | # 443 | api_key: 'xxx' 444 | 445 | # unit: standard | metric | imperial, default: metric 446 | # 447 | # The unit of the temperature. 448 | # - standard: Kelvin 449 | # - metric: Celsius 450 | # - imperial: Fahrenheit 451 | # 452 | unit: 'metric' 453 | 454 | # lang: String, default: None 455 | # 456 | # Two-letter language code 457 | # 458 | lang: 'en' 459 | 460 | # icons: List of IconSet, default: None 461 | # 462 | # Possible icons: 463 | # clear_sky partly_cloudy cloudy very_cloudy shower_rain rain 464 | # thunderstorm snow mist default 465 | # 466 | # For each weather condition, you can provide a list of two icons variants. 467 | # First is for the day and second for the night. 468 | # Or provide a single icon for both. 469 | # Each icon is optional. 470 | # 471 | icons: 472 | clear_sky: ['󰖙', '󰖔'] 473 | partly_cloudy: ['󰖕', '󰼱'] 474 | cloudy: '󰖐' 475 | very_cloudy: '󰖐' 476 | shower_rain: '󰖖' 477 | rain: '󰖖' 478 | thunderstorm: '󰖓' 479 | snow: '󰖘' 480 | mist: '󰖑' 481 | default: '󰖐' 482 | 483 | # text_mode: Bool, default: true 484 | # 485 | # Display the weather condition as a short text (instead of an icon). 486 | # It is the default if no icon is provided. 487 | # 488 | text_mode: false 489 | 490 | # placeholder: String, default: - 491 | # 492 | # Value to display when there is no data available yet. 493 | # 494 | placeholder: '-' 495 | 496 | # label: String, default: wtr 497 | # 498 | # The module label. 499 | # 500 | label: wtr 501 | 502 | # format: String, default: %v 503 | # 504 | # The module format. 505 | # 506 | format: '%v' 507 | 508 | 509 | # # # # # # # # # 510 | # Sound module # 511 | # # # # # # # # # 512 | 513 | sound: 514 | # Takes the following options: 515 | 516 | # tick: u32, default: 50 517 | # 518 | # The refresh rate in millisecond of the module thread. 519 | # 520 | tick: 100 521 | 522 | # placeholder: String, default: - 523 | # 524 | # Value to display when there is no data available yet. 525 | # 526 | placeholder: '-' 527 | 528 | # sink_name: String, default: None 529 | # 530 | # If not provided, the default sink will be used automatically. 531 | # You can get it with `pactl list sinks` command. 532 | # 533 | sink_name: "some_name" 534 | 535 | # label: String, default: sou 536 | # 537 | # The label printed when the sound is unmute. 538 | # 539 | label: sou 540 | 541 | # mute_label: String, default: .so 542 | # 543 | # The label printed when the sound is mute. 544 | # 545 | mute_label: '.so' 546 | 547 | # format: String, default: %l:%v 548 | # 549 | # The module format. 550 | # 551 | format: '%l:%v' 552 | 553 | 554 | # # # # # # # # # # # # 555 | # Temperature module # 556 | # # # # # # # # # # # # 557 | 558 | temperature: 559 | # Takes the following options: 560 | 561 | # tick: u32, default: 50 562 | # 563 | # The refresh rate in millisecond of the module thread. 564 | # 565 | tick: 100 566 | 567 | # placeholder: String, default: - 568 | # 569 | # Value to display when there is no data available yet. 570 | # 571 | placeholder: '-' 572 | 573 | # coretemp: String, default: /sys/devices/platform/coretemp.0/hwmon 574 | # 575 | # The path without the last directory level (because it varies on each kernel boot). 576 | # Under this variable directory are located the input files (see below). 577 | # For example on my machine it can be /sys/devices/platform/coretemp.0/hwmon/hwmon7 or hwmon6 etc. 578 | # The variable directory is resolved dynamically. 579 | # 580 | coretemp: /sys/devices/platform/coretemp.0/hwmon 581 | 582 | # core_inputs: u32 | List of u32 | String, default: 1 583 | # 584 | # The average temperature is calculated from one or several input files. 585 | # Input files are located in the directory /sys/devices/platform/coretemp.0/hwmon/* 586 | # and are named `tempn_input` where n is a number. 587 | # e.g. temp1_input temp2_input 588 | # Input files can contain the temperature of a cpu core. 589 | # Based on the cpu and its number of cores you want to provide 590 | # the corresponding input file number(s). 591 | # Tips: see the label files, e.g. `temp1_label`, to identify 592 | # which input files reports cpu core temperature. 593 | # 594 | # This option can be set either to: 595 | # A number to select one input file, eg. 1 for temp1_input. 596 | # A list of number to select multiple input files, eg. 597 | # core_inputs: [ 2, 6 ] to select temp2_input and temp6_input 598 | # A inclusive range to select multiple input files, eg. 599 | # core_inputs: 2..3 to select temp1_input temp2_input and temp3_input 600 | # 601 | core_inputs: 1 602 | 603 | # high_level: u32, default: 75 604 | # 605 | # The percentage above which the temperature is considered high. 606 | # 607 | high_level: 80 608 | 609 | # label: String, default: tem 610 | # 611 | # The label printed when the temperature is below high level. 612 | # 613 | label: tem 614 | 615 | # high_label: String, default: !te 616 | # 617 | # The label printed when the temperature is above high level. 618 | # 619 | high_label: '!te' 620 | 621 | # format: String, default: %l:%v 622 | # 623 | # The module format. 624 | # 625 | format: '%l:%v' 626 | 627 | 628 | # # # # # # # # # 629 | # Wired module # 630 | # # # # # # # # # 631 | 632 | wired: 633 | # Takes the following options: 634 | 635 | # tick: u32, default: 1000 636 | # 637 | # The refresh rate in millisecond of the module thread. 638 | # 639 | tick: 500 640 | 641 | # placeholder: String, default: - 642 | # 643 | # Value to display when there is no data available yet. 644 | # 645 | placeholder: '-' 646 | 647 | # discrete: bool, default: false 648 | # 649 | # If true and there is no active wired connection, print nothing. 650 | # 651 | discrete: true 652 | 653 | # interface: String, default: enp0s31f6 654 | # 655 | # The name of the wired interface. 656 | # 657 | interface: eth0 658 | 659 | # label: String, default: eth 660 | # 661 | # The label printed when a wired connection is active. 662 | # 663 | label: eth 664 | 665 | # disconnected_label: String, default: .et 666 | # 667 | # The label printed when there is no active wired connection. 668 | # 669 | disconnected_label: '.et' 670 | 671 | # format: String, default: %l 672 | # 673 | # The module format (this module has no value to print). 674 | # 675 | format: '%l' 676 | 677 | 678 | # # # # # # # # # # 679 | # Wireless module # 680 | # # # # # # # # # # 681 | 682 | wireless: 683 | # Takes the following options: 684 | 685 | # tick: u32, default: 500 686 | # 687 | # The refresh rate in millisecond of the module thread. 688 | # 689 | tick: 250 690 | 691 | # placeholder: String, default: - 692 | # 693 | # Value to display when there is no data available yet. 694 | # 695 | placeholder: '-' 696 | 697 | # display: Display, default: Signal 698 | # 699 | # enum Display { Essid, Signal } 700 | # 701 | # Print the essid name or the signal strength in percentage. 702 | # 703 | display: Essid 704 | 705 | # max_essid_len: usize, default: 10 706 | # 707 | # Limit the length of the essid name. 708 | # 709 | max_essid_len: 5 710 | 711 | # interface: String, default: wlan0 712 | # 713 | # The name of the wireless interface. 714 | # 715 | interface: wlp2s0 716 | 717 | # label: String, default: wle 718 | # 719 | # The label printed when a wireless connection is active. 720 | # 721 | label: wle 722 | 723 | # disconnected_label: String, default: .wl 724 | # 725 | # The label printed when there is no active wireless connection. 726 | # 727 | disconnected_label: '.wl' 728 | 729 | # format: String, default: %l:%v 730 | # 731 | # The module format. 732 | # 733 | format: '%l:%v' 734 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let netlink_dst = cmake::build("lib/netlink"); 3 | let audio_dst = cmake::build("lib/audio"); 4 | println!( 5 | "cargo:rustc-link-search=native={}/lib", 6 | netlink_dst.display() 7 | ); 8 | println!("cargo:rustc-link-search=native={}/lib", audio_dst.display()); 9 | println!("cargo:rustc-link-lib=dylib=nl-3"); 10 | println!("cargo:rustc-link-lib=dylib=nl-genl-3"); 11 | println!("cargo:rustc-link-lib=dylib=nl-route-3"); 12 | println!("cargo:rustc-link-lib=dylib=pulse"); 13 | } 14 | -------------------------------------------------------------------------------- /lib/audio/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | project(audio C) 3 | 4 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 5 | set(CMAKE_C_STANDARD 23) 6 | set(CMAKE_C_FLAGS "-W -Wall -Wextra -Werror") 7 | 8 | include(FindPkgConfig) 9 | 10 | add_library(audio STATIC src/audio.c) 11 | 12 | if (PkgConfig_FOUND) 13 | pkg_check_modules(LIBPULSE libpulse>=11) 14 | endif () 15 | 16 | if (NOT LIBPULSE_LINK_LIBRARIES) 17 | message(FATAL_ERROR "libpulse not found") 18 | endif () 19 | 20 | target_include_directories(audio INTERFACE ${PROJECT_BINARY_DIR}/include) 21 | target_link_libraries(audio PRIVATE ${LIBPULSE_LINK_LIBRARIES}) 22 | target_include_directories(audio PUBLIC ${LIBPULSE_INCLUDE_DIRS}) 23 | 24 | install(TARGETS audio DESTINATION lib) 25 | install(FILES include/audio.h DESTINATION include) 26 | -------------------------------------------------------------------------------- /lib/audio/include/audio.h: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #ifndef AUDIO_H 6 | #define AUDIO_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define PREFIX_ERROR "libaudio" 18 | #define APPLICATION_NAME "baru" 19 | #define NSEC_TO_SECOND(N) N / (long)1e9 20 | #define MAX_NSEC 999999999 21 | /* 22 | * get humanized volume from a pa_volume_t (aka uint32_t) 23 | * N should result from pa_cvolume_avg(pa_cvolume) 24 | * based on pulseaudio source code, see https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/blob/master/src/pulse/volume.c#L336 25 | */ 26 | #define VOLUME(N) (uint32_t)(((uint64_t)N * 100 + (uint64_t)PA_VOLUME_NORM / 2) / (uint64_t)PA_VOLUME_NORM) 27 | 28 | typedef struct timespec 29 | t_timespec; 30 | 31 | typedef struct volume { 32 | uint32_t volume; 33 | bool mute; 34 | } t_volume; 35 | 36 | typedef void(*send_cb)(void *, uint32_t, bool); 37 | 38 | typedef struct data { 39 | const char *name; 40 | bool use_default; 41 | t_volume volume; 42 | send_cb cb; 43 | pa_operation *op; 44 | } t_data; 45 | 46 | typedef struct main { 47 | uint32_t tick; 48 | bool connected; 49 | pa_context *context; 50 | pa_mainloop *mainloop; 51 | pa_mainloop_api *api; 52 | void *cb_context; 53 | t_timespec start; 54 | pa_operation *server_op; 55 | t_data *sink; 56 | t_data *source; 57 | } t_main; 58 | 59 | void run(bool *running, 60 | uint32_t tick, 61 | const char *sink_name, 62 | const char *source_name, 63 | void *cb_context, 64 | send_cb, 65 | send_cb); 66 | 67 | #endif //AUDIO_H 68 | -------------------------------------------------------------------------------- /lib/audio/src/audio.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../include/audio.h" 10 | 11 | void printe(char *err) { 12 | fprintf(stderr, "%s: %s, %s\n", PREFIX_ERROR, err, strerror(errno)); 13 | exit(EXIT_FAILURE); 14 | } 15 | 16 | void init_data(t_data *data, const char *name, send_cb cb) { 17 | data->name = name; 18 | data->cb = cb; 19 | data->op = NULL; 20 | data->use_default = name == NULL ? true : false; 21 | } 22 | 23 | void context_state_cb(pa_context *context, void *main) { 24 | pa_context_state_t state; 25 | 26 | state = pa_context_get_state(context); 27 | if (state == PA_CONTEXT_READY) { 28 | ((t_main *) main)->connected = true; 29 | } else if (state == PA_CONTEXT_FAILED) { 30 | printe("context connection failed"); 31 | } 32 | } 33 | 34 | void try_free_op(pa_operation **operation) { 35 | if (*operation != NULL) { 36 | pa_operation_unref(*operation); 37 | *operation = NULL; 38 | } 39 | } 40 | 41 | void sink_info_cb(pa_context *context, const pa_sink_info *info, int eol, void *main) { 42 | t_main *m; 43 | 44 | (void) context; 45 | m = main; 46 | if (info != NULL && eol == 0) { 47 | m->sink->volume.mute = info->mute; 48 | m->sink->volume.volume = VOLUME(pa_cvolume_avg(&info->volume)); 49 | (*m->sink->cb)(m->cb_context, m->sink->volume.volume, m->sink->volume.mute); 50 | } 51 | if (eol != 0) { 52 | try_free_op(&m->sink->op); 53 | } 54 | } 55 | 56 | void source_info_cb(pa_context *context, const pa_source_info *info, int eol, 57 | void *main) { 58 | t_main *m; 59 | 60 | (void) context; 61 | m = main; 62 | if (info != NULL && eol == 0) { 63 | m->source->volume.mute = info->mute; 64 | m->source->volume.volume = VOLUME(pa_cvolume_avg(&info->volume)); 65 | (*m->source->cb)(m->cb_context, m->source->volume.volume, m->source->volume.mute); 66 | } 67 | if (eol != 0) { 68 | try_free_op(&m->source->op); 69 | } 70 | } 71 | 72 | const char *name_switch(const char *old_name, const char *new_name) { 73 | if (old_name != NULL) { 74 | free((char *) old_name); 75 | } 76 | if ((old_name = malloc(sizeof(char) * (strlen(new_name) + 1))) == NULL) { 77 | printe("malloc failed"); 78 | } 79 | return strcpy((char *) old_name, new_name); 80 | } 81 | 82 | void 83 | server_info_cb(pa_context *context, const pa_server_info *info, void *main) { 84 | t_main *m; 85 | 86 | (void) context; 87 | m = main; 88 | if (info != NULL) { 89 | if (m->sink->use_default && (m->sink->name == NULL || strcmp(info->default_sink_name, m->sink->name) != 0)) { 90 | m->sink->name = name_switch(m->sink->name, info->default_sink_name); 91 | try_free_op(&m->sink->op); 92 | m->sink->op = pa_context_get_sink_info_by_name(m->context, m->sink->name, sink_info_cb, main); 93 | } 94 | if (m->source->use_default && 95 | (m->source->name == NULL || strcmp(info->default_source_name, m->source->name) != 0)) { 96 | m->source->name = name_switch(m->source->name, info->default_source_name); 97 | try_free_op(&m->source->op); 98 | m->source->op = pa_context_get_source_info_by_name(m->context, m->source->name, source_info_cb, main); 99 | } 100 | } 101 | try_free_op(&m->server_op); 102 | } 103 | 104 | void subscription_cb(pa_context *context, pa_subscription_event_type_t t, uint32_t idx, void *main) { 105 | t_main *m; 106 | 107 | (void) context; 108 | (void) idx; 109 | m = main; 110 | switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { 111 | case PA_SUBSCRIPTION_EVENT_SINK: 112 | try_free_op(&m->sink->op); 113 | if (m->sink->name != NULL) { 114 | m->sink->op = pa_context_get_sink_info_by_name(m->context, m->sink->name, sink_info_cb, main); 115 | } 116 | break; 117 | case PA_SUBSCRIPTION_EVENT_SOURCE: 118 | try_free_op(&m->source->op); 119 | if (m->source->name != NULL) { 120 | m->source->op = pa_context_get_source_info_by_name(m->context, m->source->name, source_info_cb, main); 121 | } 122 | break; 123 | case PA_SUBSCRIPTION_EVENT_SERVER: 124 | try_free_op(&m->server_op); 125 | m->server_op = pa_context_get_server_info(m->context, server_info_cb, main); 126 | break; 127 | default:; 128 | } 129 | } 130 | 131 | void abs_time_tick(t_timespec *start, t_timespec *end, uint32_t tick) { 132 | long int sec; 133 | long int nsec; 134 | 135 | sec = start->tv_sec + (long int) NSEC_TO_SECOND(tick); 136 | nsec = start->tv_nsec + (long int) tick; 137 | if (nsec > MAX_NSEC) { 138 | end->tv_sec = sec + 1; 139 | end->tv_nsec = nsec - MAX_NSEC; 140 | } else { 141 | end->tv_sec = sec; 142 | end->tv_nsec = nsec; 143 | } 144 | } 145 | 146 | void iterate(t_main *main) { 147 | t_timespec tick; 148 | int res; 149 | 150 | // get the time at the start of an iteration 151 | if (clock_gettime(CLOCK_REALTIME, &main->start) == -1) { 152 | printe("clock_gettime failed"); 153 | } 154 | // get the absolute time of the next tick (start time + tick value) 155 | abs_time_tick(&main->start, &tick, main->tick); 156 | 157 | // iterate the main loop 158 | while ((res = pa_mainloop_iterate(main->mainloop, 0, NULL)) > 0) {} 159 | if (res < 0) { 160 | printe("pa_mainloop_iterate failed"); 161 | } 162 | 163 | // free pa_operation objects 164 | try_free_op(&main->sink->op); 165 | try_free_op(&main->source->op); 166 | 167 | // wait for the remaining time of the tick value 168 | clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &tick, NULL); 169 | } 170 | 171 | void run(bool *running, uint32_t tick, const char *sink_name, const char *source_name, void *cb_context, 172 | send_cb sink_cb, send_cb source_cb) { 173 | pa_proplist *proplist; 174 | t_main main; 175 | t_data sink; 176 | t_data source; 177 | pa_operation *context_subscription; 178 | 179 | init_data(&sink, sink_name, sink_cb); 180 | init_data(&source, source_name, source_cb); 181 | 182 | main.tick = tick; 183 | main.connected = false; 184 | main.cb_context = cb_context; 185 | main.mainloop = pa_mainloop_new(); 186 | main.api = pa_mainloop_get_api(main.mainloop); 187 | main.server_op = NULL; 188 | main.sink = &sink; 189 | main.source = &source; 190 | 191 | proplist = pa_proplist_new(); 192 | 193 | // context creation 194 | if (pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, APPLICATION_NAME) != 0) { 195 | printe("pa_proplist_sets failed"); 196 | } 197 | main.context = pa_context_new_with_proplist(main.api, APPLICATION_NAME, proplist); 198 | 199 | // context connection to the sever 200 | pa_context_set_state_callback(main.context, context_state_cb, &main); 201 | if (pa_context_connect(main.context, NULL, PA_CONTEXT_NOFAIL, NULL) < 0) { 202 | printe("pa_context_connect failed"); 203 | } 204 | while (main.connected == false) { 205 | if (pa_mainloop_iterate(main.mainloop, 0, NULL) < 0) { 206 | printe("pa_mainloop_iterate failed"); 207 | } 208 | } 209 | 210 | // initial introspection 211 | if (sink.use_default || source.use_default) { 212 | main.server_op = pa_context_get_server_info(main.context, server_info_cb, &main); 213 | } 214 | if (!sink.use_default) { 215 | main.sink->op = pa_context_get_sink_info_by_name(main.context, sink.name, sink_info_cb, &main); 216 | } 217 | if (!source.use_default) { 218 | main.source->op = pa_context_get_source_info_by_name(main.context, source.name, source_info_cb, &main); 219 | } 220 | 221 | // subscription introspection 222 | int subscription_mask = PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE; 223 | if (sink.use_default || source.use_default) { 224 | subscription_mask |= PA_SUBSCRIPTION_MASK_SERVER; 225 | } 226 | context_subscription = pa_context_subscribe(main.context, subscription_mask, NULL, NULL); 227 | pa_context_set_subscribe_callback(main.context, subscription_cb, &main); 228 | 229 | // iterate main loop 230 | while (*running == true) { 231 | iterate(&main); 232 | } 233 | 234 | // close connection and free 235 | pa_operation_unref(context_subscription); 236 | pa_context_disconnect(main.context); 237 | pa_mainloop_free(main.mainloop); 238 | } 239 | -------------------------------------------------------------------------------- /lib/netlink/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | project(netlink) 3 | include(FindPkgConfig) 4 | 5 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 6 | set(CMAKE_C_STANDARD 23) 7 | set(CMAKE_C_FLAGS "-W -Wall -Wextra -Werror") 8 | 9 | add_library(netlink STATIC 10 | src/wireless.c 11 | src/wired.c 12 | src/common.c) 13 | 14 | if (PkgConfig_FOUND) 15 | pkg_check_modules(LIBNL libnl-3.0>=3.1 libnl-route-3.0>=3.1 libnl-genl-3.0>=3.1) 16 | endif () 17 | 18 | if (NOT LIBNL_LINK_LIBRARIES) 19 | message(FATAL_ERROR "libnl-3 not found") 20 | endif () 21 | 22 | target_include_directories(netlink INTERFACE ${PROJECT_BINARY_DIR}/include) 23 | target_link_libraries(netlink PRIVATE ${LIBNL_LINK_LIBRARIES}) 24 | target_include_directories(netlink PRIVATE ${LIBNL_INCLUDE_DIRS}) 25 | 26 | install(TARGETS netlink DESTINATION lib) 27 | install(FILES include/netlink.h DESTINATION include) 28 | -------------------------------------------------------------------------------- /lib/netlink/include/netlink.h: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // Wireless code written by Clément Dommerc 6 | 7 | #ifndef NETLINK_H 8 | #define NETLINK_H 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define NL80211 "nl80211" 17 | #define EID_SSID 0 18 | #define NOISE_FLOOR_DBM (-90) 19 | #define SIGNAL_MAX_DBM (-20) 20 | #define PREFIX_ERROR "libnetlink" 21 | #define BUF_SIZE 1024 22 | #define ESSID_MAX_SIZE 1024 23 | #define CLAMP(x, l, h) x < l ? l : \ 24 | x > h ? h : x 25 | 26 | typedef struct s_wireless { 27 | bool essid_found; 28 | bool signal_found; 29 | int nl80211_id; 30 | unsigned int if_index; 31 | char *if_name; 32 | uint8_t bssid[ETH_ALEN]; 33 | char *essid; 34 | int signal; 35 | struct nl_sock *socket; 36 | } t_wireless; 37 | 38 | /* API */ 39 | typedef struct s_wireless_data { 40 | char *essid; 41 | int32_t signal; 42 | } t_wireless_data; 43 | 44 | typedef struct s_wired_data { 45 | bool is_carrying; 46 | bool is_operational; 47 | bool has_ip; 48 | } t_wired_data; 49 | 50 | t_wireless_data *get_wireless_data(char *interface); 51 | t_wired_data *get_wired_data(char *interface); 52 | void free_data(void *data); 53 | 54 | /* HELPER */ 55 | void print_and_exit(char *err); 56 | void *alloc_mem(size_t size); 57 | 58 | #endif // NETLINK_H 59 | -------------------------------------------------------------------------------- /lib/netlink/src/common.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #include "../include/netlink.h" 6 | 7 | void print_and_exit(char *err) { 8 | fprintf(stderr, "%s: %s\n", PREFIX_ERROR, err); 9 | exit(EXIT_FAILURE); 10 | } 11 | 12 | void *alloc_mem(size_t i) { 13 | void *p; 14 | 15 | if ((p = malloc(i)) == NULL) { 16 | print_and_exit("malloc failed"); 17 | } 18 | memset(p, 0, i); 19 | return p; 20 | } -------------------------------------------------------------------------------- /lib/netlink/src/wired.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #include 6 | 7 | #include "../include/netlink.h" 8 | 9 | bool is_operational(struct rtnl_link *link) { 10 | char buf[BUF_SIZE]; 11 | uint8_t state; 12 | bool r; 13 | 14 | r = false; 15 | memset(buf, 0, BUF_SIZE); 16 | state = rtnl_link_get_operstate(link); 17 | rtnl_link_operstate2str(state, buf, BUF_SIZE); 18 | if (strcmp(buf, "up") == 0) { 19 | r = true; 20 | } 21 | return r; 22 | } 23 | 24 | bool has_ip(struct nl_cache *cache, int if_index) { 25 | struct nl_object *obj; 26 | struct rtnl_addr *addr; 27 | int index; 28 | int family; 29 | 30 | if (nl_cache_is_empty(cache)) { 31 | return false; 32 | } 33 | for (obj = nl_cache_get_first(cache); obj != NULL; obj = nl_cache_get_next(obj)) { 34 | addr = (struct rtnl_addr *) obj; 35 | index = rtnl_addr_get_ifindex(addr); 36 | family = rtnl_addr_get_family(addr); 37 | if (index == if_index 38 | && (family == AF_INET || family == AF_INET6) 39 | && rtnl_addr_get_local(addr) != NULL) { 40 | return true; 41 | } 42 | } 43 | return false; 44 | } 45 | 46 | t_wired_data *get_wired_data(char *interface) { 47 | t_wired_data *data; 48 | struct nl_sock *sk; 49 | struct nl_cache *cache; 50 | struct rtnl_link *link; 51 | int if_index; 52 | 53 | if ((sk = nl_socket_alloc()) == NULL) { 54 | print_and_exit("nl_socket_alloc failed"); 55 | } 56 | if (nl_connect(sk, NETLINK_ROUTE) != 0) { 57 | fprintf(stderr, "%s: nl_connect failed\n", PREFIX_ERROR); 58 | return NULL; 59 | } 60 | if (rtnl_addr_alloc_cache(sk, &cache) != 0) { 61 | print_and_exit("rtnl_addr_alloc_cache failed"); 62 | } 63 | if (rtnl_link_get_kernel(sk, 0, interface, &link) < 0) { 64 | fprintf(stderr, "%s: interface not found\n", PREFIX_ERROR); 65 | return NULL; 66 | } 67 | data = alloc_mem(sizeof(t_wired_data)); 68 | if_index = rtnl_link_get_ifindex(link); 69 | data->is_carrying = rtnl_link_get_carrier(link); 70 | data->is_operational = is_operational(link); 71 | data->has_ip = has_ip(cache, if_index); 72 | nl_socket_free(sk); 73 | nl_cache_free(cache); 74 | nl_object_free((struct nl_object *) link); 75 | return data; 76 | } -------------------------------------------------------------------------------- /lib/netlink/src/wireless.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // By Clément Dommerc 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "../include/netlink.h" 13 | 14 | // Based on NetworkManager/src/platform/wifi/wifi-utils-nl80211.c 15 | static uint32_t nl80211_xbm_to_percent(int32_t xbm) { 16 | xbm = CLAMP(xbm, NOISE_FLOOR_DBM, SIGNAL_MAX_DBM); 17 | return 100 - 70 * (((float) SIGNAL_MAX_DBM - (float) xbm) / ((float) SIGNAL_MAX_DBM - (float) NOISE_FLOOR_DBM)); 18 | } 19 | 20 | // Based on NetworkManager/src/platform/wifi/wifi-utils-nl80211.c 21 | static void find_ssid(uint8_t *ies, uint32_t ies_len, uint8_t **ssid, uint32_t *ssid_len) { 22 | while (ies_len > 2 && ies[0] != EID_SSID) { 23 | ies_len -= ies[1] + 2; 24 | ies += ies[1] + 2; 25 | } 26 | if (ies_len < 2 || ies_len < (uint32_t) (2 + ies[1])) { 27 | return; 28 | } 29 | *ssid_len = ies[1]; 30 | *ssid = ies + 2; 31 | } 32 | 33 | void resolve_essid(t_wireless *wireless, struct nlattr *attr) { 34 | uint32_t bss_ies_len = nla_len(attr); 35 | uint8_t *bss_ies = nla_data(attr); 36 | uint8_t *ssid = NULL; 37 | uint32_t ssid_len = 0; 38 | 39 | find_ssid(bss_ies, bss_ies_len, &ssid, &ssid_len); 40 | if (ssid_len > ESSID_MAX_SIZE) { 41 | ssid_len = ESSID_MAX_SIZE; 42 | } 43 | if (ssid) { 44 | wireless->essid = alloc_mem(sizeof(char) * (ssid_len + 1)); 45 | wireless->essid_found = true; 46 | strncpy(wireless->essid, (char *) ssid, ssid_len); 47 | } 48 | } 49 | 50 | static int station_cb(struct nl_msg *msg, void *data) { 51 | t_wireless *wireless = data; 52 | struct nlattr *tb[NL80211_ATTR_MAX + 1]; 53 | struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); 54 | struct nlattr *attr = genlmsg_attrdata(gnlh, 0); 55 | int attrlen = genlmsg_attrlen(gnlh, 0); 56 | struct nlattr *s_info[NL80211_STA_INFO_MAX + 1]; 57 | static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1]; 58 | 59 | if (nla_parse(tb, NL80211_ATTR_MAX, attr, attrlen, NULL) < 0) { 60 | return NL_SKIP; 61 | } 62 | if (tb[NL80211_ATTR_STA_INFO] == NULL) { 63 | return NL_SKIP; 64 | } 65 | if (nla_parse_nested(s_info, NL80211_STA_INFO_MAX, tb[NL80211_ATTR_STA_INFO], stats_policy) < 0) { 66 | return NL_SKIP; 67 | } 68 | if (s_info[NL80211_STA_INFO_SIGNAL] != NULL) { 69 | wireless->signal_found = true; 70 | wireless->signal = nl80211_xbm_to_percent((int8_t) nla_get_u8(s_info[NL80211_STA_INFO_SIGNAL])); 71 | } 72 | return NL_SKIP; 73 | } 74 | 75 | static int scan_cb(struct nl_msg *msg, void *data) { 76 | t_wireless *wireless = data; 77 | uint32_t status; 78 | struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); 79 | struct nlattr *attr = genlmsg_attrdata(gnlh, 0); 80 | int attrlen = genlmsg_attrlen(gnlh, 0); 81 | struct nlattr *tb[NL80211_ATTR_MAX + 1]; 82 | struct nlattr *bss[NL80211_BSS_MAX + 1]; 83 | struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = { 84 | [NL80211_BSS_BSSID] = {.type = NLA_UNSPEC}, 85 | [NL80211_BSS_INFORMATION_ELEMENTS] = {.type = NLA_UNSPEC}, 86 | [NL80211_BSS_STATUS] = {.type = NLA_U32}, 87 | }; 88 | 89 | if (nla_parse(tb, NL80211_ATTR_MAX, attr, attrlen, NULL) < 0) { 90 | return NL_SKIP; 91 | } 92 | if (tb[NL80211_ATTR_BSS] == NULL) { 93 | return NL_SKIP; 94 | } 95 | if (nla_parse_nested(bss, NL80211_BSS_MAX, tb[NL80211_ATTR_BSS], bss_policy) < 0) { 96 | return NL_SKIP; 97 | } 98 | if (bss[NL80211_BSS_STATUS] == NULL) { 99 | return NL_SKIP; 100 | } 101 | status = nla_get_u32(bss[NL80211_BSS_STATUS]); 102 | if (status != NL80211_BSS_STATUS_ASSOCIATED && status != NL80211_BSS_STATUS_IBSS_JOINED) { 103 | return NL_SKIP; 104 | } 105 | if (bss[NL80211_BSS_BSSID] == NULL) { 106 | return NL_SKIP; 107 | } 108 | memcpy(wireless->bssid, nla_data(bss[NL80211_BSS_BSSID]), ETH_ALEN); 109 | if (bss[NL80211_BSS_INFORMATION_ELEMENTS]) { 110 | resolve_essid(wireless, bss[NL80211_BSS_INFORMATION_ELEMENTS]); 111 | } 112 | return NL_SKIP; 113 | } 114 | 115 | static int send_for_station(t_wireless *wireless) { 116 | struct nl_msg *msg = NULL; 117 | int err; 118 | 119 | if ((err = nl_socket_modify_cb(wireless->socket, NL_CB_VALID, NL_CB_CUSTOM, station_cb, wireless)) < 0) { 120 | printf("%s, station nl_socket_modify_cb failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 121 | return -1; 122 | } 123 | if ((msg = nlmsg_alloc()) == NULL) { 124 | printf("%s, station nlmsg_alloc failed\n", PREFIX_ERROR); 125 | return -1; 126 | } 127 | if (genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, wireless->nl80211_id, 0, NLM_F_DUMP, NL80211_CMD_GET_STATION, 0) == 128 | NULL) { 129 | printf("%s, station genlmsg_put failed\n", PREFIX_ERROR); 130 | nlmsg_free(msg); 131 | return -1; 132 | } 133 | if ((err = nla_put_u32(msg, NL80211_ATTR_IFINDEX, wireless->if_index)) < 0) { 134 | printf("%s, station nla_put_u32 failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 135 | 136 | nlmsg_free(msg); 137 | return -1; 138 | } 139 | if ((err = nla_put(msg, NL80211_ATTR_MAC, 6, wireless->bssid)) < 0) { 140 | printf("%s, station nla_put failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 141 | nlmsg_free(msg); 142 | return -1; 143 | } 144 | if ((err = nl_send_sync(wireless->socket, msg)) < 0) { 145 | printf("%s, station nl_send_sync failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 146 | return -1; 147 | } 148 | return 0; 149 | } 150 | 151 | static int send_for_scan(t_wireless *wireless) { 152 | struct nl_msg *msg; 153 | int err; 154 | 155 | if ((err = nl_socket_modify_cb(wireless->socket, NL_CB_VALID, NL_CB_CUSTOM, scan_cb, wireless)) < 0) { 156 | printf("%s, scan nl_socket_modify_cb failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 157 | return -1; 158 | } 159 | if ((msg = nlmsg_alloc()) == NULL) { 160 | printf("%s, scan nlmsg_alloc failed\n", PREFIX_ERROR); 161 | return -1; 162 | } 163 | if (genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, wireless->nl80211_id, 0, NLM_F_DUMP, NL80211_CMD_GET_SCAN, 0) == 164 | NULL) { 165 | printf("%s, scan genlmsg_put failed\n", PREFIX_ERROR); 166 | nlmsg_free(msg); 167 | return -1; 168 | } 169 | if ((err = nla_put_u32(msg, NL80211_ATTR_IFINDEX, wireless->if_index)) < 0) { 170 | printf("%s, scan nla_put_u32 failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 171 | nlmsg_free(msg); 172 | return -1; 173 | } 174 | if ((err = nl_send_sync(wireless->socket, msg)) < 0) { 175 | printf("%s, scan nl_send_sync failed, %s\n", PREFIX_ERROR, nl_geterror(err)); 176 | return -1; 177 | } 178 | return 0; 179 | } 180 | 181 | t_wireless_data *get_wireless_data(char *interface) { 182 | t_wireless wireless; 183 | t_wireless_data *data; 184 | 185 | wireless.essid_found = false; 186 | wireless.signal_found = false; 187 | memset(&wireless, 0, sizeof(t_wireless)); 188 | wireless.if_name = interface; 189 | wireless.socket = nl_socket_alloc(); 190 | if (wireless.socket == NULL) { 191 | print_and_exit("nl_socket_alloc failed\n"); 192 | } 193 | if (genl_connect(wireless.socket) != 0) { 194 | nl_socket_free(wireless.socket); 195 | fprintf(stderr, "%s: genl_connect failed\n", PREFIX_ERROR); 196 | return NULL; 197 | } 198 | if ((wireless.nl80211_id = genl_ctrl_resolve(wireless.socket, NL80211)) < 0) { 199 | fprintf(stderr, "%s: genl_ctrl_resolve failed; %s\n", PREFIX_ERROR, nl_geterror(wireless.nl80211_id)); 200 | nl_socket_free(wireless.socket); 201 | return NULL; 202 | } 203 | if ((wireless.if_index = if_nametoindex(wireless.if_name)) == 0) { 204 | fprintf(stderr, "%s: if_nametoindex failed, %s\n", PREFIX_ERROR, strerror(errno)); 205 | nl_socket_free(wireless.socket); 206 | return NULL; 207 | } 208 | if (send_for_scan(&wireless) < 0 || send_for_station(&wireless) < 0) { 209 | nl_socket_free(wireless.socket); 210 | return NULL; 211 | } 212 | data = alloc_mem(sizeof(t_wireless_data)); 213 | data->signal = -1; 214 | if (wireless.signal_found == true) { 215 | data->signal = wireless.signal; 216 | } 217 | if (wireless.essid_found == true) { 218 | data->essid = wireless.essid; 219 | } 220 | nl_socket_free(wireless.socket); 221 | return data; 222 | } 223 | 224 | void free_data(void *data) { 225 | if (data != NULL) { 226 | free(data); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /public/baru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doums/baru/786b746044164cfa1f15fab7ff4687f8b55a2505/public/baru.png -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use clap::{Parser, ValueEnum}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone, ValueEnum)] 9 | pub enum Logs { 10 | Off, 11 | Stdout, 12 | File, 13 | } 14 | 15 | #[derive(Parser, Serialize, Deserialize, Debug, Clone)] 16 | #[command(author, version, about, long_about = None)] 17 | pub struct Cli { 18 | /// Enable app logs 19 | #[arg(short, long)] 20 | pub logs: Option, 21 | } 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::error::Error as StdError; 6 | use std::fmt::{Display, Formatter, Result as FmtResult}; 7 | use std::io::Error as IoError; 8 | use std::num::TryFromIntError; 9 | use std::num::{ParseFloatError, ParseIntError}; 10 | use std::str::Utf8Error; 11 | use std::string::FromUtf8Error; 12 | use std::sync::mpsc::{RecvError, SendError}; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Error(String); 16 | 17 | impl Display for Error { 18 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 19 | write!(f, "{}", self.0) 20 | } 21 | } 22 | 23 | impl StdError for Error {} 24 | 25 | impl Error { 26 | pub fn new(item: impl Into) -> Error { 27 | Error(item.into()) 28 | } 29 | } 30 | 31 | impl From for Error { 32 | fn from(error: String) -> Self { 33 | Error(error) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(error: IoError) -> Self { 39 | Error(error.to_string()) 40 | } 41 | } 42 | 43 | impl From> for Error { 44 | fn from(error: SendError) -> Self { 45 | Error(error.to_string()) 46 | } 47 | } 48 | 49 | impl From for Error { 50 | fn from(error: RecvError) -> Self { 51 | Error(error.to_string()) 52 | } 53 | } 54 | 55 | impl From<&str> for Error { 56 | fn from(error: &str) -> Self { 57 | Error(error.to_string()) 58 | } 59 | } 60 | 61 | impl From for Error { 62 | fn from(error: Utf8Error) -> Self { 63 | Error(error.to_string()) 64 | } 65 | } 66 | 67 | impl From for Error { 68 | fn from(error: FromUtf8Error) -> Self { 69 | Error(error.to_string()) 70 | } 71 | } 72 | 73 | impl From for Error { 74 | fn from(error: ParseIntError) -> Self { 75 | Error(error.to_string()) 76 | } 77 | } 78 | 79 | impl From for Error { 80 | fn from(error: ParseFloatError) -> Self { 81 | Error(error.to_string()) 82 | } 83 | } 84 | 85 | impl From for Error { 86 | fn from(error: TryFromIntError) -> Self { 87 | Error(error.to_string()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::time::Duration; 6 | 7 | use once_cell::sync::Lazy; 8 | use reqwest::blocking::Client; 9 | use tracing::error; 10 | 11 | pub static HTTP_CLIENT: Lazy = Lazy::new(|| { 12 | Client::builder() 13 | .timeout(Duration::from_secs(10)) 14 | .build() 15 | .inspect_err(|e| { 16 | error!("Failed to create HTTP client: {:?}", e); 17 | }) 18 | .unwrap() 19 | }); 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | pub mod cli; 6 | mod error; 7 | mod http; 8 | mod module; 9 | mod modules; 10 | mod netlink; 11 | mod pulse; 12 | pub mod signal; 13 | pub mod trace; 14 | pub mod util; 15 | 16 | use anyhow::{anyhow, Result}; 17 | use error::Error; 18 | use module::{Bar, ModuleData}; 19 | use modules::battery::Config as BatteryConfig; 20 | use modules::brightness::Config as BrightnessConfig; 21 | use modules::cpu_freq::Config as CpuFreqConfig; 22 | use modules::cpu_usage::Config as CpuUsageConfig; 23 | use modules::date_time::Config as DateTimeConfig; 24 | use modules::memory::Config as MemoryConfig; 25 | use modules::mic::Config as MicConfig; 26 | use modules::sound::Config as SoundConfig; 27 | use modules::temperature::Config as TemperatureConfig; 28 | use modules::weather::Config as WeatherConfig; 29 | use modules::wired::Config as WiredConfig; 30 | use modules::wireless::Config as WirelessConfig; 31 | use once_cell::sync::Lazy; 32 | use serde::{Deserialize, Serialize}; 33 | use std::sync::atomic::AtomicBool; 34 | use std::sync::mpsc::{self, Receiver, Sender}; 35 | use std::thread; 36 | use std::thread::JoinHandle; 37 | use tracing::{error, info, instrument}; 38 | 39 | // Global application state, used to terminate the main-loop and all modules 40 | pub static RUN: Lazy = Lazy::new(|| AtomicBool::new(true)); 41 | 42 | #[derive(Debug)] 43 | /// Message sent by modules. 44 | /// `0`: module key, 45 | /// `1`: value, 46 | /// `2`: label 47 | pub struct ModuleMsg(char, Option, Option); 48 | 49 | #[derive(Debug, Serialize, Deserialize, Clone)] 50 | pub struct Config { 51 | format: String, 52 | pub tick: Option, 53 | failed_icon: Option, 54 | pulse_tick: Option, 55 | battery: Option, 56 | brightness: Option, 57 | cpu_usage: Option, 58 | cpu_freq: Option, 59 | date_time: Option, 60 | memory: Option, 61 | mic: Option, 62 | sound: Option, 63 | temperature: Option, 64 | weather: Option, 65 | wired: Option, 66 | wireless: Option, 67 | } 68 | 69 | pub struct Baru<'a> { 70 | config: &'a Config, 71 | modules: Vec>, 72 | format: &'a str, 73 | markup_matches: Vec, 74 | channel: (Sender, Receiver), 75 | pulse: Option>>, 76 | } 77 | 78 | #[derive(Debug)] 79 | struct MarkupMatch(char, usize); 80 | 81 | impl<'a> Baru<'a> { 82 | #[instrument(skip_all)] 83 | pub fn with_config(config: &'a Config) -> Result { 84 | let mut modules = vec![]; 85 | let markup_matches = parse_format(&config.format); 86 | for markup in &markup_matches { 87 | modules.push(ModuleData::new(markup.0, config)?); 88 | } 89 | Ok(Baru { 90 | config, 91 | channel: mpsc::channel(), 92 | modules, 93 | format: &config.format, 94 | markup_matches, 95 | pulse: None, 96 | }) 97 | } 98 | 99 | #[instrument(skip_all)] 100 | pub fn start(&mut self) -> Result<()> { 101 | // check if any module needs pulse, i.e. sound or mic modules 102 | let need_pulse = self.modules.iter().any(|m| m.key == 's' || m.key == 'i'); 103 | if need_pulse { 104 | self.pulse = Some(pulse::init(self.config)?); 105 | } 106 | for data in &mut self.modules { 107 | let builder = thread::Builder::new().name(format!("mod_{}", data.module.name())); 108 | let cloned_m_conf = self.config.clone(); 109 | let tx1 = mpsc::Sender::clone(&self.channel.0); 110 | let run = data.module.run_fn(); 111 | let key = data.key; 112 | let c_name = data.module.name().to_string(); 113 | let handle = builder.spawn(move || -> Result<(), Error> { 114 | run(&RUN, key, cloned_m_conf, tx1) 115 | .inspect_err(|e| error!("[{}] module failed: {}", c_name, e))?; 116 | info!("[{}] module stopped", c_name); 117 | Ok(()) 118 | })?; 119 | data.start(handle); 120 | info!("[{}] module started", data.module.name()); 121 | } 122 | Ok(()) 123 | } 124 | 125 | #[instrument(skip(self))] 126 | fn module_output(&self, key: char) -> Result<&str> { 127 | let module = self 128 | .modules 129 | .iter() 130 | .find(|data| data.key == key) 131 | .ok_or(anyhow!("module for key \"{}\" not found", key))?; 132 | Ok(module.output()) 133 | } 134 | 135 | #[instrument(skip(self))] 136 | pub fn update(&mut self) -> Result<()> { 137 | let messages: Vec = self.channel.1.try_iter().collect(); 138 | for module in &mut self.modules { 139 | module.update_state().ok(); 140 | let mut iter = messages.iter().rev(); 141 | let message = iter.find(|v| v.0 == module.key); 142 | if let Some(value) = message { 143 | module.new_data(value.1.as_deref(), value.2.as_deref()); 144 | } 145 | } 146 | let mut output = self.format.to_string(); 147 | for v in self.markup_matches.iter().rev() { 148 | output.replace_range(v.1 - 1..v.1 + 1, self.module_output(v.0)?); 149 | } 150 | output = output.replace("\\%", "%"); 151 | println!("{}", output); 152 | Ok(()) 153 | } 154 | 155 | #[instrument(skip(self))] 156 | pub fn modules(&self) -> Vec<&str> { 157 | self.modules.iter().map(|m| m.module.name()).collect() 158 | } 159 | 160 | #[instrument(skip_all)] 161 | pub fn cleanup(&mut self) { 162 | if let Some(pulse) = self.pulse.take() { 163 | match pulse.join() { 164 | Ok(Ok(_)) => info!("pulse module terminated"), 165 | Ok(Err(e)) => error!("pulse module failed: {}", e), 166 | Err(_) => error!("pulse module panicked"), 167 | }; 168 | } 169 | } 170 | } 171 | 172 | #[instrument] 173 | fn parse_format(format: &str) -> Vec { 174 | let mut matches = vec![]; 175 | let mut iter = format.char_indices().peekable(); 176 | while let Some((i, c)) = iter.next() { 177 | if c == '%' && (i == 0 || &format[i - 1..i] != "\\") { 178 | if let Some(val) = iter.peek() { 179 | matches.push(MarkupMatch(val.1, val.0)); 180 | } 181 | } 182 | } 183 | matches 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use super::*; 189 | 190 | #[test] 191 | fn parse_empty_format() { 192 | let result = parse_format(""); 193 | assert!(result.is_empty()); 194 | } 195 | 196 | #[test] 197 | fn parse_one_char() { 198 | let result = parse_format("a"); 199 | assert!(result.is_empty()); 200 | } 201 | 202 | #[test] 203 | fn parse_one_percent() { 204 | let result = parse_format("%"); 205 | assert!(result.is_empty()); 206 | } 207 | 208 | #[test] 209 | fn parse_one_escaped_percent_i() { 210 | let result = parse_format("\\%"); 211 | assert!(result.is_empty()); 212 | } 213 | 214 | #[test] 215 | fn parse_one_escaped_percent_ii() { 216 | let result = parse_format("\\%%"); 217 | assert!(result.is_empty()); 218 | } 219 | 220 | #[test] 221 | fn parse_one_escaped_and_one_markup() { 222 | let result = parse_format("\\%%a"); 223 | assert_eq!(result.len(), 1); 224 | assert_eq!(result[0].0, 'a'); 225 | assert_eq!(result[0].1, 3); 226 | } 227 | 228 | #[test] 229 | fn parse_peaceful_markup() { 230 | let result = parse_format("%a"); 231 | assert_eq!(result.len(), 1); 232 | assert_eq!(result[0].0, 'a'); 233 | assert_eq!(result[0].1, 1); 234 | } 235 | 236 | #[test] 237 | fn parse_easy_markup() { 238 | let result = parse_format("\\%a%b"); 239 | assert_eq!(result.len(), 1); 240 | assert_eq!(result[0].0, 'b'); 241 | assert_eq!(result[0].1, 4); 242 | } 243 | 244 | #[test] 245 | fn parse_normal_markup() { 246 | let result = parse_format("\\%a%b%c"); 247 | assert_eq!(result.len(), 2); 248 | assert_eq!(result[0].0, 'b'); 249 | assert_eq!(result[0].1, 4); 250 | assert_eq!(result[1].0, 'c'); 251 | assert_eq!(result[1].1, 6); 252 | } 253 | 254 | #[test] 255 | fn parse_hard_markup() { 256 | let result = parse_format("\\%a%b%c \\% %a\\% %"); 257 | assert_eq!(result.len(), 3); 258 | assert_eq!(result[0].0, 'b'); 259 | assert_eq!(result[0].1, 4); 260 | assert_eq!(result[1].0, 'c'); 261 | assert_eq!(result[1].1, 6); 262 | assert_eq!(result[2].0, 'a'); 263 | assert_eq!(result[2].1, 12); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use anyhow::{Context, Result}; 6 | use baru::cli::Cli; 7 | use baru::{signal, trace, util, Baru, Config, RUN}; 8 | use clap::Parser; 9 | use std::env; 10 | use std::fs; 11 | use std::path::{Path, PathBuf}; 12 | use std::sync::atomic::Ordering; 13 | use std::thread; 14 | use std::time::{Duration, Instant}; 15 | use tracing::{debug, error, info}; 16 | 17 | const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME"; 18 | const APP_DIR: &str = "baru"; 19 | const CONFIG_FILE: &str = "baru.yaml"; 20 | const TICK_RATE: Duration = Duration::from_millis(50); 21 | 22 | fn main() -> Result<()> { 23 | let cli = Cli::parse(); 24 | let _g = trace::init(cli.logs).context("failed to init tracing")?; 25 | 26 | signal::catch_signals()?; 27 | 28 | let home = env::var("HOME")?; 29 | let mut config_dir = env::var(XDG_CONFIG_HOME) 30 | .map(PathBuf::from) 31 | .unwrap_or_else(|_| Path::new(&home).join(".config")); 32 | config_dir.push(APP_DIR); 33 | util::check_dir(&config_dir)?; 34 | 35 | let config_file = config_dir.join(CONFIG_FILE); 36 | info!("config file: {:?}", config_file); 37 | let content = fs::read_to_string(config_file) 38 | .inspect_err(|e| error!("failed to read config file: {}", e))?; 39 | let config: Config = serde_yaml::from_str(&content) 40 | .inspect_err(|e| error!("failed to parse config file: {}", e))?; 41 | debug!("{:#?}", config); 42 | 43 | let tick = match config.tick { 44 | Some(ms) => Duration::from_millis(ms as u64), 45 | None => TICK_RATE, 46 | }; 47 | let mut baru = Baru::with_config(&config) 48 | .inspect_err(|e| error!("failed to create baru instance {}", e))?; 49 | info!("baru instance initialized"); 50 | 51 | let modules = baru.modules(); 52 | info!("modules registered: {}", modules.len()); 53 | debug!("modules: {:?}", modules); 54 | 55 | baru.start() 56 | .inspect_err(|e| error!("failed to start {}", e))?; 57 | info!("started"); 58 | let mut iteration_start: Instant; 59 | let mut iteration_end: Duration; 60 | 61 | info!("launching main loop"); 62 | while RUN.load(Ordering::Relaxed) { 63 | iteration_start = Instant::now(); 64 | baru.update() 65 | .inspect_err(|e| error!("failed to update: {}", e))?; 66 | iteration_end = iteration_start.elapsed(); 67 | if iteration_end < tick { 68 | thread::sleep(tick - iteration_end); 69 | } 70 | } 71 | 72 | baru.cleanup(); 73 | info!("exiting"); 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /src/module.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::modules::battery::Battery; 7 | use crate::modules::brightness::Brightness; 8 | use crate::modules::cpu_freq::CpuFreq; 9 | use crate::modules::cpu_usage::CpuUsage; 10 | use crate::modules::date_time::DateTime; 11 | use crate::modules::memory::Memory; 12 | use crate::modules::mic::Mic; 13 | use crate::modules::sound::Sound; 14 | use crate::modules::temperature::Temperature; 15 | use crate::modules::weather::Weather; 16 | use crate::modules::wired::Wired; 17 | use crate::modules::wireless::Wireless; 18 | use crate::Config; 19 | use crate::ModuleMsg; 20 | 21 | use anyhow::{anyhow, Result}; 22 | use std::convert::TryFrom; 23 | use std::sync::atomic::AtomicBool; 24 | use std::sync::mpsc::Sender; 25 | use std::thread::JoinHandle; 26 | use tracing::{error, info, instrument}; 27 | 28 | const MODULE_FAILED_ICON: &str = "✗"; 29 | 30 | pub type RunPtr = fn(&AtomicBool, char, Config, Sender) -> Result<(), Error>; 31 | 32 | pub trait Bar { 33 | fn name(&self) -> &str; 34 | fn run_fn(&self) -> RunPtr; 35 | fn placeholder(&self) -> &str; 36 | fn format(&self) -> &str; 37 | } 38 | 39 | #[derive(Debug)] 40 | pub enum Module<'a> { 41 | Battery(Battery<'a>), 42 | Brightness(Brightness<'a>), 43 | CpuUsage(CpuUsage<'a>), 44 | CpuFreq(CpuFreq<'a>), 45 | DateTime(DateTime<'a>), 46 | Memory(Memory<'a>), 47 | Mic(Mic<'a>), 48 | Wired(Wired<'a>), 49 | Sound(Sound<'a>), 50 | Temperature(Temperature<'a>), 51 | Wireless(Wireless<'a>), 52 | Weather(Weather<'a>), 53 | } 54 | 55 | impl<'a> TryFrom<(char, &'a Config)> for Module<'a> { 56 | type Error = Error; 57 | 58 | fn try_from((key, config): (char, &'a Config)) -> Result { 59 | match key { 60 | 'a' => Ok(Module::Battery(Battery::with_config(config))), 61 | 'b' => Ok(Module::Brightness(Brightness::with_config(config))), 62 | 'c' => Ok(Module::CpuUsage(CpuUsage::with_config(config))), 63 | 'd' => Ok(Module::DateTime(DateTime::with_config(config))), 64 | 'e' => Ok(Module::Wired(Wired::with_config(config))), 65 | 'f' => Ok(Module::CpuFreq(CpuFreq::with_config(config))), 66 | 'i' => Ok(Module::Mic(Mic::with_config(config))), 67 | 'm' => Ok(Module::Memory(Memory::with_config(config))), 68 | 'r' => Ok(Module::Weather(Weather::with_config(config))), 69 | 's' => Ok(Module::Sound(Sound::with_config(config))), 70 | 't' => Ok(Module::Temperature(Temperature::with_config(config))), 71 | 'w' => Ok(Module::Wireless(Wireless::with_config(config))), 72 | _ => Err(Error::new(format!("unknown markup \"{}\"", key))), 73 | } 74 | } 75 | } 76 | 77 | impl<'a> Bar for Module<'a> { 78 | fn name(&self) -> &str { 79 | match self { 80 | Module::Battery(m) => m.name(), 81 | Module::Brightness(m) => m.name(), 82 | Module::CpuUsage(m) => m.name(), 83 | Module::CpuFreq(m) => m.name(), 84 | Module::DateTime(m) => m.name(), 85 | Module::Memory(m) => m.name(), 86 | Module::Mic(m) => m.name(), 87 | Module::Wired(m) => m.name(), 88 | Module::Sound(m) => m.name(), 89 | Module::Temperature(m) => m.name(), 90 | Module::Weather(m) => m.name(), 91 | Module::Wireless(m) => m.name(), 92 | } 93 | } 94 | 95 | fn run_fn(&self) -> RunPtr { 96 | match self { 97 | Module::Battery(m) => m.run_fn(), 98 | Module::Brightness(m) => m.run_fn(), 99 | Module::CpuUsage(m) => m.run_fn(), 100 | Module::CpuFreq(m) => m.run_fn(), 101 | Module::DateTime(m) => m.run_fn(), 102 | Module::Memory(m) => m.run_fn(), 103 | Module::Mic(m) => m.run_fn(), 104 | Module::Sound(m) => m.run_fn(), 105 | Module::Temperature(m) => m.run_fn(), 106 | Module::Weather(m) => m.run_fn(), 107 | Module::Wired(m) => m.run_fn(), 108 | Module::Wireless(m) => m.run_fn(), 109 | } 110 | } 111 | 112 | fn placeholder(&self) -> &str { 113 | match self { 114 | Module::Battery(m) => m.placeholder(), 115 | Module::Brightness(m) => m.placeholder(), 116 | Module::CpuUsage(m) => m.placeholder(), 117 | Module::CpuFreq(m) => m.placeholder(), 118 | Module::DateTime(m) => m.placeholder(), 119 | Module::Memory(m) => m.placeholder(), 120 | Module::Wired(m) => m.placeholder(), 121 | Module::Mic(m) => m.placeholder(), 122 | Module::Sound(m) => m.placeholder(), 123 | Module::Temperature(m) => m.placeholder(), 124 | Module::Weather(m) => m.placeholder(), 125 | Module::Wireless(m) => m.placeholder(), 126 | } 127 | } 128 | 129 | fn format(&self) -> &str { 130 | match self { 131 | Module::Battery(m) => m.format(), 132 | Module::Brightness(m) => m.format(), 133 | Module::CpuUsage(m) => m.format(), 134 | Module::CpuFreq(m) => m.format(), 135 | Module::DateTime(m) => m.format(), 136 | Module::Memory(m) => m.format(), 137 | Module::Wired(m) => m.format(), 138 | Module::Mic(m) => m.format(), 139 | Module::Sound(m) => m.format(), 140 | Module::Temperature(m) => m.format(), 141 | Module::Weather(m) => m.format(), 142 | Module::Wireless(m) => m.format(), 143 | } 144 | } 145 | } 146 | 147 | #[derive(Debug, Clone)] 148 | pub enum ModuleState { 149 | NotStarted, 150 | Running, 151 | /// Module's `run` function returned without errors 152 | Finished, 153 | /// Module's `run` function returned an error or panicked 154 | Failed, 155 | } 156 | 157 | #[derive(Debug)] 158 | pub struct ModuleData<'a> { 159 | pub key: char, 160 | pub module: Module<'a>, 161 | data: Option, 162 | state: ModuleState, 163 | handle: Option>>, 164 | failed_placeholder: String, 165 | } 166 | 167 | impl<'a> ModuleData<'a> { 168 | pub fn new(key: char, config: &'a Config) -> Result { 169 | Ok(ModuleData { 170 | key, 171 | module: Module::try_from((key, config))?, 172 | data: None, 173 | state: ModuleState::NotStarted, 174 | handle: None, 175 | failed_placeholder: config 176 | .failed_icon 177 | .as_ref() 178 | .map(|icon| format!("{}:{}", &key, icon)) 179 | .unwrap_or_else(|| format!("{}:{}", &key, MODULE_FAILED_ICON)), 180 | }) 181 | } 182 | 183 | pub fn new_data(&mut self, value: Option<&str>, label: Option<&str>) { 184 | let mut module_format = self.module.format().to_string(); 185 | module_format = match value { 186 | Some(v) => module_format.replace("%v", v), 187 | None => module_format.replace("%v", ""), 188 | }; 189 | module_format = match label { 190 | Some(l) => module_format.replace("%l", l), 191 | None => module_format.replace("%l", ""), 192 | }; 193 | self.data = Some(module_format); 194 | } 195 | 196 | pub fn output(&self) -> &str { 197 | if matches!(self.state, ModuleState::Failed) { 198 | return &self.failed_placeholder; 199 | } 200 | 201 | if let Some(data) = &self.data { 202 | data 203 | } else { 204 | self.module.placeholder() 205 | } 206 | } 207 | 208 | pub fn start(&mut self, handle: JoinHandle>) { 209 | self.handle = Some(handle); 210 | self.state = ModuleState::Running; 211 | } 212 | 213 | #[instrument(skip_all)] 214 | pub fn update_state(&mut self) -> Result<()> { 215 | let Some(handle) = &self.handle else { 216 | return Ok(()); 217 | }; 218 | if !handle.is_finished() { 219 | return Ok(()); 220 | } 221 | 222 | // module thread has finished for some reason, join it 223 | // and update the state accordingly 224 | self.state = match self 225 | .handle 226 | .take() 227 | .ok_or(anyhow!("failed to unwrap handle"))? 228 | .join() 229 | { 230 | Ok(Ok(_)) => { 231 | info!("[{}] module finished", self.module.name()); 232 | ModuleState::Finished 233 | } 234 | Ok(Err(e)) => { 235 | error!("[{}] module failed: {}", self.module.name(), e); 236 | ModuleState::Failed 237 | } 238 | Err(_) => { 239 | error!("[{}] module panicked", self.module.name()); 240 | ModuleState::Failed 241 | } 242 | }; 243 | Ok(()) 244 | } 245 | 246 | #[instrument(skip_all)] 247 | pub fn _terminate(&mut self) { 248 | if let Some(handle) = self.handle.take() { 249 | match handle.join() { 250 | Ok(Ok(_)) => info!("[{}] module terminated", self.module.name()), 251 | Ok(Err(e)) => error!("[{}] module failed: {}", self.module.name(), e), 252 | Err(_) => error!("[{}] module panicked", self.module.name()), 253 | }; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/modules/battery.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::{Config as MainConfig, ModuleMsg}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::convert::TryFrom; 10 | use std::fs::{self, File}; 11 | use std::io::{self, prelude::*, BufReader}; 12 | use std::sync::atomic::{AtomicBool, Ordering}; 13 | use std::sync::mpsc::Sender; 14 | use std::thread; 15 | use std::time::{Duration, Instant}; 16 | use tracing::{debug, instrument}; 17 | 18 | const PLACEHOLDER: &str = "-"; 19 | const SYS_PATH: &str = "/sys/class/power_supply/"; 20 | const BATTERY_NAME: &str = "BAT0"; 21 | const UEVENT: &str = "uevent"; 22 | const FULL_DESIGN: bool = false; 23 | const POWER_SUPPLY: &str = "POWER_SUPPLY"; 24 | const CHARGE_PREFIX: &str = "CHARGE"; 25 | const ENERGY_PREFIX: &str = "ENERGY"; 26 | const FULL_ATTRIBUTE: &str = "FULL"; 27 | const FULL_DESIGN_ATTRIBUTE: &str = "FULL_DESIGN"; 28 | const NOW_ATTRIBUTE: &str = "NOW"; 29 | const STATUS_ATTRIBUTE: &str = "POWER_SUPPLY_STATUS"; 30 | const FULL_LABEL: &str = "*ba"; 31 | const CHARGING_LABEL: &str = "^ba"; 32 | const DISCHARGING_LABEL: &str = "bat"; 33 | const LOW_LABEL: &str = "!ba"; 34 | const UNKNOWN_LABEL: &str = ".ba"; 35 | const LOW_LEVEL: u32 = 10; 36 | const TICK_RATE: Duration = Duration::from_millis(500); 37 | const FORMAT: &str = "%l:%v"; 38 | 39 | #[derive(Debug, Serialize, Deserialize, Clone)] 40 | pub struct Config { 41 | name: Option, 42 | low_level: Option, 43 | full_design: Option, 44 | tick: Option, 45 | placeholder: Option, 46 | full_label: Option, 47 | charging_label: Option, 48 | discharging_label: Option, 49 | low_label: Option, 50 | unknown_label: Option, 51 | format: Option, 52 | } 53 | 54 | #[derive(Debug)] 55 | pub struct InternalConfig<'a> { 56 | low_level: u32, 57 | tick: Duration, 58 | uevent: String, 59 | now_attribute: String, 60 | full_attribute: String, 61 | full_label: &'a str, 62 | charging_label: &'a str, 63 | discharging_label: &'a str, 64 | low_label: &'a str, 65 | unknown_label: &'a str, 66 | } 67 | 68 | impl<'a> TryFrom<&'a MainConfig> for InternalConfig<'a> { 69 | type Error = Error; 70 | 71 | fn try_from(config: &'a MainConfig) -> Result { 72 | let mut low_level = LOW_LEVEL; 73 | let mut name = BATTERY_NAME; 74 | let mut full_design = FULL_DESIGN; 75 | let mut tick = TICK_RATE; 76 | let mut full_label = FULL_LABEL; 77 | let mut charging_label = CHARGING_LABEL; 78 | let mut discharging_label = DISCHARGING_LABEL; 79 | let mut low_label = LOW_LABEL; 80 | let mut unknown_label = UNKNOWN_LABEL; 81 | if let Some(c) = &config.battery { 82 | if let Some(n) = &c.name { 83 | name = n; 84 | } 85 | if let Some(v) = &c.low_level { 86 | low_level = *v; 87 | } 88 | if let Some(b) = c.full_design { 89 | if b { 90 | full_design = true; 91 | } 92 | } 93 | if let Some(t) = c.tick { 94 | tick = Duration::from_millis(t as u64) 95 | } 96 | if let Some(v) = &c.full_label { 97 | full_label = v; 98 | } 99 | if let Some(v) = &c.charging_label { 100 | charging_label = v; 101 | } 102 | if let Some(v) = &c.discharging_label { 103 | discharging_label = v; 104 | } 105 | if let Some(v) = &c.low_label { 106 | low_label = v; 107 | } 108 | if let Some(v) = &c.unknown_label { 109 | unknown_label = v; 110 | } 111 | } 112 | let full_attr = match full_design { 113 | true => FULL_DESIGN_ATTRIBUTE, 114 | false => FULL_ATTRIBUTE, 115 | }; 116 | let uevent = format!("{}{}/{}", SYS_PATH, &name, UEVENT); 117 | let attribute_prefix = find_attribute_prefix(&uevent)?; 118 | Ok(InternalConfig { 119 | low_level, 120 | tick, 121 | uevent, 122 | now_attribute: format!("{}_{}_{}", POWER_SUPPLY, attribute_prefix, NOW_ATTRIBUTE), 123 | full_attribute: format!("{}_{}_{}", POWER_SUPPLY, attribute_prefix, full_attr), 124 | full_label, 125 | charging_label, 126 | discharging_label, 127 | low_label, 128 | unknown_label, 129 | }) 130 | } 131 | } 132 | 133 | #[derive(Debug)] 134 | pub struct Battery<'a> { 135 | placeholder: &'a str, 136 | format: &'a str, 137 | } 138 | 139 | impl<'a> Battery<'a> { 140 | pub fn with_config(config: &'a MainConfig) -> Self { 141 | let mut placeholder = PLACEHOLDER; 142 | let mut format = FORMAT; 143 | if let Some(c) = &config.battery { 144 | if let Some(p) = &c.placeholder { 145 | placeholder = p 146 | } 147 | if let Some(v) = &c.format { 148 | format = v; 149 | } 150 | } 151 | Battery { 152 | format, 153 | placeholder, 154 | } 155 | } 156 | } 157 | 158 | impl<'a> Bar for Battery<'a> { 159 | fn name(&self) -> &str { 160 | "battery" 161 | } 162 | 163 | fn run_fn(&self) -> RunPtr { 164 | run 165 | } 166 | 167 | fn placeholder(&self) -> &str { 168 | self.placeholder 169 | } 170 | 171 | fn format(&self) -> &str { 172 | self.format 173 | } 174 | } 175 | 176 | #[instrument(skip_all)] 177 | pub fn run( 178 | running: &AtomicBool, 179 | key: char, 180 | main_config: MainConfig, 181 | tx: Sender, 182 | ) -> Result<(), Error> { 183 | let config = InternalConfig::try_from(&main_config)?; 184 | debug!("{:#?}", config); 185 | let mut iteration_start: Instant; 186 | let mut iteration_end: Duration; 187 | while running.load(Ordering::Relaxed) { 188 | iteration_start = Instant::now(); 189 | let (energy, capacity, status) = parse_attributes( 190 | &config.uevent, 191 | &config.now_attribute, 192 | &config.full_attribute, 193 | )?; 194 | let capacity = capacity as u64; 195 | let energy = energy as u64; 196 | let battery_level = u32::try_from(100_u64 * energy / capacity)?; 197 | let label = match status.as_str() { 198 | "Full" => config.full_label, 199 | "Discharging" => { 200 | if battery_level <= config.low_level { 201 | config.low_label 202 | } else { 203 | config.discharging_label 204 | } 205 | } 206 | "Charging" => config.charging_label, 207 | _ => config.unknown_label, 208 | }; 209 | tx.send(ModuleMsg( 210 | key, 211 | Some(format!("{:3}%", battery_level)), 212 | Some(label.to_string()), 213 | ))?; 214 | iteration_end = iteration_start.elapsed(); 215 | if iteration_end < config.tick { 216 | thread::sleep(config.tick - iteration_end); 217 | } 218 | } 219 | Ok(()) 220 | } 221 | 222 | fn parse_attributes( 223 | uevent: &str, 224 | now_attribute: &str, 225 | full_attribute: &str, 226 | ) -> Result<(i32, i32, String), Error> { 227 | let file = File::open(uevent)?; 228 | let f = BufReader::new(file); 229 | let mut now = None; 230 | let mut full = None; 231 | let mut status = None; 232 | for line in f.lines() { 233 | if now.is_none() { 234 | now = parse_attribute(&line, now_attribute); 235 | } 236 | if full.is_none() { 237 | full = parse_attribute(&line, full_attribute); 238 | } 239 | if status.is_none() { 240 | status = parse_status(&line); 241 | } 242 | } 243 | if now.is_none() || full.is_none() || status.is_none() { 244 | return Err(Error::new(format!( 245 | "unable to parse the required attributes in {}", 246 | uevent 247 | ))); 248 | } 249 | Ok((now.unwrap(), full.unwrap(), status.unwrap())) 250 | } 251 | 252 | fn parse_attribute(line: &io::Result, attribute: &str) -> Option { 253 | if let Ok(l) = line { 254 | if l.starts_with(attribute) { 255 | let s = l.split('=').nth(1); 256 | if let Some(v) = s { 257 | return v.parse::().ok(); 258 | } 259 | } 260 | } 261 | None 262 | } 263 | 264 | fn parse_status(line: &io::Result) -> Option { 265 | if let Ok(l) = line { 266 | if l.starts_with(STATUS_ATTRIBUTE) { 267 | return l.split('=').nth(1).map(|s| s.to_string()); 268 | } 269 | } 270 | None 271 | } 272 | 273 | fn find_attribute_prefix<'e>(path: &str) -> Result<&'e str, Error> { 274 | let content = fs::read_to_string(path)?; 275 | let mut unit = None; 276 | if content.contains(&format!( 277 | "{}_{}_{}=", 278 | POWER_SUPPLY, ENERGY_PREFIX, FULL_DESIGN_ATTRIBUTE 279 | )) && content.contains(&format!( 280 | "{}_{}_{}=", 281 | POWER_SUPPLY, ENERGY_PREFIX, FULL_ATTRIBUTE 282 | )) && content.contains(&format!( 283 | "{}_{}_{}=", 284 | POWER_SUPPLY, ENERGY_PREFIX, NOW_ATTRIBUTE 285 | )) { 286 | unit = Some(ENERGY_PREFIX); 287 | } else if content.contains(&format!( 288 | "{}_{}_{}=", 289 | POWER_SUPPLY, CHARGE_PREFIX, FULL_DESIGN_ATTRIBUTE 290 | )) && content.contains(&format!( 291 | "{}_{}_{}=", 292 | POWER_SUPPLY, CHARGE_PREFIX, FULL_ATTRIBUTE 293 | )) && content.contains(&format!( 294 | "{}_{}_{}=", 295 | POWER_SUPPLY, CHARGE_PREFIX, NOW_ATTRIBUTE 296 | )) { 297 | unit = Some(CHARGE_PREFIX); 298 | } 299 | unit.ok_or_else(|| { 300 | Error::new(format!( 301 | "unable to find the required attributes in {}", 302 | path 303 | )) 304 | }) 305 | } 306 | -------------------------------------------------------------------------------- /src/modules/brightness.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::util::read_and_parse; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const SYS_PATH: &str = "/sys/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight"; 18 | const TICK_RATE: Duration = Duration::from_millis(50); 19 | const LABEL: &str = "bri"; 20 | const FORMAT: &str = "%l:%v"; 21 | 22 | #[derive(Debug, Serialize, Deserialize, Clone)] 23 | pub struct Config { 24 | placeholder: Option, 25 | sys_path: Option, 26 | tick: Option, 27 | label: Option, 28 | format: Option, 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct InternalConfig<'a> { 33 | sys_path: &'a str, 34 | tick: Duration, 35 | label: &'a str, 36 | } 37 | 38 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 39 | fn from(config: &'a MainConfig) -> Self { 40 | let mut sys_path = SYS_PATH; 41 | let mut tick = TICK_RATE; 42 | let mut label = LABEL; 43 | if let Some(c) = &config.brightness { 44 | if let Some(v) = &c.sys_path { 45 | sys_path = v; 46 | } 47 | if let Some(t) = c.tick { 48 | tick = Duration::from_millis(t as u64) 49 | } 50 | if let Some(v) = &c.label { 51 | label = v; 52 | } 53 | } 54 | InternalConfig { 55 | sys_path, 56 | tick, 57 | label, 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub struct Brightness<'a> { 64 | placeholder: &'a str, 65 | format: &'a str, 66 | } 67 | 68 | impl<'a> Brightness<'a> { 69 | pub fn with_config(config: &'a MainConfig) -> Self { 70 | let mut placeholder = PLACEHOLDER; 71 | let mut format = FORMAT; 72 | if let Some(c) = &config.brightness { 73 | if let Some(p) = &c.placeholder { 74 | placeholder = p 75 | } 76 | if let Some(v) = &c.format { 77 | format = v; 78 | } 79 | } 80 | Brightness { 81 | placeholder, 82 | format, 83 | } 84 | } 85 | } 86 | 87 | impl<'a> Bar for Brightness<'a> { 88 | fn name(&self) -> &str { 89 | "brightness" 90 | } 91 | 92 | fn run_fn(&self) -> RunPtr { 93 | run 94 | } 95 | 96 | fn placeholder(&self) -> &str { 97 | self.placeholder 98 | } 99 | 100 | fn format(&self) -> &str { 101 | self.format 102 | } 103 | } 104 | 105 | #[instrument(skip_all)] 106 | pub fn run( 107 | running: &AtomicBool, 108 | key: char, 109 | main_config: MainConfig, 110 | tx: Sender, 111 | ) -> Result<(), Error> { 112 | let config = InternalConfig::from(&main_config); 113 | debug!("{:#?}", config); 114 | let mut iteration_start: Instant; 115 | let mut iteration_end: Duration; 116 | while running.load(Ordering::Relaxed) { 117 | iteration_start = Instant::now(); 118 | let brightness = read_and_parse(&format!("{}/actual_brightness", config.sys_path))?; 119 | let max_brightness = read_and_parse(&format!("{}/max_brightness", config.sys_path))?; 120 | let percentage = 100 * brightness / max_brightness; 121 | tx.send(ModuleMsg( 122 | key, 123 | Some(format!("{:3}%", percentage)), 124 | Some(config.label.to_string()), 125 | ))?; 126 | iteration_end = iteration_start.elapsed(); 127 | if iteration_end < config.tick { 128 | thread::sleep(config.tick - iteration_end); 129 | } 130 | } 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /src/modules/cpu_freq.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::util::read_and_parse; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::fs::{read_dir, DirEntry}; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::sync::mpsc::Sender; 13 | use std::thread; 14 | use std::time::{Duration, Instant}; 15 | use std::{convert::TryFrom, path::Path}; 16 | use tracing::{debug, instrument}; 17 | 18 | const PLACEHOLDER: &str = "-"; 19 | const TICK_RATE: Duration = Duration::from_millis(100); 20 | const HIGH_LEVEL: u32 = 80; 21 | const LABEL: &str = "fre"; 22 | const HIGH_LABEL: &str = "!fr"; 23 | const FORMAT: &str = "%l:%v"; 24 | const SYSFS_CPUFREQ: &str = "/sys/devices/system/cpu/cpufreq"; 25 | const CPUINFO_MAX_FREQ: &str = "cpuinfo_max_freq"; 26 | const SCALING_MAX_FREQ: &str = "scaling_max_freq"; 27 | const CPUINFO_CUR_FREQ: &str = "cpuinfo_cur_freq"; 28 | const SCALING_CUR_FREQ: &str = "scaling_cur_freq"; 29 | const UNIT: Unit = Unit::Smart; 30 | const MAX_FREQ: bool = false; 31 | 32 | #[derive(Debug, Serialize, Deserialize, Clone)] 33 | pub struct Config { 34 | tick: Option, 35 | unit: Option, 36 | max_freq: Option, 37 | high_level: Option, 38 | placeholder: Option, 39 | label: Option, 40 | high_label: Option, 41 | format: Option, 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize, Copy, Clone)] 45 | enum Unit { 46 | MHz, 47 | GHz, 48 | Smart, 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct InternalConfig<'a> { 53 | high_level: u32, 54 | tick: Duration, 55 | max_freq: f32, 56 | unit: Unit, 57 | show_max_freq: bool, 58 | label: &'a str, 59 | high_label: &'a str, 60 | cur_freq_attribute: &'a str, 61 | } 62 | 63 | impl<'a> TryFrom<&'a MainConfig> for InternalConfig<'a> { 64 | type Error = Error; 65 | 66 | fn try_from(config: &'a MainConfig) -> Result { 67 | let mut tick = TICK_RATE; 68 | let mut show_max_freq = MAX_FREQ; 69 | let mut unit = UNIT; 70 | let mut high_level = HIGH_LEVEL; 71 | let mut label = LABEL; 72 | let mut high_label = HIGH_LABEL; 73 | if let Some(c) = &config.cpu_freq { 74 | if let Some(t) = c.tick { 75 | tick = Duration::from_millis(t as u64) 76 | } 77 | if let Some(c) = c.high_level { 78 | high_level = c; 79 | } 80 | if let Some(v) = c.max_freq { 81 | show_max_freq = v; 82 | } 83 | if let Some(v) = c.unit { 84 | unit = v; 85 | } 86 | if let Some(v) = &c.label { 87 | label = v; 88 | } 89 | if let Some(v) = &c.high_label { 90 | high_label = v; 91 | } 92 | }; 93 | let policy_path = format!("{}/policy0", SYSFS_CPUFREQ); 94 | let entries: Vec = read_dir(Path::new(&policy_path))? 95 | .filter_map(|entry| entry.ok()) 96 | .collect(); 97 | let cpuinfo_max_freq = entries 98 | .iter() 99 | .find(|&entry| entry.file_name().to_str() == Some(CPUINFO_MAX_FREQ)); 100 | let scaling_max_freq = entries 101 | .iter() 102 | .find(|&entry| entry.file_name().to_str() == Some(SCALING_MAX_FREQ)); 103 | let cpuinfo_cur_freq = entries 104 | .iter() 105 | .any(|entry| entry.file_name().to_str() == Some(CPUINFO_CUR_FREQ)); 106 | let scaling_cur_freq = entries 107 | .iter() 108 | .any(|entry| entry.file_name().to_str() == Some(SCALING_CUR_FREQ)); 109 | if !cpuinfo_cur_freq && !scaling_cur_freq { 110 | return Err(Error::new("fail to find current cpu freq")); 111 | } 112 | let cur_freq_attribute = if scaling_cur_freq { 113 | SCALING_CUR_FREQ 114 | } else { 115 | CPUINFO_CUR_FREQ 116 | }; 117 | let max_freq = if let Some(entry) = scaling_max_freq { 118 | read_and_parse(entry.path().to_str().unwrap())? as u32 119 | } else if let Some(entry) = cpuinfo_max_freq { 120 | read_and_parse(entry.path().to_str().unwrap())? as u32 121 | } else { 122 | return Err(Error::new("fail to find max cpu freq")); 123 | }; 124 | Ok(InternalConfig { 125 | high_level, 126 | tick, 127 | show_max_freq, 128 | max_freq: (max_freq / 1000) as f32, 129 | unit, 130 | label, 131 | high_label, 132 | cur_freq_attribute, 133 | }) 134 | } 135 | } 136 | 137 | #[derive(Debug)] 138 | pub struct CpuFreq<'a> { 139 | placeholder: &'a str, 140 | format: &'a str, 141 | } 142 | 143 | impl<'a> CpuFreq<'a> { 144 | pub fn with_config(config: &'a MainConfig) -> Self { 145 | let mut placeholder = PLACEHOLDER; 146 | let mut format = FORMAT; 147 | if let Some(c) = &config.cpu_freq { 148 | if let Some(p) = &c.placeholder { 149 | placeholder = p 150 | } 151 | if let Some(v) = &c.format { 152 | format = v; 153 | } 154 | } 155 | CpuFreq { 156 | placeholder, 157 | format, 158 | } 159 | } 160 | } 161 | 162 | impl<'a> Bar for CpuFreq<'a> { 163 | fn name(&self) -> &str { 164 | "cpu_freq" 165 | } 166 | 167 | fn run_fn(&self) -> RunPtr { 168 | run 169 | } 170 | 171 | fn placeholder(&self) -> &str { 172 | self.placeholder 173 | } 174 | 175 | fn format(&self) -> &str { 176 | self.format 177 | } 178 | } 179 | 180 | #[instrument(skip_all)] 181 | pub fn run( 182 | running: &AtomicBool, 183 | key: char, 184 | main_config: MainConfig, 185 | tx: Sender, 186 | ) -> Result<(), Error> { 187 | let config = InternalConfig::try_from(&main_config)?; 188 | debug!("{:#?}", config); 189 | let mut iteration_start: Instant; 190 | let mut iteration_end: Duration; 191 | while running.load(Ordering::Relaxed) { 192 | iteration_start = Instant::now(); 193 | let freqs: Vec = read_dir(Path::new(SYSFS_CPUFREQ))? 194 | .filter_map(|entry| entry.ok()) 195 | .filter_map(|entry| { 196 | if let Some(value) = entry.path().to_str() { 197 | read_and_parse(format!("{}/{}", value, config.cur_freq_attribute).as_str()).ok() 198 | } else { 199 | None 200 | } 201 | }) 202 | .map(|freq| freq as f32 / 1000f32) 203 | .collect(); 204 | let avg = freqs.iter().sum::() / freqs.len() as f32; 205 | let value = match config.show_max_freq { 206 | true => format!( 207 | "{}/{}", 208 | humanize(avg, config.unit), 209 | humanize(config.max_freq, config.unit) 210 | ), 211 | false => humanize(avg, config.unit), 212 | }; 213 | let percentage = ((avg * 100f32) / config.max_freq).round() as u32; 214 | let label = if percentage >= config.high_level { 215 | config.high_label 216 | } else { 217 | config.label 218 | }; 219 | tx.send(ModuleMsg(key, Some(value), Some(label.to_string())))?; 220 | iteration_end = iteration_start.elapsed(); 221 | if iteration_end < config.tick { 222 | thread::sleep(config.tick - iteration_end); 223 | } 224 | } 225 | Ok(()) 226 | } 227 | 228 | fn humanize(average: f32, unit: Unit) -> String { 229 | match unit { 230 | Unit::GHz => format!("{:3.1}GHz", average / 1000f32), 231 | Unit::MHz => format!("{:4}MHz", average.round() as u32), 232 | Unit::Smart => { 233 | let rounded = average.round(); 234 | if rounded < 1000f32 { 235 | format!("{:3}MHz", rounded as u32) 236 | } else { 237 | format!("{:3.1}GHz", average / 1000f32) 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/modules/cpu_usage.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::{Config as MainConfig, ModuleMsg}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::fs::File; 10 | use std::io::prelude::*; 11 | use std::io::BufReader; 12 | use std::sync::atomic::{AtomicBool, Ordering}; 13 | use std::sync::mpsc::Sender; 14 | use std::thread; 15 | use std::time::{Duration, Instant}; 16 | use tracing::{debug, instrument}; 17 | 18 | const PLACEHOLDER: &str = "-"; 19 | const PROC_STAT: &str = "/proc/stat"; 20 | const TICK_RATE: Duration = Duration::from_millis(500); 21 | const HIGH_LEVEL: u32 = 90; 22 | const LABEL: &str = "cpu"; 23 | const HIGH_LABEL: &str = "!cp"; 24 | const FORMAT: &str = "%l:%v"; 25 | 26 | #[derive(Debug, Serialize, Deserialize, Clone)] 27 | pub struct Config { 28 | tick: Option, 29 | high_level: Option, 30 | placeholder: Option, 31 | label: Option, 32 | high_label: Option, 33 | format: Option, 34 | } 35 | 36 | #[derive(Debug)] 37 | pub struct InternalConfig<'a> { 38 | proc_stat: &'a str, 39 | high_level: u32, 40 | tick: Duration, 41 | label: &'a str, 42 | high_label: &'a str, 43 | } 44 | 45 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 46 | fn from(config: &'a MainConfig) -> Self { 47 | let mut tick = TICK_RATE; 48 | let mut high_level = HIGH_LEVEL; 49 | let mut label = LABEL; 50 | let mut high_label = HIGH_LABEL; 51 | if let Some(c) = &config.cpu_usage { 52 | if let Some(t) = c.tick { 53 | tick = Duration::from_millis(t as u64) 54 | } 55 | if let Some(c) = c.high_level { 56 | high_level = c; 57 | } 58 | if let Some(v) = &c.label { 59 | label = v; 60 | } 61 | if let Some(v) = &c.high_label { 62 | high_label = v; 63 | } 64 | }; 65 | InternalConfig { 66 | high_level, 67 | proc_stat: PROC_STAT, 68 | tick, 69 | label, 70 | high_label, 71 | } 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | pub struct CpuUsage<'a> { 77 | placeholder: &'a str, 78 | format: &'a str, 79 | } 80 | 81 | impl<'a> CpuUsage<'a> { 82 | pub fn with_config(config: &'a MainConfig) -> Self { 83 | let mut placeholder = PLACEHOLDER; 84 | let mut format = FORMAT; 85 | if let Some(c) = &config.cpu_usage { 86 | if let Some(p) = &c.placeholder { 87 | placeholder = p 88 | } 89 | if let Some(v) = &c.format { 90 | format = v; 91 | } 92 | } 93 | CpuUsage { 94 | placeholder, 95 | format, 96 | } 97 | } 98 | } 99 | 100 | impl<'a> Bar for CpuUsage<'a> { 101 | fn name(&self) -> &str { 102 | "cpu_usage" 103 | } 104 | 105 | fn run_fn(&self) -> RunPtr { 106 | run 107 | } 108 | 109 | fn placeholder(&self) -> &str { 110 | self.placeholder 111 | } 112 | 113 | fn format(&self) -> &str { 114 | self.format 115 | } 116 | } 117 | 118 | #[instrument(skip_all)] 119 | pub fn run( 120 | running: &AtomicBool, 121 | key: char, 122 | main_config: MainConfig, 123 | tx: Sender, 124 | ) -> Result<(), Error> { 125 | let config = InternalConfig::from(&main_config); 126 | debug!("{:#?}", config); 127 | let mut prev_idle = 0; 128 | let mut prev_total = 0; 129 | let mut iteration_start: Instant; 130 | let mut iteration_end: Duration; 131 | while running.load(Ordering::Relaxed) { 132 | iteration_start = Instant::now(); 133 | let proc_stat = File::open(config.proc_stat)?; 134 | let mut reader = BufReader::new(proc_stat); 135 | let mut buf = String::new(); 136 | reader.read_line(&mut buf)?; 137 | let mut data = buf.split_whitespace(); 138 | data.next(); 139 | let times: Vec = data 140 | .map(|n| { 141 | n.parse::().unwrap_or_else(|_| { 142 | panic!("error while parsing the file \"{}\"", config.proc_stat) 143 | }) 144 | }) 145 | .collect(); 146 | let idle = times[3] + times[4]; 147 | let total = times.iter().sum(); 148 | let diff_total = total - prev_total; 149 | let diff_idle = idle - prev_idle; 150 | let usage = (100_f32 * (diff_total - diff_idle) as f32 / diff_total as f32).round() as i32; 151 | prev_total = total; 152 | prev_idle = idle; 153 | let mut label = config.label; 154 | if usage >= config.high_level as i32 { 155 | label = config.high_label; 156 | } 157 | tx.send(ModuleMsg( 158 | key, 159 | Some(format!("{:3}%", usage)), 160 | Some(label.to_string()), 161 | ))?; 162 | iteration_end = iteration_start.elapsed(); 163 | if iteration_end < config.tick { 164 | thread::sleep(config.tick - iteration_end); 165 | } 166 | } 167 | Ok(()) 168 | } 169 | -------------------------------------------------------------------------------- /src/modules/date_time.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::{Config as MainConfig, ModuleMsg}; 8 | use chrono::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const DATE_FORMAT: &str = "%a. %-e %B %Y, %-kh%M"; 18 | const TICK_RATE: Duration = Duration::from_millis(500); 19 | const FORMAT: &str = "%v"; 20 | 21 | #[derive(Debug, Serialize, Deserialize, Clone)] 22 | pub struct Config { 23 | date_format: Option, 24 | tick: Option, 25 | placeholder: Option, 26 | label: Option, 27 | format: Option, 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct InternalConfig<'a> { 32 | date_format: &'a str, 33 | tick: Duration, 34 | label: Option<&'a str>, 35 | } 36 | 37 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 38 | fn from(config: &'a MainConfig) -> Self { 39 | let mut tick = TICK_RATE; 40 | let mut date_format = DATE_FORMAT; 41 | let mut label = None; 42 | if let Some(c) = &config.date_time { 43 | if let Some(d) = &c.date_format { 44 | date_format = d; 45 | } 46 | if let Some(t) = c.tick { 47 | tick = Duration::from_millis(t as u64) 48 | } 49 | label = c.label.as_deref(); 50 | } 51 | InternalConfig { 52 | date_format, 53 | tick, 54 | label, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug)] 60 | pub struct DateTime<'a> { 61 | placeholder: &'a str, 62 | format: &'a str, 63 | } 64 | 65 | impl<'a> DateTime<'a> { 66 | pub fn with_config(config: &'a MainConfig) -> Self { 67 | let mut placeholder = PLACEHOLDER; 68 | let mut format = FORMAT; 69 | if let Some(c) = &config.date_time { 70 | if let Some(p) = &c.placeholder { 71 | placeholder = p 72 | } 73 | if let Some(v) = &c.format { 74 | format = v; 75 | } 76 | } 77 | DateTime { 78 | placeholder, 79 | format, 80 | } 81 | } 82 | } 83 | 84 | impl<'a> Bar for DateTime<'a> { 85 | fn name(&self) -> &str { 86 | "date_time" 87 | } 88 | 89 | fn run_fn(&self) -> RunPtr { 90 | run 91 | } 92 | 93 | fn placeholder(&self) -> &str { 94 | self.placeholder 95 | } 96 | 97 | fn format(&self) -> &str { 98 | self.format 99 | } 100 | } 101 | 102 | #[instrument(skip_all)] 103 | pub fn run( 104 | running: &AtomicBool, 105 | key: char, 106 | main_config: MainConfig, 107 | tx: Sender, 108 | ) -> Result<(), Error> { 109 | let config = InternalConfig::from(&main_config); 110 | debug!("{:#?}", config); 111 | let mut iteration_start: Instant; 112 | let mut iteration_end: Duration; 113 | while running.load(Ordering::Relaxed) { 114 | iteration_start = Instant::now(); 115 | tx.send(ModuleMsg( 116 | key, 117 | Some(Local::now().format(config.date_format).to_string()), 118 | config.label.map(|v| v.to_string()), 119 | ))?; 120 | iteration_end = iteration_start.elapsed(); 121 | if iteration_end < config.tick { 122 | thread::sleep(config.tick - iteration_end); 123 | } 124 | } 125 | Ok(()) 126 | } 127 | -------------------------------------------------------------------------------- /src/modules/memory.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::util::read_and_trim; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use regex::Regex; 10 | use serde::{Deserialize, Serialize}; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::sync::mpsc::Sender; 13 | use std::thread; 14 | use std::time::{Duration, Instant}; 15 | use tracing::{debug, instrument}; 16 | 17 | const PLACEHOLDER: &str = "-"; 18 | const MEMINFO: &str = "/proc/meminfo"; 19 | const DISPLAY: Display = Display::GiB; 20 | const HIGH_LEVEL: u32 = 90; 21 | const TICK_RATE: Duration = Duration::from_millis(500); 22 | const LABEL: &str = "mem"; 23 | const HIGH_LABEL: &str = "!me"; 24 | const FORMAT: &str = "%l:%v"; 25 | 26 | #[derive(Debug, Serialize, Deserialize, Copy, Clone)] 27 | enum Display { 28 | GB, 29 | GiB, 30 | Percentage, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize, Clone)] 34 | pub struct Config { 35 | high_level: Option, 36 | display: Option, 37 | tick: Option, 38 | placeholder: Option, 39 | label: Option, 40 | high_label: Option, 41 | format: Option, 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct InternalConfig<'a> { 46 | meminfo: &'a str, 47 | high_level: u32, 48 | display: Display, 49 | tick: Duration, 50 | label: &'a str, 51 | high_label: &'a str, 52 | } 53 | 54 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 55 | fn from(config: &'a MainConfig) -> Self { 56 | let mut high_level = HIGH_LEVEL; 57 | let mut display = DISPLAY; 58 | let mut tick = TICK_RATE; 59 | let mut label = LABEL; 60 | let mut high_label = HIGH_LABEL; 61 | if let Some(c) = &config.memory { 62 | if let Some(v) = &c.high_level { 63 | high_level = *v; 64 | } 65 | if let Some(v) = c.display { 66 | display = v; 67 | } 68 | if let Some(t) = c.tick { 69 | tick = Duration::from_millis(t as u64) 70 | } 71 | if let Some(v) = &c.label { 72 | label = v; 73 | } 74 | if let Some(v) = &c.high_label { 75 | high_label = v; 76 | } 77 | }; 78 | InternalConfig { 79 | meminfo: MEMINFO, 80 | high_level, 81 | display, 82 | tick, 83 | label, 84 | high_label, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug)] 90 | pub struct Memory<'a> { 91 | placeholder: &'a str, 92 | format: &'a str, 93 | } 94 | 95 | impl<'a> Memory<'a> { 96 | pub fn with_config(config: &'a MainConfig) -> Self { 97 | let mut placeholder = PLACEHOLDER; 98 | let mut format = FORMAT; 99 | if let Some(c) = &config.memory { 100 | if let Some(p) = &c.placeholder { 101 | placeholder = p 102 | } 103 | if let Some(v) = &c.format { 104 | format = v; 105 | } 106 | } 107 | Memory { 108 | placeholder, 109 | format, 110 | } 111 | } 112 | } 113 | 114 | impl<'a> Bar for Memory<'a> { 115 | fn name(&self) -> &str { 116 | "memory" 117 | } 118 | 119 | fn run_fn(&self) -> RunPtr { 120 | run 121 | } 122 | 123 | fn placeholder(&self) -> &str { 124 | self.placeholder 125 | } 126 | 127 | fn format(&self) -> &str { 128 | self.format 129 | } 130 | } 131 | 132 | #[derive(Debug)] 133 | struct MemRegex { 134 | total: Regex, 135 | free: Regex, 136 | buffers: Regex, 137 | cached: Regex, 138 | s_reclaimable: Regex, 139 | } 140 | 141 | impl MemRegex { 142 | fn new() -> Self { 143 | MemRegex { 144 | total: Regex::new(r"(?m)^MemTotal:\s*(\d+)\s*kB$").unwrap(), 145 | free: Regex::new(r"(?m)^MemFree:\s*(\d+)\s*kB$").unwrap(), 146 | buffers: Regex::new(r"(?m)^Buffers:\s*(\d+)\s*kB$").unwrap(), 147 | cached: Regex::new(r"(?m)^Cached:\s*(\d+)\s*kB$").unwrap(), 148 | s_reclaimable: Regex::new(r"(?m)^SReclaimable:\s*(\d+)\s*kB$").unwrap(), 149 | } 150 | } 151 | } 152 | 153 | #[instrument(skip_all)] 154 | pub fn run( 155 | running: &AtomicBool, 156 | key: char, 157 | main_config: MainConfig, 158 | tx: Sender, 159 | ) -> Result<(), Error> { 160 | let config = InternalConfig::from(&main_config); 161 | debug!("{:#?}", config); 162 | let mem_regex = MemRegex::new(); 163 | let mut iteration_start: Instant; 164 | let mut iteration_end: Duration; 165 | while running.load(Ordering::Relaxed) { 166 | iteration_start = Instant::now(); 167 | let meminfo = read_and_trim(config.meminfo)?; 168 | let total_kib = find_meminfo( 169 | &mem_regex.total, 170 | &meminfo, 171 | &format!("MemTotal not found in \"{}\"", config.meminfo), 172 | )?; 173 | let free = find_meminfo( 174 | &mem_regex.free, 175 | &meminfo, 176 | &format!("MemFree not found in \"{}\"", config.meminfo), 177 | )?; 178 | let buffers = find_meminfo( 179 | &mem_regex.buffers, 180 | &meminfo, 181 | &format!("Buffers not found in \"{}\"", config.meminfo), 182 | )?; 183 | let cached = find_meminfo( 184 | &mem_regex.cached, 185 | &meminfo, 186 | &format!("Cached not found in \"{}\"", config.meminfo), 187 | )?; 188 | let s_reclaimable = find_meminfo( 189 | &mem_regex.s_reclaimable, 190 | &meminfo, 191 | &format!("SReclaimable not found in \"{}\"", config.meminfo), 192 | )?; 193 | let used_kib = total_kib - free - buffers - cached - s_reclaimable; 194 | let percentage = (used_kib as f64 * 100_f64 / total_kib as f64).round() as i32; 195 | let mut total = "".to_string(); 196 | let mut used = "".to_string(); 197 | match config.display { 198 | Display::GB => { 199 | let total_go = (1024_f32 * (total_kib as f32)) / 1_000_000_000_f32; 200 | let total_mo = total_go * 10i32.pow(3) as f32; 201 | total = humanize(total_go, total_mo, "GB", "MB"); 202 | let used_go = 1024_f32 * (used_kib as f32) / 1_000_000_000_f32; 203 | let used_mo = used_go * 10i32.pow(3) as f32; 204 | used = humanize(used_go, used_mo, "GB", "MB"); 205 | } 206 | Display::GiB => { 207 | let total_gio = total_kib as f32 / 2i32.pow(20) as f32; 208 | let total_mio = total_kib as f32 / 2i32.pow(10) as f32; 209 | total = humanize(total_gio, total_mio, "GiB", "MiB"); 210 | let used_gio = used_kib as f32 / 2i32.pow(20) as f32; 211 | let used_mio = used_kib as f32 / 2i32.pow(10) as f32; 212 | used = humanize(used_gio, used_mio, "GiB", "MiB"); 213 | } 214 | _ => {} 215 | } 216 | let mut label = config.label; 217 | if percentage > config.high_level as i32 { 218 | label = config.high_label; 219 | } 220 | match config.display { 221 | Display::GB | Display::GiB => tx.send(ModuleMsg( 222 | key, 223 | Some(format!("{}/{}", used, total)), 224 | Some(label.to_string()), 225 | ))?, 226 | Display::Percentage => tx.send(ModuleMsg( 227 | key, 228 | Some(format!("{:3}%", percentage)), 229 | Some(label.to_string()), 230 | ))?, 231 | }; 232 | iteration_end = iteration_start.elapsed(); 233 | if iteration_end < config.tick { 234 | thread::sleep(config.tick - iteration_end); 235 | } 236 | } 237 | Ok(()) 238 | } 239 | 240 | fn humanize<'a>(v1: f32, v2: f32, u1: &'a str, u2: &'a str) -> String { 241 | if v1 >= 1.0 { 242 | format!("{:4.1}{}", v1, u1) 243 | } else { 244 | format!("{:4.0}{}", v2, u2) 245 | } 246 | } 247 | 248 | fn find_meminfo<'a>(regex: &Regex, meminfo: &'a str, error: &'a str) -> Result { 249 | regex 250 | .captures(meminfo) 251 | .ok_or_else(|| error.to_string())? 252 | .get(1) 253 | .ok_or_else(|| error.to_string())? 254 | .as_str() 255 | .parse::() 256 | .map_err(|err| format!("error while parsing meminfo: {}", err)) 257 | } 258 | -------------------------------------------------------------------------------- /src/modules/mic.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::pulse::PULSE; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, error, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const TICK_RATE: Duration = Duration::from_millis(50); 18 | const MUTE_LABEL: &str = ".mi"; 19 | const LABEL: &str = "mic"; 20 | const FORMAT: &str = "%l:%v"; 21 | 22 | #[derive(Debug, Serialize, Deserialize, Clone)] 23 | pub struct Config { 24 | pub source_name: Option, 25 | tick: Option, 26 | placeholder: Option, 27 | label: Option, 28 | mute_label: Option, 29 | format: Option, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct InternalConfig<'a> { 34 | tick: Duration, 35 | label: &'a str, 36 | mute_label: &'a str, 37 | } 38 | 39 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 40 | fn from(config: &'a MainConfig) -> Self { 41 | let mut tick = TICK_RATE; 42 | let mut label = LABEL; 43 | let mut mute_label = MUTE_LABEL; 44 | if let Some(c) = &config.mic { 45 | if let Some(t) = c.tick { 46 | tick = Duration::from_millis(t as u64) 47 | } 48 | if let Some(v) = &c.label { 49 | label = v; 50 | } 51 | if let Some(v) = &c.mute_label { 52 | mute_label = v; 53 | } 54 | } 55 | InternalConfig { 56 | tick, 57 | label, 58 | mute_label, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug)] 64 | pub struct Mic<'a> { 65 | placeholder: &'a str, 66 | format: &'a str, 67 | } 68 | 69 | impl<'a> Mic<'a> { 70 | pub fn with_config(config: &'a MainConfig) -> Self { 71 | let mut placeholder = PLACEHOLDER; 72 | let mut format = FORMAT; 73 | if let Some(c) = &config.mic { 74 | if let Some(p) = &c.placeholder { 75 | placeholder = p 76 | } 77 | if let Some(v) = &c.format { 78 | format = v; 79 | } 80 | } 81 | Mic { 82 | placeholder, 83 | format, 84 | } 85 | } 86 | } 87 | 88 | impl<'a> Bar for Mic<'a> { 89 | fn name(&self) -> &str { 90 | "mic" 91 | } 92 | 93 | fn run_fn(&self) -> RunPtr { 94 | run 95 | } 96 | 97 | fn placeholder(&self) -> &str { 98 | self.placeholder 99 | } 100 | 101 | fn format(&self) -> &str { 102 | self.format 103 | } 104 | } 105 | 106 | #[instrument(skip_all)] 107 | pub fn run( 108 | running: &AtomicBool, 109 | key: char, 110 | main_config: MainConfig, 111 | tx: Sender, 112 | ) -> Result<(), Error> { 113 | let config = InternalConfig::from(&main_config); 114 | debug!("{:#?}", config); 115 | let mut iteration_start: Instant; 116 | let mut iteration_end: Duration; 117 | let pulse = PULSE.get().ok_or("pulse module not initialized")?; 118 | while running.load(Ordering::Relaxed) { 119 | iteration_start = Instant::now(); 120 | if let Some(data) = pulse 121 | .lock() 122 | .map_err(|e| { 123 | error!("failed to lock pulse module: {}", e); 124 | Error::new("failed to lock pulse module") 125 | })? 126 | .source_data() 127 | { 128 | let label = match data.1 { 129 | true => config.mute_label, 130 | false => config.label, 131 | }; 132 | tx.send(ModuleMsg( 133 | key, 134 | Some(format!("{:3}%", data.0)), 135 | Some(label.to_string()), 136 | ))?; 137 | } 138 | iteration_end = iteration_start.elapsed(); 139 | if iteration_end < config.tick { 140 | thread::sleep(config.tick - iteration_end); 141 | } 142 | } 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | pub mod battery; 6 | pub mod brightness; 7 | pub mod cpu_freq; 8 | pub mod cpu_usage; 9 | pub mod date_time; 10 | pub mod memory; 11 | pub mod mic; 12 | pub mod sound; 13 | pub mod temperature; 14 | pub mod weather; 15 | pub mod wired; 16 | pub mod wireless; 17 | -------------------------------------------------------------------------------- /src/modules/sound.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::pulse::PULSE; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, error, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const TICK_RATE: Duration = Duration::from_millis(50); 18 | const MUTE_LABEL: &str = ".so"; 19 | const LABEL: &str = "sou"; 20 | const FORMAT: &str = "%l:%v"; 21 | 22 | #[derive(Debug, Serialize, Deserialize, Clone)] 23 | pub struct Config { 24 | pub sink_name: Option, 25 | tick: Option, 26 | placeholder: Option, 27 | label: Option, 28 | mute_label: Option, 29 | format: Option, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct InternalConfig<'a> { 34 | tick: Duration, 35 | label: &'a str, 36 | mute_label: &'a str, 37 | } 38 | 39 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 40 | fn from(config: &'a MainConfig) -> Self { 41 | let mut tick = TICK_RATE; 42 | let mut label = LABEL; 43 | let mut mute_label = MUTE_LABEL; 44 | if let Some(c) = &config.sound { 45 | if let Some(t) = c.tick { 46 | tick = Duration::from_millis(t as u64) 47 | } 48 | if let Some(v) = &c.label { 49 | label = v; 50 | } 51 | if let Some(v) = &c.mute_label { 52 | mute_label = v; 53 | } 54 | } 55 | InternalConfig { 56 | tick, 57 | label, 58 | mute_label, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug)] 64 | pub struct Sound<'a> { 65 | placeholder: &'a str, 66 | format: &'a str, 67 | } 68 | 69 | impl<'a> Sound<'a> { 70 | pub fn with_config(config: &'a MainConfig) -> Self { 71 | let mut placeholder = PLACEHOLDER; 72 | let mut format = FORMAT; 73 | if let Some(c) = &config.sound { 74 | if let Some(p) = &c.placeholder { 75 | placeholder = p 76 | } 77 | if let Some(v) = &c.format { 78 | format = v; 79 | } 80 | } 81 | Sound { 82 | placeholder, 83 | format, 84 | } 85 | } 86 | } 87 | 88 | impl<'a> Bar for Sound<'a> { 89 | fn name(&self) -> &str { 90 | "sound" 91 | } 92 | 93 | fn run_fn(&self) -> RunPtr { 94 | run 95 | } 96 | 97 | fn placeholder(&self) -> &str { 98 | self.placeholder 99 | } 100 | 101 | fn format(&self) -> &str { 102 | self.format 103 | } 104 | } 105 | 106 | #[instrument(skip_all)] 107 | pub fn run( 108 | running: &AtomicBool, 109 | key: char, 110 | main_config: MainConfig, 111 | tx: Sender, 112 | ) -> Result<(), Error> { 113 | let config = InternalConfig::from(&main_config); 114 | debug!("{:#?}", config); 115 | let mut iteration_start: Instant; 116 | let mut iteration_end: Duration; 117 | let pulse = PULSE.get().ok_or("pulse module not initialized")?; 118 | while running.load(Ordering::Relaxed) { 119 | iteration_start = Instant::now(); 120 | if let Some(data) = pulse 121 | .lock() 122 | .map_err(|e| { 123 | error!("failed to lock pulse module: {}", e); 124 | Error::new("failed to lock pulse module") 125 | })? 126 | .sink_data() 127 | { 128 | let label = match data.1 { 129 | true => config.mute_label, 130 | false => config.label, 131 | }; 132 | tx.send(ModuleMsg( 133 | key, 134 | Some(format!("{:3}%", data.0)), 135 | Some(label.to_string()), 136 | ))?; 137 | } 138 | iteration_end = iteration_start.elapsed(); 139 | if iteration_end < config.tick { 140 | thread::sleep(config.tick - iteration_end); 141 | } 142 | } 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /src/modules/temperature.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::util::read_and_parse; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use regex::Regex; 10 | use serde::{Deserialize, Serialize}; 11 | use std::convert::TryFrom; 12 | use std::sync::atomic::{AtomicBool, Ordering}; 13 | use std::sync::mpsc::Sender; 14 | use std::thread; 15 | use std::time::{Duration, Instant}; 16 | use std::{fs, io}; 17 | use tracing::{debug, instrument, warn}; 18 | 19 | const PLACEHOLDER: &str = "-"; 20 | const CORETEMP: &str = "/sys/devices/platform/coretemp.0/hwmon"; 21 | const HIGH_LEVEL: u32 = 75; 22 | const INPUT: u32 = 1; 23 | const TICK_RATE: Duration = Duration::from_millis(50); 24 | const LABEL: &str = "tem"; 25 | const HIGH_LABEL: &str = "!te"; 26 | const FORMAT: &str = "%l:%v"; 27 | 28 | #[derive(Debug, Serialize, Deserialize, Clone)] 29 | #[serde(untagged)] 30 | enum CoreInputs { 31 | Single(u32), 32 | Range(String), 33 | List(Vec), 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize, Clone)] 37 | pub struct Config { 38 | coretemp: Option, 39 | high_level: Option, 40 | core_inputs: Option, 41 | tick: Option, 42 | placeholder: Option, 43 | label: Option, 44 | high_label: Option, 45 | format: Option, 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct InternalConfig<'a> { 50 | coretemp: &'a str, 51 | high_level: u32, 52 | tick: Duration, 53 | inputs: Vec, 54 | label: &'a str, 55 | high_label: &'a str, 56 | } 57 | 58 | impl<'a> Default for InternalConfig<'a> { 59 | fn default() -> Self { 60 | InternalConfig { 61 | coretemp: CORETEMP, 62 | high_level: HIGH_LEVEL, 63 | tick: TICK_RATE, 64 | inputs: vec![INPUT], 65 | label: LABEL, 66 | high_label: HIGH_LABEL, 67 | } 68 | } 69 | } 70 | 71 | #[instrument(skip(path))] 72 | fn check_input_file(path: &str, n: u32) -> bool { 73 | fs::metadata(format!("{path}/temp{n}_input")) 74 | .map(|m| m.is_file()) 75 | .inspect_err(|e| { 76 | warn!("input file not found `temp{n}_input`, {}", e); 77 | }) 78 | .unwrap_or(false) 79 | } 80 | 81 | fn check_dir(path: &str) -> Result { 82 | let meta = fs::metadata(path)?; 83 | Ok(meta.is_dir()) 84 | } 85 | 86 | #[instrument] 87 | fn get_inputs(core_inputs: &CoreInputs, temp_dir: &str) -> Option> { 88 | let re = Regex::new(r"^(\d+)\.\.(\d+)$").unwrap(); 89 | 90 | match core_inputs { 91 | CoreInputs::Single(n) => { 92 | if check_input_file(temp_dir, *n) { 93 | Some(vec![*n]) 94 | } else { 95 | None 96 | } 97 | } 98 | CoreInputs::Range(range) => { 99 | if let Some(captured) = re.captures(range) { 100 | let start = captured.get(1).unwrap().as_str().parse::().unwrap(); 101 | let end = captured.get(2).unwrap().as_str().parse::().unwrap(); 102 | if (start..end).is_empty() { 103 | warn!("invalid range: start must be less than end"); 104 | return None; 105 | } 106 | let inputs = (start..end + 1) 107 | .filter(|i| check_input_file(temp_dir, *i)) 108 | .collect(); 109 | return Some(inputs); 110 | } 111 | warn!("invalid range format: expected \"start..end\""); 112 | None 113 | } 114 | CoreInputs::List(list) => Some( 115 | list.iter() 116 | .filter(|i| check_input_file(temp_dir, **i)) 117 | .copied() 118 | .collect(), 119 | ), 120 | } 121 | } 122 | 123 | impl<'a> TryFrom<&'a MainConfig> for InternalConfig<'a> { 124 | type Error = Error; 125 | 126 | fn try_from(config: &'a MainConfig) -> Result { 127 | let coretemp = config 128 | .temperature 129 | .as_ref() 130 | .and_then(|c| c.coretemp.as_deref()) 131 | .unwrap_or(CORETEMP); 132 | check_dir(coretemp)?; 133 | let temp_dir = find_temp_dir(coretemp)?; 134 | 135 | let internal_cfg = config 136 | .temperature 137 | .as_ref() 138 | .map(|c| { 139 | let inputs = c 140 | .core_inputs 141 | .as_ref() 142 | .and_then(|i| get_inputs(i, &temp_dir)) 143 | .map(|mut i| { 144 | if i.is_empty() { 145 | warn!("no input files found, using default input {}", INPUT); 146 | i.push(INPUT); 147 | } 148 | i 149 | }) 150 | .unwrap_or(vec![INPUT]); 151 | 152 | InternalConfig { 153 | coretemp, 154 | high_level: c.high_level.unwrap_or(HIGH_LEVEL), 155 | tick: c 156 | .tick 157 | .map_or(TICK_RATE, |t| Duration::from_millis(t as u64)), 158 | inputs, 159 | label: c.label.as_deref().unwrap_or(LABEL), 160 | high_label: c.high_label.as_deref().unwrap_or(HIGH_LABEL), 161 | } 162 | }) 163 | .unwrap_or_default(); 164 | 165 | Ok(internal_cfg) 166 | } 167 | } 168 | 169 | #[derive(Debug)] 170 | pub struct Temperature<'a> { 171 | placeholder: &'a str, 172 | format: &'a str, 173 | } 174 | 175 | impl<'a> Temperature<'a> { 176 | pub fn with_config(config: &'a MainConfig) -> Self { 177 | let mut placeholder = PLACEHOLDER; 178 | let mut format = FORMAT; 179 | if let Some(c) = &config.temperature { 180 | if let Some(p) = &c.placeholder { 181 | placeholder = p 182 | } 183 | if let Some(v) = &c.format { 184 | format = v; 185 | } 186 | } 187 | Temperature { 188 | placeholder, 189 | format, 190 | } 191 | } 192 | } 193 | 194 | impl<'a> Bar for Temperature<'a> { 195 | fn name(&self) -> &str { 196 | "temperature" 197 | } 198 | 199 | fn run_fn(&self) -> RunPtr { 200 | run 201 | } 202 | 203 | fn placeholder(&self) -> &str { 204 | self.placeholder 205 | } 206 | 207 | fn format(&self) -> &str { 208 | self.format 209 | } 210 | } 211 | 212 | #[instrument(skip_all)] 213 | pub fn run( 214 | running: &AtomicBool, 215 | key: char, 216 | main_config: MainConfig, 217 | tx: Sender, 218 | ) -> Result<(), Error> { 219 | let config = InternalConfig::try_from(&main_config)?; 220 | debug!("{:#?}", config); 221 | let temp_dir = find_temp_dir(config.coretemp)?; 222 | let mut iteration_start: Instant; 223 | let mut iteration_end: Duration; 224 | while running.load(Ordering::Relaxed) { 225 | iteration_start = Instant::now(); 226 | let mut inputs = vec![]; 227 | for i in &config.inputs { 228 | inputs.push(read_and_parse(&format!("{}/temp{}_input", temp_dir, i))?) 229 | } 230 | let sum: i32 = inputs.iter().sum(); 231 | let average = ((sum as f32 / inputs.len() as f32) / 1000_f32).round() as i32; 232 | let mut label = config.label; 233 | if average >= config.high_level as i32 { 234 | label = config.high_label; 235 | } 236 | tx.send(ModuleMsg( 237 | key, 238 | Some(format!("{:3}°", average)), 239 | Some(label.to_string()), 240 | ))?; 241 | iteration_end = iteration_start.elapsed(); 242 | if iteration_end < config.tick { 243 | thread::sleep(config.tick - iteration_end); 244 | } 245 | } 246 | Ok(()) 247 | } 248 | 249 | fn find_temp_dir(str_path: &str) -> Result { 250 | let entries = fs::read_dir(str_path).map_err(|err| { 251 | format!( 252 | "error while reading the directory \"{}\": {}", 253 | str_path, err 254 | ) 255 | })?; 256 | for entry in entries { 257 | let entry = entry?; 258 | let path = entry.path(); 259 | if path.is_dir() { 260 | if let Some(p) = path.to_str() { 261 | return Ok(p.to_string()); 262 | } 263 | } 264 | } 265 | Err(Error::new(format!( 266 | "error while resolving coretemp path: no directory found under \"{}\"", 267 | str_path 268 | ))) 269 | } 270 | -------------------------------------------------------------------------------- /src/modules/weather.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::http::HTTP_CLIENT; 7 | use crate::module::{Bar, RunPtr}; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::convert::TryFrom; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::sync::mpsc::Sender; 13 | use std::thread; 14 | use std::time::{Duration, Instant}; 15 | use tracing::{debug, error, instrument, trace, warn}; 16 | 17 | const PLACEHOLDER: &str = "-"; 18 | const TICK_RATE: Duration = Duration::from_secs(120); 19 | const OPENWEATHER_API: &str = "https://api.openweathermap.org/data/2.5/weather"; 20 | const LABEL: &str = "wtr"; 21 | const FORMAT: &str = "%v"; 22 | const DEFAULT_W_ICON: &str = "*"; 23 | const DEFAULT_LOCATION: Location = Location::Coordinates(Coord { 24 | lat: 42.38, 25 | lon: 8.94, 26 | }); 27 | 28 | #[derive(Debug, Serialize, Deserialize, Clone)] 29 | #[serde(untagged)] 30 | enum IconSet { 31 | DayOnly(String), 32 | DayAndNight((String, String)), 33 | } 34 | 35 | #[derive(Debug, Serialize, Deserialize, Clone)] 36 | struct WeatherIcons { 37 | clear_sky: Option, 38 | partly_cloudy: Option, 39 | cloudy: Option, 40 | very_cloudy: Option, 41 | shower_rain: Option, 42 | rain: Option, 43 | thunderstorm: Option, 44 | snow: Option, 45 | mist: Option, 46 | default: Option, 47 | } 48 | 49 | impl WeatherIcons { 50 | fn icon(&self, code: u32) -> &Option { 51 | match code { 52 | 1 => &self.clear_sky, 53 | 2 => &self.partly_cloudy, 54 | 3 => &self.cloudy, 55 | 4 => &self.very_cloudy, 56 | 9 => &self.shower_rain, 57 | 10 => &self.rain, 58 | 11 => &self.thunderstorm, 59 | 13 => &self.snow, 60 | 50 => &self.mist, 61 | _ => { 62 | warn!("unknown weather icon code: {}", code); 63 | &self.clear_sky 64 | } 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 70 | #[serde(rename_all = "snake_case")] 71 | enum Unit { 72 | Standard, 73 | #[default] 74 | Metric, 75 | Imperial, 76 | } 77 | 78 | impl Unit { 79 | /// Get the corresponding value for the OpenWeather API `units` URL parameter. 80 | /// see https://openweathermap.org/current#data 81 | fn to_api(&self) -> &str { 82 | match self { 83 | Unit::Standard => "standard", 84 | Unit::Metric => "metric", 85 | Unit::Imperial => "imperial", 86 | } 87 | } 88 | 89 | fn temp_symbol(&self) -> &str { 90 | match self { 91 | Unit::Standard => "K", 92 | Unit::Metric => "°", // "°C" 93 | Unit::Imperial => "°", // "°F" 94 | } 95 | } 96 | } 97 | 98 | #[derive(Debug, Serialize, Deserialize, Clone)] 99 | struct Coord { 100 | lat: f32, 101 | lon: f32, 102 | } 103 | 104 | #[derive(Debug, Serialize, Deserialize, Clone)] 105 | #[serde(untagged)] 106 | enum Location { 107 | /// deprecated - city name, zip-code or city ID 108 | City(String), 109 | /// Latitude and longitude 110 | Coordinates(Coord), 111 | } 112 | 113 | #[derive(Debug, Serialize, Deserialize, Clone)] 114 | pub struct Config { 115 | location: Location, 116 | api_key: String, 117 | unit: Option, 118 | // two-letter language code 119 | lang: Option, 120 | icons: Option, 121 | text_mode: Option, 122 | // Update interval in seconds 123 | tick: Option, 124 | placeholder: Option, 125 | label: Option, 126 | format: Option, 127 | } 128 | 129 | #[derive(Debug)] 130 | pub struct InternalConfig<'a> { 131 | location: Location, 132 | api_key: String, 133 | unit: Unit, 134 | lang: Option<&'a str>, 135 | icons: Option, 136 | text_mode: bool, 137 | tick: Duration, 138 | label: &'a str, 139 | } 140 | 141 | impl<'a> Default for InternalConfig<'a> { 142 | fn default() -> Self { 143 | InternalConfig { 144 | location: DEFAULT_LOCATION, 145 | api_key: String::new(), 146 | unit: Unit::default(), 147 | lang: None, 148 | icons: None, 149 | text_mode: true, 150 | tick: TICK_RATE, 151 | label: LABEL, 152 | } 153 | } 154 | } 155 | 156 | impl<'a> TryFrom<&'a MainConfig> for InternalConfig<'a> { 157 | type Error = Error; 158 | 159 | fn try_from(config: &'a MainConfig) -> Result { 160 | let internal_cfg = config 161 | .weather 162 | .as_ref() 163 | .map(|c| InternalConfig { 164 | location: c.location.to_owned(), 165 | api_key: c.api_key.to_owned(), 166 | unit: c.unit.to_owned().unwrap_or_default(), 167 | lang: c.lang.as_deref(), 168 | icons: c.icons.to_owned(), 169 | text_mode: c.icons.is_none() || c.text_mode.is_some_and(|b| b), 170 | tick: c.tick.map_or(TICK_RATE, |t| Duration::from_secs(t as u64)), 171 | label: c.label.as_deref().unwrap_or(LABEL), 172 | }) 173 | .unwrap_or_default(); 174 | 175 | Ok(internal_cfg) 176 | } 177 | } 178 | 179 | #[derive(Debug)] 180 | pub struct Weather<'a> { 181 | placeholder: &'a str, 182 | format: &'a str, 183 | } 184 | 185 | impl<'a> Weather<'a> { 186 | pub fn with_config(config: &'a MainConfig) -> Self { 187 | Weather { 188 | placeholder: config 189 | .weather 190 | .as_ref() 191 | .and_then(|c| c.placeholder.as_deref()) 192 | .unwrap_or(PLACEHOLDER), 193 | format: config 194 | .weather 195 | .as_ref() 196 | .and_then(|c| c.format.as_deref()) 197 | .unwrap_or(FORMAT), 198 | } 199 | } 200 | } 201 | 202 | impl<'a> Bar for Weather<'a> { 203 | fn name(&self) -> &str { 204 | "weather" 205 | } 206 | 207 | fn run_fn(&self) -> RunPtr { 208 | run 209 | } 210 | 211 | fn placeholder(&self) -> &str { 212 | self.placeholder 213 | } 214 | 215 | fn format(&self) -> &str { 216 | self.format 217 | } 218 | } 219 | 220 | fn build_url(config: &InternalConfig) -> String { 221 | let location = match &config.location { 222 | Location::City(city) => format!("q={city}"), 223 | Location::Coordinates(Coord { lat, lon }) => format!("lat={lat}&lon={lon}"), 224 | }; 225 | let mut url = format!( 226 | "{OPENWEATHER_API}?{location}&units={}&appid={}", 227 | config.unit.to_api(), 228 | config.api_key, 229 | ); 230 | if let Some(lang) = config.lang { 231 | url.push_str(&format!("&lang={}", lang)); 232 | } 233 | url 234 | } 235 | 236 | fn get_icon<'icon_cfg>(icon: &str, icons: &'icon_cfg WeatherIcons) -> &'icon_cfg str { 237 | let default = icons.default.as_deref().unwrap_or(DEFAULT_W_ICON); 238 | let code = icon[0..2] 239 | .parse::() 240 | .inspect_err(|e| error!("failed to parse weather code: {}", e)) 241 | .unwrap_or(99); 242 | icons.icon(code).as_ref().map_or(default, |i| match i { 243 | IconSet::DayOnly(icon) => icon, 244 | IconSet::DayAndNight((day, night)) => match icon.ends_with('d') { 245 | true => day, 246 | false => night, 247 | }, 248 | }) 249 | } 250 | 251 | fn get_output(json: JsonResponse, config: &InternalConfig) -> String { 252 | let temp = json.main.temp.round() as u32; 253 | let t_symbol = config.unit.temp_symbol(); 254 | let data = json 255 | .weather 256 | .first() 257 | .ok_or("no weather data") 258 | .inspect_err(|_| warn!("no weather data in response")); 259 | if config.text_mode { 260 | let desc = data.map_or("N/A", |w| w.description.as_str()); 261 | return format!("{desc} {temp}{t_symbol}"); 262 | } 263 | let icon_code = data.map_or(DEFAULT_W_ICON, |w| w.icon.as_str()); 264 | trace!("icon code: {}", icon_code); 265 | let icon = config 266 | .icons 267 | .as_ref() 268 | .map_or(DEFAULT_W_ICON, |i| get_icon(icon_code, i)); 269 | format!("{icon} {temp}{t_symbol}") 270 | } 271 | 272 | #[instrument(skip_all)] 273 | pub fn run( 274 | running: &AtomicBool, 275 | key: char, 276 | main_config: MainConfig, 277 | tx: Sender, 278 | ) -> Result<(), Error> { 279 | let config = InternalConfig::try_from(&main_config)?; 280 | debug!("{:#?}", config); 281 | let mut iteration_start: Instant; 282 | let mut iteration_end: Duration; 283 | let url = build_url(&config); 284 | debug!("openweather URL: {}", url); 285 | while running.load(Ordering::Relaxed) { 286 | iteration_start = Instant::now(); 287 | let response = HTTP_CLIENT 288 | .get(&url) 289 | .send() 290 | .inspect_err(|e| error!("request failed, {}", e)) 291 | .ok(); 292 | if let Some(res) = response { 293 | let output = res 294 | .json::() 295 | .inspect_err(|e| error!("failed to parse response body, {}", e)) 296 | .inspect(|json| trace!("response body: {:#?}", json)) 297 | .ok() 298 | .map(|json| get_output(json, &config)); 299 | if let Some(text) = output { 300 | tx.send(ModuleMsg(key, Some(text), Some(config.label.to_owned())))?; 301 | } 302 | } 303 | iteration_end = iteration_start.elapsed(); 304 | if iteration_end < config.tick { 305 | thread::sleep(config.tick - iteration_end); 306 | } 307 | } 308 | Ok(()) 309 | } 310 | 311 | #[derive(Default, Debug, Clone, Deserialize)] 312 | struct JsonResponse { 313 | weather: Vec, 314 | main: MainData, 315 | } 316 | 317 | #[derive(Default, Debug, Clone, Deserialize)] 318 | struct WeatherData { 319 | description: String, 320 | icon: String, 321 | } 322 | 323 | #[derive(Default, Debug, Clone, Deserialize)] 324 | struct MainData { 325 | temp: f64, 326 | } 327 | -------------------------------------------------------------------------------- /src/modules/wired.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::netlink::{self, WiredState}; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const TICK_RATE: Duration = Duration::from_millis(1000); 18 | const INTERFACE: &str = "enp0s31f6"; 19 | const DISCRETE: bool = false; 20 | const LABEL: &str = "eth"; 21 | const DISCONNECTED_LABEL: &str = ".et"; 22 | const FORMAT: &str = "%l"; 23 | 24 | #[derive(Debug, Serialize, Deserialize, Clone)] 25 | pub struct Config { 26 | tick: Option, 27 | interface: Option, 28 | discrete: Option, 29 | placeholder: Option, 30 | label: Option, 31 | disconnected_label: Option, 32 | format: Option, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct InternalConfig<'a> { 37 | interface: &'a str, 38 | discrete: bool, 39 | tick: Duration, 40 | label: &'a str, 41 | disconnected_label: &'a str, 42 | } 43 | 44 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 45 | fn from(config: &'a MainConfig) -> Self { 46 | let mut tick = TICK_RATE; 47 | let mut interface = INTERFACE; 48 | let mut discrete = DISCRETE; 49 | let mut label = LABEL; 50 | let mut disconnected_label = DISCONNECTED_LABEL; 51 | if let Some(c) = &config.wired { 52 | if let Some(t) = c.tick { 53 | tick = Duration::from_millis(t as u64) 54 | } 55 | if let Some(i) = &c.interface { 56 | interface = i 57 | } 58 | if let Some(b) = c.discrete { 59 | discrete = b; 60 | } 61 | if let Some(v) = &c.label { 62 | label = v; 63 | } 64 | if let Some(v) = &c.disconnected_label { 65 | disconnected_label = v; 66 | } 67 | }; 68 | InternalConfig { 69 | interface, 70 | discrete, 71 | tick, 72 | label, 73 | disconnected_label, 74 | } 75 | } 76 | } 77 | 78 | #[derive(Debug)] 79 | pub struct Wired<'a> { 80 | placeholder: &'a str, 81 | format: &'a str, 82 | } 83 | 84 | impl<'a> Wired<'a> { 85 | pub fn with_config(config: &'a MainConfig) -> Self { 86 | let mut placeholder = PLACEHOLDER; 87 | let mut format = FORMAT; 88 | if let Some(c) = &config.wired { 89 | if let Some(p) = &c.placeholder { 90 | placeholder = p 91 | } 92 | if let Some(v) = &c.format { 93 | format = v; 94 | } 95 | } 96 | Wired { 97 | placeholder, 98 | format, 99 | } 100 | } 101 | } 102 | 103 | impl<'a> Bar for Wired<'a> { 104 | fn name(&self) -> &str { 105 | "wired" 106 | } 107 | 108 | fn run_fn(&self) -> RunPtr { 109 | run 110 | } 111 | 112 | fn placeholder(&self) -> &str { 113 | self.placeholder 114 | } 115 | 116 | fn format(&self) -> &str { 117 | self.format 118 | } 119 | } 120 | 121 | #[instrument(skip_all)] 122 | pub fn run( 123 | running: &AtomicBool, 124 | key: char, 125 | main_config: MainConfig, 126 | tx: Sender, 127 | ) -> Result<(), Error> { 128 | let config = InternalConfig::from(&main_config); 129 | debug!("{:#?}", config); 130 | let mut iteration_start: Instant; 131 | let mut iteration_end: Duration; 132 | while running.load(Ordering::Relaxed) { 133 | iteration_start = Instant::now(); 134 | if let Some(state) = netlink::wired_data(config.interface) { 135 | if let WiredState::Connected = state { 136 | tx.send(ModuleMsg(key, None, Some(config.label.to_string())))?; 137 | } else if config.discrete { 138 | tx.send(ModuleMsg(key, None, None))?; 139 | } else { 140 | tx.send(ModuleMsg( 141 | key, 142 | None, 143 | Some(config.disconnected_label.to_string()), 144 | ))?; 145 | } 146 | } 147 | iteration_end = iteration_start.elapsed(); 148 | if iteration_end < config.tick { 149 | thread::sleep(config.tick - iteration_end); 150 | } 151 | } 152 | Ok(()) 153 | } 154 | -------------------------------------------------------------------------------- /src/modules/wireless.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::module::{Bar, RunPtr}; 7 | use crate::netlink::{self, WirelessState}; 8 | use crate::{Config as MainConfig, ModuleMsg}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::mpsc::Sender; 12 | use std::thread; 13 | use std::time::{Duration, Instant}; 14 | use tracing::{debug, instrument}; 15 | 16 | const PLACEHOLDER: &str = "-"; 17 | const TICK_RATE: Duration = Duration::from_millis(500); 18 | const DISPLAY: Display = Display::Signal; 19 | const MAX_ESSID_LEN: usize = 10; 20 | const INTERFACE: &str = "wlan0"; 21 | const LABEL: &str = "wle"; 22 | const DISCONNECTED_LABEL: &str = ".wl"; 23 | const FORMAT: &str = "%l:%v"; 24 | 25 | #[derive(Debug, Serialize, Deserialize, Copy, Clone)] 26 | enum Display { 27 | Essid, 28 | Signal, 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize, Clone)] 32 | pub struct Config { 33 | tick: Option, 34 | display: Option, 35 | max_essid_len: Option, 36 | interface: Option, 37 | placeholder: Option, 38 | label: Option, 39 | disconnected_label: Option, 40 | format: Option, 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct InternalConfig<'a> { 45 | display: Display, 46 | max_essid_len: usize, 47 | interface: &'a str, 48 | tick: Duration, 49 | label: &'a str, 50 | disconnected_label: &'a str, 51 | } 52 | 53 | impl<'a> From<&'a MainConfig> for InternalConfig<'a> { 54 | fn from(config: &'a MainConfig) -> Self { 55 | let mut tick = TICK_RATE; 56 | let mut display = DISPLAY; 57 | let mut max_essid_len = MAX_ESSID_LEN; 58 | let mut interface = INTERFACE; 59 | let mut label = LABEL; 60 | let mut disconnected_label = DISCONNECTED_LABEL; 61 | if let Some(c) = &config.wireless { 62 | if let Some(t) = c.tick { 63 | tick = Duration::from_millis(t as u64) 64 | } 65 | if let Some(d) = &c.display { 66 | display = *d 67 | } 68 | if let Some(m) = c.max_essid_len { 69 | max_essid_len = m 70 | } 71 | if let Some(i) = &c.interface { 72 | interface = i 73 | } 74 | if let Some(v) = &c.label { 75 | label = v; 76 | } 77 | if let Some(v) = &c.disconnected_label { 78 | disconnected_label = v; 79 | } 80 | }; 81 | InternalConfig { 82 | display, 83 | max_essid_len, 84 | interface, 85 | tick, 86 | label, 87 | disconnected_label, 88 | } 89 | } 90 | } 91 | 92 | #[derive(Debug)] 93 | pub struct Wireless<'a> { 94 | placeholder: &'a str, 95 | format: &'a str, 96 | } 97 | 98 | impl<'a> Wireless<'a> { 99 | pub fn with_config(config: &'a MainConfig) -> Self { 100 | let mut placeholder = PLACEHOLDER; 101 | let mut format = FORMAT; 102 | if let Some(c) = &config.wireless { 103 | if let Some(p) = &c.placeholder { 104 | placeholder = p 105 | } 106 | if let Some(v) = &c.format { 107 | format = v; 108 | } 109 | } 110 | Wireless { 111 | placeholder, 112 | format, 113 | } 114 | } 115 | } 116 | 117 | impl<'a> Bar for Wireless<'a> { 118 | fn name(&self) -> &str { 119 | "wireless" 120 | } 121 | 122 | fn run_fn(&self) -> RunPtr { 123 | run 124 | } 125 | 126 | fn placeholder(&self) -> &str { 127 | self.placeholder 128 | } 129 | 130 | fn format(&self) -> &str { 131 | self.format 132 | } 133 | } 134 | 135 | #[instrument(skip_all)] 136 | pub fn run( 137 | running: &AtomicBool, 138 | key: char, 139 | main_config: MainConfig, 140 | tx: Sender, 141 | ) -> Result<(), Error> { 142 | let config = InternalConfig::from(&main_config); 143 | debug!("{:#?}", config); 144 | let mut iteration_start: Instant; 145 | let mut iteration_end: Duration; 146 | while running.load(Ordering::Relaxed) { 147 | iteration_start = Instant::now(); 148 | let label; 149 | let mut essid = "".to_owned(); 150 | let mut signal = None; 151 | if let Some(state) = netlink::wireless_data(config.interface) { 152 | if let WirelessState::Connected(data) = state { 153 | label = config.label; 154 | if let Some(strength) = data.signal { 155 | signal = Some(strength); 156 | }; 157 | if let Some(val) = data.essid { 158 | essid = if val.chars().count() > config.max_essid_len { 159 | val[..config.max_essid_len].to_owned() 160 | } else { 161 | val 162 | } 163 | } 164 | } else { 165 | label = config.disconnected_label; 166 | } 167 | match config.display { 168 | Display::Essid => tx.send(ModuleMsg(key, Some(essid), Some(label.to_string())))?, 169 | Display::Signal => { 170 | if let Some(s) = signal { 171 | tx.send(ModuleMsg( 172 | key, 173 | Some(format!("{:3}%", s)), 174 | Some(label.to_string()), 175 | ))?; 176 | } else { 177 | tx.send(ModuleMsg( 178 | key, 179 | Some(" ?%".to_string()), 180 | Some(label.to_string()), 181 | ))?; 182 | } 183 | } 184 | } 185 | } 186 | iteration_end = iteration_start.elapsed(); 187 | if iteration_end < config.tick { 188 | thread::sleep(config.tick - iteration_end); 189 | } 190 | } 191 | Ok(()) 192 | } 193 | -------------------------------------------------------------------------------- /src/netlink.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ffi::{c_void, CStr, CString}; 6 | use std::os::raw::{c_char, c_int}; 7 | 8 | #[derive(Debug)] 9 | #[repr(C)] 10 | pub struct NlWirelessData { 11 | essid: *const c_char, 12 | signal: c_int, 13 | } 14 | 15 | #[derive(Debug)] 16 | #[repr(C)] 17 | pub struct NlWiredData { 18 | is_carrying: bool, 19 | is_operational: bool, 20 | has_ip: bool, 21 | } 22 | 23 | pub enum WirelessState { 24 | Disconnected, 25 | Connected(WirelessData), 26 | } 27 | 28 | pub enum WiredState { 29 | Disconnected, 30 | NotPlugged, 31 | Connected, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct WirelessData { 36 | pub essid: Option, 37 | pub signal: Option, 38 | } 39 | 40 | #[link(name = "netlink", kind = "static")] 41 | extern "C" { 42 | fn get_wireless_data(interface: *const c_char) -> *const NlWirelessData; 43 | fn get_wired_data(interface: *const c_char) -> *const NlWiredData; 44 | fn free_data(data: *const c_void); 45 | } 46 | 47 | pub fn wireless_data(interface: &str) -> Option { 48 | let c_interface = CString::new(interface).expect("CString::new failed"); 49 | unsafe { 50 | let nl_data = get_wireless_data(c_interface.as_ptr()); 51 | if nl_data.is_null() { 52 | return None; 53 | } 54 | let signal_ptr = (*nl_data).signal; 55 | let essid_ptr = (*nl_data).essid; 56 | let signal = if signal_ptr == -1 { 57 | None 58 | } else { 59 | Some(signal_ptr) 60 | }; 61 | let mut essid = None; 62 | if !essid_ptr.is_null() { 63 | essid = Some(CStr::from_ptr(essid_ptr).to_string_lossy().into_owned()); 64 | free_data(essid_ptr.cast()); 65 | }; 66 | free_data(nl_data.cast()); 67 | if signal.is_none() && essid.is_none() { 68 | Some(WirelessState::Disconnected) 69 | } else { 70 | Some(WirelessState::Connected(WirelessData { signal, essid })) 71 | } 72 | } 73 | } 74 | 75 | pub fn wired_data(interface: &str) -> Option { 76 | let c_interface = CString::new(interface).expect("CString::new failed"); 77 | unsafe { 78 | let data = get_wired_data(c_interface.as_ptr()); 79 | if data.is_null() { 80 | return None; 81 | } 82 | let is_op = (*data).is_operational; 83 | let is_carrying = (*data).is_carrying; 84 | let has_ip = (*data).has_ip; 85 | free_data(data.cast()); 86 | if is_carrying && is_op && has_ip { 87 | Some(WiredState::Connected) 88 | } else if is_carrying { 89 | Some(WiredState::Disconnected) 90 | } else { 91 | Some(WiredState::NotPlugged) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/pulse.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::error::Error; 6 | use crate::Config; 7 | use anyhow::Result; 8 | use once_cell::sync::OnceCell; 9 | use std::os::raw::c_char; 10 | use std::sync::mpsc::{self, Receiver, Sender}; 11 | use std::sync::Mutex; 12 | use std::thread::{self, JoinHandle}; 13 | use std::{ffi::CString, ptr}; 14 | use tracing::{error, info, instrument, warn}; 15 | 16 | const PULSE_RATE: u32 = 50_000_000; // in nanosecond 17 | 18 | pub type Callback = extern "C" fn(*const CallbackContext, u32, bool); 19 | 20 | #[derive(Copy, Clone, Debug)] 21 | #[repr(C)] 22 | pub struct PulseData(pub u32, pub bool); 23 | 24 | #[repr(C)] 25 | pub struct CallbackContext(Sender, Sender); 26 | 27 | /// 0: sink, 1: source 28 | pub struct Pulse(Receiver, Receiver); 29 | 30 | pub static PULSE: OnceCell> = OnceCell::new(); 31 | 32 | #[instrument(skip_all)] 33 | pub fn init(config: &Config) -> Result>> { 34 | let (pulse, handle) = Pulse::new(config).inspect_err(|e| { 35 | error!("error pulse module: {}", e); 36 | })?; 37 | 38 | info!("initializing pulse module"); 39 | PULSE 40 | .set(Mutex::new(pulse)) 41 | .inspect_err(|_| { 42 | warn!("error initializing pulse module: already initialized"); 43 | }) 44 | .ok(); 45 | Ok(handle) 46 | } 47 | 48 | impl Pulse { 49 | #[instrument(skip_all)] 50 | pub fn new(config: &Config) -> Result<(Self, JoinHandle>), Error> { 51 | let (sink_tx, sink_rx) = mpsc::channel(); 52 | let (source_tx, source_rx) = mpsc::channel(); 53 | let tick = match config.pulse_tick { 54 | Some(val) => val * 1e6 as u32, 55 | None => PULSE_RATE, 56 | }; 57 | let mut sink_name = None; 58 | let mut source_name = None; 59 | if let Some(c) = &config.sound { 60 | sink_name = c.sink_name.clone(); 61 | } 62 | if let Some(c) = &config.mic { 63 | source_name = c.source_name.clone(); 64 | } 65 | let builder = thread::Builder::new().name("pulse".into()); 66 | let handle = builder.spawn(move || -> Result<(), Error> { 67 | let cb_context = CallbackContext(sink_tx, source_tx); 68 | pulse_run( 69 | tick, 70 | sink_name, 71 | source_name, 72 | &cb_context, 73 | sink_cb, 74 | source_cb, 75 | ); 76 | info!("pulse module stopped"); 77 | Ok(()) 78 | })?; 79 | Ok((Pulse(sink_rx, source_rx), handle)) 80 | } 81 | 82 | pub fn sink_data(&self) -> Option { 83 | self.0.try_iter().last() 84 | } 85 | 86 | pub fn source_data(&self) -> Option { 87 | self.1.try_iter().last() 88 | } 89 | } 90 | 91 | extern "C" fn sink_cb(context: *const CallbackContext, volume: u32, mute: bool) { 92 | unsafe { 93 | (*context) 94 | .0 95 | .send(PulseData(volume, mute)) 96 | .expect("in pulse module, failed to send sink data"); 97 | } 98 | } 99 | 100 | extern "C" fn source_cb(context: *const CallbackContext, volume: u32, mute: bool) { 101 | unsafe { 102 | (*context) 103 | .1 104 | .send(PulseData(volume, mute)) 105 | .expect("in pulse module, failed to send source data"); 106 | } 107 | } 108 | 109 | #[link(name = "audio", kind = "static")] 110 | extern "C" { 111 | fn run( 112 | run: *const bool, 113 | tick: u32, 114 | sink_name: *const c_char, 115 | source_name: *const c_char, 116 | cb_context: *const CallbackContext, 117 | sink_cb: Callback, 118 | source_cb: Callback, 119 | ); 120 | } 121 | 122 | pub fn pulse_run( 123 | tick: u32, 124 | sink_name: Option, 125 | source_name: Option, 126 | callback_context: &CallbackContext, 127 | sink_cb: Callback, 128 | source_cb: Callback, 129 | ) { 130 | let context_ptr: *const CallbackContext = callback_context; 131 | let mut ptr_sink = ptr::null(); 132 | let mut ptr_source = ptr::null(); 133 | let c_string_sink; 134 | let c_string_source; 135 | if let Some(s) = sink_name { 136 | c_string_sink = CString::new(s).expect("CString::new failed"); 137 | ptr_sink = c_string_sink.as_ptr(); 138 | }; 139 | if let Some(s) = source_name { 140 | c_string_source = CString::new(s).expect("CString::new failed"); 141 | ptr_source = c_string_source.as_ptr(); 142 | }; 143 | 144 | unsafe { 145 | run( 146 | super::RUN.as_ptr(), 147 | tick, 148 | ptr_sink, 149 | ptr_source, 150 | context_ptr, 151 | sink_cb, 152 | source_cb, 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/signal.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use crate::RUN; 6 | 7 | use anyhow::Result; 8 | use signal_hook::consts::{SIGINT, SIGQUIT, SIGTERM}; 9 | use signal_hook::iterator::Signals; 10 | use std::process::exit; 11 | use std::sync::atomic::Ordering; 12 | use std::thread; 13 | use std::time::Duration; 14 | use tracing::info; 15 | 16 | const SIGNALS: [i32; 3] = [SIGINT, SIGTERM, SIGQUIT]; 17 | const EXIT_TIMEOUT: Duration = Duration::from_millis(500); 18 | 19 | pub fn catch_signals() -> Result<()> { 20 | let mut signals = Signals::new(SIGNALS)?; 21 | let builder = thread::Builder::new().name("signal_handler".into()); 22 | 23 | builder.spawn(move || { 24 | if let Some(sig) = signals.forever().next() { 25 | match sig { 26 | SIGINT => info!("received {sig}:SIGINT"), 27 | SIGTERM => info!("received {sig}:SIGTERM"), 28 | SIGQUIT => info!("received {sig}:SIGQUIT"), 29 | _ => {} 30 | } 31 | RUN.store(false, Ordering::Relaxed); 32 | // wait for the main app thread to close and exit 33 | // if it takes too long force exit 34 | thread::sleep(EXIT_TIMEOUT); 35 | info!("force exit"); 36 | exit(0); 37 | } 38 | })?; 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/trace.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::path::{Path, PathBuf}; 6 | use std::{env, fs}; 7 | 8 | use anyhow::Result; 9 | use tracing_appender::{non_blocking::WorkerGuard, rolling}; 10 | use tracing_subscriber::filter::LevelFilter; 11 | use tracing_subscriber::EnvFilter; 12 | 13 | use crate::cli::Logs; 14 | use crate::util; 15 | 16 | const XDG_CACHE_HOME: &str = "XDG_CACHE_HOME"; 17 | const LOG_DIR: &str = "baru"; 18 | const LOG_FILE: &str = "baru.log"; 19 | const LOG_FILE_OLD: &str = "baru.old.log"; 20 | 21 | fn rotate_log_file(log_dir: PathBuf) -> Result<()> { 22 | let log_file = log_dir.join(LOG_FILE); 23 | if log_file.is_file() { 24 | let old_file = log_dir.join(LOG_FILE_OLD); 25 | fs::rename(&log_file, &old_file).inspect_err(|e| { 26 | eprintln!( 27 | "failed to rename log file during log rotation {}: {e}", 28 | log_file.display() 29 | ) 30 | })?; 31 | } 32 | Ok(()) 33 | } 34 | 35 | pub fn init(logs: Option) -> Result> { 36 | let Some(l) = logs else { 37 | return Ok(None); 38 | }; 39 | 40 | let filter = EnvFilter::builder() 41 | .with_default_directive(LevelFilter::INFO.into()) 42 | .from_env() 43 | .unwrap(); 44 | 45 | match l { 46 | Logs::Stdout => { 47 | tracing_subscriber::fmt() 48 | .with_env_filter(filter) 49 | .compact() 50 | .init(); 51 | Ok(None) 52 | } 53 | Logs::File => { 54 | let home = env::var("HOME")?; 55 | let cache_dir = env::var(XDG_CACHE_HOME) 56 | .map(PathBuf::from) 57 | .unwrap_or_else(|_| Path::new(&home).join(".cache")); 58 | let log_dir = cache_dir.join(LOG_DIR); 59 | util::check_dir(&log_dir)?; 60 | rotate_log_file(log_dir.clone()).ok(); 61 | 62 | let appender = rolling::never(log_dir, LOG_FILE); 63 | let (writer, guard) = tracing_appender::non_blocking(appender); 64 | 65 | tracing_subscriber::fmt() 66 | .with_env_filter(filter) 67 | .compact() 68 | .with_ansi(false) 69 | .with_writer(writer) 70 | .init(); 71 | Ok(Some(guard)) 72 | } 73 | _ => Ok(None), 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use anyhow::{Context, Result}; 6 | use std::{fs, path::PathBuf}; 7 | use tracing::{debug, error}; 8 | 9 | use crate::error::Error; 10 | 11 | /// Check if a directory exists, if not create it including all 12 | /// parent components 13 | pub fn check_dir(path: &PathBuf) -> Result<()> { 14 | if !path.is_dir() { 15 | debug!("directory `{}` does not exist, creating it", path.display()); 16 | return fs::create_dir_all(path) 17 | .inspect_err(|e| error!("Failed to create directory `{}`: {e}", path.display())) 18 | .context(format!("Failed to create directory `{}`", path.display())); 19 | } 20 | Ok(()) 21 | } 22 | 23 | pub fn read_and_trim(file: &str) -> Result { 24 | let content = fs::read_to_string(file) 25 | .inspect_err(|e| error!("failed to read the file `{}`: {}", file, e))?; 26 | Ok(content.trim().to_string()) 27 | } 28 | 29 | pub fn read_and_parse(file: &str) -> Result { 30 | let content = read_and_trim(file)?; 31 | let data = content 32 | .parse::() 33 | .inspect_err(|e| error!("failed to parse the file `{}`: {}", file, e))?; 34 | Ok(data) 35 | } 36 | --------------------------------------------------------------------------------