├── .github └── workflows │ ├── publish-wiki.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── 3270 │ ├── 3270NerdFont-Condensed.ttf │ ├── 3270NerdFont-Regular.ttf │ ├── 3270NerdFont-SemiCondensed.ttf │ ├── 3270NerdFontMono-Condensed.ttf │ ├── 3270NerdFontMono-Regular.ttf │ ├── 3270NerdFontMono-SemiCondensed.ttf │ ├── 3270NerdFontPropo-Condensed.ttf │ ├── 3270NerdFontPropo-Regular.ttf │ ├── 3270NerdFontPropo-SemiCondensed.ttf │ ├── LICENSE.txt │ └── README.md ├── bar-rs ├── crates └── bar-rs_derive │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── lib.rs ├── default_config ├── horizontal.ini └── vertical.ini ├── install.sh ├── src ├── button.rs ├── config │ ├── anchor.rs │ ├── enabled_modules.rs │ ├── insets.rs │ ├── mod.rs │ ├── module_config.rs │ ├── parse.rs │ ├── popup_config.rs │ └── thrice.rs ├── event_action.rs ├── fill.rs ├── helpers │ └── mod.rs ├── list.rs ├── listeners │ ├── hyprland.rs │ ├── mod.rs │ ├── niri.rs │ ├── reload.rs │ └── wayfire.rs ├── main.rs ├── modules │ ├── battery.rs │ ├── cpu.rs │ ├── date.rs │ ├── disk_usage.rs │ ├── hyprland │ │ ├── mod.rs │ │ ├── window.rs │ │ └── workspaces.rs │ ├── media.rs │ ├── memory.rs │ ├── mod.rs │ ├── niri │ │ ├── mod.rs │ │ ├── window.rs │ │ └── workspaces.rs │ ├── sys_tray.rs │ ├── time.rs │ ├── volume.rs │ └── wayfire │ │ ├── mod.rs │ │ ├── window.rs │ │ └── workspaces.rs ├── registry.rs ├── resolvers.rs └── tooltip.rs └── wiki ├── Home.md ├── Modules.md ├── Modules:-Battery.md ├── Modules:-CPU.md ├── Modules:-Date-and-Time.md ├── Modules:-Disk-usage.md ├── Modules:-Hyprland.md ├── Modules:-Media.md ├── Modules:-Memory.md ├── Modules:-Niri.md ├── Modules:-Volume.md ├── Modules:-Wayfire.md └── Popups.md /.github/workflows/publish-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish wiki 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - wiki/** 7 | - .github/workflows/publish-wiki.yml 8 | concurrency: 9 | group: publish-wiki 10 | cancel-in-progress: true 11 | permissions: 12 | contents: write 13 | jobs: 14 | publish-wiki: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: Andrew-Chen-Wang/github-wiki-action@v4 19 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | toolchain: 20 | - stable 21 | - beta 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install libudev 25 | run: sudo apt-get install -y libudev-dev librust-xkbcommon-sys-dev 26 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 27 | - run: cargo build --verbose 28 | 29 | test: 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | toolchain: 34 | - stable 35 | - beta 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Install libudev 39 | run: sudo apt-get install -y libudev-dev librust-xkbcommon-sys-dev 40 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 41 | - run: cargo test --verbose 42 | 43 | clippy: 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | toolchain: 48 | - stable 49 | - beta 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Install libudev 53 | run: sudo apt-get install -y libudev-dev librust-xkbcommon-sys-dev 54 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 55 | - run: rustup component add clippy 56 | - run: cargo clippy --verbose 57 | 58 | rustfmt: 59 | runs-on: ubuntu-latest 60 | strategy: 61 | matrix: 62 | toolchain: 63 | - stable 64 | - beta 65 | steps: 66 | - uses: actions/checkout@v4 67 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 68 | - run: rustup component add rustfmt 69 | - run: cargo fmt --check --verbose 70 | 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bar-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | chrono = "0.4.39" 8 | configparser = "3.1.0" 9 | ctrlc = "3.4.5" 10 | directories = "5.0.1" 11 | hyprland = { git = "https://github.com/hyprland-community/hyprland-rs", branch = "master" } 12 | #iced = { git = "https://github.com/pop-os/iced.git", branch = "master", features = [ 13 | # "tokio", 14 | # "wayland", 15 | # "winit" 16 | #] } 17 | iced = { git = "https://github.com/Faervan/iced_pop-os.git", branch = "master", features = [ 18 | "tokio", 19 | "wayland", 20 | "winit", 21 | "image" 22 | ] } 23 | notify = "7.0.0" 24 | system-tray = "0.5.0" 25 | tokio = { version = "1.42.0", features = ["io-util", "macros", "process", "sync"] } 26 | udev = { version = "0.9.1", features = ["mio"] } 27 | bar-rs_derive = { path = "crates/bar-rs_derive"} 28 | downcast-rs = "1.2.1" 29 | csscolorparser = "0.7.0" 30 | wayfire-rs = "0.2.2" 31 | serde_json = "1.0.135" 32 | niri-ipc = "=0.1.10" 33 | handlebars = "6.3.0" 34 | serde = { version = "1.0.217", features = ["derive"] } 35 | reqwest = "0.12.12" 36 | libc = "0.2.169" 37 | 38 | [profile.dev.package."*"] 39 | opt-level = 3 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bar-rs 2 | 3 | 4 | 5 | 6 | A simple status bar, written using [iced-rs](https://github.com/iced-rs/iced/) (specifically the [pop-os fork](https://github.com/pop-os/iced/) of iced, which supports the [wlr layer shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1)) 7 | 8 | > [!Note] 9 | > `bar-rs` is currently undergoing a full rewrite, see [#24](https://github.com/Faervan/bar-rs/issues/24) for the reasons. 10 | > Progress is a bit slow since I am working on some other, smaller projects which I will complete first. 11 | 12 | ![image](https://github.com/user-attachments/assets/c62d8399-0f80-4c3b-8cb8-a325db13fc32) 13 | 14 | ![image](https://github.com/user-attachments/assets/d71b0fc2-a9fb-43e9-b358-9b1f2cb3d487) 15 | 16 | ![image](https://github.com/user-attachments/assets/d0073653-01ed-4084-9c33-0d161cd98ec7) 17 | 18 | 19 | 20 | Currently bar-rs supports only a small amount of configuration. It works on Wayland compositors implementing the [wlr layer shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1#compositor-support), but right now only features [Hyprland](https://github.com/hyprwm/Hyprland/), [Niri](https://github.com/YaLTeR/niri/) and [Wayfire](https://github.com/WayfireWM/wayfire/) modules for active workspace and window display. 21 | 22 | For a list of all currently supported modules, see [the Wiki](https://github.com/Faervan/bar-rs/wiki#modules) 23 | 24 | ## Features 25 | - [x] Dynamic module activation/ordering 26 | - [x] Hot config reloading 27 | - [x] very basic style customization 28 | - [x] basic vertical bar support 29 | - [x] a base set of useful modules 30 | - [x] Module interactivity (popups, buttons) 31 | - [x] hyprland workspace + window modules 32 | - [x] wayfire workspace + window modules 33 | - [x] niri workspace + window modules 34 | - [ ] sway workspace + window modules 35 | - [ ] custom modules 36 | - [ ] additional modules (wifi, pacman updates...) 37 | - [ ] system tray support 38 | - [ ] plugin api (for custom rust modules) 39 | - [ ] custom fonts 40 | - [ ] X11 support 41 | - ... 42 | 43 | ## Installation 44 | I aim for a release on the `AUR` after the [first milestone](https://github.com/Faervan/bar-rs/milestone/1) is reached. For now, you have to build bar-rs yourself. 45 | 46 |
47 |

Building

48 | 49 | To use bar-rs you have to build the project yourself (very straight forward on an up-to-date system like Arch, harder on "stable" ones like Debian due to outdated system libraries) 50 | 51 | ```sh 52 | # Clone the project 53 | git clone https://github.com/faervan/bar-rs.git 54 | cd bar-rs 55 | 56 | # Build the project - This might take a while 57 | cargo build --release 58 | 59 | # Install the bar-rs helper script to easily launch and kill bar-rs 60 | bash install.sh 61 | 62 | # Optional: Clean unneeded build files afterwards: 63 | find target/release/* ! -name bar-rs ! -name . -type d,f -exec rm -r {} + 64 | ``` 65 |
66 | 67 |
68 |

Updating

69 | 70 | Enter the project directory again. 71 | 72 | ```sh 73 | # Update the project 74 | git pull 75 | 76 | # Build the project - This will be considerably faster if you didn't clean the build files after installing 77 | cargo build --release 78 | 79 | # Optional: Clean unneeded build files afterwards: 80 | find target/release/* ! -name bar-rs ! -name . -type d,f -exec rm -r {} + 81 | ``` 82 |
83 | 84 |
85 |

Extra dependencies

86 | 87 | bar-rs depends on the following cli utilities: 88 | - free 89 | - grep 90 | - awk 91 | - printf 92 | - pactl 93 | - wpctl 94 | - playerctl 95 |
96 | 97 |
98 |

Usage

99 | 100 | Launch bar-rs using the `bar-rs` script (after installing it using the `install.sh` script): 101 | ```sh 102 | bar-rs open 103 | ``` 104 | 105 | Alternatively, you may launch bar-rs directly: 106 | 107 | ```sh 108 | ./target/release/bar-rs 109 | # or using cargo: 110 | cargo run --release 111 | ``` 112 |
113 | 114 | ## Configuration 115 | Example configurations can be found in [default_config](https://github.com/Faervan/bar-rs/tree/main/default_config).
116 | See [the Wiki](https://github.com/Faervan/bar-rs/wiki) for more. 117 | 118 | ## Logs 119 | If bar-rs is launched via the `bar-rs` script, it's logs are saved to `/tmp/bar-rs.log` and should only contain anything if there is an error. 120 | If an error occurs and all dependencies are installed on your system, please feel free to open an [issue](https://github.com/faervan/bar-rs/issues) 121 | 122 | ## Recommendations + feature requests 123 | If you have an idea on what could improve bar-rs, or you would like to see a specific feature implemented, please open an [issue](https://github.com/faervan/bar-rs/issues). 124 | 125 | ## Contributing 126 | If you want to contribute, create an [issue](https://github.com/faervan/bar-rs/issues) about the feature you'd like to implement or comment on an existing one. You may also contact me on [matrix](https://matrix.to/#/@faervan:matrix.org) or [discord](https://discord.com/users/738658712620630076). 127 | 128 | Contributing by creating new modules should be pretty easy and straight forward if you know a bit about rust. You just have to implement the `Module` and `Builder` traits for your new module and register it in `src/modules/mod.rs`.
129 | Take a look at [docs.iced.rs](https://docs.iced.rs/iced/) for info about what to place in the `view()` method of the `Module` trait. 130 | 131 | ## Extra credits 132 | Next to all the great crates this projects depends on (see `Cargo.toml`) and the cli utils listed in [Extra dependencies](#extra-dependencies), bar-rs also uses [NerdFont](https://www.nerdfonts.com/) (see `assets/3270`) 133 | -------------------------------------------------------------------------------- /assets/3270/3270NerdFont-Condensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFont-Condensed.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFont-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFont-Regular.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFont-SemiCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFont-SemiCondensed.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontMono-Condensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontMono-Condensed.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontMono-Regular.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontMono-SemiCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontMono-SemiCondensed.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontPropo-Condensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontPropo-Condensed.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontPropo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontPropo-Regular.ttf -------------------------------------------------------------------------------- /assets/3270/3270NerdFontPropo-SemiCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faervan/bar-rs/3bafbe05fee87d84366dbcb5ae305c7b6fafd5ba/assets/3270/3270NerdFontPropo-SemiCondensed.ttf -------------------------------------------------------------------------------- /assets/3270/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 The 3270font Authors (https://github.com/rbanffy/3270font) 2 | 3 | Copyright (c) 2011-2022, Ricardo Banffy. 4 | Copyright (c) 1993-2011, Paul Mattes. 5 | Copyright (c) 2004-2005, Don Russell. 6 | Copyright (c) 2004, Dick Altenbern. 7 | Copyright (c) 1990, Jeff Sparkes. 8 | Copyright (c) 1989, Georgia Tech Research Corporation (GTRC), Atlanta, GA 30332. 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are 13 | met: 14 | 15 | * Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | * Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimer in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | * Neither the name of Ricardo Banffy, Paul Mattes, Don Russell, 23 | Dick Altenbern, Jeff Sparkes, GTRC nor the names of their contributors 24 | may be used to endorse or promote products derived from this software 25 | without specific prior written permission. 26 | 27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 28 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 29 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 30 | IN NO EVENT SHALL RICARDO BANFFY, PAUL MATTES, DON RUSSELL, DICK ALTENBERN, JEFF 31 | SPARKES OR GTRC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 32 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 33 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 35 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 36 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | 38 | The Debian Logo glyph is based on the Debian Open Use Logo and is 39 | Copyright (c) 1999 Software in the Public Interest, Inc., and it is 40 | incorporated here under the terms of the Creative Commons 41 | Attribution-ShareAlike 3.0 Unported License. The logo is released 42 | under the terms of the GNU Lesser General Public License, version 3 or 43 | any later version, or, at your option, of the Creative Commons 44 | Attribution-ShareAlike 3.0 Unported License. 45 | 46 | Ubuntu, the Ubuntu logo and the Circle of Friends symbol are 47 | registered trademarks of Canonical Ltd. 48 | 49 | The Fontforge SFD font description file is optionally licensed under 50 | the SIL Open Font License v1.1 with no Reserved Font Name. This 51 | license is available with a FAQ at http://scripts.sil.org/OFL. 52 | -------------------------------------------------------------------------------- /assets/3270/README.md: -------------------------------------------------------------------------------- 1 | # Nerd Fonts 2 | 3 | This is an archived font from the Nerd Fonts release v3.1.1. 4 | 5 | For more information see: 6 | * https://github.com/ryanoasis/nerd-fonts/ 7 | * https://github.com/ryanoasis/nerd-fonts/releases/latest/ 8 | 9 | # IBM 3270 10 | 11 | A 3270 font in a modern format. 12 | 13 | For more information have a look at the upstream website: https://github.com/rbanffy/3270font 14 | 15 | Version: 3.0.1 16 | 17 | ## Which font? 18 | 19 | ### TL;DR 20 | 21 | * Pick your font family: 22 | * If you are limited to monospaced fonts (because of your terminal, etc) then pick a font with `Nerd Font Mono` (or `NFM`). 23 | * If you want to have bigger icons (usually around 1.5 normal letters wide) pick a font without `Mono` i.e. `Nerd Font` (or `NF`). Most terminals support this, but ymmv. 24 | * If you work in a proportional context (GUI elements or edit a presentation etc) pick a font with `Nerd Font Propo` (or `NFP`). 25 | 26 | ### Ligatures 27 | 28 | Ligatures are generally preserved in the patched fonts. 29 | Nerd Fonts `v2.0.0` had no ligatures in the `Nerd Font Mono` fonts, this has been dropped with `v2.1.0`. 30 | If you have a ligature-aware terminal and don't want ligatures you can (usually) disable them in the terminal settings. 31 | 32 | ### Explanation 33 | 34 | Once you narrow down your font choice of family (`Droid Sans`, `Inconsolata`, etc) and style (`bold`, `italic`, etc) you have 2 main choices: 35 | 36 | #### `Option 1: Download already patched font` 37 | 38 | * For a stable version download a font package from the [release page](https://github.com/ryanoasis/nerd-fonts/releases) 39 | * Or download the development version from the folders here 40 | 41 | #### `Option 2: Patch your own font` 42 | 43 | * Patch your own variations with the various options provided by the font patcher (i.e. not include all symbols for smaller font size) 44 | 45 | For more information see: [The FAQ](https://github.com/ryanoasis/nerd-fonts/wiki/FAQ-and-Troubleshooting#which-font) 46 | 47 | [SIL-RFN]:http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web_fonts_and_RFNs#14cbfd4a 48 | 49 | -------------------------------------------------------------------------------- /bar-rs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | project_path="" 4 | pid_file="/tmp/bar-rs.pid" 5 | log_file="/tmp/bar-rs.log" 6 | 7 | open() { 8 | target="release" 9 | for arg in "$@"; do 10 | if [ "$arg" == "--debug" ]; then 11 | target="debug" 12 | break 13 | fi 14 | done 15 | 16 | if [ -f "$pid_file" ] && pid=$(<"$pid_file") && kill -0 "$pid" 2>/dev/null; then 17 | echo -e "bar-rs is already running with PID $pid,\nrun \`bar-rs kill\` to close it" 18 | exit 1 19 | fi 20 | 21 | cmd="$project_path/target/$target/bar-rs" 22 | 23 | if [ ! -f "$cmd" ]; then 24 | echo -e "$cmd does not exist, make sure to build bar-rs using:\n\t\`cargo build --release\`\nor reinstall this script if you've moved the project directory:\n\t\`bash install.sh\`" 25 | exit 1 26 | fi 27 | 28 | RUST_BACKTRACE=full nohup $cmd > $log_file 2>&1 & 29 | echo $! > $pid_file 30 | } 31 | 32 | close() { 33 | if [ -f "$pid_file" ]; then 34 | pid=$(<"$pid_file") 35 | kill -2 $pid 36 | rm $pid_file 37 | else 38 | echo "PID file ($pid_file) does not exist. Exiting." 39 | exit 1 40 | fi 41 | } 42 | 43 | uninstall() { 44 | path=$(realpath "$0") 45 | if [ "$(dirname $path)" == "/usr/local/bin" ]; then 46 | if [ "$UID" -ne 0 -a "$EUID" -ne 0 ]; then 47 | sudo rm $path 48 | else 49 | rm $path 50 | fi 51 | else 52 | echo This script is not installed to /usr/local/bin, you should remove it manually if desired 53 | exit 1 54 | fi 55 | } 56 | 57 | # MAIN 58 | case "$1" in 59 | open) 60 | open $@ 61 | ;; 62 | kill) 63 | close 64 | ;; 65 | reopen) 66 | close 67 | open $@ 68 | ;; 69 | uninstall) 70 | uninstall 71 | ;; 72 | *) 73 | echo "bar-rs: bar-rs [open | kill | reopen | uninstall] [--debug]" 74 | ;; 75 | esac 76 | -------------------------------------------------------------------------------- /crates/bar-rs_derive/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bar-rs_derive" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "proc-macro2", 10 | "quote", 11 | "syn", 12 | ] 13 | 14 | [[package]] 15 | name = "proc-macro2" 16 | version = "1.0.92" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 19 | dependencies = [ 20 | "unicode-ident", 21 | ] 22 | 23 | [[package]] 24 | name = "quote" 25 | version = "1.0.38" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 28 | dependencies = [ 29 | "proc-macro2", 30 | ] 31 | 32 | [[package]] 33 | name = "syn" 34 | version = "2.0.94" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" 37 | dependencies = [ 38 | "proc-macro2", 39 | "quote", 40 | "unicode-ident", 41 | ] 42 | 43 | [[package]] 44 | name = "unicode-ident" 45 | version = "1.0.14" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 48 | -------------------------------------------------------------------------------- /crates/bar-rs_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bar-rs_derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | proc-macro2 = "1.0.92" 11 | quote = "1.0.38" 12 | syn = "2.0.94" 13 | -------------------------------------------------------------------------------- /crates/bar-rs_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, Data, DeriveInput, Fields}; 4 | 5 | #[proc_macro_derive(Builder)] 6 | pub fn derive_builder(input: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(input as DeriveInput); 8 | let ident = input.ident; 9 | let default = match input.data { 10 | Data::Struct(data) => match data.fields { 11 | Fields::Unit => Some(quote! {#ident}), 12 | _ => None, 13 | }, 14 | _ => None, 15 | } 16 | .unwrap_or_else(|| quote! {#ident::default()}); 17 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 18 | quote! { 19 | impl #impl_generics crate::registry::Builder for #ident #ty_generics #where_clause { 20 | type Output = Self; 21 | fn build() -> Self::Output { 22 | #default 23 | } 24 | } 25 | } 26 | .into() 27 | } 28 | -------------------------------------------------------------------------------- /default_config/horizontal.ini: -------------------------------------------------------------------------------- 1 | [general] 2 | # monitor = DP-1 3 | anchor = top 4 | hot_reloading = true 5 | # hard_reloading = true 6 | 7 | [modules] 8 | left = workspaces, window 9 | center = date, time 10 | right = media, volume, cpu, memory 11 | 12 | [style] 13 | spacing = 10 20 20 14 | padding = 0 10 15 | 16 | [module_style] 17 | font_size = 17 18 | icon_size = 20 19 | text_color = white 20 | icon_color = white 21 | 22 | [module:time] 23 | icon_size = 24 24 | 25 | [module:battery] 26 | spacing = 5 27 | 28 | [module:media] 29 | spacing = 20 30 | 31 | [module:hyprland.workspaces] 32 | active_color = black 33 | active_background = rgba(255, 255, 255, 0.6) 34 | active_padding = -2 10 -1 5 35 | spacing = 15 36 | 37 | [module:wayfire.workspaces] 38 | (0, 0) = 󰈹 39 | (1, 0) =  40 | (2, 0) = 󰓓 41 | (0, 1) =  42 | (1, 1) =  43 | fallback_icon =  44 | icon_size = 18 45 | 46 | [module:wayfire.window] 47 | max_length = 50 48 | 49 | [module:niri.workspaces] 50 | spacing = 15 51 | padding = 0 12 0 6 52 | icon_margin = -2 0 0 0 53 | icon_size = 25 54 | active_size = 25 55 | 56 | [module:niri.window] 57 | max_length = 50 58 | # show_app_id = true 59 | -------------------------------------------------------------------------------- /default_config/vertical.ini: -------------------------------------------------------------------------------- 1 | [general] 2 | # monitor = DP-1 3 | anchor = left 4 | hot_reloading = true 5 | # hard_reloading = true 6 | 7 | [style] 8 | padding = 20 5 9 | width = 65 10 | 11 | [modules] 12 | left = time, date 13 | center = workspaces 14 | right = volume, battery, cpu, memory 15 | 16 | [module:time] 17 | icon_size = 24 18 | 19 | [module:date] 20 | format = %a %d. %b 21 | 22 | [module:battery] 23 | format = {{capacity}}% 24 | 25 | [module:hyprland.workspaces] 26 | active_color = black 27 | active_background = rgba(255, 255, 255, 0.5) 28 | 29 | [module:niri.workspaces] 30 | icon_size = 24 31 | active_size = 24 32 | icon_margin = 0 0 0 20 33 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | directory=$(dirname $(realpath "$0")) 4 | 5 | sed -i "s|project_path=\"\"|project_path=\"$directory\"|" $directory/bar-rs 6 | 7 | cp_cmd="cp $directory/bar-rs /usr/local/bin" 8 | chmod_cmd="chmod +x /usr/local/bin/bar-rs" 9 | 10 | if [ "$UID" -ne 0 -a "$EUID" -ne 0 ]; then 11 | sudo $cp_cmd 12 | sudo $chmod_cmd 13 | else 14 | $cp_cmd 15 | $chmod_cmd 16 | fi 17 | 18 | sed -i "s|project_path=\"$directory\"|project_path=\"\"|" $directory/bar-rs 19 | 20 | echo -e "Uninstall bar-rs by running \`bar-rs uninstall\`\n" 21 | echo You need to build the project before you can open the bar: 22 | echo -e "\`cargo build --release\` to build for release (recommended)" 23 | echo -e "\`cargo build\` to build for debug (not recommended)" 24 | 25 | echo Done 26 | -------------------------------------------------------------------------------- /src/button.rs: -------------------------------------------------------------------------------- 1 | /// Literally 100% copypasta from https://github.com/iced-rs/iced/blob/master/widget/src/button.rs 2 | use iced::core::widget::tree; 3 | use iced::core::{keyboard, overlay, renderer, touch}; 4 | use iced::{ 5 | core::{ 6 | event, layout, mouse, 7 | widget::{Operation, Tree}, 8 | Clipboard, Layout, Shell, Widget, 9 | }, 10 | id::Id, 11 | widget::button::{Catalog, Status, Style, StyleFn}, 12 | Element, Event, Length, Padding, Rectangle, Size, 13 | }; 14 | use iced::{Background, Color, Vector}; 15 | 16 | type EventHandlerFn<'a, Message> = Box< 17 | dyn Fn( 18 | iced::Event, 19 | iced::core::Layout, 20 | iced::mouse::Cursor, 21 | &mut dyn iced::core::Clipboard, 22 | &Rectangle, 23 | ) -> Message 24 | + 'a, 25 | >; 26 | 27 | enum ButtonEventHandler<'a, Message> 28 | where 29 | Message: Clone, 30 | { 31 | Message(Message), 32 | F(EventHandlerFn<'a, Message>), 33 | FMaybe(EventHandlerFn<'a, Option>), 34 | } 35 | 36 | impl ButtonEventHandler<'_, Message> 37 | where 38 | Message: Clone, 39 | { 40 | fn get( 41 | &self, 42 | event: iced::Event, 43 | layout: iced::core::Layout, 44 | cursor: iced::mouse::Cursor, 45 | clipboard: &mut dyn iced::core::Clipboard, 46 | viewport: &Rectangle, 47 | ) -> Option { 48 | match self { 49 | ButtonEventHandler::Message(msg) => Some(msg.clone()), 50 | ButtonEventHandler::F(f) => Some(f(event, layout, cursor, clipboard, viewport)), 51 | ButtonEventHandler::FMaybe(f) => f(event, layout, cursor, clipboard, viewport), 52 | } 53 | } 54 | } 55 | 56 | pub struct Button<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> 57 | where 58 | Renderer: iced::core::Renderer, 59 | Theme: Catalog, 60 | Message: Clone, 61 | { 62 | content: Element<'a, Message, Theme, Renderer>, 63 | on_event: Option>, 64 | id: Id, 65 | width: Length, 66 | height: Length, 67 | padding: Padding, 68 | clip: bool, 69 | class: Theme::Class<'a>, 70 | } 71 | 72 | impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> 73 | where 74 | Renderer: iced::core::Renderer, 75 | Theme: Catalog, 76 | Message: Clone, 77 | { 78 | /// Creates a new [`Button`] with the given content. 79 | pub fn new(content: impl Into>) -> Self { 80 | let content = content.into(); 81 | let size = content.as_widget().size_hint(); 82 | 83 | Button { 84 | content, 85 | id: Id::unique(), 86 | on_event: None, 87 | width: size.width.fluid(), 88 | height: size.height.fluid(), 89 | padding: Padding::ZERO, 90 | clip: false, 91 | class: Theme::default(), 92 | } 93 | } 94 | 95 | /// Defines the on_event action of the [`Button`] 96 | pub fn on_event(mut self, msg: Message) -> Self { 97 | self.on_event = Some(ButtonEventHandler::Message(msg)); 98 | self 99 | } 100 | 101 | /// Defines the on_event action of the [`Button`], if Some 102 | pub fn on_event_maybe(mut self, msg: Option) -> Self { 103 | if let Some(msg) = msg { 104 | self.on_event = Some(ButtonEventHandler::Message(msg)); 105 | } 106 | self 107 | } 108 | 109 | /// Determines the on_event action of the [`Button`] using a closure 110 | pub fn on_event_with(mut self, f: F) -> Self 111 | where 112 | F: Fn( 113 | iced::Event, 114 | iced::core::Layout, 115 | iced::mouse::Cursor, 116 | &mut dyn iced::core::Clipboard, 117 | &Rectangle, 118 | ) -> Message 119 | + 'a, 120 | { 121 | self.on_event = Some(ButtonEventHandler::F(Box::new(f))); 122 | self 123 | } 124 | 125 | /// Determines the on_event action of the [`Button`] with a closure, if Some 126 | pub fn on_event_maybe_with(self, f: Option) -> Self 127 | where 128 | F: Fn( 129 | iced::Event, 130 | iced::core::Layout, 131 | iced::mouse::Cursor, 132 | &mut dyn iced::core::Clipboard, 133 | &Rectangle, 134 | ) -> Message 135 | + 'a, 136 | { 137 | if let Some(f) = f { 138 | self.on_event_with(f) 139 | } else { 140 | self 141 | } 142 | } 143 | 144 | /// Determines the on_event action of the [`Button`] using a closure which might return a Message 145 | pub fn on_event_try(mut self, f: F) -> Self 146 | where 147 | F: Fn( 148 | iced::Event, 149 | iced::core::Layout, 150 | iced::mouse::Cursor, 151 | &mut dyn iced::core::Clipboard, 152 | &Rectangle, 153 | ) -> Option 154 | + 'a, 155 | { 156 | self.on_event = Some(ButtonEventHandler::FMaybe(Box::new(f))); 157 | self 158 | } 159 | 160 | /// Sets the width of the [`Button`]. 161 | pub fn width(mut self, width: impl Into) -> Self { 162 | self.width = width.into(); 163 | self 164 | } 165 | 166 | /// Sets the height of the [`Button`]. 167 | pub fn height(mut self, height: impl Into) -> Self { 168 | self.height = height.into(); 169 | self 170 | } 171 | 172 | /// Sets the [`Padding`] of the [`Button`]. 173 | pub fn padding>(mut self, padding: P) -> Self { 174 | self.padding = padding.into(); 175 | self 176 | } 177 | 178 | /// Sets whether the contents of the [`Button`] should be clipped on 179 | /// overflow. 180 | pub fn clip(mut self, clip: bool) -> Self { 181 | self.clip = clip; 182 | self 183 | } 184 | 185 | /// Sets the style of the [`Button`]. 186 | #[must_use] 187 | pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self 188 | where 189 | Theme::Class<'a>: From>, 190 | { 191 | self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); 192 | self 193 | } 194 | 195 | /// Sets the [`Id`] of the [`Button`]. 196 | pub fn id(mut self, id: Id) -> Self { 197 | self.id = id; 198 | self 199 | } 200 | } 201 | 202 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 203 | struct State { 204 | is_hovered: bool, 205 | is_pressed: bool, 206 | is_focused: bool, 207 | } 208 | 209 | impl<'a, Message, Theme, Renderer> Widget 210 | for Button<'a, Message, Theme, Renderer> 211 | where 212 | Message: 'a + Clone, 213 | Renderer: 'a + iced::core::Renderer, 214 | Theme: Catalog, 215 | { 216 | fn tag(&self) -> tree::Tag { 217 | tree::Tag::of::() 218 | } 219 | 220 | fn state(&self) -> tree::State { 221 | tree::State::new(State::default()) 222 | } 223 | 224 | fn children(&self) -> Vec { 225 | vec![Tree::new(&self.content)] 226 | } 227 | 228 | fn diff(&mut self, tree: &mut Tree) { 229 | tree.diff_children(std::slice::from_mut(&mut self.content)); 230 | } 231 | 232 | fn size(&self) -> Size { 233 | Size { 234 | width: self.width, 235 | height: self.height, 236 | } 237 | } 238 | 239 | fn layout( 240 | &self, 241 | tree: &mut Tree, 242 | renderer: &Renderer, 243 | limits: &layout::Limits, 244 | ) -> layout::Node { 245 | layout::padded(limits, self.width, self.height, self.padding, |limits| { 246 | self.content 247 | .as_widget() 248 | .layout(&mut tree.children[0], renderer, limits) 249 | }) 250 | } 251 | 252 | fn operate( 253 | &self, 254 | tree: &mut Tree, 255 | layout: Layout<'_>, 256 | renderer: &Renderer, 257 | operation: &mut dyn Operation, 258 | ) { 259 | operation.container(None, layout.bounds(), &mut |operation| { 260 | self.content.as_widget().operate( 261 | &mut tree.children[0], 262 | layout.children().next().unwrap(), 263 | renderer, 264 | operation, 265 | ); 266 | }); 267 | } 268 | 269 | fn on_event( 270 | &mut self, 271 | tree: &mut Tree, 272 | event: Event, 273 | layout: Layout<'_>, 274 | cursor: mouse::Cursor, 275 | renderer: &Renderer, 276 | clipboard: &mut dyn Clipboard, 277 | shell: &mut Shell<'_, Message>, 278 | viewport: &Rectangle, 279 | ) -> event::Status { 280 | if let event::Status::Captured = self.content.as_widget_mut().on_event( 281 | &mut tree.children[0], 282 | event.clone(), 283 | layout.children().next().unwrap(), 284 | cursor, 285 | renderer, 286 | clipboard, 287 | shell, 288 | viewport, 289 | ) { 290 | return event::Status::Captured; 291 | } 292 | 293 | match event { 294 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) 295 | | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) 296 | | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) 297 | | Event::Touch(touch::Event::FingerPressed { .. }) => { 298 | if self.on_event.is_some() { 299 | let bounds = layout.bounds(); 300 | 301 | if cursor.is_over(bounds) { 302 | let state = tree.state.downcast_mut::(); 303 | 304 | state.is_pressed = true; 305 | 306 | return event::Status::Captured; 307 | } 308 | } 309 | } 310 | Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) 311 | | Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) 312 | | Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) 313 | | Event::Touch(touch::Event::FingerLifted { .. }) => { 314 | if let Some(on_press) = self.on_event.as_ref() { 315 | let state = tree.state.downcast_mut::(); 316 | 317 | if state.is_pressed { 318 | state.is_pressed = false; 319 | 320 | let bounds = layout.bounds(); 321 | 322 | if cursor.is_over(bounds) { 323 | if let Some(msg) = 324 | on_press.get(event, layout, cursor, clipboard, viewport) 325 | { 326 | shell.publish(msg); 327 | } 328 | } 329 | 330 | return event::Status::Captured; 331 | } 332 | } 333 | } 334 | Event::Keyboard(keyboard::Event::KeyPressed { ref key, .. }) => { 335 | if let Some(on_press) = self.on_event.as_ref() { 336 | let state = tree.state.downcast_mut::(); 337 | if state.is_focused 338 | && matches!(key, keyboard::Key::Named(keyboard::key::Named::Enter)) 339 | { 340 | state.is_pressed = true; 341 | if let Some(msg) = on_press.get(event, layout, cursor, clipboard, viewport) 342 | { 343 | shell.publish(msg); 344 | } 345 | return event::Status::Captured; 346 | } 347 | } 348 | } 349 | Event::Touch(touch::Event::FingerLost { .. }) 350 | | Event::Mouse(mouse::Event::CursorLeft) => { 351 | let state = tree.state.downcast_mut::(); 352 | state.is_hovered = false; 353 | state.is_pressed = false; 354 | } 355 | _ => {} 356 | } 357 | 358 | event::Status::Ignored 359 | } 360 | 361 | fn draw( 362 | &self, 363 | tree: &Tree, 364 | renderer: &mut Renderer, 365 | theme: &Theme, 366 | renderer_style: &renderer::Style, 367 | layout: Layout<'_>, 368 | cursor: mouse::Cursor, 369 | viewport: &Rectangle, 370 | ) { 371 | let bounds = layout.bounds(); 372 | let content_layout = layout.children().next().unwrap(); 373 | let is_mouse_over = cursor.is_over(bounds); 374 | 375 | let status = if self.on_event.is_none() { 376 | Status::Disabled 377 | } else if is_mouse_over { 378 | let state = tree.state.downcast_ref::(); 379 | 380 | if state.is_pressed { 381 | Status::Pressed 382 | } else { 383 | Status::Hovered 384 | } 385 | } else { 386 | Status::Active 387 | }; 388 | 389 | let style = theme.style(&self.class, status); 390 | 391 | if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 { 392 | renderer.fill_quad( 393 | renderer::Quad { 394 | bounds, 395 | border: style.border, 396 | shadow: style.shadow, 397 | }, 398 | style 399 | .background 400 | .unwrap_or(Background::Color(Color::TRANSPARENT)), 401 | ); 402 | } 403 | 404 | let viewport = if self.clip { 405 | bounds.intersection(viewport).unwrap_or(*viewport) 406 | } else { 407 | *viewport 408 | }; 409 | 410 | self.content.as_widget().draw( 411 | &tree.children[0], 412 | renderer, 413 | theme, 414 | &renderer::Style { 415 | text_color: style.text_color, 416 | icon_color: style.icon_color.unwrap_or(renderer_style.icon_color), 417 | scale_factor: renderer_style.scale_factor, 418 | }, 419 | content_layout, 420 | cursor, 421 | &viewport, 422 | ); 423 | } 424 | 425 | fn mouse_interaction( 426 | &self, 427 | _tree: &Tree, 428 | layout: Layout<'_>, 429 | cursor: mouse::Cursor, 430 | _viewport: &Rectangle, 431 | _renderer: &Renderer, 432 | ) -> mouse::Interaction { 433 | let is_mouse_over = cursor.is_over(layout.bounds()); 434 | 435 | if is_mouse_over && self.on_event.is_some() { 436 | mouse::Interaction::Pointer 437 | } else { 438 | mouse::Interaction::default() 439 | } 440 | } 441 | 442 | fn overlay<'b>( 443 | &'b mut self, 444 | tree: &'b mut Tree, 445 | layout: Layout<'_>, 446 | renderer: &Renderer, 447 | translation: Vector, 448 | ) -> Option> { 449 | self.content.as_widget_mut().overlay( 450 | &mut tree.children[0], 451 | layout.children().next().unwrap(), 452 | renderer, 453 | translation, 454 | ) 455 | } 456 | 457 | fn id(&self) -> Option { 458 | Some(self.id.clone()) 459 | } 460 | 461 | fn set_id(&mut self, id: Id) { 462 | self.id = id; 463 | } 464 | } 465 | 466 | impl<'a, Message, Theme, Renderer> From> 467 | for Element<'a, Message, Theme, Renderer> 468 | where 469 | Message: Clone + 'a, 470 | Theme: Catalog + 'a, 471 | Renderer: iced::core::Renderer + 'a, 472 | { 473 | fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { 474 | Self::new(button) 475 | } 476 | } 477 | 478 | pub fn button<'a, Message, Theme, Renderer>( 479 | content: impl Into>, 480 | ) -> Button<'a, Message, Theme, Renderer> 481 | where 482 | Theme: Catalog + 'a, 483 | Renderer: iced::core::Renderer, 484 | Message: Clone, 485 | { 486 | Button::new(content) 487 | } 488 | -------------------------------------------------------------------------------- /src/config/anchor.rs: -------------------------------------------------------------------------------- 1 | use iced::platform_specific::shell::commands::layer_surface::Anchor; 2 | 3 | #[derive(Debug, Default, Clone, Copy)] 4 | pub enum BarAnchor { 5 | Left, 6 | Right, 7 | #[default] 8 | Top, 9 | Bottom, 10 | } 11 | 12 | impl BarAnchor { 13 | pub fn vertical(&self) -> bool { 14 | match self { 15 | BarAnchor::Top | BarAnchor::Bottom => false, 16 | BarAnchor::Left | BarAnchor::Right => true, 17 | } 18 | } 19 | } 20 | 21 | impl From for String { 22 | fn from(anchor: BarAnchor) -> String { 23 | match anchor { 24 | BarAnchor::Top => "top", 25 | BarAnchor::Bottom => "bottom", 26 | BarAnchor::Left => "left", 27 | BarAnchor::Right => "right", 28 | } 29 | .to_string() 30 | } 31 | } 32 | 33 | impl From<&BarAnchor> for Anchor { 34 | fn from(anchor: &BarAnchor) -> Self { 35 | match anchor { 36 | BarAnchor::Top => Anchor::TOP, 37 | BarAnchor::Bottom => Anchor::BOTTOM, 38 | BarAnchor::Left => Anchor::LEFT, 39 | BarAnchor::Right => Anchor::RIGHT, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/config/enabled_modules.rs: -------------------------------------------------------------------------------- 1 | use configparser::ini::Ini; 2 | 3 | #[derive(Debug)] 4 | pub struct EnabledModules { 5 | pub left: Vec, 6 | pub center: Vec, 7 | pub right: Vec, 8 | } 9 | 10 | impl Default for EnabledModules { 11 | fn default() -> Self { 12 | let vec = |list: &[&str]| list.iter().map(|i| i.to_string()).collect(); 13 | 14 | Self { 15 | left: vec(&["hyprland.workspaces", "hyprland.window"]), 16 | center: vec(&["date", "time"]), 17 | right: vec(&["media", "volume", "cpu", "memory"]), 18 | } 19 | } 20 | } 21 | 22 | impl From<&Ini> for EnabledModules { 23 | fn from(ini: &Ini) -> Self { 24 | let get = |field: &str| { 25 | ini.get("modules", field) 26 | .map(|value| value.split(',').map(|v| v.trim().to_string()).collect()) 27 | }; 28 | 29 | let default = Self::default(); 30 | 31 | Self { 32 | left: get("left").unwrap_or(default.left), 33 | center: get("center").unwrap_or(default.center), 34 | right: get("right").unwrap_or(default.right), 35 | } 36 | } 37 | } 38 | 39 | impl EnabledModules { 40 | pub fn get_all(&self) -> impl Iterator { 41 | self.left 42 | .iter() 43 | .chain(self.center.iter()) 44 | .chain(self.right.iter()) 45 | } 46 | 47 | pub fn contains(&self, x: &String) -> bool { 48 | self.left.contains(x) || self.center.contains(x) || self.right.contains(x) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/config/insets.rs: -------------------------------------------------------------------------------- 1 | use iced::{runtime::platform_specific::wayland::layer_surface::IcedMargin, Padding, Radius}; 2 | 3 | pub struct Insets { 4 | a: f32, 5 | b: f32, 6 | c: f32, 7 | d: f32, 8 | } 9 | 10 | impl Insets { 11 | pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self { 12 | Self { a, b, c, d } 13 | } 14 | } 15 | 16 | impl From for IcedMargin { 17 | fn from(insets: Insets) -> Self { 18 | Self { 19 | top: insets.a as i32, 20 | right: insets.b as i32, 21 | bottom: insets.c as i32, 22 | left: insets.d as i32, 23 | } 24 | } 25 | } 26 | 27 | impl From for Padding { 28 | fn from(insets: Insets) -> Self { 29 | Self { 30 | top: insets.a, 31 | right: insets.b, 32 | bottom: insets.c, 33 | left: insets.d, 34 | } 35 | } 36 | } 37 | 38 | impl From for Radius { 39 | fn from(insets: Insets) -> Self { 40 | Self { 41 | top_left: insets.a, 42 | top_right: insets.b, 43 | bottom_right: insets.c, 44 | bottom_left: insets.d, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::TypeId, 3 | collections::{HashMap, HashSet}, 4 | fs::{create_dir_all, File}, 5 | io::Write, 6 | path::PathBuf, 7 | sync::Arc, 8 | }; 9 | 10 | use anchor::BarAnchor; 11 | use configparser::ini::{Ini, IniDefault}; 12 | use directories::ProjectDirs; 13 | pub use enabled_modules::EnabledModules; 14 | use handlebars::Handlebars; 15 | use iced::{ 16 | futures::{channel::mpsc::Sender, SinkExt}, 17 | platform_specific::shell::commands::layer_surface::KeyboardInteractivity, 18 | }; 19 | use module_config::ModuleConfig; 20 | use popup_config::PopupConfig; 21 | use tokio::sync::mpsc; 22 | 23 | use crate::{registry::Registry, Message}; 24 | pub use thrice::Thrice; 25 | 26 | pub mod anchor; 27 | mod enabled_modules; 28 | mod insets; 29 | pub mod module_config; 30 | pub mod parse; 31 | pub mod popup_config; 32 | mod thrice; 33 | 34 | #[derive(Debug)] 35 | pub struct Config { 36 | pub hard_reload: bool, 37 | pub enabled_modules: EnabledModules, 38 | pub enabled_listeners: HashSet, 39 | pub module_config: ModuleConfig, 40 | pub popup_config: PopupConfig, 41 | pub anchor: BarAnchor, 42 | pub monitor: Option, 43 | pub kb_focus: KeyboardInteractivity, 44 | } 45 | 46 | impl Config { 47 | fn default(registry: &Registry) -> Self { 48 | let enabled_modules = EnabledModules::default(); 49 | Self { 50 | hard_reload: false, 51 | enabled_listeners: registry 52 | .enabled_listeners(&enabled_modules, &None) 53 | .chain( 54 | registry 55 | .all_listeners() 56 | .flat_map(|(l_id, l)| { 57 | l.config().into_iter().map(move |option| (l_id, option)) 58 | }) 59 | .filter_map(|(l_id, option)| option.default.then_some(*l_id)), 60 | ) 61 | .collect(), 62 | enabled_modules, 63 | module_config: ModuleConfig::default(), 64 | popup_config: PopupConfig::default(), 65 | anchor: BarAnchor::default(), 66 | monitor: None, 67 | kb_focus: KeyboardInteractivity::None, 68 | } 69 | } 70 | 71 | pub fn exclusive_zone(&self) -> i32 { 72 | (match self.anchor { 73 | BarAnchor::Left | BarAnchor::Right => self.module_config.global.width.unwrap_or(30), 74 | BarAnchor::Top | BarAnchor::Bottom => self.module_config.global.height.unwrap_or(30), 75 | }) as i32 76 | } 77 | } 78 | 79 | pub fn get_config_dir() -> PathBuf { 80 | let config_dir = ProjectDirs::from("fun.killarchive", "faervan", "bar-rs") 81 | .map(|dirs| dirs.config_local_dir().to_path_buf()) 82 | .unwrap_or_else(|| { 83 | eprintln!("Failed to get config directory"); 84 | PathBuf::from("") 85 | }); 86 | let _ = create_dir_all(&config_dir); 87 | let config_file = config_dir.join("bar-rs.ini"); 88 | 89 | if let Ok(mut file) = File::create_new(&config_file) { 90 | file.write_all(include_bytes!("../../default_config/horizontal.ini")) 91 | .unwrap_or_else(|e| { 92 | eprintln!( 93 | "Failed to write default config to {}: {e}", 94 | config_file.to_string_lossy() 95 | ) 96 | }); 97 | } 98 | 99 | config_file 100 | } 101 | 102 | pub fn read_config(path: &PathBuf, registry: &mut Registry, templates: &mut Handlebars) -> Config { 103 | let mut ini = Ini::new(); 104 | let mut defaults = IniDefault::default(); 105 | defaults.delimiters = vec!['=']; 106 | ini.load_defaults(defaults); 107 | let Ok(_) = ini.load(path) else { 108 | eprintln!("Failed to read config from {}", path.to_string_lossy()); 109 | return Config::default(registry); 110 | }; 111 | let config: Config = (&ini, &*registry).into(); 112 | let empty_map = HashMap::new(); 113 | registry 114 | .get_modules_mut(config.enabled_modules.get_all(), &config) 115 | .map(|m| { 116 | let name = m.name(); 117 | let cfg_map = ini 118 | .get_map_ref() 119 | .get(&format!("module:{}", name)) 120 | .unwrap_or(&empty_map); 121 | let popup_cfg_map = ini 122 | .get_map_ref() 123 | .get(&format!("module_popup:{}", name)) 124 | .unwrap_or(&empty_map); 125 | (m, cfg_map, popup_cfg_map) 126 | }) 127 | .for_each(|(m, cfg_map, popup_cfg_map)| m.read_config(cfg_map, popup_cfg_map, templates)); 128 | config 129 | } 130 | 131 | pub async fn get_config(sender: &mut Sender) -> (Arc, Arc) { 132 | let (sx, mut rx) = mpsc::channel(1); 133 | sender 134 | .send(Message::GetConfig(sx)) 135 | .await 136 | .unwrap_or_else(|err| { 137 | eprintln!("Trying to request config failed with err: {err}"); 138 | }); 139 | rx.recv().await.unwrap() 140 | } 141 | 142 | pub struct ConfigEntry { 143 | pub section: String, 144 | pub name: String, 145 | pub default: bool, 146 | } 147 | 148 | impl ConfigEntry { 149 | pub fn new(section: S, name: S, default: bool) -> Self { 150 | Self { 151 | section: section.to_string(), 152 | name: name.to_string(), 153 | default, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/config/module_config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use configparser::ini::Ini; 4 | use iced::{ 5 | runtime::platform_specific::wayland::layer_surface::IcedMargin, Background, Border, Color, 6 | Padding, 7 | }; 8 | 9 | use crate::modules::OnClickAction; 10 | 11 | use super::{parse::StringExt, Thrice}; 12 | 13 | #[derive(Debug, Default)] 14 | pub struct ModuleConfig { 15 | pub global: GlobalModuleConfig, 16 | pub local: LocalModuleConfig, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct GlobalModuleConfig { 21 | pub spacing: Thrice, 22 | pub width: Option, 23 | pub height: Option, 24 | pub margin: IcedMargin, 25 | pub padding: Padding, 26 | pub background_color: Color, 27 | } 28 | 29 | impl Default for GlobalModuleConfig { 30 | fn default() -> Self { 31 | Self { 32 | spacing: 20_f32.into(), 33 | width: None, 34 | height: None, 35 | margin: IcedMargin::default(), 36 | padding: Padding::default(), 37 | background_color: Color::from_rgba(0., 0., 0., 0.5), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct LocalModuleConfig { 44 | pub text_color: Color, 45 | pub icon_color: Color, 46 | pub font_size: f32, 47 | pub icon_size: f32, 48 | pub text_margin: Padding, 49 | pub icon_margin: Padding, 50 | pub spacing: f32, 51 | pub margin: Padding, 52 | pub padding: Padding, 53 | pub background: Option, 54 | pub border: Border, 55 | pub action: OnClickAction, 56 | } 57 | 58 | impl Default for LocalModuleConfig { 59 | fn default() -> Self { 60 | Self { 61 | text_color: Color::WHITE, 62 | icon_color: Color::WHITE, 63 | font_size: 16., 64 | icon_size: 20., 65 | text_margin: Padding::default(), 66 | icon_margin: Padding::default(), 67 | spacing: 10., 68 | margin: Padding::default(), 69 | padding: Padding::default(), 70 | background: None, 71 | border: Border::default(), 72 | action: OnClickAction::default(), 73 | } 74 | } 75 | } 76 | 77 | #[derive(Default, Debug)] 78 | pub struct ModuleConfigOverride { 79 | pub text_color: Option, 80 | pub icon_color: Option, 81 | pub font_size: Option, 82 | pub icon_size: Option, 83 | pub text_margin: Option, 84 | pub icon_margin: Option, 85 | pub spacing: Option, 86 | pub margin: Option, 87 | pub padding: Option, 88 | pub background: Option>, 89 | pub border: Option, 90 | pub action: Option, 91 | } 92 | 93 | impl From<&HashMap>> for ModuleConfigOverride { 94 | fn from(map: &HashMap>) -> Self { 95 | Self { 96 | text_color: map.get("text_color").and_then(|s| s.into_color()), 97 | icon_color: map.get("icon_color").and_then(|s| s.into_color()), 98 | font_size: map.get("font_size").and_then(|s| s.into_float()), 99 | icon_size: map.get("icon_size").and_then(|s| s.into_float()), 100 | text_margin: map 101 | .get("text_margin") 102 | .and_then(|s| s.into_insets().map(|i| i.into())), 103 | icon_margin: map 104 | .get("icon_margin") 105 | .and_then(|s| s.into_insets().map(|i| i.into())), 106 | spacing: map.get("spacing").and_then(|s| s.into_float()), 107 | margin: map 108 | .get("margin") 109 | .and_then(|s| s.into_insets().map(|i| i.into())), 110 | padding: map 111 | .get("padding") 112 | .and_then(|s| s.into_insets().map(|i| i.into())), 113 | background: map.get("background").map(|s| s.into_background()), 114 | border: { 115 | let color = map.get("border_color").and_then(|s| s.into_color()); 116 | let width = map.get("border_width").and_then(|s| s.into_float()); 117 | let radius = map 118 | .get("border_radius") 119 | .and_then(|s| s.into_insets().map(|i| i.into())); 120 | if color.is_some() || width.is_some() || radius.is_some() { 121 | Some(Border { 122 | color: color.unwrap_or_default(), 123 | width: width.unwrap_or_default(), 124 | radius: radius.unwrap_or_default(), 125 | }) 126 | } else { 127 | None 128 | } 129 | }, 130 | action: { 131 | let left = map 132 | .get("on_click") 133 | .and_then(|s| s.as_ref().map(|s| s.into())); 134 | let center = map 135 | .get("on_middle_click") 136 | .and_then(|s| s.as_ref().map(|s| s.into())); 137 | let right = map 138 | .get("on_right_click") 139 | .and_then(|s| s.as_ref().map(|s| s.into())); 140 | if left.is_some() || center.is_some() || right.is_some() { 141 | Some(OnClickAction { 142 | left, 143 | center, 144 | right, 145 | }) 146 | } else { 147 | None 148 | } 149 | }, 150 | } 151 | } 152 | } 153 | 154 | impl From<&Ini> for ModuleConfig { 155 | fn from(ini: &Ini) -> Self { 156 | let global = Self::default().global; 157 | let local = Self::default().local; 158 | let section = "style"; 159 | let module_section = "module_style"; 160 | ModuleConfig { 161 | global: GlobalModuleConfig { 162 | background_color: ini 163 | .get(section, "background") 164 | .into_color() 165 | .unwrap_or(global.background_color), 166 | spacing: ini 167 | .get(section, "spacing") 168 | .into_thrice_float() 169 | .unwrap_or(global.spacing), 170 | height: ini.get(section, "height").and_then(|v| v.parse().ok()), 171 | width: ini.get(section, "width").and_then(|v| v.parse().ok()), 172 | margin: ini 173 | .get(section, "margin") 174 | .into_insets() 175 | .map(|i| i.into()) 176 | .unwrap_or(global.margin), 177 | padding: ini 178 | .get(section, "padding") 179 | .into_insets() 180 | .map(|i| i.into()) 181 | .unwrap_or(global.padding), 182 | }, 183 | local: LocalModuleConfig { 184 | text_color: ini 185 | .get(module_section, "text_color") 186 | .into_color() 187 | .unwrap_or(local.text_color), 188 | icon_color: ini 189 | .get(module_section, "icon_color") 190 | .into_color() 191 | .unwrap_or(local.icon_color), 192 | font_size: ini 193 | .get(module_section, "font_size") 194 | .into_float() 195 | .unwrap_or(local.font_size), 196 | icon_size: ini 197 | .get(module_section, "icon_size") 198 | .into_float() 199 | .unwrap_or(local.icon_size), 200 | text_margin: ini 201 | .get(module_section, "text_margin") 202 | .into_insets() 203 | .map(|i| i.into()) 204 | .unwrap_or(local.text_margin), 205 | icon_margin: ini 206 | .get(module_section, "icon_margin") 207 | .into_insets() 208 | .map(|i| i.into()) 209 | .unwrap_or(local.icon_margin), 210 | spacing: ini 211 | .get(module_section, "spacing") 212 | .into_float() 213 | .unwrap_or(local.spacing), 214 | margin: ini 215 | .get(module_section, "margin") 216 | .into_insets() 217 | .map(|i| i.into()) 218 | .unwrap_or(local.margin), 219 | padding: ini 220 | .get(module_section, "padding") 221 | .into_insets() 222 | .map(|i| i.into()) 223 | .unwrap_or(local.padding), 224 | background: ini.get(module_section, "background").into_background(), 225 | border: { 226 | let color = ini 227 | .get(module_section, "border_color") 228 | .into_color() 229 | .unwrap_or(local.border.color); 230 | let width = ini 231 | .get(module_section, "border_width") 232 | .into_float() 233 | .unwrap_or(local.border.width); 234 | let radius = ini 235 | .get(module_section, "border_radius") 236 | .into_insets() 237 | .map(|i| i.into()) 238 | .unwrap_or(local.border.radius); 239 | Border { 240 | color, 241 | width, 242 | radius, 243 | } 244 | }, 245 | action: { 246 | let left = ini.get(module_section, "on_click").map(|s| (&s).into()); 247 | let center = ini 248 | .get(module_section, "on_middle_click") 249 | .map(|s| (&s).into()); 250 | let right = ini 251 | .get(module_section, "on_right_click") 252 | .map(|s| (&s).into()); 253 | OnClickAction { 254 | left, 255 | center, 256 | right, 257 | } 258 | }, 259 | }, 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/config/parse.rs: -------------------------------------------------------------------------------- 1 | use configparser::ini::Ini; 2 | use iced::{ 3 | platform_specific::shell::commands::layer_surface::KeyboardInteractivity, Background, Color, 4 | }; 5 | 6 | use crate::{registry::Registry, OptionExt}; 7 | 8 | use super::{anchor::BarAnchor, insets::Insets, Config, Thrice}; 9 | 10 | impl From<(&Ini, &Registry)> for Config { 11 | fn from((ini, registry): (&Ini, &Registry)) -> Self { 12 | let enabled_modules = ini.into(); 13 | let default = Self::default(registry); 14 | Self { 15 | hard_reload: ini 16 | .get("general", "hard_reloading") 17 | .into_bool() 18 | .unwrap_or(default.hard_reload), 19 | enabled_listeners: registry 20 | .all_listeners() 21 | .fold(vec![], |mut acc, (id, l)| { 22 | l.config().into_iter().for_each(|option| { 23 | if ini 24 | .get(&option.section, &option.name) 25 | .into_bool() 26 | .unwrap_or(option.default) 27 | { 28 | acc.push(*id); 29 | } 30 | }); 31 | acc 32 | }) 33 | .into_iter() 34 | .chain(registry.enabled_listeners(&enabled_modules, &None)) 35 | .collect(), 36 | enabled_modules, 37 | module_config: ini.into(), 38 | popup_config: ini.into(), 39 | anchor: ini 40 | .get("general", "anchor") 41 | .into_anchor() 42 | .unwrap_or(default.anchor), 43 | monitor: ini.get("general", "monitor"), 44 | kb_focus: ini 45 | .get("general", "kb_focus") 46 | .into_kb_focus() 47 | .unwrap_or(default.kb_focus), 48 | } 49 | } 50 | } 51 | 52 | pub trait StringExt { 53 | fn into_bool(self) -> Option; 54 | fn into_color(self) -> Option; 55 | fn into_float(self) -> Option; 56 | fn into_thrice_float(self) -> Option>; 57 | fn into_anchor(self) -> Option; 58 | fn into_insets(self) -> Option; 59 | fn into_background(self) -> Option; 60 | fn into_kb_focus(self) -> Option; 61 | } 62 | 63 | impl StringExt for &Option { 64 | fn into_bool(self) -> Option { 65 | self.as_ref().and_then(|v| match v.to_lowercase().as_str() { 66 | "0" | "f" | "n" | "no" | "false" | "disabled" | "disable" | "off" => Some(false), 67 | "1" | "t" | "y" | "yes" | "true" | "enabled" | "enable" | "on" => Some(true), 68 | _ => None, 69 | }) 70 | } 71 | fn into_color(self) -> Option { 72 | self.as_ref().and_then(|color| { 73 | csscolorparser::parse(color) 74 | .map(|v| v.into_ext()) 75 | .ok() 76 | .map_none(|| println!("Failed to parse color!")) 77 | }) 78 | } 79 | fn into_float(self) -> Option { 80 | self.as_ref().and_then(|v| v.parse().ok()) 81 | } 82 | fn into_thrice_float(self) -> Option> { 83 | self.as_ref().and_then(|value| { 84 | if let [left, center, right] = value.split_whitespace().collect::>()[..] { 85 | left.parse() 86 | .and_then(|l| center.parse().map(|c| (l, c))) 87 | .and_then(|(l, c)| right.parse().map(|r| (l, c, r))) 88 | .ok() 89 | .map(|all| all.into()) 90 | } else { 91 | value.parse::().ok().map(|all| all.into()) 92 | } 93 | .map_none(|| eprintln!("Failed to parse value as float")) 94 | }) 95 | } 96 | fn into_anchor(self) -> Option { 97 | self.as_ref().and_then(|value| match value.as_str() { 98 | "top" => Some(BarAnchor::Top), 99 | "bottom" => Some(BarAnchor::Bottom), 100 | "left" => Some(BarAnchor::Left), 101 | "right" => Some(BarAnchor::Right), 102 | _ => None, 103 | }) 104 | } 105 | fn into_insets(self) -> Option { 106 | self.as_ref().and_then(|value| { 107 | let values = value 108 | .split_whitespace() 109 | .filter_map(|i| i.parse::().ok()) 110 | .collect::>(); 111 | match values[..] { 112 | [all] => Some(Insets::new(all, all, all, all)), 113 | [vertical, horizontal] => { 114 | Some(Insets::new(vertical, horizontal, vertical, horizontal)) 115 | } 116 | [top, right, bottom, left] => Some(Insets::new(top, right, bottom, left)), 117 | _ => { 118 | eprintln!("Failed to parse value as insets"); 119 | None 120 | } 121 | } 122 | }) 123 | } 124 | fn into_background(self) -> Option { 125 | self.into_color().map(Background::Color) 126 | } 127 | fn into_kb_focus(self) -> Option { 128 | self.as_ref().and_then(|v| match v.as_str() { 129 | "none" => Some(KeyboardInteractivity::None), 130 | "on_demand" => Some(KeyboardInteractivity::OnDemand), 131 | "exclusive" => Some(KeyboardInteractivity::Exclusive), 132 | _ => None, 133 | }) 134 | } 135 | } 136 | 137 | pub trait IntoExt { 138 | fn into_ext(self) -> T; 139 | } 140 | 141 | impl IntoExt for csscolorparser::Color { 142 | fn into_ext(self) -> Color { 143 | Color { 144 | r: self.r, 145 | g: self.g, 146 | b: self.b, 147 | a: self.a, 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/config/popup_config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use configparser::ini::Ini; 4 | use iced::{Background, Border, Color, Padding}; 5 | 6 | use super::parse::StringExt; 7 | 8 | #[derive(Debug)] 9 | pub struct PopupConfig { 10 | pub width: i32, 11 | pub height: i32, 12 | /// Whether the content of the popup should fill the size of the popup window 13 | pub fill_content_to_size: bool, 14 | pub padding: Padding, 15 | pub text_color: Color, 16 | pub icon_color: Color, 17 | pub font_size: f32, 18 | pub icon_size: f32, 19 | pub text_margin: Padding, 20 | pub icon_margin: Padding, 21 | pub spacing: f32, 22 | pub background: Background, 23 | pub border: Border, 24 | } 25 | 26 | impl Default for PopupConfig { 27 | fn default() -> Self { 28 | Self { 29 | width: 300, 30 | height: 300, 31 | fill_content_to_size: false, 32 | padding: [10, 20].into(), 33 | text_color: Color::WHITE, 34 | icon_color: Color::WHITE, 35 | font_size: 14., 36 | icon_size: 24., 37 | text_margin: Padding::default(), 38 | icon_margin: Padding::default(), 39 | spacing: 0., 40 | background: Background::Color(Color { 41 | r: 0., 42 | g: 0., 43 | b: 0., 44 | a: 0.8, 45 | }), 46 | border: Border::default().rounded(8), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Default)] 52 | pub struct PopupConfigOverride { 53 | pub width: Option, 54 | pub height: Option, 55 | pub fill_content_to_size: Option, 56 | pub padding: Option, 57 | pub text_color: Option, 58 | pub icon_color: Option, 59 | pub font_size: Option, 60 | pub icon_size: Option, 61 | pub text_margin: Option, 62 | pub icon_margin: Option, 63 | pub spacing: Option, 64 | pub background: Option, 65 | pub border: Option, 66 | } 67 | 68 | impl From<&Ini> for PopupConfig { 69 | fn from(ini: &Ini) -> Self { 70 | let default = Self::default(); 71 | let section = "popup_style"; 72 | Self { 73 | width: ini 74 | .get(section, "width") 75 | .and_then(|s| s.parse().ok()) 76 | .unwrap_or(default.width), 77 | height: ini 78 | .get(section, "height") 79 | .and_then(|s| s.parse().ok()) 80 | .unwrap_or(default.height), 81 | fill_content_to_size: ini 82 | .get(section, "fill_content_to_size") 83 | .into_bool() 84 | .unwrap_or(default.fill_content_to_size), 85 | padding: ini 86 | .get(section, "padding") 87 | .into_insets() 88 | .map(|i| i.into()) 89 | .unwrap_or(default.padding), 90 | text_color: ini 91 | .get(section, "text_color") 92 | .into_color() 93 | .unwrap_or(default.text_color), 94 | icon_color: ini 95 | .get(section, "icon_color") 96 | .into_color() 97 | .unwrap_or(default.icon_color), 98 | font_size: ini 99 | .get(section, "font_size") 100 | .into_float() 101 | .unwrap_or(default.font_size), 102 | icon_size: ini 103 | .get(section, "icon_size") 104 | .into_float() 105 | .unwrap_or(default.icon_size), 106 | text_margin: ini 107 | .get(section, "text_margin") 108 | .into_insets() 109 | .map(|i| i.into()) 110 | .unwrap_or(default.text_margin), 111 | icon_margin: ini 112 | .get(section, "icon_margin") 113 | .into_insets() 114 | .map(|i| i.into()) 115 | .unwrap_or(default.icon_margin), 116 | spacing: ini 117 | .get(section, "spacing") 118 | .into_float() 119 | .unwrap_or(default.spacing), 120 | background: ini 121 | .get(section, "background") 122 | .into_background() 123 | .unwrap_or(default.background), 124 | border: { 125 | let color = ini 126 | .get(section, "border_color") 127 | .into_color() 128 | .unwrap_or(default.border.color); 129 | let width = ini 130 | .get(section, "border_width") 131 | .into_float() 132 | .unwrap_or(default.border.width); 133 | let radius = ini 134 | .get(section, "border_radius") 135 | .into_insets() 136 | .map(|i| i.into()) 137 | .unwrap_or(default.border.radius); 138 | Border { 139 | color, 140 | width, 141 | radius, 142 | } 143 | }, 144 | } 145 | } 146 | } 147 | 148 | impl PopupConfigOverride { 149 | pub fn update(&mut self, config: &HashMap>) { 150 | if let Some(width) = config 151 | .get("width") 152 | .and_then(|s| s.as_ref().and_then(|v| v.parse().ok())) 153 | { 154 | self.width = Some(width); 155 | } 156 | if let Some(height) = config 157 | .get("height") 158 | .and_then(|s| s.as_ref().and_then(|v| v.parse().ok())) 159 | { 160 | self.height = Some(height); 161 | } 162 | self.fill_content_to_size = config 163 | .get("fill_content_to_size") 164 | .and_then(|s| s.into_bool()); 165 | self.padding = config 166 | .get("padding") 167 | .and_then(|s| s.into_insets().map(|i| i.into())); 168 | self.text_color = config.get("text_color").and_then(|s| s.into_color()); 169 | self.icon_color = config.get("icon_color").and_then(|s| s.into_color()); 170 | self.font_size = config.get("font_size").and_then(|s| s.into_float()); 171 | self.icon_size = config.get("icon_size").and_then(|s| s.into_float()); 172 | self.text_margin = config 173 | .get("text_margin") 174 | .and_then(|s| s.into_insets().map(|i| i.into())); 175 | self.icon_margin = config 176 | .get("icon_margin") 177 | .and_then(|s| s.into_insets().map(|i| i.into())); 178 | self.spacing = config.get("spacing").and_then(|s| s.into_float()); 179 | self.background = config.get("background").and_then(|s| s.into_background()); 180 | self.border = { 181 | let color = config.get("border_color").and_then(|s| s.into_color()); 182 | let width = config.get("border_width").and_then(|s| s.into_float()); 183 | let radius = config 184 | .get("border_radius") 185 | .and_then(|s| s.into_insets().map(|i| i.into())); 186 | if color.is_some() || width.is_some() || radius.is_some() { 187 | Some(Border { 188 | color: color.unwrap_or_default(), 189 | width: width.unwrap_or_default(), 190 | radius: radius.unwrap_or_default(), 191 | }) 192 | } else { 193 | None 194 | } 195 | }; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/config/thrice.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Thrice { 3 | pub left: T, 4 | pub center: T, 5 | pub right: T, 6 | } 7 | 8 | impl From for Thrice { 9 | fn from(value: f32) -> Self { 10 | Self { 11 | left: value, 12 | center: value, 13 | right: value, 14 | } 15 | } 16 | } 17 | 18 | impl From<(f32, f32, f32)> for Thrice { 19 | fn from((left, center, right): (f32, f32, f32)) -> Self { 20 | Self { 21 | left, 22 | center, 23 | right, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/event_action.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/fill.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | widget::{text::Rich, Container, Text}, 3 | Alignment::Center, 4 | Length::Fill, 5 | }; 6 | 7 | use crate::config::anchor::BarAnchor; 8 | 9 | pub trait FillExt { 10 | fn fill(self, anchor: &BarAnchor) -> Self; 11 | fn fillx(self, vertical: bool) -> Self; 12 | fn fill_maybe(self, fill: bool) -> Self; 13 | } 14 | 15 | impl FillExt for Text<'_> { 16 | fn fill(self, anchor: &BarAnchor) -> Self { 17 | self.fillx(anchor.vertical()) 18 | } 19 | fn fillx(self, vertical: bool) -> Self { 20 | match vertical { 21 | true => self.width(Fill), 22 | false => self.height(Fill), 23 | } 24 | .center() 25 | } 26 | fn fill_maybe(self, fill: bool) -> Self { 27 | match fill { 28 | true => self.height(Fill).width(Fill), 29 | false => self, 30 | } 31 | } 32 | } 33 | 34 | impl FillExt for Rich<'_, Link> 35 | where 36 | Link: Clone, 37 | { 38 | fn fill(self, anchor: &BarAnchor) -> Self { 39 | self.fillx(anchor.vertical()) 40 | } 41 | fn fillx(self, vertical: bool) -> Self { 42 | match vertical { 43 | true => self.center(), 44 | false => self.height(Fill).align_y(Center), 45 | } 46 | } 47 | fn fill_maybe(self, fill: bool) -> Self { 48 | match fill { 49 | true => self.height(Fill).width(Fill), 50 | false => self, 51 | } 52 | } 53 | } 54 | 55 | impl FillExt for Container<'_, Message> { 56 | fn fill(self, anchor: &BarAnchor) -> Self { 57 | self.fillx(anchor.vertical()) 58 | } 59 | fn fillx(self, vertical: bool) -> Self { 60 | match vertical { 61 | true => self.width(Fill), 62 | false => self.height(Fill), 63 | } 64 | } 65 | fn fill_maybe(self, fill: bool) -> Self { 66 | match fill { 67 | true => self.height(Fill).width(Fill), 68 | false => self, 69 | } 70 | } 71 | } 72 | 73 | impl FillExt for iced::widget::button::Button<'_, Message> 74 | where 75 | Message: Clone, 76 | { 77 | fn fill(self, anchor: &BarAnchor) -> Self { 78 | self.fillx(anchor.vertical()) 79 | } 80 | fn fillx(self, vertical: bool) -> Self { 81 | match vertical { 82 | true => self.width(Fill), 83 | false => self.height(Fill), 84 | } 85 | } 86 | fn fill_maybe(self, fill: bool) -> Self { 87 | match fill { 88 | true => self.height(Fill).width(Fill), 89 | false => self, 90 | } 91 | } 92 | } 93 | 94 | impl FillExt for crate::button::Button<'_, Message> 95 | where 96 | Message: Clone, 97 | { 98 | fn fill(self, anchor: &BarAnchor) -> Self { 99 | self.fillx(anchor.vertical()) 100 | } 101 | fn fillx(self, vertical: bool) -> Self { 102 | match vertical { 103 | true => self.width(Fill), 104 | false => self.height(Fill), 105 | } 106 | } 107 | fn fill_maybe(self, fill: bool) -> Self { 108 | match fill { 109 | true => self.height(Fill).width(Fill), 110 | false => self, 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub trait UnEscapeString { 2 | /// Unescape special characters like '\n' and '\t' 3 | fn unescape(self) -> Option; 4 | } 5 | 6 | impl UnEscapeString for Option<&Option> { 7 | fn unescape(self) -> Option { 8 | self.and_then(|s| { 9 | s.as_ref() 10 | .map(|s| s.replace(r"\n", "\n").replace(r"\t", "\t")) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | widget::{column, row, Column, Container, Row}, 3 | Alignment, Element, Padding, Pixels, 4 | }; 5 | 6 | use crate::config::anchor::BarAnchor; 7 | 8 | pub trait DynamicAlign { 9 | fn align(self, anchor: &BarAnchor, alignment: Alignment) -> Self; 10 | } 11 | 12 | impl DynamicAlign for Container<'_, Message> { 13 | fn align(self, anchor: &BarAnchor, alignment: Alignment) -> Self { 14 | match anchor.vertical() { 15 | true => self.align_y(alignment), 16 | false => self.align_x(alignment), 17 | } 18 | } 19 | } 20 | 21 | pub enum List<'a, Message, Theme, Renderer> { 22 | Row(Row<'a, Message, Theme, Renderer>), 23 | Column(Column<'a, Message, Theme, Renderer>), 24 | } 25 | 26 | impl<'a, Message, Theme, Renderer> List<'a, Message, Theme, Renderer> 27 | where 28 | Renderer: iced::core::Renderer, 29 | { 30 | pub fn new(anchor: &BarAnchor) -> List<'a, Message, Theme, Renderer> { 31 | match anchor.vertical() { 32 | true => List::Column(Column::new()), 33 | false => List::Row(Row::new()), 34 | } 35 | } 36 | 37 | pub fn with_children( 38 | anchor: &BarAnchor, 39 | children: impl IntoIterator>, 40 | ) -> Self { 41 | match anchor.vertical() { 42 | true => List::Column(Column::with_children(children)), 43 | false => List::Row(Row::with_children(children)), 44 | } 45 | } 46 | 47 | pub fn spacing(self, amount: impl Into) -> List<'a, Message, Theme, Renderer> { 48 | match self { 49 | List::Row(row) => List::Row(row.spacing(amount)), 50 | List::Column(col) => List::Column(col.spacing(amount)), 51 | } 52 | } 53 | 54 | pub fn padding

(self, padding: P) -> List<'a, Message, Theme, Renderer> 55 | where 56 | P: Into, 57 | { 58 | match self { 59 | List::Row(row) => List::Row(row.padding(padding)), 60 | List::Column(col) => List::Column(col.padding(padding)), 61 | } 62 | } 63 | } 64 | 65 | pub fn list<'a, Message, Theme, Renderer>( 66 | anchor: &BarAnchor, 67 | children: impl IntoIterator>, 68 | ) -> List<'a, Message, Theme, Renderer> 69 | where 70 | Renderer: iced::core::Renderer, 71 | { 72 | match anchor.vertical() { 73 | true => List::Column(column(children)), 74 | false => List::Row(row(children)), 75 | } 76 | } 77 | 78 | impl<'a, Message, Theme, Renderer> From> 79 | for Element<'a, Message, Theme, Renderer> 80 | where 81 | Message: 'a, 82 | Theme: 'a, 83 | Renderer: iced::core::Renderer + 'a, 84 | { 85 | fn from(list: List<'a, Message, Theme, Renderer>) -> Self { 86 | match list { 87 | List::Row(row) => Self::new(row), 88 | List::Column(col) => Self::new(col), 89 | } 90 | } 91 | } 92 | 93 | macro_rules! list { 94 | ($anchor:expr) => ( 95 | $crate::list::List::new($anchor) 96 | ); 97 | ($anchor:expr, $($x:expr),+ $(,)?) => ( 98 | $crate::list::List::with_children($anchor, [$(iced::core::Element::from($x)),+]) 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/listeners/hyprland.rs: -------------------------------------------------------------------------------- 1 | use bar_rs_derive::Builder; 2 | use hyprland::{data::Client, event_listener::AsyncEventListener, shared::HyprDataActiveOptional}; 3 | use iced::{futures::SinkExt, stream, Subscription}; 4 | 5 | use crate::{ 6 | config::ConfigEntry, 7 | modules::hyprland::{ 8 | window::update_window, 9 | workspaces::{get_workspaces, HyprWorkspaceMod}, 10 | }, 11 | Message, 12 | }; 13 | 14 | use super::Listener; 15 | 16 | #[derive(Debug, Builder)] 17 | pub struct HyprListener; 18 | 19 | impl Listener for HyprListener { 20 | fn config(&self) -> Vec { 21 | vec![] 22 | } 23 | fn subscription(&self) -> Subscription { 24 | Subscription::run(|| { 25 | stream::channel(1, |mut sender| async move { 26 | let workspaces = get_workspaces(None).await; 27 | sender 28 | .send(Message::update(move |reg| { 29 | let ws = reg.get_module_mut::(); 30 | ws.active = workspaces.0; 31 | ws.open = workspaces.1; 32 | })) 33 | .await 34 | .unwrap_or_else(|err| { 35 | eprintln!("Trying to send workspaces failed with err: {err}"); 36 | }); 37 | if let Ok(window) = Client::get_active_async().await { 38 | update_window(&mut sender, window.map(|w| w.title)).await; 39 | } 40 | 41 | let mut listener = AsyncEventListener::new(); 42 | 43 | let senderx = sender.clone(); 44 | listener.add_active_window_changed_handler(move |data| { 45 | let mut sender = senderx.clone(); 46 | Box::pin(async move { 47 | update_window(&mut sender, data.map(|window| window.title)).await; 48 | }) 49 | }); 50 | 51 | let senderx = sender.clone(); 52 | listener.add_workspace_changed_handler(move |data| { 53 | let mut sender = senderx.clone(); 54 | Box::pin(async move { 55 | let workspaces = get_workspaces(Some(data.id)).await; 56 | sender 57 | .send(Message::update(move |reg| { 58 | let ws = reg.get_module_mut::(); 59 | ws.active = workspaces.0; 60 | ws.open = workspaces.1; 61 | })) 62 | .await 63 | .unwrap_or_else(|err| { 64 | eprintln!("Trying to send workspaces failed with err: {err}"); 65 | }); 66 | }) 67 | }); 68 | 69 | listener 70 | .start_listener_async() 71 | .await 72 | .expect("Failed to listen for hyprland events"); 73 | }) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/listeners/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, fmt::Debug}; 2 | 3 | use downcast_rs::{impl_downcast, Downcast}; 4 | use hyprland::HyprListener; 5 | use iced::Subscription; 6 | use niri::NiriListener; 7 | use reload::ReloadListener; 8 | use wayfire::WayfireListener; 9 | 10 | use crate::{config::ConfigEntry, registry::Registry, Message}; 11 | 12 | pub mod hyprland; 13 | pub mod niri; 14 | mod reload; 15 | pub mod wayfire; 16 | 17 | pub trait Listener: Any + Debug + Send + Sync + Downcast { 18 | fn config(&self) -> Vec { 19 | vec![] 20 | } 21 | fn subscription(&self) -> Subscription; 22 | } 23 | impl_downcast!(Listener); 24 | 25 | pub fn register_listeners(registry: &mut Registry) { 26 | registry.register_listener::(); 27 | registry.register_listener::(); 28 | registry.register_listener::(); 29 | registry.register_listener::(); 30 | } 31 | -------------------------------------------------------------------------------- /src/listeners/niri.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env, sync::Arc}; 2 | 3 | use bar_rs_derive::Builder; 4 | use iced::{futures::SinkExt, stream, Subscription}; 5 | use niri_ipc::{socket::SOCKET_PATH_ENV, Event, Request}; 6 | use tokio::{ 7 | io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, 8 | net::UnixStream, 9 | sync::mpsc, 10 | }; 11 | 12 | use crate::{ 13 | config::ConfigEntry, 14 | modules::niri::{NiriWindowMod, NiriWorkspaceMod}, 15 | registry::Registry, 16 | Message, UpdateFn, 17 | }; 18 | 19 | use super::Listener; 20 | 21 | #[derive(Debug, Builder)] 22 | pub struct NiriListener; 23 | 24 | impl Listener for NiriListener { 25 | fn config(&self) -> Vec { 26 | vec![] 27 | } 28 | fn subscription(&self) -> Subscription { 29 | Subscription::run(|| { 30 | stream::channel(1, |mut sender| async move { 31 | let (sx, mut rx) = mpsc::channel(1); 32 | sender 33 | .send(Message::GetReceiver(sx, |reg| { 34 | reg.get_module::().sender.subscribe() 35 | })) 36 | .await 37 | .unwrap(); 38 | let mut receiver = rx.recv().await.unwrap(); 39 | drop(rx); 40 | let socket_path = env::var(SOCKET_PATH_ENV).expect("No niri socket was found!"); 41 | let mut socket = UnixStream::connect(&socket_path).await.unwrap(); 42 | let mut buf = serde_json::to_string(&Request::EventStream).unwrap(); 43 | socket.write_all(buf.as_bytes()).await.unwrap(); 44 | socket.shutdown().await.unwrap(); 45 | let mut reader = BufReader::new(socket); 46 | reader 47 | .read_line(&mut buf) 48 | .await 49 | .map_err(|e| { 50 | eprintln!("Failed to build an event stream with niri: {e}"); 51 | }) 52 | .ok(); 53 | buf.clear(); 54 | loop { 55 | tokio::select! { 56 | Ok(_) = reader.read_line(&mut buf) => { 57 | let reply = serde_json::from_str::(&buf); 58 | type F = Box; 59 | let msg: Option = match reply { 60 | Ok(event) => match event { 61 | Event::WorkspacesChanged { workspaces } => Some(Box::new(move |reg| { 62 | let active_ws = workspaces 63 | .iter() 64 | .find_map(|ws| ws.is_focused.then_some(ws.id)); 65 | let mut workspaces: HashMap> = 66 | workspaces.into_iter().fold(HashMap::new(), |mut acc, ws| { 67 | match acc 68 | .get_mut(ws.output.as_ref().unwrap_or(&String::new())) 69 | { 70 | Some(workspaces) => workspaces.push(ws), 71 | None => { 72 | acc.insert( 73 | ws.output.clone().unwrap_or_default(), 74 | vec![ws], 75 | ); 76 | } 77 | } 78 | acc 79 | }); 80 | for (_, workspaces) in workspaces.iter_mut() { 81 | workspaces.sort_by(|a, b| a.idx.cmp(&b.idx)); 82 | } 83 | let ws_mod = reg.get_module_mut::(); 84 | ws_mod.focused = active_ws.unwrap(); 85 | ws_mod.workspaces = workspaces 86 | })), 87 | Event::WorkspaceActivated { id, focused } => match focused { 88 | true => Some(Box::new(move |reg| { 89 | reg.get_module_mut::().focused = id 90 | })), 91 | false => None, 92 | }, 93 | Event::WindowsChanged { windows } => Some(Box::new(move |reg| { 94 | let window_mod = reg.get_module_mut::(); 95 | window_mod.focused = 96 | windows.iter().find(|w| w.is_focused).map(|w| w.id); 97 | window_mod.windows = windows 98 | .into_iter() 99 | .map(|w| (w.id, w)) 100 | .collect() 101 | })), 102 | Event::WindowFocusChanged { id } => Some(Box::new(move |reg| { 103 | reg.get_module_mut::().focused = id 104 | })), 105 | Event::WindowOpenedOrChanged { window } => Some(Box::new(move |reg| { 106 | let window_mod = reg.get_module_mut::(); 107 | if window.is_focused { 108 | window_mod.focused = Some(window.id); 109 | } 110 | window_mod 111 | .windows 112 | .insert(window.id, window); 113 | })), 114 | Event::WindowClosed { id } => Some(Box::new(move |reg| { 115 | reg.get_module_mut::().windows.remove(&id); 116 | })), 117 | _ => None, 118 | }, 119 | Err(err) => { 120 | eprintln!("Failed to decode Niri IPC msg as Event: {err}"); 121 | None 122 | } 123 | }; 124 | if let Some(msg) = msg { 125 | sender 126 | .send(Message::Update(Arc::new(UpdateFn(msg)))) 127 | .await 128 | .unwrap(); 129 | } 130 | buf.clear(); 131 | } 132 | Ok(action) = receiver.recv() => { 133 | if let Some(id) = action.downcast_ref::() { 134 | let mut socket = UnixStream::connect(&socket_path).await.unwrap(); 135 | let buf = serde_json::to_string(&Request::Action(niri_ipc::Action::FocusWorkspace { reference: niri_ipc::WorkspaceReferenceArg::Id(*id) })).unwrap(); 136 | socket.write_all(buf.as_bytes()).await.unwrap(); 137 | socket.shutdown().await.unwrap(); 138 | } 139 | } 140 | } 141 | } 142 | }) 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/listeners/reload.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf, time::Duration}; 2 | 3 | use bar_rs_derive::Builder; 4 | use iced::{ 5 | futures::{executor, SinkExt}, 6 | stream, Subscription, 7 | }; 8 | use notify::{ 9 | event::{ModifyKind, RemoveKind}, 10 | Config, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, 11 | }; 12 | use tokio::time::sleep; 13 | 14 | use crate::{ 15 | config::{get_config, ConfigEntry}, 16 | Message, 17 | }; 18 | 19 | use super::Listener; 20 | 21 | #[derive(Debug, Builder)] 22 | pub struct ReloadListener; 23 | 24 | impl Listener for ReloadListener { 25 | fn config(&self) -> Vec { 26 | vec![ConfigEntry::new("general", "hot_reloading", true)] 27 | } 28 | 29 | fn subscription(&self) -> Subscription { 30 | Subscription::run(|| { 31 | stream::channel(1, |mut sender| async move { 32 | let config_path = get_config(&mut sender).await.0; 33 | let config_pathx = config_path.clone(); 34 | 35 | let mut watcher = RecommendedWatcher::new( 36 | move |result: Result| { 37 | let event = result.unwrap(); 38 | 39 | if event.paths.contains(&config_pathx) && (matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) 40 | || matches!(event.kind, EventKind::Remove(RemoveKind::File))) 41 | { 42 | executor::block_on(async { 43 | sender.send(Message::ReloadConfig) 44 | .await 45 | .unwrap_or_else(|err| { 46 | eprintln!("Trying to request config reload failed with err: {err}"); 47 | }); 48 | }); 49 | } 50 | }, 51 | Config::default() 52 | ).unwrap(); 53 | 54 | watcher 55 | .watch( 56 | config_path.parent().unwrap_or(&default_config_path()), 57 | RecursiveMode::Recursive, 58 | ) 59 | .unwrap(); 60 | 61 | loop { 62 | sleep(Duration::from_secs(1)).await; 63 | } 64 | }) 65 | }) 66 | } 67 | } 68 | 69 | fn default_config_path() -> PathBuf { 70 | format!( 71 | "{}/.config/bar-rs", 72 | env::var("HOME").expect("Env $HOME is not set?") 73 | ) 74 | .into() 75 | } 76 | -------------------------------------------------------------------------------- /src/listeners/wayfire.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, time::Duration}; 2 | 3 | use bar_rs_derive::Builder; 4 | use iced::{ 5 | futures::{channel::mpsc::Sender, SinkExt}, 6 | stream, Subscription, 7 | }; 8 | use serde_json::Value; 9 | use tokio::time::sleep; 10 | use wayfire_rs::ipc::WayfireSocket; 11 | 12 | use crate::{ 13 | modules::wayfire::{WayfireWindowMod, WayfireWorkspaceMod}, 14 | Message, 15 | }; 16 | 17 | use super::Listener; 18 | 19 | #[derive(Debug, Builder)] 20 | pub struct WayfireListener; 21 | 22 | async fn send_first_values( 23 | socket: &mut WayfireSocket, 24 | sender: &mut Sender, 25 | ) -> Result<(), Box> { 26 | let title = socket.get_focused_view().await.ok().map(|v| v.title); 27 | let workspace = socket.get_focused_output().await?.workspace; 28 | sender 29 | .send(Message::update(move |reg| { 30 | reg.get_module_mut::().title = title; 31 | reg.get_module_mut::().active = (workspace.x, workspace.y); 32 | })) 33 | .await?; 34 | Ok(()) 35 | } 36 | 37 | impl Listener for WayfireListener { 38 | fn subscription(&self) -> iced::Subscription { 39 | Subscription::run(|| { 40 | stream::channel(1, |mut sender| async move { 41 | let Ok(mut socket) = WayfireSocket::connect().await else { 42 | eprintln!("Failed to connect to wayfire socket"); 43 | return; 44 | }; 45 | 46 | send_first_values(&mut socket, &mut sender) 47 | .await 48 | .unwrap_or_else(|e| { 49 | eprintln!("Failed to send initial wayfire module data: {e}") 50 | }); 51 | 52 | socket 53 | .watch(Some(vec![ 54 | "wset-workspace-changed".to_string(), 55 | "view-focused".to_string(), 56 | "view-title-changed".to_string(), 57 | "view-unmapped".to_string(), 58 | ])) 59 | .await 60 | .expect("Failed to watch wayfire socket (but we're connected already)!"); 61 | 62 | let mut active_window = None; 63 | 64 | while let Ok(Value::Object(msg)) = socket.read_message().await { 65 | match msg.get("event") { 66 | Some(Value::String(val)) if val == "wset-workspace-changed" => { 67 | let Some(Value::Object(obj)) = msg.get("new-workspace") else { 68 | continue; 69 | }; 70 | 71 | // serde_json::Value::Object => (i64, i64) 72 | if let Some((x, y)) = obj.get("x").and_then(|x| { 73 | x.as_i64().and_then(|x| { 74 | obj.get("y").and_then(|y| y.as_i64().map(|y| (x, y))) 75 | }) 76 | }) { 77 | // With this wayfire will send an additional msg, see the None 78 | // match arm... No idea why tho 79 | sleep(Duration::from_millis(150)).await; 80 | let title = socket.get_focused_view().await.ok().map(|v| v.title); 81 | active_window = title.clone(); 82 | sender 83 | .send(Message::update(move |reg| { 84 | reg.get_module_mut::().active = (x, y); 85 | reg.get_module_mut::().title = title 86 | })) 87 | .await 88 | .unwrap(); 89 | } 90 | } 91 | 92 | Some(Value::String(val)) 93 | if val == "view-focused" || val == "view-title-changed" => 94 | { 95 | let Some(Value::String(title)) = msg 96 | .get("view") 97 | .and_then(|v| v.as_object()) 98 | .and_then(|o| o.get("title").map(|t| t.to_owned())) 99 | else { 100 | continue; 101 | }; 102 | match Some(&title) == active_window.as_ref() { 103 | true => continue, 104 | false => active_window = Some(title.clone()), 105 | } 106 | sender 107 | .send(Message::update(move |reg| { 108 | reg.get_module_mut::().title = Some(title) 109 | })) 110 | .await 111 | .unwrap(); 112 | } 113 | 114 | // That sure seems useless, but we need the view-unmapped events that 115 | // somehow end up in the None match arm 116 | Some(Value::String(val)) if val == "view-unmapped" => {} 117 | 118 | None => { 119 | if let Some("ok") = msg.get("result").and_then(|r| r.as_str()) { 120 | let Some(title) = msg.get("info").map(|info| { 121 | if info.is_null() { 122 | return None; 123 | } 124 | info.as_object() 125 | .and_then(|obj| obj.get("title")) 126 | .and_then(|t| t.as_str()) 127 | .map(|s| s.to_string()) 128 | }) else { 129 | continue; 130 | }; 131 | match title == active_window { 132 | true => continue, 133 | false => active_window = title.clone(), 134 | } 135 | sender 136 | .send(Message::update(move |reg| { 137 | reg.get_module_mut::().title = title 138 | })) 139 | .await 140 | .unwrap(); 141 | }; 142 | } 143 | 144 | _ => eprintln!("got unknown event from wayfire ipc: {msg:#?}"), 145 | } 146 | } 147 | 148 | eprintln!("Failed to read messages from the Wayfire socket!"); 149 | }) 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/modules/cpu.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | fs::File, 4 | hash::Hash, 5 | io::{self, BufRead, BufReader}, 6 | num, 7 | time::Duration, 8 | }; 9 | 10 | use bar_rs_derive::Builder; 11 | use handlebars::Handlebars; 12 | use iced::widget::{button::Style, container, scrollable, Container, Text}; 13 | use iced::{futures::SinkExt, stream, widget::text, Element, Subscription}; 14 | use tokio::time::sleep; 15 | 16 | use crate::{ 17 | button::button, 18 | config::{ 19 | anchor::BarAnchor, 20 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 21 | popup_config::{PopupConfig, PopupConfigOverride}, 22 | }, 23 | fill::FillExt, 24 | helpers::UnEscapeString, 25 | impl_on_click, impl_wrapper, Message, NERD_FONT, 26 | }; 27 | 28 | use super::Module; 29 | 30 | #[derive(Debug, Builder)] 31 | pub struct CpuMod { 32 | avg_usage: CpuStats, 33 | cores: BTreeMap>, 34 | cfg_override: ModuleConfigOverride, 35 | popup_cfg_override: PopupConfigOverride, 36 | icon: Option, 37 | } 38 | 39 | impl Default for CpuMod { 40 | fn default() -> Self { 41 | Self { 42 | avg_usage: Default::default(), 43 | cores: BTreeMap::new(), 44 | cfg_override: Default::default(), 45 | popup_cfg_override: PopupConfigOverride { 46 | width: Some(150), 47 | height: Some(350), 48 | ..Default::default() 49 | }, 50 | icon: None, 51 | } 52 | } 53 | } 54 | 55 | impl Module for CpuMod { 56 | fn name(&self) -> String { 57 | "cpu".to_string() 58 | } 59 | 60 | fn view( 61 | &self, 62 | config: &LocalModuleConfig, 63 | popup_config: &PopupConfig, 64 | anchor: &BarAnchor, 65 | _handlebars: &Handlebars, 66 | ) -> Element { 67 | button( 68 | list![ 69 | anchor, 70 | container( 71 | text!("{}", self.icon.as_ref().unwrap_or(&"󰻠".to_string())) 72 | .fill(anchor) 73 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 74 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 75 | .font(NERD_FONT) 76 | ) 77 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 78 | container( 79 | text!["{}%", self.avg_usage.all] 80 | .fill(anchor) 81 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 82 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 83 | ) 84 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 85 | ] 86 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)), 87 | ) 88 | .on_event_with(Message::popup::( 89 | self.popup_cfg_override.width.unwrap_or(popup_config.width), 90 | self.popup_cfg_override 91 | .height 92 | .unwrap_or(popup_config.height), 93 | anchor, 94 | )) 95 | .style(|_, _| Style::default()) 96 | .into() 97 | } 98 | 99 | fn popup_view<'a>( 100 | &'a self, 101 | config: &'a PopupConfig, 102 | template: &Handlebars, 103 | ) -> Element<'a, Message> { 104 | let fmt_text = |text: Text<'a>| -> Container<'a, Message> { 105 | container( 106 | text.size( 107 | self.popup_cfg_override 108 | .font_size 109 | .unwrap_or(config.font_size), 110 | ) 111 | .color( 112 | self.popup_cfg_override 113 | .text_color 114 | .unwrap_or(config.text_color), 115 | ), 116 | ) 117 | .padding( 118 | self.popup_cfg_override 119 | .text_margin 120 | .unwrap_or(config.text_margin), 121 | ) 122 | }; 123 | let ctx = BTreeMap::from([ 124 | ("total", self.avg_usage.all.to_string()), 125 | ("user", self.avg_usage.user.to_string()), 126 | ("system", self.avg_usage.system.to_string()), 127 | ("guest", self.avg_usage.guest.to_string()), 128 | ( 129 | "cores", 130 | self.cores 131 | .iter() 132 | .map(|(ty, stats)| { 133 | let core = BTreeMap::from([ 134 | ("index", ty.get_core_index().to_string()), 135 | ("total", stats.all.to_string()), 136 | ("user", stats.user.to_string()), 137 | ("system", stats.system.to_string()), 138 | ("guest", stats.guest.to_string()), 139 | ]); 140 | template 141 | .render("cpu_core", &core) 142 | .map_err(|e| eprintln!("Failed to render cpu core stats: {e}")) 143 | .unwrap_or_default() 144 | }) 145 | .collect::>() 146 | .join("\n"), 147 | ), 148 | ]); 149 | let format = template 150 | .render("cpu", &ctx) 151 | .map_err(|e| eprintln!("Failed to render cpu stats: {e}")) 152 | .unwrap_or_default(); 153 | container(scrollable(fmt_text(text(format)))) 154 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding)) 155 | .style(|_| container::Style { 156 | background: Some( 157 | self.popup_cfg_override 158 | .background 159 | .unwrap_or(config.background), 160 | ), 161 | border: self.popup_cfg_override.border.unwrap_or(config.border), 162 | ..Default::default() 163 | }) 164 | .fill_maybe( 165 | self.popup_cfg_override 166 | .fill_content_to_size 167 | .unwrap_or(config.fill_content_to_size), 168 | ) 169 | .into() 170 | } 171 | 172 | impl_wrapper!(); 173 | 174 | fn read_config( 175 | &mut self, 176 | config: &HashMap>, 177 | popup_config: &HashMap>, 178 | templates: &mut Handlebars, 179 | ) { 180 | self.cfg_override = config.into(); 181 | self.popup_cfg_override.update(popup_config); 182 | self.icon = config.get("icon").and_then(|v| v.clone()); 183 | templates 184 | .register_template_string( 185 | "cpu", 186 | popup_config 187 | .get("format") 188 | .unescape() 189 | .unwrap_or("Total: {{total}}%\nUser: {{user}}%\nSystem: {{system}}%\nGuest: {{guest}}%\n{{cores}}".to_string()), 190 | ) 191 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}")); 192 | templates 193 | .register_template_string( 194 | "cpu_core", 195 | popup_config 196 | .get("format_core") 197 | .unescape() 198 | .unwrap_or("Core {{index}}: {{total}}%".to_string()), 199 | ) 200 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}")); 201 | } 202 | 203 | impl_on_click!(); 204 | 205 | fn subscription(&self) -> Option> { 206 | Some(Subscription::run(|| { 207 | stream::channel(1, |mut sender| async move { 208 | let interval: u64 = 500; 209 | let gap: u64 = 2000; 210 | loop { 211 | let Ok(mut raw_stats1) = read_raw_stats() 212 | .map_err(|e| eprintln!("Failed to read cpu stats from /proc/stat: {e:?}")) 213 | else { 214 | return; 215 | }; 216 | sleep(Duration::from_millis(interval)).await; 217 | let Ok(mut raw_stats2) = read_raw_stats() else { 218 | eprintln!("Failed to read cpu stats from /proc/stat"); 219 | return; 220 | }; 221 | 222 | let avg = ( 223 | &raw_stats1.remove(&CpuType::All).unwrap(), 224 | &raw_stats2.remove(&CpuType::All).unwrap(), 225 | ) 226 | .into(); 227 | 228 | let cores = raw_stats1 229 | .into_iter() 230 | .filter_map(|(ty, stats1)| { 231 | raw_stats2 232 | .get(&ty) 233 | .map(|stats2| (ty, (&stats1, stats2).into())) 234 | }) 235 | .collect(); 236 | 237 | sender 238 | .send(Message::update(move |reg| { 239 | let m = reg.get_module_mut::(); 240 | m.avg_usage = avg; 241 | m.cores = cores 242 | })) 243 | .await 244 | .unwrap_or_else(|err| { 245 | eprintln!("Trying to send cpu_usage failed with err: {err}"); 246 | }); 247 | 248 | sleep(Duration::from_millis(gap)).await; 249 | } 250 | }) 251 | })) 252 | } 253 | } 254 | 255 | #[derive(Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] 256 | enum CpuType { 257 | #[default] 258 | All, 259 | Core(u8), 260 | } 261 | 262 | impl CpuType { 263 | fn get_core_index(&self) -> u8 { 264 | match self { 265 | CpuType::All => 255, 266 | CpuType::Core(index) => *index, 267 | } 268 | } 269 | } 270 | 271 | impl From<&str> for CpuType { 272 | fn from(value: &str) -> Self { 273 | value 274 | .strip_prefix("cpu") 275 | .and_then(|v| v.parse().ok().map(Self::Core)) 276 | .unwrap_or(Self::All) 277 | } 278 | } 279 | 280 | #[derive(Default, Debug)] 281 | struct CpuStats { 282 | all: T, 283 | user: T, 284 | system: T, 285 | guest: T, 286 | total: T, 287 | } 288 | 289 | impl TryFrom<&str> for CpuStats { 290 | type Error = ReadError; 291 | fn try_from(value: &str) -> Result { 292 | let values: Result, num::ParseIntError> = 293 | value.split_whitespace().map(|p| p.parse()).collect(); 294 | // Documentation can be found at 295 | // https://docs.kernel.org/filesystems/proc.html#miscellaneous-kernel-statistics-in-proc-stat 296 | let [user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice] = 297 | values?[..] 298 | else { 299 | return Err(ReadError::ValueListInvalid); 300 | }; 301 | let all = user + nice + system + irq + softirq; 302 | Ok(CpuStats { 303 | all, 304 | user: user + nice, 305 | system, 306 | guest: guest + guest_nice, 307 | total: all + idle + iowait + steal, 308 | }) 309 | } 310 | } 311 | 312 | impl From<(&CpuStats, &CpuStats)> for CpuStats { 313 | fn from((stats1, stats2): (&CpuStats, &CpuStats)) -> Self { 314 | let delta_all = stats2.all - stats1.all; 315 | let delta_user = stats2.user - stats1.user; 316 | let delta_system = stats2.system - stats1.system; 317 | let delta_guest = stats2.guest - stats1.guest; 318 | let delta_total = stats2.total - stats1.total; 319 | if delta_total == 0 { 320 | return Self::default(); 321 | } 322 | Self { 323 | all: ((delta_all as f32 / delta_total as f32) * 100.) as u8, 324 | user: ((delta_user as f32 / delta_total as f32) * 100.) as u8, 325 | system: ((delta_system as f32 / delta_total as f32) * 100.) as u8, 326 | guest: ((delta_guest as f32 / delta_total as f32) * 100.) as u8, 327 | total: 0, 328 | } 329 | } 330 | } 331 | 332 | fn read_raw_stats() -> Result>, ReadError> { 333 | let file = File::open("/proc/stat")?; 334 | let reader = BufReader::new(file); 335 | let lines = reader.lines().filter_map(|l| { 336 | l.ok().and_then(|line| { 337 | let (cpu, data) = line.split_once(' ')?; 338 | Some((cpu.into(), data.try_into().ok()?)) 339 | }) 340 | }); 341 | Ok(lines.collect()) 342 | } 343 | 344 | #[allow(dead_code)] 345 | #[derive(Debug)] 346 | enum ReadError { 347 | IoError(io::Error), 348 | ParseError(num::ParseIntError), 349 | ValueListInvalid, 350 | } 351 | 352 | impl From for ReadError { 353 | fn from(value: io::Error) -> Self { 354 | Self::IoError(value) 355 | } 356 | } 357 | 358 | impl From for ReadError { 359 | fn from(value: num::ParseIntError) -> Self { 360 | Self::ParseError(value) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/modules/date.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bar_rs_derive::Builder; 4 | use chrono::Local; 5 | use handlebars::Handlebars; 6 | use iced::widget::{container, text}; 7 | use iced::Element; 8 | 9 | use crate::config::popup_config::PopupConfig; 10 | use crate::{ 11 | config::{ 12 | anchor::BarAnchor, 13 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 14 | }, 15 | fill::FillExt, 16 | Message, NERD_FONT, 17 | }; 18 | use crate::{impl_on_click, impl_wrapper}; 19 | 20 | use super::Module; 21 | 22 | #[derive(Debug, Builder)] 23 | pub struct DateMod { 24 | cfg_override: ModuleConfigOverride, 25 | icon: String, 26 | fmt: String, 27 | } 28 | 29 | impl Default for DateMod { 30 | fn default() -> Self { 31 | Self { 32 | cfg_override: Default::default(), 33 | icon: "".to_string(), 34 | fmt: "%a, %d. %b".to_string(), 35 | } 36 | } 37 | } 38 | 39 | impl Module for DateMod { 40 | fn name(&self) -> String { 41 | "date".to_string() 42 | } 43 | 44 | fn view( 45 | &self, 46 | config: &LocalModuleConfig, 47 | _popup_config: &PopupConfig, 48 | anchor: &BarAnchor, 49 | _handlebars: &Handlebars, 50 | ) -> Element { 51 | let time = Local::now(); 52 | list![ 53 | anchor, 54 | container( 55 | text!("{}", self.icon) 56 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 57 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 58 | .font(NERD_FONT) 59 | .fill(anchor) 60 | ) 61 | .fill(anchor) 62 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 63 | container( 64 | text!("{}", time.format(&self.fmt)) 65 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 66 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 67 | .fill(anchor) 68 | ) 69 | .fill(anchor) 70 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 71 | ] 72 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 73 | .into() 74 | } 75 | 76 | impl_wrapper!(); 77 | 78 | fn read_config( 79 | &mut self, 80 | config: &HashMap>, 81 | _popup_config: &HashMap>, 82 | _templates: &mut Handlebars, 83 | ) { 84 | let default = Self::default(); 85 | self.cfg_override = config.into(); 86 | self.icon = config 87 | .get("icon") 88 | .and_then(|v| v.clone()) 89 | .unwrap_or(default.icon); 90 | self.fmt = config 91 | .get("format") 92 | .and_then(|v| v.clone()) 93 | .unwrap_or(default.fmt); 94 | } 95 | 96 | impl_on_click!(); 97 | } 98 | -------------------------------------------------------------------------------- /src/modules/disk_usage.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | ffi::CString, 4 | mem, 5 | }; 6 | 7 | use bar_rs_derive::Builder; 8 | use handlebars::Handlebars; 9 | use iced::{ 10 | widget::{button::Style, container, scrollable, text, Container, Text}, 11 | Element, 12 | }; 13 | use libc::{__errno_location, statvfs}; 14 | 15 | use crate::{ 16 | button::button, 17 | config::{ 18 | anchor::BarAnchor, 19 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 20 | popup_config::{PopupConfig, PopupConfigOverride}, 21 | }, 22 | fill::FillExt, 23 | helpers::UnEscapeString, 24 | impl_on_click, impl_wrapper, Message, NERD_FONT, 25 | }; 26 | 27 | use super::Module; 28 | 29 | #[derive(Debug, Builder, Default)] 30 | pub struct DiskUsageMod { 31 | icon: Option, 32 | cfg_override: ModuleConfigOverride, 33 | popup_cfg_override: PopupConfigOverride, 34 | path: CString, 35 | } 36 | 37 | #[derive(Debug, Default)] 38 | /// All values are represented in megabytes, except the `_perc` fields 39 | struct FileSystemStats { 40 | total: u64, 41 | free: u64, 42 | used: u64, 43 | /// Free space in percentage points 44 | free_perc: u8, 45 | /// Used space in percentage points 46 | used_perc: u8, 47 | } 48 | 49 | impl From for BTreeMap<&'static str, u64> { 50 | fn from(value: FileSystemStats) -> Self { 51 | BTreeMap::from([ 52 | ("total", value.total), 53 | ("total_gb", value.total / 1000), 54 | ("used", value.used), 55 | ("used_gb", value.used / 1000), 56 | ("free", value.free), 57 | ("free_gb", value.free / 1000), 58 | ("used_perc", value.used_perc.into()), 59 | ("free_perc", value.free_perc.into()), 60 | ]) 61 | } 62 | } 63 | 64 | impl From for FileSystemStats { 65 | fn from(value: statvfs) -> Self { 66 | let free_perc = (value.f_bavail as f32 / value.f_blocks as f32 * 100.) as u8; 67 | Self { 68 | total: value.f_blocks * value.f_frsize / 1_000_000, 69 | free: value.f_bavail * value.f_frsize / 1_000_000, 70 | used: (value.f_blocks - value.f_bavail) * value.f_frsize / 1_000_000, 71 | free_perc, 72 | used_perc: 100 - free_perc, 73 | } 74 | } 75 | } 76 | 77 | impl Module for DiskUsageMod { 78 | fn name(&self) -> String { 79 | "disk_usage".to_string() 80 | } 81 | 82 | fn view( 83 | &self, 84 | config: &LocalModuleConfig, 85 | popup_config: &PopupConfig, 86 | anchor: &BarAnchor, 87 | handlebars: &Handlebars, 88 | ) -> Element { 89 | let Ok(stats) = get_stats(&self.path) else { 90 | return "Error".into(); 91 | }; 92 | let ctx: BTreeMap<&'static str, u64> = stats.into(); 93 | let format = handlebars 94 | .render("disk_usage", &ctx) 95 | .map_err(|e| eprintln!("Failed to render disk_usage stats: {e}")) 96 | .unwrap_or_default(); 97 | button( 98 | list![ 99 | anchor, 100 | container( 101 | text!("{}", self.icon.as_ref().unwrap_or(&"󰦚".to_string())) 102 | .fill(anchor) 103 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 104 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 105 | .font(NERD_FONT) 106 | ) 107 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 108 | container( 109 | text(format) 110 | .fill(anchor) 111 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 112 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 113 | ) 114 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 115 | ] 116 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)), 117 | ) 118 | .on_event_with(Message::popup::( 119 | self.popup_cfg_override.width.unwrap_or(popup_config.width), 120 | self.popup_cfg_override 121 | .height 122 | .unwrap_or(popup_config.height), 123 | anchor, 124 | )) 125 | .style(|_, _| Style::default()) 126 | .into() 127 | } 128 | 129 | fn popup_view<'a>( 130 | &'a self, 131 | config: &'a PopupConfig, 132 | template: &Handlebars, 133 | ) -> Element<'a, Message> { 134 | let fmt_text = |text: Text<'a>| -> Container<'a, Message> { 135 | container( 136 | text.size( 137 | self.popup_cfg_override 138 | .font_size 139 | .unwrap_or(config.font_size), 140 | ) 141 | .color( 142 | self.popup_cfg_override 143 | .text_color 144 | .unwrap_or(config.text_color), 145 | ), 146 | ) 147 | .padding( 148 | self.popup_cfg_override 149 | .text_margin 150 | .unwrap_or(config.text_margin), 151 | ) 152 | }; 153 | let Ok(stats) = get_stats(&self.path) else { 154 | return "Error".into(); 155 | }; 156 | let ctx: BTreeMap<&'static str, u64> = stats.into(); 157 | let format = template 158 | .render("disk_usage_popup", &ctx) 159 | .map_err(|e| eprintln!("Failed to render disk_usage stats: {e}")) 160 | .unwrap_or_default(); 161 | container(scrollable(fmt_text(text(format)))) 162 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding)) 163 | .style(|_| container::Style { 164 | background: Some( 165 | self.popup_cfg_override 166 | .background 167 | .unwrap_or(config.background), 168 | ), 169 | border: self.popup_cfg_override.border.unwrap_or(config.border), 170 | ..Default::default() 171 | }) 172 | .fill_maybe( 173 | self.popup_cfg_override 174 | .fill_content_to_size 175 | .unwrap_or(config.fill_content_to_size), 176 | ) 177 | .into() 178 | } 179 | 180 | impl_wrapper!(); 181 | 182 | fn read_config( 183 | &mut self, 184 | config: &HashMap>, 185 | popup_config: &HashMap>, 186 | templates: &mut Handlebars, 187 | ) { 188 | self.cfg_override = config.into(); 189 | self.popup_cfg_override.update(popup_config); 190 | self.icon = config.get("icon").and_then(|v| v.clone()); 191 | self.path = config 192 | .get("path") 193 | .and_then(|v| v.clone().and_then(|v| CString::new(v).ok())) 194 | .unwrap_or_else(|| CString::new("/").unwrap()); 195 | templates 196 | .register_template_string( 197 | "disk_usage", 198 | config 199 | .get("format") 200 | .unescape() 201 | .unwrap_or("{{used_perc}}%".to_string()), 202 | ) 203 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}")); 204 | templates 205 | .register_template_string( 206 | "disk_usage_popup", 207 | popup_config 208 | .get("format") 209 | .unescape() 210 | .unwrap_or("Total: {{total_gb}} GB\nUsed: {{used_gb}} GB ({{used_perc}}%)\nFree: {{free_gb}} GB ({{free_perc}}%)".to_string()), 211 | ) 212 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}")); 213 | } 214 | 215 | impl_on_click!(); 216 | } 217 | 218 | /// Get file system statistics using the statvfs system call, see 219 | /// https://man7.org/linux/man-pages/man3/statvfs.3.html 220 | fn get_stats(path: &CString) -> Result { 221 | let mut raw_stats: statvfs = unsafe { mem::zeroed() }; 222 | if unsafe { libc::statvfs(path.as_ptr(), &mut raw_stats) } != 0 { 223 | eprintln!( 224 | "Got an error while executing the statvfs syscall: {}", 225 | unsafe { *__errno_location() } 226 | ); 227 | return Err(()); 228 | } 229 | Ok(raw_stats.into()) 230 | } 231 | -------------------------------------------------------------------------------- /src/modules/hyprland/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod window; 2 | pub mod workspaces; 3 | -------------------------------------------------------------------------------- /src/modules/hyprland/window.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use iced::widget::{container, rich_text, span, text}; 6 | use iced::{ 7 | futures::{channel::mpsc::Sender, SinkExt}, 8 | Element, 9 | }; 10 | 11 | use crate::config::popup_config::PopupConfig; 12 | use crate::tooltip::ElementExt; 13 | use crate::{ 14 | config::{ 15 | anchor::BarAnchor, 16 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 17 | }, 18 | fill::FillExt, 19 | listeners::hyprland::HyprListener, 20 | modules::{require_listener, Message, Module}, 21 | }; 22 | use crate::{impl_on_click, impl_wrapper}; 23 | 24 | #[derive(Debug, Builder)] 25 | pub struct HyprWindowMod { 26 | title: Option, 27 | max_length: usize, 28 | cfg_override: ModuleConfigOverride, 29 | } 30 | 31 | impl Default for HyprWindowMod { 32 | fn default() -> Self { 33 | Self { 34 | title: None, 35 | max_length: 25, 36 | cfg_override: Default::default(), 37 | } 38 | } 39 | } 40 | 41 | impl HyprWindowMod { 42 | pub fn get_title(&self) -> Option { 43 | self.title 44 | .as_ref() 45 | .map(|title| match title.len() > self.max_length { 46 | true => format!( 47 | "{}...", 48 | title.chars().take(self.max_length - 3).collect::() 49 | ), 50 | false => title.to_string(), 51 | }) 52 | } 53 | } 54 | 55 | impl Module for HyprWindowMod { 56 | fn name(&self) -> String { 57 | "hyprland.window".to_string() 58 | } 59 | 60 | fn active(&self) -> bool { 61 | self.title.is_some() 62 | } 63 | 64 | fn view( 65 | &self, 66 | config: &LocalModuleConfig, 67 | _popup_config: &PopupConfig, 68 | anchor: &BarAnchor, 69 | _handlebars: &Handlebars, 70 | ) -> Element { 71 | container( 72 | rich_text([span(self.get_title().unwrap_or_default()) 73 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 74 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))]) 75 | .fill(anchor), 76 | ) 77 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)) 78 | .tooltip_maybe( 79 | self.get_title() 80 | .and_then(|t| (t.len() > self.max_length).then_some(text(t).size(12))), 81 | ) 82 | } 83 | 84 | impl_wrapper!(); 85 | 86 | fn requires(&self) -> Vec { 87 | vec![require_listener::()] 88 | } 89 | 90 | fn read_config( 91 | &mut self, 92 | config: &HashMap>, 93 | _popup_config: &HashMap>, 94 | _templates: &mut Handlebars, 95 | ) { 96 | self.cfg_override = config.into(); 97 | self.max_length = config 98 | .get("max_length") 99 | .and_then(|v| v.as_ref().and_then(|v| v.parse().ok())) 100 | .unwrap_or(Self::default().max_length); 101 | } 102 | 103 | impl_on_click!(); 104 | } 105 | 106 | pub async fn update_window(sender: &mut Sender, title: Option) { 107 | sender 108 | .send(Message::update(move |reg| { 109 | reg.get_module_mut::().title = title 110 | })) 111 | .await 112 | .unwrap_or_else(|err| { 113 | eprintln!("Trying to send workspaces failed with err: {err}"); 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /src/modules/hyprland/workspaces.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap, time::Duration}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use hyprland::{ 6 | data::{Workspace, Workspaces}, 7 | shared::{HyprData, HyprDataActive, HyprDataVec}, 8 | }; 9 | use iced::{ 10 | widget::{container, rich_text, span}, 11 | Background, Border, Color, Element, Padding, 12 | }; 13 | use tokio::time::sleep; 14 | 15 | use crate::{ 16 | config::{ 17 | anchor::BarAnchor, 18 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 19 | parse::StringExt, 20 | popup_config::PopupConfig, 21 | }, 22 | fill::FillExt, 23 | impl_on_click, impl_wrapper, 24 | list::list, 25 | listeners::hyprland::HyprListener, 26 | modules::{require_listener, Module}, 27 | Message, NERD_FONT, 28 | }; 29 | 30 | #[derive(Debug, Builder)] 31 | pub struct HyprWorkspaceMod { 32 | pub active: usize, 33 | // (Name, Fullscreen state) 34 | pub open: Vec<(String, bool)>, 35 | cfg_override: ModuleConfigOverride, 36 | icon_padding: Padding, 37 | icon_background: Option, 38 | icon_border: Border, 39 | active_padding: Option, 40 | active_size: f32, 41 | active_color: Color, 42 | active_background: Option, 43 | active_icon_border: Border, 44 | } 45 | 46 | impl Default for HyprWorkspaceMod { 47 | fn default() -> Self { 48 | Self { 49 | active: 0, 50 | open: vec![], 51 | cfg_override: ModuleConfigOverride::default(), 52 | icon_padding: Padding::default(), 53 | icon_background: None, 54 | icon_border: Border::default(), 55 | active_padding: None, 56 | active_size: 20., 57 | active_color: Color::WHITE, 58 | active_background: None, 59 | active_icon_border: Border::default().rounded(8), 60 | } 61 | } 62 | } 63 | 64 | impl Module for HyprWorkspaceMod { 65 | fn name(&self) -> String { 66 | "hyprland.workspaces".to_string() 67 | } 68 | 69 | fn view( 70 | &self, 71 | config: &LocalModuleConfig, 72 | _popup_config: &PopupConfig, 73 | anchor: &BarAnchor, 74 | _handlebars: &Handlebars, 75 | ) -> Element { 76 | list( 77 | anchor, 78 | self.open.iter().enumerate().map(|(id, (ws, _))| { 79 | let mut span = span(ws) 80 | .padding(self.icon_padding) 81 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 82 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 83 | .background_maybe(self.icon_background) 84 | .border(self.icon_border) 85 | .font(NERD_FONT); 86 | if id == self.active { 87 | span = span 88 | .padding(self.active_padding.unwrap_or(self.icon_padding)) 89 | .size(self.active_size) 90 | .color(self.active_color) 91 | .background_maybe(self.active_background) 92 | .border(self.active_icon_border); 93 | } 94 | container(rich_text![span].fill(anchor)) 95 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)) 96 | .into() 97 | }), 98 | ) 99 | .padding(self.cfg_override.padding.unwrap_or(config.padding)) 100 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 101 | .into() 102 | } 103 | 104 | impl_wrapper!(); 105 | 106 | fn requires(&self) -> Vec { 107 | vec![require_listener::()] 108 | } 109 | 110 | fn read_config( 111 | &mut self, 112 | config: &HashMap>, 113 | _popup_config: &HashMap>, 114 | _templates: &mut Handlebars, 115 | ) { 116 | let default = Self::default(); 117 | self.cfg_override = config.into(); 118 | self.icon_padding = config 119 | .get("icon_padding") 120 | .and_then(|v| v.into_insets().map(|i| i.into())) 121 | .unwrap_or(default.icon_padding); 122 | self.icon_background = config 123 | .get("icon_background") 124 | .map(|v| v.into_background()) 125 | .unwrap_or(default.icon_background); 126 | self.icon_border = { 127 | let color = config.get("icon_border_color").and_then(|s| s.into_color()); 128 | let width = config.get("icon_border_width").and_then(|s| s.into_float()); 129 | let radius = config 130 | .get("icon_border_radius") 131 | .and_then(|s| s.into_insets().map(|i| i.into())); 132 | if color.is_some() || width.is_some() || radius.is_some() { 133 | Border { 134 | color: color.unwrap_or_default(), 135 | width: width.unwrap_or(1.), 136 | radius: radius.unwrap_or(8_f32.into()), 137 | } 138 | } else { 139 | default.active_icon_border 140 | } 141 | }; 142 | self.active_padding = config 143 | .get("active_padding") 144 | .map(|v| v.into_insets().map(|i| i.into())) 145 | .unwrap_or(default.active_padding); 146 | self.active_size = config 147 | .get("active_size") 148 | .and_then(|v| v.into_float()) 149 | .unwrap_or(default.active_size); 150 | self.active_color = config 151 | .get("active_color") 152 | .and_then(|v| v.into_color()) 153 | .unwrap_or(default.active_color); 154 | self.active_background = config 155 | .get("active_background") 156 | .map(|v| v.into_background()) 157 | .unwrap_or(default.active_background); 158 | self.active_icon_border = { 159 | let color = config 160 | .get("active_border_color") 161 | .and_then(|s| s.into_color()); 162 | let width = config 163 | .get("active_border_width") 164 | .and_then(|s| s.into_float()); 165 | let radius = config 166 | .get("active_border_radius") 167 | .and_then(|s| s.into_insets().map(|i| i.into())); 168 | if color.is_some() || width.is_some() || radius.is_some() { 169 | Border { 170 | color: color.unwrap_or_default(), 171 | width: width.unwrap_or(1.), 172 | radius: radius.unwrap_or(8_f32.into()), 173 | } 174 | } else { 175 | default.active_icon_border 176 | } 177 | }; 178 | } 179 | 180 | impl_on_click!(); 181 | } 182 | 183 | pub async fn get_workspaces(active: Option) -> (usize, Vec<(String, bool)>) { 184 | // Sleep a bit, to reduce the probability that a nonexisting ws is still reported active 185 | sleep(Duration::from_millis(10)).await; 186 | let Ok(workspaces) = Workspaces::get_async().await else { 187 | eprintln!("[hyprland.workspaces] Failed to get Workspaces!"); 188 | return (0, vec![]); 189 | }; 190 | let mut open = workspaces.to_vec(); 191 | open.sort_by(|a, b| a.id.cmp(&b.id)); 192 | ( 193 | open.iter() 194 | .position(|ws| { 195 | ws.id 196 | == active 197 | .unwrap_or_else(|| Workspace::get_active().map(|ws| ws.id).unwrap_or(0)) 198 | }) 199 | .unwrap_or(0), 200 | open.into_iter() 201 | .map(|ws| (ws.name, ws.fullscreen)) 202 | .collect(), 203 | ) 204 | } 205 | -------------------------------------------------------------------------------- /src/modules/memory.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::Command}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use iced::widget::container; 6 | use iced::{widget::text, Element}; 7 | 8 | use crate::config::popup_config::PopupConfig; 9 | use crate::{ 10 | config::{ 11 | anchor::BarAnchor, 12 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 13 | }, 14 | fill::FillExt, 15 | Message, NERD_FONT, 16 | }; 17 | use crate::{impl_on_click, impl_wrapper}; 18 | 19 | use super::Module; 20 | 21 | #[derive(Debug, Default, Builder)] 22 | pub struct MemoryMod { 23 | cfg_override: ModuleConfigOverride, 24 | icon: Option, 25 | } 26 | 27 | impl Module for MemoryMod { 28 | fn name(&self) -> String { 29 | "memory".to_string() 30 | } 31 | 32 | fn view( 33 | &self, 34 | config: &LocalModuleConfig, 35 | _popup_config: &PopupConfig, 36 | anchor: &BarAnchor, 37 | _handlebars: &Handlebars, 38 | ) -> Element { 39 | let usage = Command::new("sh") 40 | .arg("-c") 41 | .arg("free | grep Mem | awk '{printf \"%.0f\", $3/$2 * 100.0}'") 42 | .output() 43 | .map(|out| String::from_utf8_lossy(&out.stdout).to_string()) 44 | .unwrap_or_else(|e| { 45 | eprintln!("Failed to get memory usage. err: {e}"); 46 | "0".to_string() 47 | }) 48 | .parse() 49 | .unwrap_or_else(|e| { 50 | eprintln!("Failed to parse memory usage (output from free), e: {e}"); 51 | 999 52 | }); 53 | 54 | list![ 55 | anchor, 56 | container( 57 | text!("{}", self.icon.as_ref().unwrap_or(&"󰍛".to_string())) 58 | .fill(anchor) 59 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 60 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 61 | .font(NERD_FONT) 62 | ) 63 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 64 | container( 65 | text!["{}%", usage] 66 | .fill(anchor) 67 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 68 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 69 | ) 70 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 71 | ] 72 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 73 | .into() 74 | } 75 | 76 | impl_wrapper!(); 77 | 78 | fn read_config( 79 | &mut self, 80 | config: &HashMap>, 81 | _popup_config: &HashMap>, 82 | _templates: &mut Handlebars, 83 | ) { 84 | self.cfg_override = config.into(); 85 | self.icon = config.get("icon").and_then(|v| v.clone()); 86 | } 87 | 88 | impl_on_click!(); 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | fmt::Debug, 5 | }; 6 | 7 | use battery::BatteryMod; 8 | use cpu::CpuMod; 9 | use date::DateMod; 10 | use disk_usage::DiskUsageMod; 11 | use downcast_rs::{impl_downcast, Downcast}; 12 | use handlebars::Handlebars; 13 | use hyprland::{window::HyprWindowMod, workspaces::HyprWorkspaceMod}; 14 | use iced::{ 15 | theme::Palette, 16 | widget::{container, Container}, 17 | Alignment, Color, Event, Theme, 18 | }; 19 | use iced::{widget::container::Style, Element, Subscription}; 20 | use media::MediaMod; 21 | use memory::MemoryMod; 22 | use niri::{NiriWindowMod, NiriWorkspaceMod}; 23 | use time::TimeMod; 24 | use volume::VolumeMod; 25 | use wayfire::{WayfireWindowMod, WayfireWorkspaceMod}; 26 | 27 | use crate::{ 28 | config::{anchor::BarAnchor, module_config::LocalModuleConfig, popup_config::PopupConfig}, 29 | fill::FillExt, 30 | listeners::Listener, 31 | registry::Registry, 32 | Message, 33 | }; 34 | 35 | pub mod battery; 36 | pub mod cpu; 37 | pub mod date; 38 | pub mod disk_usage; 39 | pub mod hyprland; 40 | pub mod media; 41 | pub mod memory; 42 | pub mod niri; 43 | pub mod sys_tray; 44 | pub mod time; 45 | pub mod volume; 46 | pub mod wayfire; 47 | 48 | pub trait Module: Any + Debug + Send + Sync + Downcast { 49 | /// The name used to enable the Module in the config. 50 | fn name(&self) -> String; 51 | /// Whether the module is currently active and should be shown. 52 | fn active(&self) -> bool { 53 | true 54 | } 55 | /// What the module actually shows. 56 | /// See [widgets-and-elements](https://docs.iced.rs/iced/#widgets-and-elements). 57 | fn view( 58 | &self, 59 | config: &LocalModuleConfig, 60 | popup_config: &PopupConfig, 61 | anchor: &BarAnchor, 62 | template: &Handlebars, 63 | ) -> Element; 64 | /// The wrapper around this module, which defines things like background color or border for 65 | /// this module. 66 | fn wrapper<'a>( 67 | &'a self, 68 | config: &'a LocalModuleConfig, 69 | content: Element<'a, Message>, 70 | anchor: &BarAnchor, 71 | ) -> Element<'a, Message> { 72 | container( 73 | container(content) 74 | .fill(anchor) 75 | .padding(config.padding) 76 | .style(|_| Style { 77 | background: config.background, 78 | border: config.border, 79 | ..Default::default() 80 | }), 81 | ) 82 | .fill(anchor) 83 | .padding(config.margin) 84 | .into() 85 | } 86 | /// The module may optionally have a subscription listening for external events. 87 | /// See [passive-subscriptions](https://docs.iced.rs/iced/#passive-subscriptions). 88 | fn subscription(&self) -> Option> { 89 | None 90 | } 91 | /// Modules may require shared subscriptions. Add `require_listener::()` 92 | /// for every [Listener] this module requires. 93 | fn requires(&self) -> Vec { 94 | vec![] 95 | } 96 | #[allow(unused_variables)] 97 | /// Read configuration options from the config section of this module 98 | fn read_config( 99 | &mut self, 100 | config: &HashMap>, 101 | popup_config: &HashMap>, 102 | templates: &mut Handlebars, 103 | ) { 104 | } 105 | #[allow(unused_variables)] 106 | /// The action to perform on a on_click event 107 | fn on_click<'a>( 108 | &'a self, 109 | event: iced::Event, 110 | config: &'a LocalModuleConfig, 111 | ) -> Option<&'a dyn Action> { 112 | None 113 | } 114 | #[allow(unused_variables, dead_code)] 115 | /// Handle an action (likely produced by a user interaction). 116 | fn handle_action(&mut self, action: &dyn Action) {} 117 | #[allow(unused_variables)] 118 | /// The view of a popup 119 | fn popup_view<'a>( 120 | &'a self, 121 | config: &'a PopupConfig, 122 | template: &Handlebars, 123 | ) -> Element<'a, Message> { 124 | "Missing implementation".into() 125 | } 126 | /// The wrapper around a popup 127 | fn popup_wrapper<'a>( 128 | &'a self, 129 | config: &'a PopupConfig, 130 | anchor: &BarAnchor, 131 | template: &Handlebars, 132 | ) -> Element<'a, Message> { 133 | let align = |elem: Container<'a, Message>| -> Container<'a, Message> { 134 | match anchor { 135 | BarAnchor::Top => elem.align_y(Alignment::Start), 136 | BarAnchor::Bottom => elem.align_y(Alignment::End), 137 | BarAnchor::Left => elem.align_x(Alignment::Start), 138 | BarAnchor::Right => elem.align_x(Alignment::End), 139 | } 140 | }; 141 | align(container(self.popup_view(config, template)).fill(anchor)).into() 142 | } 143 | /// The theme of a popup 144 | fn popup_theme(&self) -> Theme { 145 | Theme::custom( 146 | "Default popup theme".to_string(), 147 | Palette { 148 | background: Color::TRANSPARENT, 149 | text: Color::WHITE, 150 | primary: Color::WHITE, 151 | success: Color::WHITE, 152 | danger: Color::WHITE, 153 | }, 154 | ) 155 | } 156 | } 157 | impl_downcast!(Module); 158 | 159 | pub trait Action: Any + Debug + Send + Sync + Downcast { 160 | fn as_message(&self) -> Message; 161 | } 162 | impl_downcast!(Action); 163 | 164 | impl From<&String> for Box { 165 | fn from(value: &String) -> Box { 166 | Box::new(CommandAction(value.clone())) 167 | } 168 | } 169 | 170 | #[derive(Debug)] 171 | pub struct CommandAction(String); 172 | 173 | impl Action for CommandAction { 174 | fn as_message(&self) -> Message { 175 | Message::command_sh(&self.0) 176 | } 177 | } 178 | 179 | #[derive(Debug, Default)] 180 | pub struct OnClickAction { 181 | pub left: Option>, 182 | pub center: Option>, 183 | pub right: Option>, 184 | } 185 | 186 | impl OnClickAction { 187 | pub fn event(&self, event: Event) -> Option<&dyn Action> { 188 | match event { 189 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)) => { 190 | self.left.as_deref() 191 | } 192 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Middle)) => { 193 | self.center.as_deref() 194 | } 195 | Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Right)) => { 196 | self.right.as_deref() 197 | } 198 | _ => None, 199 | } 200 | } 201 | } 202 | 203 | pub fn require_listener() -> TypeId 204 | where 205 | T: Listener, 206 | { 207 | TypeId::of::() 208 | } 209 | 210 | pub fn register_modules(registry: &mut Registry) { 211 | registry.register_module::(); 212 | registry.register_module::(); 213 | registry.register_module::(); 214 | registry.register_module::(); 215 | registry.register_module::(); 216 | registry.register_module::(); 217 | registry.register_module::(); 218 | registry.register_module::(); 219 | registry.register_module::(); 220 | registry.register_module::(); 221 | registry.register_module::(); 222 | registry.register_module::(); 223 | registry.register_module::(); 224 | registry.register_module::(); 225 | } 226 | 227 | #[macro_export] 228 | macro_rules! impl_wrapper { 229 | () => { 230 | fn wrapper<'a>( 231 | &'a self, 232 | config: &'a LocalModuleConfig, 233 | content: Element<'a, Message>, 234 | anchor: &BarAnchor, 235 | ) -> Element<'a, Message> { 236 | iced::widget::container( 237 | $crate::button::button(content) 238 | .fill(anchor) 239 | .padding(self.cfg_override.padding.unwrap_or(config.padding)) 240 | .on_event_try(|evt, _, _, _, _| { 241 | self.on_click(evt, config).map(|evt| evt.as_message()) 242 | }) 243 | .style(|_, _| iced::widget::button::Style { 244 | background: self.cfg_override.background.unwrap_or(config.background), 245 | border: self.cfg_override.border.unwrap_or(config.border), 246 | ..Default::default() 247 | }), 248 | ) 249 | .fill(anchor) 250 | .padding(self.cfg_override.margin.unwrap_or(config.margin)) 251 | .into() 252 | } 253 | }; 254 | } 255 | 256 | #[macro_export] 257 | macro_rules! impl_on_click { 258 | () => { 259 | fn on_click<'a>( 260 | &'a self, 261 | event: iced::Event, 262 | config: &'a LocalModuleConfig, 263 | ) -> Option<&'a dyn $crate::modules::Action> { 264 | self.cfg_override 265 | .action 266 | .as_ref() 267 | .unwrap_or(&config.action) 268 | .event(event) 269 | } 270 | }; 271 | } 272 | -------------------------------------------------------------------------------- /src/modules/niri/mod.rs: -------------------------------------------------------------------------------- 1 | mod window; 2 | mod workspaces; 3 | 4 | pub use window::NiriWindowMod; 5 | pub use workspaces::NiriWorkspaceMod; 6 | -------------------------------------------------------------------------------- /src/modules/niri/window.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::{any::TypeId, collections::HashMap}; 3 | 4 | use bar_rs_derive::Builder; 5 | use handlebars::Handlebars; 6 | use iced::widget::button::Style; 7 | use iced::widget::{container, scrollable, text}; 8 | use iced::Element; 9 | use niri_ipc::Window; 10 | 11 | use crate::button::button; 12 | use crate::config::popup_config::{PopupConfig, PopupConfigOverride}; 13 | use crate::helpers::UnEscapeString; 14 | use crate::{ 15 | config::{ 16 | anchor::BarAnchor, 17 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 18 | parse::StringExt, 19 | }, 20 | fill::FillExt, 21 | listeners::niri::NiriListener, 22 | modules::{require_listener, Module}, 23 | Message, 24 | }; 25 | use crate::{impl_on_click, impl_wrapper}; 26 | 27 | #[derive(Debug, Builder)] 28 | pub struct NiriWindowMod { 29 | // (title, app_id) 30 | pub windows: HashMap, 31 | pub focused: Option, 32 | max_length: usize, 33 | show_app_id: bool, 34 | cfg_override: ModuleConfigOverride, 35 | popup_cfg_override: PopupConfigOverride, 36 | } 37 | 38 | impl Default for NiriWindowMod { 39 | fn default() -> Self { 40 | Self { 41 | windows: HashMap::new(), 42 | focused: None, 43 | max_length: 25, 44 | show_app_id: false, 45 | cfg_override: Default::default(), 46 | popup_cfg_override: PopupConfigOverride { 47 | width: Some(400), 48 | height: Some(250), 49 | ..Default::default() 50 | }, 51 | } 52 | } 53 | } 54 | 55 | impl NiriWindowMod { 56 | fn get_title(&self) -> Option<&String> { 57 | self.focused.and_then(|id| { 58 | self.windows.get(&id).and_then(|w| match self.show_app_id { 59 | true => w.app_id.as_ref(), 60 | false => w.title.as_ref(), 61 | }) 62 | }) 63 | } 64 | 65 | fn trimmed_title(&self) -> String { 66 | self.get_title() 67 | .map(|title| match title.len() > self.max_length { 68 | true => format!( 69 | "{}...", 70 | &title.chars().take(self.max_length - 3).collect::() 71 | ), 72 | false => title.to_string(), 73 | }) 74 | .unwrap_or_default() 75 | } 76 | } 77 | 78 | impl Module for NiriWindowMod { 79 | fn name(&self) -> String { 80 | "niri.window".to_string() 81 | } 82 | 83 | fn active(&self) -> bool { 84 | self.focused.is_some() 85 | } 86 | 87 | fn view( 88 | &self, 89 | config: &LocalModuleConfig, 90 | popup_config: &PopupConfig, 91 | anchor: &BarAnchor, 92 | _handlebars: &Handlebars, 93 | ) -> Element { 94 | button( 95 | text(self.trimmed_title()) 96 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 97 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 98 | .fill(anchor), 99 | ) 100 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)) 101 | .on_event_with(Message::popup::( 102 | self.popup_cfg_override.width.unwrap_or(popup_config.width), 103 | self.popup_cfg_override 104 | .height 105 | .unwrap_or(popup_config.height), 106 | anchor, 107 | )) 108 | .style(|_, _| Style::default()) 109 | .into() 110 | } 111 | 112 | fn popup_view<'a>( 113 | &'a self, 114 | config: &'a PopupConfig, 115 | template: &Handlebars, 116 | ) -> Element<'a, Message> { 117 | container(scrollable( 118 | container( 119 | if let Some(window) = self.focused.and_then(|id| self.windows.get(&id)) { 120 | let unset = String::from("Unset"); 121 | let window_id = window.id.to_string(); 122 | let workspace_id = window.workspace_id.unwrap_or_default().to_string(); 123 | let ctx = BTreeMap::from([ 124 | ("title", window.title.as_ref().unwrap_or(&unset)), 125 | ("app_id", window.app_id.as_ref().unwrap_or(&unset)), 126 | ("window_id", &window_id), 127 | ("workspace_id", &workspace_id), 128 | ]); 129 | text(template.render("niri.window", &ctx).unwrap_or_default()) 130 | } else { 131 | "No window focused".into() 132 | } 133 | .color( 134 | self.popup_cfg_override 135 | .text_color 136 | .unwrap_or(config.text_color), 137 | ) 138 | .size( 139 | self.popup_cfg_override 140 | .font_size 141 | .unwrap_or(config.font_size), 142 | ), 143 | ) 144 | .padding( 145 | self.popup_cfg_override 146 | .text_margin 147 | .unwrap_or(config.text_margin), 148 | ), 149 | )) 150 | .padding(self.popup_cfg_override.padding.unwrap_or(config.padding)) 151 | .style(|_| container::Style { 152 | background: Some( 153 | self.popup_cfg_override 154 | .background 155 | .unwrap_or(config.background), 156 | ), 157 | border: self.popup_cfg_override.border.unwrap_or(config.border), 158 | ..Default::default() 159 | }) 160 | .fill_maybe( 161 | self.popup_cfg_override 162 | .fill_content_to_size 163 | .unwrap_or(config.fill_content_to_size), 164 | ) 165 | .into() 166 | } 167 | 168 | impl_wrapper!(); 169 | 170 | fn requires(&self) -> Vec { 171 | vec![require_listener::()] 172 | } 173 | 174 | fn read_config( 175 | &mut self, 176 | config: &HashMap>, 177 | popup_config: &HashMap>, 178 | templates: &mut Handlebars, 179 | ) { 180 | let default = Self::default(); 181 | self.cfg_override = config.into(); 182 | self.popup_cfg_override.update(popup_config); 183 | self.max_length = config 184 | .get("max_length") 185 | .and_then(|v| v.as_ref().and_then(|v| v.parse().ok())) 186 | .unwrap_or(default.max_length); 187 | self.show_app_id = config 188 | .get("show_app_id") 189 | .and_then(|v| v.into_bool()) 190 | .unwrap_or(default.show_app_id); 191 | templates 192 | .register_template_string( 193 | "niri.window", 194 | popup_config 195 | .get("format") 196 | .unescape() 197 | .unwrap_or("Title: {{title}}\nApplication ID: {{app_id}}\nWindow ID: {{window_id}}\nWorkspace ID: {{workspace_id}}".to_string()), 198 | ) 199 | .unwrap_or_else(|e| eprintln!("Failed to parse battery popup format: {e}")); 200 | } 201 | 202 | impl_on_click!(); 203 | } 204 | -------------------------------------------------------------------------------- /src/modules/niri/workspaces.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | sync::Arc, 5 | }; 6 | 7 | use bar_rs_derive::Builder; 8 | use handlebars::Handlebars; 9 | use iced::{ 10 | widget::{button, container, text}, 11 | Background, Border, Color, Element, Padding, 12 | }; 13 | use niri_ipc::Workspace; 14 | use tokio::sync::broadcast; 15 | 16 | use crate::{ 17 | config::{ 18 | anchor::BarAnchor, 19 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 20 | parse::StringExt, 21 | popup_config::PopupConfig, 22 | }, 23 | fill::FillExt, 24 | impl_on_click, impl_wrapper, list, 25 | listeners::niri::NiriListener, 26 | modules::{require_listener, Module}, 27 | Message, NERD_FONT, 28 | }; 29 | 30 | #[derive(Debug, Builder)] 31 | pub struct NiriWorkspaceMod { 32 | pub workspaces: HashMap>, 33 | pub focused: u64, 34 | pub sender: broadcast::Sender>, 35 | cfg_override: ModuleConfigOverride, 36 | icon_padding: Padding, 37 | icon_background: Option, 38 | icon_border: Border, 39 | active_padding: Option, 40 | active_size: f32, 41 | active_color: Color, 42 | active_background: Option, 43 | active_icon_border: Border, 44 | // Output, (idx, icon) 45 | icons: HashMap>, 46 | fallback_icon: String, 47 | active_fallback_icon: String, 48 | output_order: Vec, 49 | } 50 | 51 | impl Default for NiriWorkspaceMod { 52 | fn default() -> Self { 53 | Self { 54 | workspaces: HashMap::new(), 55 | focused: 0, 56 | sender: broadcast::channel(1).0, 57 | cfg_override: Default::default(), 58 | icon_padding: Padding::default(), 59 | icon_background: None, 60 | icon_border: Border::default(), 61 | active_padding: None, 62 | active_size: 20., 63 | active_color: Color::WHITE, 64 | active_background: None, 65 | active_icon_border: Border::default().rounded(8), 66 | icons: HashMap::new(), 67 | fallback_icon: String::from(""), 68 | active_fallback_icon: String::from(""), 69 | output_order: vec![], 70 | } 71 | } 72 | } 73 | 74 | impl NiriWorkspaceMod { 75 | fn sort_by_outputs<'a, F, I>(&'a self, f: F) -> Vec> 76 | where 77 | F: Fn((&'a String, &'a Vec)) -> I, 78 | I: Iterator>, 79 | { 80 | match self.output_order.is_empty() { 81 | true => self 82 | .workspaces 83 | .iter() 84 | .flat_map(f) 85 | .collect::>>(), 86 | false => self 87 | .output_order 88 | .iter() 89 | .filter_map(|o| self.workspaces.get_key_value(o)) 90 | .flat_map(f) 91 | .collect::>>(), 92 | } 93 | } 94 | } 95 | 96 | impl Module for NiriWorkspaceMod { 97 | fn name(&self) -> String { 98 | "niri.workspaces".to_string() 99 | } 100 | 101 | fn view( 102 | &self, 103 | config: &LocalModuleConfig, 104 | _popup_config: &PopupConfig, 105 | anchor: &BarAnchor, 106 | _handlebars: &Handlebars, 107 | ) -> Element { 108 | list( 109 | anchor, 110 | self.sort_by_outputs(|(output, workspaces)| { 111 | workspaces.iter().map(|ws| { 112 | let mut text = text( 113 | self.icons 114 | .get(&output.to_lowercase()) 115 | .and_then(|icons| icons.get(&ws.idx)) 116 | .unwrap_or(match ws.id == self.focused { 117 | true => &self.active_fallback_icon, 118 | false => &self.fallback_icon, 119 | }), 120 | ) 121 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 122 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 123 | .font(NERD_FONT); 124 | let mut btn_style = button::Style { 125 | background: self.icon_background, 126 | border: self.icon_border, 127 | ..Default::default() 128 | }; 129 | let id = ws.id; 130 | if id == self.focused { 131 | text = text.size(self.active_size).color(self.active_color); 132 | btn_style.background = self.active_background; 133 | btn_style.border = self.active_icon_border; 134 | } 135 | container( 136 | button(text) 137 | .padding(match id == self.focused { 138 | true => self.active_padding.unwrap_or(self.icon_padding), 139 | false => self.icon_padding, 140 | }) 141 | .style(move |_, _| btn_style) 142 | .on_press(Message::action(move |reg| { 143 | reg.get_module::() 144 | .sender 145 | .send(Arc::new(id)) 146 | .unwrap(); 147 | })), 148 | ) 149 | .fill(anchor) 150 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)) 151 | .into() 152 | }) 153 | }), 154 | ) 155 | .padding(self.cfg_override.padding.unwrap_or(config.padding)) 156 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 157 | .into() 158 | } 159 | 160 | impl_wrapper!(); 161 | 162 | fn requires(&self) -> Vec { 163 | vec![require_listener::()] 164 | } 165 | 166 | fn read_config( 167 | &mut self, 168 | config: &HashMap>, 169 | _popup_config: &HashMap>, 170 | _templates: &mut Handlebars, 171 | ) { 172 | let default = Self::default(); 173 | self.cfg_override = config.into(); 174 | self.icon_padding = config 175 | .get("icon_padding") 176 | .and_then(|v| v.into_insets().map(|i| i.into())) 177 | .unwrap_or(default.icon_padding); 178 | self.icon_background = config 179 | .get("icon_background") 180 | .map(|v| v.into_background()) 181 | .unwrap_or(default.icon_background); 182 | self.icon_border = { 183 | let color = config.get("icon_border_color").and_then(|s| s.into_color()); 184 | let width = config.get("icon_border_width").and_then(|s| s.into_float()); 185 | let radius = config 186 | .get("icon_border_radius") 187 | .and_then(|s| s.into_insets().map(|i| i.into())); 188 | if color.is_some() || width.is_some() || radius.is_some() { 189 | Border { 190 | color: color.unwrap_or_default(), 191 | width: width.unwrap_or(1.), 192 | radius: radius.unwrap_or(8_f32.into()), 193 | } 194 | } else { 195 | default.active_icon_border 196 | } 197 | }; 198 | self.active_padding = config 199 | .get("active_padding") 200 | .map(|v| v.into_insets().map(|i| i.into())) 201 | .unwrap_or(default.active_padding); 202 | self.active_size = config 203 | .get("active_size") 204 | .and_then(|v| v.into_float()) 205 | .unwrap_or(default.active_size); 206 | self.active_color = config 207 | .get("active_color") 208 | .and_then(|v| v.into_color()) 209 | .unwrap_or(default.active_color); 210 | self.active_background = config 211 | .get("active_background") 212 | .map(|v| v.into_background()) 213 | .unwrap_or(default.active_background); 214 | self.active_icon_border = { 215 | let color = config 216 | .get("active_border_color") 217 | .and_then(|s| s.into_color()); 218 | let width = config 219 | .get("active_border_width") 220 | .and_then(|s| s.into_float()); 221 | let radius = config 222 | .get("active_border_radius") 223 | .and_then(|s| s.into_insets().map(|i| i.into())); 224 | if color.is_some() || width.is_some() || radius.is_some() { 225 | Border { 226 | color: color.unwrap_or_default(), 227 | width: width.unwrap_or(1.), 228 | radius: radius.unwrap_or(8_f32.into()), 229 | } 230 | } else { 231 | default.active_icon_border 232 | } 233 | }; 234 | self.fallback_icon = config 235 | .get("fallback_icon") 236 | .and_then(|v| v.clone()) 237 | .unwrap_or(default.fallback_icon); 238 | self.active_fallback_icon = config 239 | .get("active_fallback_icon") 240 | .and_then(|v| v.clone()) 241 | .unwrap_or(default.active_fallback_icon); 242 | self.output_order = config 243 | .get("output_order") 244 | .and_then(|v| v.clone()) 245 | .map(|v| v.split(',').map(|v| v.trim().to_string()).collect()) 246 | .unwrap_or(default.output_order); 247 | config.iter().for_each(|(key, val)| { 248 | let Some(val) = val.clone() else { 249 | return; 250 | }; 251 | if let [output, idx] = key.split(':').map(|i| i.trim()).collect::>()[..] { 252 | if let Ok(idx) = idx.parse() { 253 | match self.icons.get_mut(output) { 254 | Some(icons) => { 255 | icons.insert(idx, val); 256 | } 257 | None => { 258 | self.icons 259 | .insert(output.to_string(), HashMap::from([(idx, val)])); 260 | } 261 | } 262 | } 263 | } 264 | }); 265 | } 266 | 267 | impl_on_click!(); 268 | } 269 | -------------------------------------------------------------------------------- /src/modules/sys_tray.rs: -------------------------------------------------------------------------------- 1 | use iced::{futures::Stream, stream}; 2 | //use system_tray::client::Client; 3 | 4 | use crate::Message; 5 | 6 | pub fn _system_tray() -> impl Stream { 7 | stream::channel(1, |mut _sender| async move { 8 | /*let client = Client::new().await.unwrap(); 9 | let mut tray_rx = client.subscribe(); 10 | 11 | let initial_items = client.items(); 12 | 13 | println!("initial_items: {initial_items:#?}\n\n"); 14 | 15 | // do something with initial items... 16 | drop(initial_items); 17 | 18 | while let Ok(ev) = tray_rx.recv().await { 19 | println!("{ev:#?}"); // do something with event... 20 | }*/ 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/time.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bar_rs_derive::Builder; 4 | use chrono::Local; 5 | use handlebars::Handlebars; 6 | use iced::widget::{container, text}; 7 | use iced::Element; 8 | 9 | use crate::config::popup_config::PopupConfig; 10 | use crate::{ 11 | config::{ 12 | anchor::BarAnchor, 13 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 14 | }, 15 | fill::FillExt, 16 | Message, NERD_FONT, 17 | }; 18 | use crate::{impl_on_click, impl_wrapper}; 19 | 20 | use super::Module; 21 | 22 | #[derive(Debug, Builder)] 23 | pub struct TimeMod { 24 | cfg_override: ModuleConfigOverride, 25 | icon: String, 26 | fmt: String, 27 | } 28 | 29 | impl Default for TimeMod { 30 | fn default() -> Self { 31 | Self { 32 | cfg_override: Default::default(), 33 | icon: "".to_string(), 34 | fmt: "%H:%M".to_string(), 35 | } 36 | } 37 | } 38 | 39 | impl Module for TimeMod { 40 | fn name(&self) -> String { 41 | "time".to_string() 42 | } 43 | 44 | fn view( 45 | &self, 46 | config: &LocalModuleConfig, 47 | _popup_config: &PopupConfig, 48 | anchor: &BarAnchor, 49 | _handlebars: &Handlebars, 50 | ) -> Element { 51 | let time = Local::now(); 52 | list![ 53 | anchor, 54 | container( 55 | text!("{}", self.icon) 56 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 57 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 58 | .font(NERD_FONT) 59 | .fill(anchor) 60 | ) 61 | .fill(anchor) 62 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 63 | container( 64 | text!("{}", time.format(&self.fmt)) 65 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 66 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 67 | .fill(anchor) 68 | ) 69 | .fill(anchor) 70 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 71 | ] 72 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 73 | .into() 74 | } 75 | 76 | impl_wrapper!(); 77 | 78 | fn read_config( 79 | &mut self, 80 | config: &HashMap>, 81 | _popup_config: &HashMap>, 82 | _templates: &mut Handlebars, 83 | ) { 84 | let default = Self::default(); 85 | self.cfg_override = config.into(); 86 | self.icon = config 87 | .get("icon") 88 | .and_then(|v| v.clone()) 89 | .unwrap_or(default.icon); 90 | self.fmt = config 91 | .get("format") 92 | .and_then(|v| v.clone()) 93 | .unwrap_or(default.fmt); 94 | } 95 | 96 | impl_on_click!(); 97 | } 98 | -------------------------------------------------------------------------------- /src/modules/volume.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::Stdio}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use iced::widget::{button, container}; 6 | use iced::{futures::SinkExt, stream, widget::text, Element, Subscription}; 7 | use tokio::{ 8 | io::{AsyncBufReadExt, BufReader}, 9 | process::Command, 10 | }; 11 | 12 | use crate::config::popup_config::PopupConfig; 13 | use crate::{ 14 | config::{ 15 | anchor::BarAnchor, 16 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 17 | }, 18 | fill::FillExt, 19 | Message, NERD_FONT, 20 | }; 21 | use crate::{impl_on_click, impl_wrapper}; 22 | 23 | use super::Module; 24 | 25 | #[derive(Default, Debug, Builder)] 26 | pub struct VolumeMod { 27 | level: u16, 28 | icon: &'static str, 29 | cfg_override: ModuleConfigOverride, 30 | } 31 | 32 | impl Module for VolumeMod { 33 | fn name(&self) -> String { 34 | "volume".to_string() 35 | } 36 | 37 | fn view( 38 | &self, 39 | config: &LocalModuleConfig, 40 | _popup_config: &PopupConfig, 41 | anchor: &BarAnchor, 42 | _handlebars: &Handlebars, 43 | ) -> Element { 44 | list![ 45 | anchor, 46 | button( 47 | text!("{}", self.icon) 48 | .fill(anchor) 49 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 50 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 51 | .font(NERD_FONT) 52 | ) 53 | .style(|_, _| button::Style::default()) 54 | .on_press(Message::command_sh( 55 | "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" 56 | )) 57 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)), 58 | container( 59 | text!["{}%", self.level,] 60 | .fill(anchor) 61 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 62 | .color(self.cfg_override.text_color.unwrap_or(config.text_color)) 63 | ) 64 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)), 65 | ] 66 | .spacing(self.cfg_override.spacing.unwrap_or(config.spacing)) 67 | .into() 68 | } 69 | 70 | impl_wrapper!(); 71 | 72 | fn read_config( 73 | &mut self, 74 | config: &HashMap>, 75 | _popup_config: &HashMap>, 76 | _templates: &mut Handlebars, 77 | ) { 78 | self.cfg_override = config.into(); 79 | } 80 | 81 | impl_on_click!(); 82 | 83 | fn subscription(&self) -> Option> { 84 | Some(Subscription::run(|| { 85 | stream::channel(1, |mut sender| async move { 86 | let volume = || { 87 | Message::update(move |reg| { 88 | let vmod = reg.get_module_mut::(); 89 | let volume = get_volume(); 90 | vmod.level = volume.0; 91 | vmod.icon = volume.1; 92 | }) 93 | }; 94 | 95 | sender.send(volume()).await.unwrap_or_else(|err| { 96 | eprintln!("Trying to send volume failed with err: {err}"); 97 | }); 98 | 99 | let mut child = Command::new("sh") 100 | .arg("-c") 101 | .arg("pactl subscribe") 102 | .stdout(Stdio::piped()) 103 | .spawn() 104 | .expect("Failed to spawn pactl to monitor volume changes"); 105 | 106 | let stdout = child 107 | .stdout 108 | .take() 109 | .expect("child did not have a handle to stdout"); 110 | 111 | let mut reader = BufReader::new(stdout).lines(); 112 | 113 | while let Some(line) = reader.next_line().await.unwrap() { 114 | if line.contains("'change' on sink") { 115 | sender.send(volume()).await.unwrap_or_else(|err| { 116 | eprintln!("Trying to send volume failed with err: {err}"); 117 | }); 118 | } 119 | } 120 | }) 121 | })) 122 | } 123 | } 124 | 125 | fn get_volume() -> (u16, &'static str) { 126 | let volume = String::from_utf8( 127 | std::process::Command::new("sh") 128 | .arg("-c") 129 | .arg("wpctl get-volume @DEFAULT_AUDIO_SINK@") 130 | .output() 131 | .expect("Couldn't get volume from wpctl") 132 | .stdout, 133 | ) 134 | .expect("Couldn't convert output from wpctl to String"); 135 | let mut volume = volume 136 | .as_str() 137 | .strip_prefix("Volume: ") 138 | .unwrap_or_else(|| { 139 | eprintln!( 140 | "Failed to get volume from wpctl, tried: `wpctl get-volume @DEFAULT_AUDIO_SINK@`" 141 | ); 142 | "0" 143 | }) 144 | .trim(); 145 | let mut muted = false; 146 | if let Some(x) = volume.strip_suffix(" [MUTED]") { 147 | volume = x; 148 | muted = true; 149 | } 150 | let volume = volume.parse::().unwrap(); 151 | let volume = (volume * 100.) as u16; 152 | ( 153 | volume, 154 | match muted { 155 | true => "󰖁", 156 | false => match volume { 157 | n if n >= 50 => "󰕾", 158 | n if n >= 25 => "󰖀", 159 | _ => "󰕿", 160 | }, 161 | }, 162 | ) 163 | } 164 | -------------------------------------------------------------------------------- /src/modules/wayfire/mod.rs: -------------------------------------------------------------------------------- 1 | mod window; 2 | mod workspaces; 3 | 4 | pub use window::WayfireWindowMod; 5 | pub use workspaces::WayfireWorkspaceMod; 6 | -------------------------------------------------------------------------------- /src/modules/wayfire/window.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use iced::widget::{container, rich_text, span, text}; 6 | use iced::Element; 7 | 8 | use crate::config::popup_config::PopupConfig; 9 | use crate::tooltip::ElementExt; 10 | use crate::{ 11 | config::{ 12 | anchor::BarAnchor, 13 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 14 | }, 15 | fill::FillExt, 16 | listeners::wayfire::WayfireListener, 17 | modules::Module, 18 | Message, 19 | }; 20 | use crate::{impl_on_click, impl_wrapper}; 21 | 22 | #[derive(Debug, Builder)] 23 | pub struct WayfireWindowMod { 24 | pub title: Option, 25 | max_length: usize, 26 | cfg_override: ModuleConfigOverride, 27 | } 28 | 29 | impl Default for WayfireWindowMod { 30 | fn default() -> Self { 31 | Self { 32 | title: None, 33 | max_length: 25, 34 | cfg_override: Default::default(), 35 | } 36 | } 37 | } 38 | 39 | impl WayfireWindowMod { 40 | pub fn get_title(&self) -> Option { 41 | self.title 42 | .as_ref() 43 | .map(|title| match title.len() > self.max_length { 44 | true => format!( 45 | "{}...", 46 | title.chars().take(self.max_length - 3).collect::() 47 | ), 48 | false => title.to_string(), 49 | }) 50 | } 51 | } 52 | 53 | impl Module for WayfireWindowMod { 54 | fn name(&self) -> String { 55 | "wayfire.window".to_string() 56 | } 57 | 58 | fn active(&self) -> bool { 59 | self.title.is_some() 60 | } 61 | 62 | fn view( 63 | &self, 64 | config: &LocalModuleConfig, 65 | _popup_config: &PopupConfig, 66 | anchor: &BarAnchor, 67 | _handlebars: &Handlebars, 68 | ) -> Element { 69 | container( 70 | rich_text([span(self.get_title().unwrap_or_default()) 71 | .size(self.cfg_override.font_size.unwrap_or(config.font_size)) 72 | .color(self.cfg_override.text_color.unwrap_or(config.text_color))]) 73 | .fill(anchor), 74 | ) 75 | .padding(self.cfg_override.text_margin.unwrap_or(config.text_margin)) 76 | .tooltip_maybe( 77 | self.get_title() 78 | .and_then(|t| (t.len() > self.max_length).then_some(text(t).size(12))), 79 | ) 80 | } 81 | 82 | impl_wrapper!(); 83 | 84 | fn requires(&self) -> Vec { 85 | vec![TypeId::of::()] 86 | } 87 | 88 | fn read_config( 89 | &mut self, 90 | config: &HashMap>, 91 | _popup_config: &HashMap>, 92 | _templates: &mut Handlebars, 93 | ) { 94 | self.cfg_override = config.into(); 95 | self.max_length = config 96 | .get("max_length") 97 | .and_then(|v| v.as_ref().and_then(|v| v.parse().ok())) 98 | .unwrap_or(Self::default().max_length); 99 | } 100 | 101 | impl_on_click!(); 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/wayfire/workspaces.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap}; 2 | 3 | use bar_rs_derive::Builder; 4 | use handlebars::Handlebars; 5 | use iced::widget::{container, rich_text, span}; 6 | use iced::Element; 7 | use iced::Padding; 8 | 9 | use crate::config::parse::StringExt; 10 | use crate::config::popup_config::PopupConfig; 11 | use crate::{ 12 | config::{ 13 | anchor::BarAnchor, 14 | module_config::{LocalModuleConfig, ModuleConfigOverride}, 15 | }, 16 | fill::FillExt, 17 | listeners::wayfire::WayfireListener, 18 | modules::Module, 19 | Message, NERD_FONT, 20 | }; 21 | use crate::{impl_on_click, impl_wrapper}; 22 | 23 | /// I am unaware of a IPC method that gives a list of currently active workspaces (the ones with an 24 | /// open window), and this is generally tricky here, since all workspaces of a wset grid are active 25 | /// in a way. It would probably be posible to calculate the workspace of each active window 26 | /// manually, but I'm too lazy to do that atm. 27 | 28 | #[derive(Debug, Default, Builder)] 29 | pub struct WayfireWorkspaceMod { 30 | pub active: (i64, i64), 31 | icons: HashMap<(i64, i64), String>, 32 | cfg_override: ModuleConfigOverride, 33 | icon_padding: Padding, 34 | fallback_icon: Option, 35 | } 36 | 37 | impl Module for WayfireWorkspaceMod { 38 | fn name(&self) -> String { 39 | "wayfire.workspaces".to_string() 40 | } 41 | 42 | fn view( 43 | &self, 44 | config: &LocalModuleConfig, 45 | _popup_config: &PopupConfig, 46 | anchor: &BarAnchor, 47 | _handlebars: &Handlebars, 48 | ) -> Element { 49 | container( 50 | rich_text([span( 51 | self.icons 52 | .get(&self.active) 53 | .or(self.fallback_icon.as_ref()) 54 | .cloned() 55 | .unwrap_or(format!("{}/{}", self.active.0, self.active.1)), 56 | ) 57 | .padding(self.icon_padding) 58 | .size(self.cfg_override.icon_size.unwrap_or(config.icon_size)) 59 | .color(self.cfg_override.icon_color.unwrap_or(config.icon_color)) 60 | .font(NERD_FONT)]) 61 | .fill(anchor), 62 | ) 63 | .padding(self.cfg_override.icon_margin.unwrap_or(config.icon_margin)) 64 | .into() 65 | } 66 | 67 | impl_wrapper!(); 68 | 69 | fn requires(&self) -> Vec { 70 | vec![TypeId::of::()] 71 | } 72 | 73 | fn read_config( 74 | &mut self, 75 | config: &HashMap>, 76 | _popup_config: &HashMap>, 77 | _templates: &mut Handlebars, 78 | ) { 79 | self.cfg_override = config.into(); 80 | self.icon_padding = config 81 | .get("icon_padding") 82 | .and_then(|v| v.into_insets().map(|i| i.into())) 83 | .unwrap_or(Self::default().icon_padding); 84 | self.fallback_icon = config.get("fallback_icon").and_then(|v| v.clone()); 85 | config.iter().for_each(|(key, val)| { 86 | if let Some(key) = key 87 | .strip_prefix('(') 88 | .and_then(|v| v.strip_suffix(')')) 89 | .and_then(|v| { 90 | let [x, y] = v.split(',').map(|item| item.trim()).collect::>()[..] 91 | else { 92 | return None; 93 | }; 94 | x.parse().and_then(|x| y.parse().map(|y| (x, y))).ok() 95 | }) 96 | { 97 | self.icons.insert(key, val.clone().unwrap_or(String::new())); 98 | } 99 | }); 100 | } 101 | 102 | impl_on_click!(); 103 | } 104 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::{HashMap, HashSet}, 4 | fmt::Debug, 5 | }; 6 | 7 | use crate::{ 8 | config::{Config, EnabledModules}, 9 | listeners::Listener, 10 | modules::Module, 11 | OptionExt, 12 | }; 13 | 14 | pub trait Builder: Any { 15 | type Output; 16 | fn build() -> Self::Output; 17 | } 18 | 19 | #[allow(clippy::type_complexity)] 20 | #[derive(Default, Debug)] 21 | pub struct Registry { 22 | modules: HashMap>, 23 | listeners: HashMap>, 24 | module_names: HashMap, 25 | resolvers: HashMap) -> Option>, 26 | } 27 | 28 | #[allow(dead_code)] 29 | impl Registry { 30 | pub fn register_module(&mut self) 31 | where 32 | T::Output: Module, 33 | { 34 | let output = T::build(); 35 | let type_id = TypeId::of::(); 36 | self.module_names.insert(output.name(), type_id); 37 | self.modules.insert(type_id, Box::new(output)); 38 | } 39 | 40 | pub fn register_listener(&mut self) 41 | where 42 | T::Output: Listener, 43 | { 44 | let output = T::build(); 45 | self.listeners.insert(TypeId::of::(), Box::new(output)); 46 | } 47 | 48 | pub fn try_get_module(&self) -> Option<&T> { 49 | let id = &TypeId::of::(); 50 | self.modules.get(id).and_then(|t| t.downcast_ref::()) 51 | } 52 | 53 | pub fn try_get_listener(&self) -> Option<&T> { 54 | let id = &TypeId::of::(); 55 | self.listeners.get(id).and_then(|t| t.downcast_ref::()) 56 | } 57 | 58 | pub fn try_get_module_mut(&mut self) -> Option<&mut T> { 59 | let id = &TypeId::of::(); 60 | self.modules.get_mut(id).and_then(|t| t.downcast_mut::()) 61 | } 62 | 63 | pub fn try_get_listener_mut(&mut self) -> Option<&mut T> { 64 | let id = &TypeId::of::(); 65 | self.listeners 66 | .get_mut(id) 67 | .and_then(|t| t.downcast_mut::()) 68 | } 69 | 70 | pub fn get_module_by_id(&self, id: TypeId) -> &dyn Module { 71 | self.modules.get(&id).unwrap().as_ref() 72 | } 73 | 74 | pub fn get_module(&self) -> &T { 75 | self.try_get_module().unwrap() 76 | } 77 | 78 | pub fn get_listener(&self) -> &T { 79 | self.try_get_listener().unwrap() 80 | } 81 | 82 | pub fn get_module_mut(&mut self) -> &mut T { 83 | self.try_get_module_mut().unwrap() 84 | } 85 | 86 | pub fn get_listener_mut(&mut self) -> &mut T { 87 | self.try_get_listener_mut().unwrap() 88 | } 89 | 90 | pub fn get_modules<'a, I>( 91 | &'a self, 92 | enabled: I, 93 | config: &'a Config, 94 | ) -> impl Iterator> 95 | where 96 | I: Iterator, 97 | { 98 | enabled.filter_map(|id| { 99 | self.module_names 100 | .get(id) 101 | .copied() 102 | .or_else(|| self.resolvers.get(id).and_then(|f| f(Some(config)))) 103 | .and_then(|id| self.modules.get(&id)) 104 | }) 105 | } 106 | 107 | pub fn get_modules_mut<'a, I>( 108 | &'a mut self, 109 | enabled: I, 110 | config: &Config, 111 | ) -> impl Iterator> 112 | where 113 | I: Iterator, 114 | { 115 | let resolver_types = self 116 | .resolvers 117 | .values() 118 | .filter_map(|r| r(Some(config))) 119 | .collect::>(); 120 | let type_ids = self 121 | .module_names 122 | .iter() 123 | .collect::>(); 124 | let enabled: HashSet<&String> = enabled.collect(); 125 | self.modules.values_mut().filter(move |m| { 126 | let name = m.name(); 127 | enabled.contains(&name) 128 | || type_ids 129 | .get(&name) 130 | .is_some_and(|ty| resolver_types.contains(*ty)) 131 | }) 132 | } 133 | 134 | pub fn get_listeners<'a>( 135 | &'a self, 136 | enabled: &'a HashSet, 137 | ) -> impl Iterator> { 138 | enabled 139 | .iter() 140 | .map(|id| self.listeners.get(id).expect("Listener was not registered")) 141 | } 142 | 143 | pub fn enabled_listeners<'a>( 144 | &'a self, 145 | modules: &'a EnabledModules, 146 | config: &'a Option<&Config>, 147 | ) -> impl Iterator + 'a { 148 | modules 149 | .get_all() 150 | .filter_map(|m| { 151 | self.module_names 152 | .get(m) 153 | .copied() 154 | .or_else(|| self.resolvers.get(m).and_then(|f| f(*config))) 155 | .map_none(|| { 156 | if !m.is_empty() { 157 | eprintln!("No Module named {m} is registered") 158 | } 159 | }) 160 | .and_then(|m_id| self.modules.get(&m_id).map(|m| m.requires())) 161 | }) 162 | .flat_map(|required| required.into_iter()) 163 | } 164 | 165 | pub fn all_listeners(&self) -> impl Iterator)> { 166 | self.listeners.iter() 167 | } 168 | 169 | pub fn add_resolver(&mut self, name: S, f: fn(Option<&Config>) -> Option) { 170 | self.resolvers.insert(name.to_string(), f); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/resolvers.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, env}; 2 | 3 | use crate::{ 4 | config::Config, 5 | modules::{ 6 | hyprland::{window::HyprWindowMod, workspaces::HyprWorkspaceMod}, 7 | niri::{NiriWindowMod, NiriWorkspaceMod}, 8 | wayfire::{WayfireWindowMod, WayfireWorkspaceMod}, 9 | }, 10 | registry::Registry, 11 | }; 12 | 13 | pub fn register_resolvers(registry: &mut Registry) { 14 | registry.add_resolver("window", window); 15 | registry.add_resolver("workspaces", workspaces); 16 | } 17 | 18 | fn window(_config: Option<&Config>) -> Option { 19 | env::var("XDG_CURRENT_DESKTOP") 20 | .ok() 21 | .and_then(|var| match var.as_str() { 22 | "niri" => Some(TypeId::of::()), 23 | "Hyprland" => Some(TypeId::of::()), 24 | "Wayfire:wlroots" => Some(TypeId::of::()), 25 | _ => None, 26 | }) 27 | } 28 | 29 | fn workspaces(_config: Option<&Config>) -> Option { 30 | env::var("XDG_CURRENT_DESKTOP") 31 | .ok() 32 | .and_then(|var| match var.as_str() { 33 | "niri" => Some(TypeId::of::()), 34 | "Hyprland" => Some(TypeId::of::()), 35 | "Wayfire:wlroots" => Some(TypeId::of::()), 36 | _ => None, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/tooltip.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::container; 2 | use iced::widget::tooltip::Position; 3 | use iced::{ 4 | widget::{container::Style, Tooltip}, 5 | Element, 6 | }; 7 | use iced::{Background, Border, Color, Theme}; 8 | 9 | pub trait ElementExt<'a, Message, Renderer> 10 | where 11 | Message: 'a, 12 | Renderer: iced::core::text::Renderer + 'a, 13 | { 14 | fn tooltip( 15 | self, 16 | tooltip: impl Into>, 17 | ) -> Tooltip<'a, Message, Theme, Renderer>; 18 | fn tooltip_maybe( 19 | self, 20 | tooltip: Option>>, 21 | ) -> Element<'a, Message, Theme, Renderer>; 22 | } 23 | 24 | impl<'a, Message, Renderer, Elem> ElementExt<'a, Message, Renderer> for Elem 25 | where 26 | Message: 'a, 27 | Renderer: iced::core::text::Renderer + 'a, 28 | Elem: Into>, 29 | { 30 | fn tooltip( 31 | self, 32 | tooltip: impl Into>, 33 | ) -> Tooltip<'a, Message, Theme, Renderer> { 34 | iced::widget::tooltip( 35 | self, 36 | container(tooltip).padding([2, 10]).style(|_| Style { 37 | text_color: Some(Color::WHITE), 38 | background: Some(Background::Color(Color::BLACK)), 39 | border: Border { 40 | color: Color::WHITE, 41 | width: 1., 42 | radius: 5_f32.into(), 43 | }, 44 | ..Default::default() 45 | }), 46 | Position::Bottom, 47 | ) 48 | } 49 | fn tooltip_maybe( 50 | self, 51 | tooltip: Option>>, 52 | ) -> Element<'a, Message, Theme, Renderer> { 53 | match tooltip { 54 | Some(t) => self.tooltip(t).into(), 55 | None => self.into(), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /wiki/Home.md: -------------------------------------------------------------------------------- 1 | # Welcome to the bar-rs wiki! 2 | 3 | While the configuration options aren't extensive at the moment, it's still good to know what tools you've got!
4 | There are some configuration examples at [default_config](https://github.com/Faervan/bar-rs/blob/main/default_config) 5 | 6 | *If you find that this wiki contains wrong information or is missing something critical, please open an [issue](https://github.com/Faervan/bar-rs/issues/new?template=Blank+issue).* 7 | 8 | ## Config path 9 | On Linux, the config path is `$XDG_DATA_HOME/bar-rs/bar-rs.ini` or `$HOME/.local/share/bar-rs/bar-rs.ini` 10 | 11 | **Example:**
12 | `/home/alice/.config/bar-rs/bar-rs.ini` 13 | 14 | If it isn't, you may check [here](https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_local_dir) 15 | 16 | ## Syntax 17 | bar-rs uses an ini-like configuration (as provided by [configparser](https://docs.rs/configparser/latest/configparser/)), which should be pretty easy to understand and use. 18 | 19 | It looks like this: 20 | ```ini 21 | [section] 22 | key = value 23 | ``` 24 | 25 | ## Data types 26 | | Data type | Description | Examples | 27 | | --------- | ----------- | -------- | 28 | | bool | Either yes or no | `true` or `false`, `1` or `0`, `enabled` or `disabled`... | 29 | | Color | A color as defined in the [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) | `rgba(255, 0, 0, 0.5)`, `blue`, `rgb(255, 255, 255)` | 30 | | String | Just a String | `DP-1` | 31 | | float | A floating point number | `20`, `5.8` | 32 | | u32 | A positive integer of range $2^{32}$ (0 to 4_294_967_295) | `0`, `50`, `1920` | 33 | | i32 | A signed integer (positive or negative) of range $2^{32}$ (-2_147_483_648 to 2_147_483_647) | `-500`, `2147483647` | 34 | | usize | A positive integer of range 0 - a lot (depends on your architecture, but probably enough) | `0`, `100000` | 35 | | Value list | A list of values, separated by spaces. | `20 5 20` | 36 | | Insets | A list of four values, representing all four directions (usually top, right, bottom and right). If one value is provided, it is used for all four sides. If two values are provided, the first is used for top and bottom and the second for left and right. | `0 20 5 10`, `0`, `0 10` | 37 | 38 | ## General 39 | The general section contains three options: 40 | | Option | Description | Data type | Default | 41 | | ------ | ----------- | --------- | ------- | 42 | | monitor | The monitor on which bar-rs should open. If this is set, bar-rs will override the default values of `width` and `height` (only the defaults, not the ones you specify). | String | / | 43 | | hot_reloading | Whether bar-rs should monitor the config file for changes | bool | true | 44 | | hard_reloading | Whether bar-rs should reopen and reload all modules (required for `anchor`, `width`, `height`, `margin` and e.g. workspace names set in the `niri.workspaces` module to be hot-reloadable) | bool | false | 45 | | anchor | The anchor to use. Can be `top`, `bottom`, `left` or `right`. This decides whether the bar is vertical or not. | String | top | 46 | | kb_focus | Defines whether bar-rs should be focusable. Can be `none` (no focus), `on_demand` (when you click on it) or `exclusive` (always stay focused). | String | none | 47 | 48 | **Example:** 49 | ```ini 50 | [general] 51 | monitor = DP-1 52 | hot_reloading = true 53 | hard_reloading = false 54 | anchor = top 55 | ``` 56 | 57 | ## General Styling 58 | | Option | Description | Data type | Default | 59 | | ------ | ----------- | --------- | ------- | 60 | | background | Background color of the status bar | Color | rgba(0, 0, 0, 0.5) | 61 | | width | The total width of the bar. The default depends on whether the bar is vertical or horizontal. | u32 | 30 or 1920 | 62 | | height | The total height of the bar. The default depends on whether the bar is vertical or horizontal. | u32 | 1080 or 30 | 63 | | margin | The margin between the bar and the screen edge, depending on the anchor. | float | 0 | 64 | | padding | The padding between the bar edges and the actual contents of the bar. | Insets (float) | 0 | 65 | | spacing | Space between the modules, can be different for left, center and right | Value list (float) | 20 10 15 | 66 | 67 | **Example:** 68 | ```ini 69 | [style] 70 | background = rgba(0, 0, 0, 0.5) 71 | width = 1890 72 | height = 30 73 | margin = 5 74 | padding = 0 75 | spacing = 20 5 20 76 | ``` 77 | -------------------------------------------------------------------------------- /wiki/Modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | The `[module]` section sets the enabled modules for each side: 3 | 4 | **Example:** 5 | ```ini 6 | [modules] 7 | left = workspaces, window 8 | center = date, time 9 | right = media, volume, cpu, memory 10 | ``` 11 | 12 | The following modules are currently available: 13 | 14 | | Module | Description | 15 | | ------ | ----------- | 16 | | [cpu](./Modules:-CPU.md) | Shows the current CPU usage | 17 | | [memory](./Modules:-Memory.md) | Shows the current memory usage | 18 | | [time](./Modules:-Date-and-Time.md) | Shows the local time | 19 | | [date](./Modules:-Date-and-Time.md) | Shows the local date | 20 | | [battery](./Modules:-Battery.md) | Shows the current capacity and remaining time | 21 | | [media](./Modules:-Media.md) | Shows the currently playing media as reported by `playerctl` | 22 | | [volume](./Modules:-Volume.md) | Shows the current audio volume as reported by `wpctl`, updated by `pactl` | 23 | | [disk_usage](./Modules:-Disk-usage.md) | Shows filesystem statistics fetched by the `statvfs` syscall | 24 | | [hyprland.window](./Modules:-Hyprland.md) | Shows the title of the currently focused window | 25 | | [hyprland.workspaces](./Modules:-Hyprland.md) | Shows the currently open workspaces | 26 | | [wayfire.window](./Modules:-Wayfire.md) | Shows the title of the currently focused window | 27 | | [wayfire.workspaces](./Modules:-Wayfire.md) | Shows the currently open workspace | 28 | | [niri.window](./Modules:-Niri.md) | Shows the title or app_id of the currently focused window | 29 | | [niri.workspaces](./Modules:-Niri.md) | Shows the currently open workspaces | 30 | 31 | To configure modules individually use a section name like this: 32 | ```ini 33 | [module:{{name}}] 34 | ``` 35 | where `{{name}}` is the name of the module, e. g. `cpu` 36 | 37 | **Example:** 38 | ```ini 39 | [module:time] 40 | icon_size = 24 41 | format = %H:%M 42 | 43 | [module:hyprland.workspaces] 44 | active_color = black 45 | active_background = rgba(255, 255, 255, 0.5) 46 | ``` 47 | 48 | ## Module Styling 49 | section name: `[module_style]` 50 | This section sets default values for all modules, which can be overridden for each module individually. 51 | | Option | Description | Data type | Default | 52 | | ------ | ----------- | --------- | ------- | 53 | | background | Background color of the status bar | Color | None | 54 | | spacing | Space between the modules, can be different for left, center and right | Value list (float) | 10 | 55 | | margin | The margin around this module. | Insets (float) | 0 | 56 | | padding | The padding surrounding the module content. | Insets (float) | 0 | 57 | | font_size | Default font size | float | 16 | 58 | | icon_size | Default icon size | float | 20 | 59 | | text_color | Default text color | Color | white | 60 | | icon_color | Default icon color | Color | white | 61 | | text_margin | The margin around the text of this module (can be used adjust the text position, negative values allowed). | Insets (float) | 0 | 62 | | icon_margin | The margin around the icon of this module (can be used adjust the icon position, negative values allowed). | Insets (float) | 0 | 63 | | border_color | The color of the border around this module. | Color | None | 64 | | border_width | The width of the border. | float | 1 | 65 | | border_radius | The radius (corner rounding) of the border. | Insets (float) | 0 | 66 | | on_click | A command to be executed when you click the module with the left mouse button. | String | / | 67 | | on_middle_click | A command to be executed when you click the module with the middle mouse button. | String | / | 68 | | on_right_click | A command to be executed when you click the module with the right mouse button. | String | / | 69 | 70 | ### Resolvers 71 | Resolvers are can be used instead of module names and are mapped to modules on specific conditions. 72 | 73 | Currently bar-rs has two resolvers: **window** and **workspaces**, which map to `hyprland.window`, `wayfire.window` or `niri.window` or `hyprland.workspaces`, `wayfire.workspaces` or `niri.workspaces`, respectively, depending on the environment variable `XDG_CURRENT_DESKTOP`. 74 | 75 | Defined in [src/resolvers.rs](https://github.com/Faervan/bar-rs/blob/main/src/resolvers.rs) 76 | 77 | -------------------------------------------------------------------------------- /wiki/Modules:-Battery.md: -------------------------------------------------------------------------------- 1 | # Battery 2 | Name: `battery` 3 | 4 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:battery`. 5 | | Option | Description | Data type | Default | 6 | | ------ | ----------- | --------- | ------- | 7 | | format | The format of this module | String | {{capacity}}% ({{hours}}h {{minutes}}min left) | 8 | 9 | ## Popup configuration 10 | You can override the default settings defined in [Popup Styling](./Popups.md) by setting them in this section: `module_popup:battery`. 11 | | Option | Description | Data type | Default | 12 | | ------ | ----------- | --------- | ------- | 13 | | format | the format of the popup text | String | `{{name}}: {{state}}\n\t{{icon}} {{capacity}}% ({{energy}} Wh)\n\thealth: {{health}}%{{time_remaining}}\n\tmodel: {{model}}` | 14 | | format_time | the format of the remaining battery time left (to full or to empty) | String | `\n\t{{hours}}h {{minutes}}min remaining` | 15 | 16 | `format` supports: 17 | - `name` (The name of the battery) 18 | - `state` (The charging state of the battery) 19 | - `icon` (The icon of the battery) 20 | - `capacity` (The capacity of the battery) 21 | - `energy` (The energy of the battery, in `Wh`) 22 | - `health` (The health of the battery: energy_full / energy_full_design) 23 | - `time_remaining` (The remaining battery time left (to full or to empty)) 24 | - `model` (The battery model) 25 | 26 | `format_time` supports: 27 | - `hours` 28 | - `minutes` 29 | -------------------------------------------------------------------------------- /wiki/Modules:-CPU.md: -------------------------------------------------------------------------------- 1 | # Cpu 2 | Name: `cpu` 3 | 4 | Shows the cpu usage, also has a popup which can show more stats, including each core individually.
5 | This module reads stats from `/proc/stat`, see [kernel.org](https://docs.kernel.org/filesystems/proc.html#miscellaneous-kernel-statistics-in-proc-stat) 6 | 7 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:cpu`. 8 | | Option | Description | Data type | Default | 9 | | ------ | ----------- | --------- | ------- | 10 | | icon | the icon to use | String | 󰻠 | 11 | 12 | ## Popup configuration 13 | You can override the default settings defined in [Popup Styling](./Popups.md) by setting them in this section: `module_popup:cpu`. 14 | | Option | Description | Data type | Default | 15 | | ------ | ----------- | --------- | ------- | 16 | | format | the format of the popup text | String | `Total: {{total}}%\nUser: {{user}}%\nSystem: {{system}}%\nGuest: {{guest}}%\n{{cores}}` | 17 | | format_core | the format of the cpu core | String | `Core {{index}}: {{total}}%` | 18 | 19 | both `format` and `format_core` support: 20 | - `total`: The total cpu/core usage 21 | - `user`: The userspace cpu/core usage 22 | - `system`: the kernelspace cpu/core usage 23 | - `guest`: the usage of processes running in a guest session 24 | 25 | `format` additionally supports: 26 | - `cores`: all cores ordered by their id (ascending), separated by line breaks 27 | 28 | `format_core` additionally supports: 29 | - `index`: The index of the core 30 | -------------------------------------------------------------------------------- /wiki/Modules:-Date-and-Time.md: -------------------------------------------------------------------------------- 1 | # Date and time modules 2 | These modules are basically identical. 3 | 4 | ## Date 5 | Name: `date` 6 | 7 | Shows the date. 8 | 9 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section. 10 | | Option | Description | Data type | Default | 11 | | ------ | ----------- | --------- | ------- | 12 | | icon | the icon to use | String |  | 13 | | format | How to format the date. See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for the syntax. | String | `%a, %d. %b` | 14 | 15 | ## Time 16 | Name: `time` 17 | 18 | Shows the time. 19 | 20 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section. 21 | | Option | Description | Data type | Default | 22 | | ------ | ----------- | --------- | ------- | 23 | | icon | the icon to use | String |  | 24 | | format | How to format the time. See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) for the syntax. | String | `%H:%M` | 25 | -------------------------------------------------------------------------------- /wiki/Modules:-Disk-usage.md: -------------------------------------------------------------------------------- 1 | # Disk usage 2 | Name: `disk_usage` 3 | 4 | Shows the disk usage, also has a popup which can show more stats.
5 | This module obtains the stats via the `statvfs` syscall, see [man7.org](https://man7.org/linux/man-pages/man3/statvfs.3.html) 6 | 7 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:disk_usage`. 8 | | Option | Description | Data type | Default | 9 | | ------ | ----------- | --------- | ------- | 10 | | icon | the icon to use | String | 󰦚 | 11 | | path | some directory, which determines the filesystem of interest | String | `/` | 12 | | format | the content of the module text | String | `{{used_perc}}%` | 13 | 14 | ## Popup configuration 15 | You can override the default settings defined in [Popup Styling](./Popups.md) by setting them in this section: `module_popup:disk_usage`. 16 | | Option | Description | Data type | Default | 17 | | ------ | ----------- | --------- | ------- | 18 | | format | the format of the popup text | String | `Total: {{total_gb}} GB\nUsed: {{used_gb}} GB ({{used_perc}}%)\nFree: {{free_gb}} GB ({{free_perc}}%)` | 19 | 20 | `format` provides the following variables: 21 | - `total`: The total filesystem space in mb 22 | - `total_gb`: The total filesystem space in gb 23 | - `used`: The used space in mb 24 | - `used_gb`: The used space in gb 25 | - `free`: The free space in mb 26 | - `free_gb`: The free space in gb 27 | - `used_perc`: the percentage of used space in the filesystem 28 | - `free_perc`: the percentage of free space in the filesystem 29 | -------------------------------------------------------------------------------- /wiki/Modules:-Hyprland.md: -------------------------------------------------------------------------------- 1 | # Hyprland modules 2 | Add this line to your `~/.config/hypr/hyprland.conf` to launch bar-rs on startup: 3 | ``` 4 | exec-once = bar-rs open 5 | ``` 6 | 7 | bar-rs supports two modules for the [Hyprland](https://github.com/hyprwm/Hyprland/) wayland compositor: 8 | 9 | ## Hyprland window 10 | Name: `hyprland.window` 11 | 12 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:hyprland.window`. 13 | | Option | Description | Data type | Default | 14 | | ------ | ----------- | --------- | ------- | 15 | | max_length | the maximum character length of the title | usize | 25 | 16 | 17 | ## Hyprland workspaces 18 | Name: `hyprland.workspaces` 19 | 20 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:hyprland.workspaces`. 21 | | Option | Description | Data type | Default | 22 | | ------ | ----------- | --------- | ------- | 23 | | icon_padding | Padding for the icon, only useful with a background or border. | Insets (float) | 0 | 24 | | icon_background | Background of the icons. | Color | None | 25 | | icon_border_color | Color of the border around the icons. | Color | / | 26 | | icon_border_width | Width of the border around the icons. | float | 1 | 27 | | icon_border_radius | Radius of the border around the icons. | Insets (float) | 0 | 28 | | active_padding | Padding for the active icon, only useful with a background or border. | Insets (float) | 0 | 29 | | active_size | Size of the currently active icon. | float | 20 | 30 | | active_color | the color for the currently focused workspace | Color | black | 31 | | active_background | the background color for the currently focused workspace | Color | rgba(255, 255, 255, 0.5) | 32 | | active_border_color | Color of the border around the active icon. | Color | / | 33 | | active_border_width | Width of the border around the active icon. | float | 1 | 34 | | active_border_radius | Radius of the border around the active icon. | Insets (float) | 0 | 35 | 36 | To have the `hyprland.workspaces` module show some nice workspace icons, set rules for your workspaces like this: 37 | ``` 38 | workspace = 1, defaultName:󰈹 39 | ``` 40 | 41 | > \[!TIP] 42 | > Find some nice icons to use as workspace names [here](https://www.nerdfonts.com/cheat-sheet) 43 | -------------------------------------------------------------------------------- /wiki/Modules:-Media.md: -------------------------------------------------------------------------------- 1 | # Media 2 | Name: `media` 3 | 4 | Shows the currently playing media title and artist and offers basic playback control using a popup.
5 | This module depends on `playerctl`. 6 | 7 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:media`. 8 | | Option | Description | Data type | Default | 9 | | ------ | ----------- | --------- | ------- | 10 | | icon | the icon to use | String |  | 11 | | max_length | the maximum character length to show | usize | 35 | 12 | | max_title_length | the maximum character length of the title part of the media. Only applies if `max_length` is reached and the media has an artist | usize | 20 | 13 | 14 | ## Popup configuration 15 | You can override the default settings defined in [Popup Styling](./Popups.md) by setting them in this section: `module_popup:media`. 16 | | Option | Description | Data type | Default | 17 | | ------ | ----------- | --------- | ------- | 18 | | format | the format of the popup text | String | `{{title}}{{status}}\nin: {{album}}\nby: {{artist}}\n{{length}}` | 19 | | format_length | the format of length of the media | String | `{{minutes}}min {{seconds}}sec` | 20 | 21 | `format` supports: 22 | - `title` (The title of the playing media) 23 | - `artist` (The artist of the playing media) 24 | - `album` (The album of the playing media) 25 | - `status` (The status of the playing media: empty if playing, `" (paused)"` if paused) 26 | - `length` (The length of the playing media, it's format is determined by `format_length`) 27 | 28 | `format_length` supports: 29 | - `minutes` 30 | - `seconds` 31 | -------------------------------------------------------------------------------- /wiki/Modules:-Memory.md: -------------------------------------------------------------------------------- 1 | # Memory 2 | Name: `memory` 3 | 4 | This module shows the percentage of memory usage.
5 | Depends on `free`, `grep`, `awk` and `printf`. 6 | 7 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:memory`. 8 | | Option | Description | Data type | Default | 9 | | ------ | ----------- | --------- | ------- | 10 | | icon | the icon to use | String | 󰍛 | 11 | -------------------------------------------------------------------------------- /wiki/Modules:-Niri.md: -------------------------------------------------------------------------------- 1 | # Niri modules 2 | Add this to your `~/.config/niri/config.kdl` to launch bar-rs on startup: 3 | ```kdl 4 | spawn-at-startup "bar-rs" "open" 5 | ``` 6 | 7 | bar-rs supports two modules for the [Niri](https://github.com/YaLTeR/niri) wayland compositor: 8 | 9 | ## Niri window 10 | Name: `niri.window` 11 | 12 | This module shows the name or app_id of the currently focused window. A popup is also available. 13 | 14 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:niri.window`. 15 | | Option | Description | Data type | Default | 16 | | ------ | ----------- | --------- | ------- | 17 | | max_length | the maximum character length of the title | usize | 25 | 18 | | show_app_id | Show the app_id instead of the window title | bool | false | 19 | 20 | ### Popup configuration 21 | You can override the default settings defined in [Popup Styling](./Popups.md) by setting them in this section: `module_popup:niri.window`. 22 | | Option | Description | Data type | Default | 23 | | ------ | ----------- | --------- | ------- | 24 | | format | the format of the popup text | String | `Title: {{title}}\nApplication ID: {{app_id}}\nWindow ID: {{window_id}}\nWorkspace ID: {{workspace_id}}` | 25 | 26 | ```ini 27 | [module_popup:niri.window] 28 | format = {{title}}\n{{app_id}} 29 | ``` 30 | 31 | This supports: 32 | - `title` (The active window title) 33 | - `app_id` (The active window's application id) 34 | - `window_id` (The active window's id) 35 | - `workspace_id` (The id of the active workspace) 36 | 37 | ## Niri workspaces 38 | Name: `niri.workspaces` 39 | 40 | This module shows the currently open workspaces and allows to change your workspace by clicking on a workspace icon. 41 | 42 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:niri.workspaces`. 43 | | Option | Description | Data type | Default | 44 | | ------ | ----------- | --------- | ------- | 45 | | icon_padding | Padding for the icon, only useful with a background or border. | Insets (float) | 0 | 46 | | icon_background | Background of the icons. | Color | None | 47 | | icon_border_color | Color of the border around the icons. | Color | / | 48 | | icon_border_width | Width of the border around the icons. | float | 1 | 49 | | icon_border_radius | Radius of the border around the icons. | Insets (float) | 0 | 50 | | active_padding | Padding for the active icon, only useful with a background or border. | Insets (float) | 0 | 51 | | active_size | Size of the currently active icon. | float | 20 | 52 | | active_color | the color for the currently focused workspace | Color | black | 53 | | active_background | the background color for the currently focused workspace | Color | rgba(255, 255, 255, 0.5) | 54 | | active_border_color | Color of the border around the active icon. | Color | / | 55 | | active_border_width | Width of the border around the active icon. | float | 1 | 56 | | active_border_radius | Radius of the border around the active icon. | Insets (float) | 0 | 57 | | Output: n | the name of the nth workspace on the given output (monitor) | String | / | 58 | | output_order | the order of the workspaces, depending on their output (monitor) | Value list (String) | / | 59 | | fallback_icon | the icon to use for unnamed workspaces | String |  | 60 | | active_fallback_icon | the icon to use for unnamed workspaces when active | String |  | 61 | 62 | > \[!TIP] 63 | > Find some nice icons to use as workspace names [here](https://www.nerdfonts.com/cheat-sheet) 64 | 65 | **Example:** 66 | ```ini 67 | [module:niri.workspaces] 68 | spacing = 15 69 | padding = 0 12 0 6 70 | icon_margin = -2 0 0 0 71 | icon_size = 25 72 | active_size = 25 73 | output_order = DP-1, HDMI-A-1 74 | DP-1: 1 = 󰈹 75 | DP-1: 2 =  76 | DP-1: 3 = 󰓓 77 | DP-1: 4 =  78 | DP-1: 5 =  79 | ``` 80 | -------------------------------------------------------------------------------- /wiki/Modules:-Volume.md: -------------------------------------------------------------------------------- 1 | # Volume 2 | Name: `volume` 3 | 4 | This module shows the audio volume. Sound can be toggled (muted or unmuted) by clicking on the volume icon.
5 | This module depends on `wpctl` and `pactl`. 6 | 7 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:volume`. 8 | -------------------------------------------------------------------------------- /wiki/Modules:-Wayfire.md: -------------------------------------------------------------------------------- 1 | # Wayfire modules 2 | Add this to your `~/.config/wayfire.ini` to launch bar-rs on startup: 3 | ```ini 4 | [autostart] 5 | bar = bar-rs open 6 | ``` 7 | 8 | bar-rs supports two modules for the [Wayfire](https://github.com/WayfireWM/wayfire/) wayland compositor: 9 | 10 | ## Wayfire window 11 | Name: `wayfire.window` 12 | 13 | Shows the name of the currently open window. 14 | 15 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:wayfire.window`. 16 | | Option | Description | Data type | Default | 17 | | ------ | ----------- | --------- | ------- | 18 | | max_length | the maximum character length of the title | usize | 25 | 19 | 20 | ## Wayfire workspaces 21 | Name: `wayfire.workspaces` 22 | 23 | Shows the name of the currently focused workspace. 24 | 25 | You can override the default settings defined in [Module Styling](./Modules.md) by setting them in this section: `module:wayfire.workspaces`. 26 | | Option | Description | Data type | Default | 27 | | ------ | ----------- | --------- | ------- | 28 | | icon_padding | Padding for the icon, useful to adjust the icon position. | Insets (float) | 0 | 29 | | fallback_icon | Default icon to use | String | / | 30 | | (row, column) | the name of the workspace | String | fallback_icon or `row/column` | 31 | 32 | > \[!TIP] 33 | > Find some nice icons to use as workspace names [here](https://www.nerdfonts.com/cheat-sheet) 34 | 35 | **Example:** 36 | ```ini 37 | [module:wayfire.workspaces] 38 | fallback_icon =  39 | (0, 0) = 󰈹 40 | (1, 0) =  41 | (2, 0) = 󰓓 42 | (0, 1) =  43 | (1, 1) =  44 | ``` 45 | -------------------------------------------------------------------------------- /wiki/Popups.md: -------------------------------------------------------------------------------- 1 | # Popups 2 | Extra windows that open on click to show some more info or allow for additional actions. 3 | 4 | ## Popup Styling 5 | section name: `[popup_style]` 6 | This section sets default values for all module popups, which can be overridden for each module individually. 7 | | Option | Description | Data type | Default | 8 | | ------ | ----------- | --------- | ------- | 9 | | width | The width of the popup | i32 | 300 | 10 | | height | The height of the popup | i32 | 300 | 11 | | fill_content_to_size | Whether the content of the module should fill the entire width and height | bool | false | 12 | | padding | The padding surrounding the popup content. | Insets (float) | 10 20 | 13 | | text_color | Default text color | Color | white | 14 | | icon_color | Default icon color | Color | white | 15 | | font_size | Default font size | float | 14 | 16 | | icon_size | Default icon size | float | 24 | 17 | | text_margin | The margin around the text of this popup (can be used adjust the text position, negative values allowed). | Insets (float) | 0 | 18 | | icon_margin | The margin around the icon of this popup (can be used adjust the icon position, negative values allowed). | Insets (float) | 0 | 19 | | spacing | Space between elements in the popup | float | 0 | 20 | | background | Background color of the popup | Color | rgba(255, 255, 255, 0.8) | 21 | | border_color | The color of the border around this popup. | Color | None | 22 | | border_width | The width of the border. | float | 0 | 23 | | border_radius | The radius (corner rounding) of the border. | Insets (float) | 8 | 24 | --------------------------------------------------------------------------------