1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 10:41:03 +02:00

Merge branch 'develop' into refactor/dbus-traits

This commit is contained in:
Reinout Meliesie 2024-05-31 18:01:08 +02:00
commit 96d0d9e85b
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
100 changed files with 4647 additions and 1780 deletions

21
.github/scripts/ubuntu_setup.sh vendored Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
# sudo needed for github runner, not available by default for cross images
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
SUDO=""
fi
# Needed for cross-compilation
if [ -n "$CROSS_DEB_ARCH" ]; then
$SUDO dpkg --add-architecture "$CROSS_DEB_ARCH"
fi
# CROSS_DEB_ARCH is empty for native builds
$SUDO apt-get update && $SUDO apt-get install --assume-yes \
libssl-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-layer-shell-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libluajit-5.1-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}

68
.github/workflows/binary.yml vendored Normal file
View file

@ -0,0 +1,68 @@
name: Binary
on:
workflow_run:
workflows: [Deploy]
types: [completed]
release:
types: [created]
workflow_dispatch:
jobs:
get_last_release:
runs-on: ubuntu-latest
outputs:
latest_release_tag: ${{ steps.latest-release.outputs.LATEST_RELEASE_TAG }}
steps:
- uses: actions/checkout@v4
- name: Extract the latest release
id: latest-release
run: echo "LATEST_RELEASE_TAG=$(gh release ls --json isLatest,tagName -q '.[] | select(.isLatest == true) | .tagName')" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
name: Build
runs-on: ubuntu-latest
needs: get_last_release
strategy:
fail-fast: false
matrix:
platform:
- {target: x86_64-unknown-linux-gnu, zipext: ".tar.gz"}
- {target: aarch64-unknown-linux-gnu, zipext: ".tar.gz"}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.get_last_release.outputs.latest_release_tag }}
- uses: taiki-e/install-action@v2
with:
tool: cross
- name: Add OpenSSL crate (vendored)
run: cargo add openssl --features vendored
- name: Cross Build Release
run: cross build --locked --release --target=${{ matrix.platform.target }}
- name: Get name of Binary from metadata
run: echo "BINARY_NAME=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].targets[] | select( .kind | map(. == "bin") | any ) | .name')" >> $GITHUB_ENV
- name: Compress the built binary
if: ${{ matrix.platform.zipext == '.tar.gz' }}
run: tar -zcvf ${{env.BINARY_NAME}}-${{needs.get_last_release.outputs.latest_release_tag}}-${{matrix.platform.target}}.tar.gz -C target/${{matrix.platform.target}}/release ${{env.BINARY_NAME}}
- name: Upload to release
run: gh release upload ${{needs.get_last_release.outputs.latest_release_tag}} ${{env.BINARY_NAME}}-${{needs.get_last_release.outputs.latest_release_tag}}-${{matrix.platform.target}}${{matrix.platform.zipext}}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
on-failure:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' }}
steps:
- run: echo 'The triggering workflow Deploy failed'
- run: exit 1

View file

@ -32,9 +32,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --no-default-features --features config+json
@ -53,9 +51,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --all-targets --all-features
@ -72,9 +68,7 @@ jobs:
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Build
run: cargo build --verbose
@ -82,4 +76,4 @@ jobs:
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
command: test

View file

@ -18,9 +18,7 @@ jobs:
override: true
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
run: ./.github/scripts/ubuntu_setup.sh
- name: Update CHANGELOG
id: changelog
@ -48,4 +46,4 @@ jobs:
- uses: katyo/publish-crates@v1
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
.direnv/

View file

@ -1,15 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used" />
<option name="command" value="clippy -- &#10;-W clippy::pedantic &#10;-W clippy::unwrap_used&#10;-A clippy::cast_possible_wrap&#10;-A clippy::cast_possible_truncation&#10;-A clippy::cast_sign_loss&#10;-A clippy::cast_precision_loss&#10;-A clippy::cast_lossless&#10;-A clippy::module_name_repetitions" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="false" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">

View file

@ -2,18 +2,17 @@
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package ironbar --bin ironbar" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs>
<env name="IRONBAR_LOG" value="info" />
<env name="IRONBAR_CONFIG" value="examples/test.corn" />
</envs>
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs>
<env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="debug" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">

View file

@ -4,6 +4,85 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.15.1] - 2024-05-05
Release to bump hyprland-rs version due to Hyprland v0.40 socket path breaking change.
### :memo: Documentation Changes
- [`47b6c47`](https://github.com/JakeStanger/ironbar/commit/47b6c477242ad52aae77a6820740d9c5f4bfc263) - **compiling**: add lua deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1971f3b`](https://github.com/JakeStanger/ironbar/commit/1971f3bb1ef3d059b29b99527e77ffaaf92240aa) - **volume**: update deprecated volume token *(PR [#567](https://github.com/JakeStanger/ironbar/pull/567) by [@drendog](https://github.com/drendog))*
## [v0.15.0] - 2024-04-28
### :sparkles: New Features
- [`f4384b6`](https://github.com/JakeStanger/ironbar/commit/f4384b6252e86d4e2558e1c36810d4ef903bd58c) - enable use of markup in clock module format and format_popup, and update documentation to reflect supporting Pango markup in both *commit by [@Dridus](https://github.com/Dridus))*
- [`76a6816`](https://github.com/JakeStanger/ironbar/commit/76a68165f09a6d07f8e95008cb9fe3d40d99abe0) - **upower**: add new formatting properties *(commit by [@Disr0](https://github.com/Disr0))*
- [`b037a55`](https://github.com/JakeStanger/ironbar/commit/b037a55fb78d05cce0e03bad27a10cbdf743c573) - **tray**: add `direction` option *(commit by [@calops](https://github.com/calops))*
- [`72440e6`](https://github.com/JakeStanger/ironbar/commit/72440e69c9e665f3e82e569e770747fc63765b53) - **tray**: icon size setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a70956b`](https://github.com/JakeStanger/ironbar/commit/a70956bb3b17f559fda1fdca444e271ae9d3c4cd) - add new volume module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7742a46`](https://github.com/JakeStanger/ironbar/commit/7742a465780ed5db80cdb518a834200082a5e936) - swaync notifications module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ba00445`](https://github.com/JakeStanger/ironbar/commit/ba004455b25fb51d28a5ec0cdf0f510c2157eb94) - **tray**: option to prefer theme-provided icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`994f4a4`](https://github.com/JakeStanger/ironbar/commit/994f4a4a123452607dd591e1e358ec218a3cb5ae) - ability to add custom modules instead native modules *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`46cbaca`](https://github.com/JakeStanger/ironbar/commit/46cbaca5e08a5be8945486d007c0f7315d10b351) - option to disable module popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`46224d8`](https://github.com/JakeStanger/ironbar/commit/46224d8a541699a04b2311e89766dded781863d6) - **custom**: ability to add modules/widgets to buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`702b0a6`](https://github.com/JakeStanger/ironbar/commit/702b0a63bf75204d03f9229f1667cb2e77c1b8b8) - Add orientation support for clock *(commit by [@ClaireNeveu](https://github.com/ClaireNeveu))*
- [`70b2c59`](https://github.com/JakeStanger/ironbar/commit/70b2c592b284965382182098b0b90b40bdac9965) - Add orientation support for custom label and button *(commit by [@ClaireNeveu](https://github.com/ClaireNeveu))*
- [`44be585`](https://github.com/JakeStanger/ironbar/commit/44be58594b296ff6a1a7d902c88aa01116322538) - Add orientation and direction support for sys info *(commit by [@ClaireNeveu](https://github.com/ClaireNeveu))*
- [`cfaba87`](https://github.com/JakeStanger/ironbar/commit/cfaba87f2fe470667eea4eca0504f6e8651c90f3) - **ipc**: ironvar list command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b0a05b7`](https://github.com/JakeStanger/ironbar/commit/b0a05b7cda1d07af6673a5ee9fb8105ed1497a36) - new cairo module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`d03c752`](https://github.com/JakeStanger/ironbar/commit/d03c752f9a0ac849fe3f1a93d7c3de4f743c7f00) - **launcher**: option to reverse order *(commit by [@SerraPi](https://github.com/SerraPi))*
### :bug: Bug Fixes
- [`30b11db`](https://github.com/JakeStanger/ironbar/commit/30b11db43503f4a78fde8f17fa3af6ce99375cc2) - **tray**: cannot activate menu options with right click *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f68d95a`](https://github.com/JakeStanger/ironbar/commit/f68d95a740c02434866c662d2cd915a0c5253ba5) - **logging**: log file growing indefinitely *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6fe9c54`](https://github.com/JakeStanger/ironbar/commit/6fe9c541347b7bdd69e3d735f07a17a5d4b124ca) - **clipboard**: unable to paste large images into xwayland *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a10466e`](https://github.com/JakeStanger/ironbar/commit/a10466e7e9dafd29e80994eccccdd398e9434b95) - **popup**: re-posiiton on resize due to content change *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0675b91`](https://github.com/JakeStanger/ironbar/commit/0675b917f2beeed3e6b626dad8fe34b8063d9c83) - **tray**: icons ignoring scaling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c62d475`](https://github.com/JakeStanger/ironbar/commit/c62d47555ec31baa1a7094491e2977a832f4cfcc) - **tray**: submenus not working *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f263849`](https://github.com/JakeStanger/ironbar/commit/f2638497fac4f0e350d069857e6e7437cb756669) - **launcher**: not resolving icon for some apps *(commit by [@slowsage](https://github.com/slowsage))*
- [`cf44c46`](https://github.com/JakeStanger/ironbar/commit/cf44c461db7a3e5093c69c12fcef57cf9675c6e2) - **workspaces**: favourites not persisting for initally opened workspaces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`180f874`](https://github.com/JakeStanger/ironbar/commit/180f8748bbe52affbbfe8c5ec045c753e63d554d) - **music** - Handle NoActivePlayer (playerctld) , NoReply, NoMethod, ServiceUnknown DBus errors in mpris. *(commit by [@slowsage](https://github.com/slowsage))*
- [`3ba8b4b`](https://github.com/JakeStanger/ironbar/commit/3ba8b4bd9611bd82b251fbaf51f4b313f36f1c89) - regressions introduced by [#505](https://github.com/JakeStanger/ironbar/pull/505) *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f50a65e`](https://github.com/JakeStanger/ironbar/commit/f50a65eab5edfa3a96e4e3b7e54de754ead1eb21) - upower module should display correctly for vertical bars *(commit by [@ClaireNeveu](https://github.com/ClaireNeveu))*
- [`188abc3`](https://github.com/JakeStanger/ironbar/commit/188abc33e910a708061517b13e36125f9d7736d3) - **tray**: icon colour channels are being incorrectly rendered *(commit by [@rdnelson](https://github.com/rdnelson))*
- [`ea2b208`](https://github.com/JakeStanger/ironbar/commit/ea2b20816d459aafe79578f155454d50684f5fad) - **focused**: incorrectly clearing when unfocused window title changes *(PR [#556](https://github.com/JakeStanger/ironbar/pull/556) by [@JakeStanger](https://github.com/JakeStanger))*
- :arrow_lower_right: *fixes issue [#488](https://github.com/JakeStanger/ironbar/issues/488) opened by [@bluebyt](https://github.com/bluebyt)*
- :arrow_lower_right: *fixes issue [#554](https://github.com/JakeStanger/ironbar/issues/554) opened by [@DanteDragan](https://github.com/DanteDragan)*
### :recycle: Refactors
- [`a55ba8c`](https://github.com/JakeStanger/ironbar/commit/a55ba8c523ff19fa607a31bac589a55b48db39ad) - rename `get_orientation` method to `orientation` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`86c5b69`](https://github.com/JakeStanger/ironbar/commit/86c5b69f18356201db5c3a314f36e0f68e74c357) - **tray**: tidy imports *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`00a6a84`](https://github.com/JakeStanger/ironbar/commit/00a6a84ca6af6f3c64183ec08fdff7430770d39b) - **upower**: cheaper string building *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b912619`](https://github.com/JakeStanger/ironbar/commit/b912619d61a74921c854ea6464e0922e5c107a27) - **image**: add debug logging *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c7b6ee8`](https://github.com/JakeStanger/ironbar/commit/c7b6ee8bc00e92d075b8c66105c29e3df0906145) - add dead_code allow to fix build warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`004ea76`](https://github.com/JakeStanger/ironbar/commit/004ea76da5af3e8750e5a02a8780f62337b06844) - **tray**: complete client rewrite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`706e040`](https://github.com/JakeStanger/ironbar/commit/706e040e25b54c128b0364a8e6982f2372da0b99) - split bar/top-level config structs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1b35354`](https://github.com/JakeStanger/ironbar/commit/1b353542722ac70b99e5a4f846e68ae68a2870fd) - fix clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9245188`](https://github.com/JakeStanger/ironbar/commit/9245188af7830b09aa564ab83f1db2e2a4cffb2c) - better error handling for client initialization *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`314bfe7`](https://github.com/JakeStanger/ironbar/commit/314bfe7abecec66692d138b49186865c9132c6ef) - **nix**: simplify flake *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`76a6816`](https://github.com/JakeStanger/ironbar/commit/76a68165f09a6d07f8e95008cb9fe3d40d99abe0) - correct formatting tokens in upower *(commit by [@Disr0](https://github.com/Disr0))*
- [`e26e213`](https://github.com/JakeStanger/ironbar/commit/e26e213c4e409018f3b5c35b0319f5db8c0fa3bb) - improve info about logging *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`163a70e`](https://github.com/JakeStanger/ironbar/commit/163a70e690e2a9950c23ef8164dafd762a6a1020) - **readme**: update nix caching info *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6a03c46`](https://github.com/JakeStanger/ironbar/commit/6a03c46146b612e53fa866ad98263d7cc29aacc8) - **readme**: add [mixxc](https://github.com/Elvyria/Mixxc) acknowledgement *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3a3d3d7`](https://github.com/JakeStanger/ironbar/commit/3a3d3d75cd4fd8d474edc4c6ddb2c47bce60df16) - **readme**: add void package *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`fc42f6c`](https://github.com/JakeStanger/ironbar/commit/fc42f6c540131576d6eaf1201e78ba216861947d) - **readme**: add repology badge *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8e9db14`](https://github.com/JakeStanger/ironbar/commit/8e9db141f8a668063ece3622ec91c3e22c3a87a3) - **macros**: add missing comment *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bb02a21`](https://github.com/JakeStanger/ironbar/commit/bb02a21d0efcad07ba0598a38ff56abfc7c06107) - **compiling**: add missing notifications feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ee8873a`](https://github.com/JakeStanger/ironbar/commit/ee8873a94a904d895a2005fa3593628c9500fc0c) - **custom**: add native examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dffb3e5`](https://github.com/JakeStanger/ironbar/commit/dffb3e5d543d33c10146d43384b8a3c03ef3aab7) - **workspaces**: fix typo that results in a non working config *(commit by [@nyadiia](https://github.com/nyadiia))*
- [`782b955`](https://github.com/JakeStanger/ironbar/commit/782b9554a2a24123acfebcc80401abf051c7dc06) - fix issues with several more toml examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### Note for maintainers
The addition of new modules requires the following new build dependencies:
- `libpulse`
- `luajit`
It also requires `lua-lgi` as a runtime dependency.
## [v0.14.1] - 2024-02-10
### :bug: Bug Fixes
- [`1c9c9bb`](https://github.com/JakeStanger/ironbar/commit/1c9c9bbece878286939abacfaec0daaecc559243) - **cli**: error when launched via `swaybar_command` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -24,7 +103,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`af7e037`](https://github.com/JakeStanger/ironbar/commit/af7e037dd5a24cff0959e2fd5f04e3eb49418b23) - **dynamic string**: test pango attributes with ironvars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`7d51155`](https://github.com/JakeStanger/ironbar/commit/7d51155a3e6e6e1d77f2e3d2b8e6c73831b15c0e) - update CHANGELOG.md for v0.14.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`754e339`](https://github.com/JakeStanger/ironbar/commit/754e33952eaf7794d00c831c46aab007684ff0b2) - add info on speeding up compilation time *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cb2f9b0`](https://github.com/JakeStanger/ironbar/commit/cb2f9b0aaff1519516664ab04a3a195d29983b4e) - **examples**: fix issues with example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1b54276`](https://github.com/JakeStanger/ironbar/commit/1b54276bea6268131fca7c3f453284ca0aee4b9b) - **compilation**: add sccache section *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -69,7 +147,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`18b4784`](https://github.com/JakeStanger/ironbar/commit/18b47844f94067bbf029cf4b6b94153a742d6af1) - **wayland**: simplify task spawning code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`e5281e9`](https://github.com/JakeStanger/ironbar/commit/e5281e96193a2f42d52a0eb736473cdb378dd243) - update CHANGELOG.md for v0.13.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b9c41af`](https://github.com/JakeStanger/ironbar/commit/b9c41af0f73c85c0daf6f0af2fd1339c79e66758) - **workspaces**: add missing `.inactive` selector *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`abd1f80`](https://github.com/JakeStanger/ironbar/commit/abd1f8054821cedef594ebcb22a914feb230c9f1) - **examples**: update discord icon, temporarily disable random label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ccc6ff2`](https://github.com/JakeStanger/ironbar/commit/ccc6ff2d943ba46d0f9a36478933cda8b14b7ab1) - **readme**: add nixpkgs details *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -126,7 +203,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -158,7 +234,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -220,7 +295,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -261,7 +335,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`82875cd`](https://github.com/JakeStanger/ironbar/commit/82875cde687628f3ee3436343068825440128599) - update CHANGELOG.md for v0.10.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7c36f5c`](https://github.com/JakeStanger/ironbar/commit/7c36f5cb0cf03191c9b03e2455b63829a64e402e) - fix a couple of issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`83a4916`](https://github.com/JakeStanger/ironbar/commit/83a49165c42fa793ef1224f93cbc147bc69de894) - **compiling**: add info about build deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5bbe64b`](https://github.com/JakeStanger/ironbar/commit/5bbe64bb86fb2db0921e284a1560db2f6c1a1920) - **clock**: format table *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -301,7 +374,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`15f0857`](https://github.com/JakeStanger/ironbar/commit/15f0857859d5d4a590b60b6b1a4347b4b84a58a1) - replace icon loading with improved general image loading *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1ed3220`](https://github.com/JakeStanger/ironbar/commit/1ed3220733c2dcb7c5e5cbf377b3324d3183609e) - update CHANGELOG.md for v0.9.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - **music**: remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6a39905`](https://github.com/JakeStanger/ironbar/commit/6a39905b4333582fbcda81a66a9b91055333d698) - **compiling**: add missing full stop *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7b23e61`](https://github.com/JakeStanger/ironbar/commit/7b23e61e7dedf2736a30580b6c1aa84e002c462c) - **wiki**: update screenshots and examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -340,7 +412,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`0d7ab54`](https://github.com/JakeStanger/ironbar/commit/0d7ab541604691455ed39c73e039ac0635307bc8) - remove redundant clone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`b97f018`](https://github.com/JakeStanger/ironbar/commit/b97f018e81aa55a871a12aa3e1e4b07b1f8eb50f) - update CHANGELOG.md for v0.8.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c223892`](https://github.com/JakeStanger/ironbar/commit/c223892a57b29ae56431fc585b8cec503f3206c7) - **workspaces**: update for hyprland/new ordering option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -379,7 +450,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`907a565`](https://github.com/JakeStanger/ironbar/commit/907a565f3d418a276dfb454e1189ddede1814291) - **dynamic label**: do not run if cannot initialise gtk *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1c032ae`](https://github.com/JakeStanger/ironbar/commit/1c032ae8e3a38b82c286bab7d102842f14b708e1) - update CHANGELOG.md for v0.7.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`58d55db`](https://github.com/JakeStanger/ironbar/commit/58d55db6600fe2f9b23ae8ec6a50a686d2acaf65) - migrate wiki into main repo *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c480296`](https://github.com/JakeStanger/ironbar/commit/c48029664d5f58bf73faa2931f34b38b8b184d25) - **script**: improve doc comment *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8c77410`](https://github.com/JakeStanger/ironbar/commit/8c774100f1c8ea051284c6950339a2c8ed59a52a) - **script**: add information on new mode options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -415,7 +485,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`bc625b9`](https://github.com/JakeStanger/ironbar/commit/bc625b929b8644ce92f275b5d98cdf74b93fe067) - clippy & fmt *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`9d9c275`](https://github.com/JakeStanger/ironbar/commit/9d9c2753137331ae85ac8ab7d75a6de9a9c82042) - update CHANGELOG.md for v0.6.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27d0479`](https://github.com/JakeStanger/ironbar/commit/27d04795af1c25fe5f765c7480d5dd5d096a8ab7) - **readme**: add warning about crate being outdated *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a06c4bc`](https://github.com/JakeStanger/ironbar/commit/a06c4bccca6cb51935605ac9239e63024fb7c663) - **examples**: add full system info config *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0a331f3`](https://github.com/JakeStanger/ironbar/commit/0a331f31381f0d967793c0d8b7a14e2a43bf666f) - **readme**: remove warning about outdated cargo package *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -441,7 +510,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`daafa09`](https://github.com/JakeStanger/ironbar/commit/daafa0943e5b9886b09fd18d6fff04558fb02335) - update CHANGELOG.md for v0.5.2 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b352181`](https://github.com/JakeStanger/ironbar/commit/b352181b3d232ccc79ffc1d9e22a633729d01a47) - update json example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bb4fe7f`](https://github.com/JakeStanger/ironbar/commit/bb4fe7f7f58fa2a6d0a2259bd9442700d2c884f7) - **readme**: credit smithay client toolkit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -504,4 +572,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0
[v0.14.0]: https://github.com/JakeStanger/ironbar/compare/v0.13.0...v0.14.0
[v0.14.1]: https://github.com/JakeStanger/ironbar/compare/v0.14.0...v0.14.1
[v0.14.1]: https://github.com/JakeStanger/ironbar/compare/v0.14.0...v0.14.1
[v0.15.0]: https://github.com/JakeStanger/ironbar/compare/v0.14.3...v0.15.0
[v0.15.1]: https://github.com/JakeStanger/ironbar/compare/v0.15.0...v0.15.1

731
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "ironbar"
version = "0.15.0-pre"
version = "0.16.0-pre"
edition = "2021"
license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar"
@ -11,6 +11,7 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features]
default = [
"cli",
"cairo",
"clipboard",
"clock",
"config+all",
@ -46,6 +47,8 @@ http = ["dep:reqwest"]
"config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["nix"]
clock = ["chrono"]
@ -59,7 +62,7 @@ music = ["regex"]
"music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd-utils"]
networkmanager = ["futures-lite", "zbus"]
networkmanager = ["futures-lite", "futures-signals", "zbus"]
notifications = ["zbus"]
@ -71,7 +74,7 @@ upower = ["upower_dbus", "zbus", "futures-lite"]
volume = ["libpulse-binding"]
workspaces = ["futures-util"]
workspaces = ["futures-lite"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
@ -81,7 +84,7 @@ workspaces = ["futures-util"]
gtk = "0.18.1"
gtk-layer-shell = "0.8.0"
glib = "0.18.5"
tokio = { version = "1.36.0", features = [
tokio = { version = "1.37.0", features = [
"macros",
"rt-multi-thread",
"time",
@ -92,65 +95,71 @@ tokio = { version = "1.36.0", features = [
] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-error = { version = "0.2.0" , default-features = false }
tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2"
serde = { version = "1.0.197", features = ["derive"] }
indexmap = "2.2.5"
color-eyre = "0.6.3"
serde = { version = "1.0.203", features = ["derive"] }
indexmap = "2.2.6"
dirs = "5.0.1"
walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false }
wayland-client = "0.31.1"
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [
"calloop",
] }
universal-config = { version = "0.4.3", default_features = false }
universal-config = { version = "0.5.0", default_features = false }
ctrlc = "3.4.2"
cfg-if = "1.0.0"
# cli
clap = { version = "4.5.2", optional = true, features = ["derive"] }
clap = { version = "4.5.4", optional = true, features = ["derive"] }
# ipc
serde_json = { version = "1.0.114", optional = true }
serde_json = { version = "1.0.117", optional = true }
# http
reqwest = { version = "0.11.25", optional = true }
reqwest = { version = "0.12.4", default_features = false, features = ["default-tls", "http2"], optional = true }
# cairo
lua-src = { version = "546.0.2", optional = true }
mlua = { version = "0.9.8", optional = true, features = ["luajit"] }
cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
nix = { version = "0.29.0", optional = true, features = ["event", "fs"] }
# clock
chrono = { version = "0.4.35", optional = true, features = ["unstable-locales"] }
chrono = { version = "0.4.38", optional = true, default_features = false, features = ["clock", "unstable-locales"] }
# music
mpd-utils = { version = "0.2.0", optional = true }
mpd-utils = { version = "0.2.1", optional = true }
mpris = { version = "2.0.1", optional = true }
# networkmanager
futures-signals = { version = "0.3.33", optional = true }
# sys_info
sysinfo = { version = "0.29.11", optional = true }
# tray
system-tray = { version = "0.1.5", optional = true }
system-tray = { version = "0.2.0", optional = true }
# upower
upower_dbus = { version = "0.3.2", optional = true }
# volume
libpulse-binding = { version = "2.28.1", optional = true }
# libpulse-glib-binding = { version = "2.27.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.13", features = ["silent"], optional = true }
hyprland = { version = "0.4.0-alpha.2", features = ["silent"], optional = true }
futures-util = { version = "0.3.30", optional = true }
# shared
regex = { version = "1.10.3", default-features = false, features = [
futures-lite = { version = "2.3.0", optional = true } # networkmanager, upower, workspaces
regex = { version = "1.10.4", default-features = false, features = [
"std",
], optional = true } # music, sys_info
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower
zbus = { version = "3.15.2", optional = true } # networkmanager, notifications, upower
zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # networkmanager, notifications, upower

7
Cross.toml Normal file
View file

@ -0,0 +1,7 @@
[build]
pre-build = "./.github/scripts/ubuntu_setup.sh"
[target.aarch64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main"
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main"

View file

@ -57,6 +57,12 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
## Installation
[![Packaging status](https://repology.org/badge/vertical-allrepos/ironbar.svg)](https://repology.org/project/ironbar/versions)
Ironbar can be installed from source or using your preferred package manager.
It is also recommended to install a [Nerd Font](https://www.nerdfonts.com/#home) for displaying symbols.
### Cargo
[crate](https://crates.io/crates/ironbar)
@ -130,6 +136,14 @@ A flake is included with the repo which can be used with Home Manager.
CI builds are automatically cached by Garnix.
You can use their binary cache by following the steps [here](https://garnix.io/docs/caching).
### Void Linux
[void package](https://github.com/void-linux/void-packages/tree/master/srcpkgs/ironbar)
```sh
xbps-install ironbar
```
### Source
[repo](https://github.com/jakestanger/ironbar)
@ -183,4 +197,4 @@ All are welcome, but I ask a few basic things to help make things easier. Please
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
- [Mixxc](https://github.com/Elvyria/Mixxc) - Basis for Ironbar's PulseAudio client code and a cool standalone volume widget.
- [Mixxc](https://github.com/Elvyria/Mixxc) - Basis for Ironbar's PulseAudio client code and a cool standalone volume widget.

View file

@ -9,6 +9,8 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar
```
It is also recommended to install a [Nerd Font](https://www.nerdfonts.com/#home) for displaying symbols.
## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
@ -22,6 +24,8 @@ pacman -S gtk3 gtk-layer-shell
pacman -S openssl
# for volume support
pacman -S libpulse
# for lua/cairo support
pacman -S luajit lua51-lgi
```
### Ubuntu/Debian
@ -32,6 +36,8 @@ apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
apt install libssl-dev
# for volume support
apt install libpulse-dev
# for lua/cairo support
apt install luajit-dev lua-lgi
```
### Fedora
@ -41,7 +47,9 @@ dnf install gtk3-devel gtk-layer-shell-devel
# for http support
dnf install openssl-devel
# for volume support
dnf install libpulseaudio-devel
dnf install pulseaudio-libs-devel
# for lua/cairo support
dnf install luajit-devel lua-lgi
```
## Features
@ -49,8 +57,8 @@ dnf install libpulseaudio-devel
By default, all features are enabled for convenience. This can result in a significant compile time.
If you know you are not going to need all the features, you can compile with only the features you need.
As of `v0.10.0`, compiling with no features is about 33% faster.
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
As of `v0.15.0`, compiling with no features is about 50% faster.
On a 3800X, it takes about 45 seconds for no features and 90 seconds for all.
This difference is expected to increase as the bar develops.
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
@ -77,6 +85,7 @@ cargo build --release --no-default-features \
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | |
| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| focused | Enables the `focused` module. |
@ -84,6 +93,7 @@ cargo build --release --no-default-features \
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |
| music+mpd | Enables the `music` module with MPD support. |
| notifications | Enables the `notiications` module. |
| sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |

View file

@ -9,6 +9,8 @@ If you want to see some ready-to-go config files check
the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples)
and the example pages in the sidebar.
The examples make use of [Nerd Fonts](https://www.nerdfonts.com/#home) for displaying symbols.
## 1. Create config file
The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`.
@ -271,27 +273,43 @@ Check [here](config) for an example config file for a fully configured bar in ea
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|--------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
| Name | Type | Default | Description |
|--------------------|-----------------------------------------|---------|---------------------------------------------------------------|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
> [!TIP]
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).
> [!Note]
> All bar-level options listed in the below section can also be defined at the top-level.
# 3.2 Bar-level options
The following table lists each of the bar-level bar config options:
| Name | Type | Default | Description |
|-------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
### 3.2 Module-level options
Each module must include a `type` key.
The following table lists each of the module-level options that are present on **all** modules.
For details on available modules and each of their config options, check the sidebar.
@ -299,31 +317,31 @@ For information on the `Script` type, and embedding scripts in strings, see [her
#### Events
| Name | Type | Default | Description |
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
| Name | Type | Default | Description |
|-------------------|--------------------|---------|------------------------------------------------------------|
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
#### Visibility
| Name | Type | Default | Description |
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
| `disable_popup` | `boolean` | `false` | Prevents the popup from opening on-click for this widget. |
#### Appearance
| Name | Type | Default | Description |
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
| Name | Type | Default | Description |
|-----------|----------|---------|-----------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -79,7 +79,7 @@ Responds with `ok`.
### `get`
Gets an [ironvar](ironvars) value.
Gets an [ironvar](ironvars) value.
Responds with `ok_value` if the value exists, otherwise `error`.
@ -104,6 +104,20 @@ Responds with `ok`.
}
```
### list
Gets a list of all [ironvar](ironvars) values.
Responds with `ok_value`.
Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `.
```json
{
"type": "list"
}
```
### `load_css`
Loads an additional CSS stylesheet, with hot-reloading enabled.

View file

@ -20,9 +20,11 @@
## Custom
- [Power Menu](power-menu)
- [Weather](weather)
# Modules
- [Cairo](cairo)
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)

View file

@ -0,0 +1,468 @@
Creates a button on the bar which displays the current weather condition and temperature.
Clicking the button opens a popup with forecast information for the next few days.
Weather information is fetched from [wttr.in](https://wttr.in) via an external script.
You will need to set up the script to be run as a service.
![custom weather widget, with popup open](https://f.jstanger.dev/github/ironbar/custom-weather.png)
## Configuration
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "custom",
"class": "weather",
"bar": [
{
"type": "button",
"label": "#weather_current",
"on_click": "popup:toggle"
}
],
"popup": [
{
"type": "box",
"orientation": "vertical",
"widgets": [
{
"type": "label",
"name": "header",
"label": "Forecast"
},
{
"type": "box",
"widgets": [
{
"type": "box",
"name": "dates",
"orientation": "vertical",
"widgets": [
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_0"
},
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_1"
},
{
"type": "label",
"class": "weather-date",
"label": "#weather_date_2"
}
]
},
{
"type": "box",
"name": "temps",
"orientation": "vertical",
"widgets": [
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_0"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_0"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_0"
}
]
},
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_1"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_1"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_1"
}
]
},
{
"type": "box",
"widgets": [
{
"type": "label",
"class": "weather-high",
"label": " #weather_high_2"
},
{
"type": "label",
"class": "weather-avg",
"label": " #weather_avg_2"
},
{
"type": "label",
"class": "weather-low",
"label": " #weather_low_2"
}
]
}
]
}
]
}
]
}
]
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "custom"
class = "weather"
[[end.bar]]
type = "button"
label = "#weather_current"
on_click = "popup:toggle"
[[end.popup]]
type = "box"
orientation = "vertical"
[[end.popup.widgets]]
type = "label"
name = "header"
label = "Forecast"
[[end.popup.widgets]]
type = "box"
[[end.popup.widgets.widgets]]
type = "box"
name = "dates"
orientation = "vertical"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_0"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_1"
[[end.popup.widgets.widgets.widgets]]
type = "label"
class = "weather-date"
label = "#weather_date_2"
[[end.popup.widgets.widgets]]
type = "box"
name = "temps"
orientation = "vertical"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_0"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_0"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_0"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_1"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_1"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_1"
[[end.popup.widgets.widgets.widgets]]
type = "box"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-high"
label = " #weather_high_2"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-avg"
label = " #weather_avg_2"
[[end.popup.widgets.widgets.widgets.widgets]]
type = "label"
class = "weather-low"
label = " #weather_low_2"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: custom
class: weather
bar:
- type: button
label: '#weather_current'
on_click: popup:toggle
popup:
- type: box
orientation: vertical
widgets:
- type: label
name: header
label: Forecast
- type: box
widgets:
- type: box
name: dates
orientation: vertical
widgets:
- type: label
class: weather-date
label: '#weather_date_0'
- type: label
class: weather-date
label: '#weather_date_1'
- type: label
class: weather-date
label: '#weather_date_2'
- type: box
name: temps
orientation: vertical
widgets:
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_0'
- type: label
class: weather-avg
label: ' #weather_avg_0'
- type: label
class: weather-low
label: ' #weather_low_0'
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_1'
- type: label
class: weather-avg
label: ' #weather_avg_1'
- type: label
class: weather-low
label: ' #weather_low_1'
- type: box
widgets:
- type: label
class: weather-high
label: ' #weather_high_2'
- type: label
class: weather-avg
label: ' #weather_avg_2'
- type: label
class: weather-low
label: ' #weather_low_2'
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$weather = {
type = "custom"
class = "weather"
bar = [ { type = "button" label = "#weather_current" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Forecast" }
{
type = "box"
widgets = [
{ type = "box" name="dates" orientation = "vertical" widgets = [
{ type = "label" class="weather-date" label = "#weather_date_0" }
{ type = "label" class="weather-date" label = "#weather_date_1" }
{ type = "label" class="weather-date" label = "#weather_date_2" }
]}
{ type = "box" name="temps" orientation = "vertical" widgets = [
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_0" }
{ type = "label" class="weather-avg" label = " #weather_avg_0" }
{ type = "label" class="weather-low" label = " #weather_low_0" }
]
}
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_1" }
{ type = "label" class="weather-avg" label = " #weather_avg_1" }
{ type = "label" class="weather-low" label = " #weather_low_1" }
]
}
{
type = "box"
widgets = [
{ type = "label" class="weather-high" label = " #weather_high_2" }
{ type = "label" class="weather-avg" label = " #weather_avg_2" }
{ type = "label" class="weather-low" label = " #weather_low_2" }
]
}
] }
]
}
]
} ]
}
} in {
end = [ $weather ]
}
```
</details>
## Script
Run the following script on a timer. Ensure to fill out your city name.
```js
#!/usr/bin/env zx
const location = "Canterbury";
// JS uses Sunday as first day
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
// bar logic
const data = await fetch(`https://wttr.in/${location}?format=%c %t|%m %t|%S|%s`)
.then(r => r.text());
const [day, night, sunrise, sunset] = data.replaceAll("+", "").split("|");
const [sunriseH, sunriseM, sunriseS] = sunrise.split(":");
const [sunsetH, sunsetM, sunsetS] = sunset.split(":");
const currentTime = new Date();
const sunriseTime = new Date(currentTime);
sunriseTime.setHours(sunriseH);
sunriseTime.setMinutes(sunriseM);
sunriseTime.setSeconds(sunriseS);
const sunsetTime = new Date(currentTime);
sunsetTime.setHours(sunsetH);
sunsetTime.setMinutes(sunsetM);
sunsetTime.setSeconds(sunsetS);
let value = day;
if(currentTime < sunriseTime || currentTime > sunsetTime) value = night;
await $`ironbar set weather_current ${value}`;
// popup logic
const forecast = await fetch(`https://wttr.in/${location}?format=j1`).then(r => r.json());
for (const i in forecast.weather) {
const report = forecast.weather[i];
const day = weekday[new Date(report.date).getDay()];
await $`ironbar set weather_date_${i} ${day}`;
await $`ironbar set weather_avg_${i} ${report.avgtempC.padStart(2, "0")}`;
await $`ironbar set weather_high_${i} ${report.maxtempC.padStart(2, "0")}`;
await $`ironbar set weather_low_${i} ${report.mintempC.padStart(2, "0")}`;
}
```
## Styling
```css
.popup-weather #header {
font-size: 1.8em;
padding-bottom: 0.4em;
margin-bottom: 0.6em;
border-bottom: 1px solid @color-border;
}
.popup-weather .weather-date {
font-size: 1.5em;
padding-right: 1em;
}
.popup-weather .weather-avg {
margin-left: 0.5em;
margin-right: 0.5em;
}
/*
this is a hack to align the different font sizes on left/right
you may need to adjust for different fonts
*/
.popup-weather #temps label {
padding-top: 0.2em;
margin-bottom: 0.7em;
}
```

215
docs/modules/Cairo.md Normal file
View file

@ -0,0 +1,215 @@
Allows you to render custom content using the Lua and the Cairo drawing library.
This is an advanced feature which provides a powerful escape hatch, allowing you to fetch data and render anything
using an embedded scripting environment.
Scripts are automatically hot-reloaded.
> [!NOTE]
> The Lua engine uses LuaJIT 5.1, and requires the use of a library called `lgi`.
> Ensure you have the correct lua-lgi package installed.
![Circle clock](https://f.jstanger.dev/github/ironbar/cairo-clock.png)
## Configuration
> Type: `cairo`
| Name | Type | Default | Description |
|--------------------|-----------|---------|----------------------------------------------------|
| `path` | `string` | `null` | The path to the Lua script to load. |
| `frequency` | `float` | `200` | The number of milliseconds between each draw call. |
| `width` | `integer` | `42` | The canvas width in pixels. |
| `height` | `integer` | `42` | The canvas height in pixels. |
<details>
<summary>JSON</summary>
```json
{
"center": [
{
"type": "cairo",
"path": ".config/ironbar/clock.lua",
"frequency": 100,
"width": 300,
"height": 300
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[center]]
type = "cairo"
path = ".config/ironbar/clock.lua"
frequency = 100
width = 300
height = 300
```
</details>
<details>
<summary>YAML</summary>
```yaml
center:
- type: cairo
path: .config/ironbar/clock.lua
frequency: 100
width: 300
height: 300
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$config_dir = ".config/ironbar"
$cairo = {
type = "cairo"
path = "$config_dir/clock.lua"
frequency = 100
width = 300
height = 300
}
} in {
center = [ $cairo ]
}
```
</details>
### Script
Every script must contain a function called `draw`.
This takes a single parameter, which is the Cairo context.
Outside of this, you can do whatever you like.
The full lua `stdlib` is available, and you can load in additional system packages as desired.
The most basic example, which draws a red square, can be seen below:
```lua
function draw(cr)
cr:set_source_rgb(1.0, 0.0, 0.0)
cr:paint()
end
```
A longer example, used to create the clock in the image at the top of the page, is shown below:
<details>
<summary>Circle clock</summary>
```lua
function get_ms()
local ms = tostring(io.popen('date +%s%3N'):read('a')):sub(-4, 9999)
return tonumber(ms) / 1000
end
function draw(cr)
local center_x = 150
local center_y = 150
local radius = 130
local date_table = os.date("*t")
local hours = date_table["hour"]
local minutes = date_table["min"]
local seconds = date_table["sec"]
local ms = get_ms()
local label_seconds = seconds
seconds = seconds + ms
local hours_str = tostring(hours)
if string.len(hours_str) == 1 then
hours_str = "0" .. hours_str
end
local minutes_str = tostring(minutes)
if string.len(minutes_str) == 1 then
minutes_str = "0" .. minutes_str
end
local seconds_str = tostring(label_seconds)
if string.len(seconds_str) == 1 then
seconds_str = "0" .. seconds_str
end
local font_size = radius / 5.5
cr:set_source_rgb(1.0, 1.0, 1.0)
cr:move_to(center_x - font_size * 2.5 + 10, center_y + font_size / 2.5)
cr:set_font_size(font_size)
cr:show_text(hours_str .. ':' .. minutes_str .. ':' .. seconds_str)
cr:stroke()
if hours > 12 then
hours = hours - 12
end
local line_width = radius / 8
local start_angle = -math.pi / 2
local end_angle = start_angle + ((hours + minutes / 60 + seconds / 3600) / 12) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius, start_angle, end_angle)
cr:stroke()
end_angle = start_angle + ((minutes + seconds / 60) / 60) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius * 0.8, start_angle, end_angle)
cr:stroke()
if seconds == 0 then
seconds = 60
end
end_angle = start_angle + (seconds / 60) * 2 * math.pi
cr:set_line_width(line_width)
cr:arc(center_x, center_y, radius * 0.6, start_angle, end_angle)
cr:stroke()
return 0
end
```
</details>
> [!TIP]
> The C documentation for the Cairo context interface can be found [here](https://www.cairographics.org/manual/cairo-cairo-t.html).
> The Lua interface provides a slightly friendlier API which restructures things slightly.
> The `cairo_` prefix is dropped, and the `cairo_t *cr` parameters are replaced with a namespaced call.
> For example, `cairo_paint (cairo_t *cr)` becomes `cr:paint()`
> [!TIP]
> Ironbar's Cairo module has similar functionality to the popular Conky program.
> You can often re-use scripts with little work.
### Initialization
You can optionally create an `init.lua` file in your config directory.
Any code in here will be executed once, on bar startup.
As variables and functions are global by default in Lua,
this provides a mechanism for sharing code between multiple modules.
## Styling
| Selector | Description |
|----------|-------------------------|
| `.cairo` | Cairo widget container. |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -47,7 +47,7 @@ Supports plain text and images.
type = "clipboard"
max_items = 3
[[end.truncate]]
[end.truncate]
mode = "end"
length = 50
```

View file

@ -13,6 +13,7 @@ Clicking on the widget opens a popup with the time and a calendar.
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Pango markup is supported. |
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. Pango markup is supported. |
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the time on the clock button. |
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>

View file

@ -1,6 +1,11 @@
Allows you to compose custom modules consisting of multiple widgets, including popups.
Allows you to compose custom modules consisting of multiple modules and widgets, including popups.
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
The module provides a set of utility widgets, such as containers, labels and buttons.
In addition to these, you can also add any native module.
Paired with the other custom modules such as Cairo,
this provides a powerful declarative interface for constructing your own interfaces.
If you only intend to run a single script, prefer the [script](script) module,
or [label](label) if you only need a single text label.
@ -13,6 +18,11 @@ or [label](label) if you only need a single text label.
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
It is well worth looking at the examples.
| Name | Type | Default | Description |
|---------|------------------------|------------|------------------------------------------|
| `bar` | `(Module or Widget)[]` | `[]` | Modules and widgets to add to the bar. |
| `popup` | `(Module or Widget)[]` | `null` | Modules and widgets to add to the popup. |
### `Widget`
There are many widget types, each with their own config options.
@ -36,7 +46,7 @@ A container to place nested widgets inside.
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this box. |
#### Label
@ -47,6 +57,7 @@ A text label. Pango markup is supported.
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the label. |
#### Button
@ -54,10 +65,12 @@ A clickable button, which can run a command when clicked.
> Type `button`
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|--------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. Ignored if `widgets` is set. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this button. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the button. |
#### Image
@ -197,6 +210,7 @@ to help get your head around what's going on:
<button class="power-btn" label="" on_click="!reboot" />
</box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
<clock disable_popup="true" />
</box>
</popup>
</custom>
@ -252,6 +266,10 @@ to help get your head around what's going on:
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
},
{
"type": "clock",
"disable_popup": true
}
]
}
@ -309,6 +327,10 @@ type = 'button'
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
[[end.popup.widgets]]
type = 'clock'
disable_popup = true
```
</details>
@ -345,6 +367,8 @@ end:
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
- type: clock
disable_popup: true
type: custom
```
@ -370,6 +394,7 @@ let {
]
}
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
{ type = "clock" disable_popup = true }
]
}

View file

@ -18,7 +18,7 @@ Optionally displays a launchable set of favourites.
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
<details>
<summary>JSON</summary>
@ -32,7 +32,8 @@ Optionally displays a launchable set of favourites.
"discord"
],
"show_names": false,
"show_icons": true
"show_icons": true,
"reversed": false
}
]
}
@ -51,6 +52,7 @@ type = "launcher"
favorites = ["firefox", "discord"]
show_names = false
show_icons = true
reversed = false
```
</details>
@ -66,6 +68,7 @@ start:
- discord
show_names: false
show_icons: true
reversed: false
```
</details>
@ -81,7 +84,7 @@ start:
favorites = [ "firefox" "discord" ]
show_names = false
show_icons = true
reversed = false
}
]
}

View file

@ -69,7 +69,7 @@ format = "{title} / {artist}"
music_dir = "/home/jake/Music"
truncate = "end"
[[start.icons]]
[start.icons]
play = ""
pause = ""
```

View file

@ -1,4 +1,10 @@
Displays network connectivity information. Requires NetworkManager.
Displays the current network connection state of NetworkManager.
Supports wired ethernet, wifi, cellular data and VPN connections among others.
> [!NOTE]
> This module uses NetworkManager's so-called primary connection, and therefore inherits its limitation of only being able to display the "top-level" connection.
> For example, if we have a VPN connection over a wifi connection it will only display the former, until it is disconnected, at which point it will display the latter.
> A solution to this is currently in the works.
## Configuration
@ -9,57 +15,53 @@ Displays network connectivity information. Requires NetworkManager.
| `icon_size` | `integer` | `24` | Size to render icon at. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "networkmanager",
"icon_size": 32
}
]
}
```
<summary>JSON</summary>
```json
{
"end": [
{
"type": "networkmanager",
"icon_size": 32
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "networkmanager"
icon_size = 32
```
<summary>TOML</summary>
```toml
[[end]]
type = "networkmanager"
icon_size = 32
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "networkmanager"
icon_size: 32
```
<summary>YAML</summary>
```yaml
end:
- type: "networkmanager"
icon_size: 32
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "networkmanager"
icon_size = 32
}
]
}
```
<summary>Corn</summary>
```corn
{
end = [
{
type = "networkmanager"
icon_size = 32
}
]
}
```
</details>
## Styling

View file

@ -54,7 +54,7 @@ Clicking the widget opens the SwayNC panel.
type = "notifications"
show_count = true
[[end.icons]]
[end.icons]
closed_none = "󰍥"
closed_some = "󱥂"
closed_dnd = "󱅯"

View file

@ -19,6 +19,8 @@ Pango markup is supported.
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. |
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). |
<details>
<summary>JSON</summary>

View file

@ -1,4 +1,4 @@
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
![Screenshot showing icon tray widget](https://user-images.githubusercontent.com/5057870/184540135-78ffd79d-f802-4c79-b09a-05a733dadc55.png)
@ -6,10 +6,11 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
> Type: `tray`
| Name | Type | Default | Description |
|-------------|----------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
| Name | Type | Default | Description |
|----------------------|-----------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
| `icon_size` | `integer` | `16` | Size in pixels to display tray icons as. |
| `prefer_theme_icons` | `bool` | `true` | Requests that icons from the theme be used over the item-provided item. Most items only provide one or the other so this will have no effect in most circumstances. |
<details>
<summary>JSON</summary>
@ -54,12 +55,12 @@ end:
```corn
{
end = [
end = [
{
type = "tray"
direction = "top_to_bottom"
type = "tray"
direction = "top_to_bottom"
}
]
]
}
```

View file

@ -84,11 +84,10 @@ and will be replaced with values from the current battery state:
| Selector | Description |
|---------------------------------|--------------------------------|
| `.upower` | Upower widget container. |
| `.upower .button` | Upower widget button. |
| `.upower .button .contents` | Upower widget button contents. |
| `.upower .button .icon` | Upower widget battery icon. |
| `.upower .button .label` | Upower widget button label. |
| `.upower` | Upower widget button. |
| `.upower .contents` | Upower widget button contents. |
| `.upower .icon` | Upower widget battery icon. |
| `.upower .label` | Upower widget button label. |
| `.popup-upower` | Upower popup box. |
| `.popup-upower .upower-details` | Label inside the popup. |

View file

@ -52,7 +52,7 @@ type = "volume"
format = "{icon} {percentage}%"
max_volume = 100
[[end.icons]]
[end.icons]
volume_high = "󰕾"
volume_medium = "󰖀"
volume_low = "󰕿"

View file

@ -48,7 +48,7 @@ type = "workspaces"
all_monitors = false
favorites = ["1", "2", "3"]
[[end.name_map]]
[end.name_map]
1 = ""
2 = ""
3 = ""

View file

@ -55,7 +55,7 @@ let {
interval.networks = 3
format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
@ -81,7 +81,7 @@ let {
$volume = {
type = "volume"
format = "{icon} {volume}%"
format = "{icon} {percentage}%"
max_volume = 100
icons.volume_high = "󰕾"
icons.volume_medium = "󰖀"

View file

@ -63,7 +63,7 @@
"networks": 3
},
"format": [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
@ -74,7 +74,7 @@
},
{
"type": "volume",
"format": "{icon} {volume}%",
"format": "{icon} {percentage}%",
"max_volume": 100,
"icons": {
"volume_high": "󰕾",

View file

@ -53,7 +53,7 @@ interval = 500
[[end]]
type = "sys_info"
format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
@ -71,7 +71,7 @@ networks = 3
[[end]]
type = "volume"
format = "{icon} {volume}%"
format = "{icon} {percentage}%"
max_volume = 100
[end.icons]

View file

@ -43,7 +43,7 @@ end:
disks: 300
networks: 3
format:
-  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
-  {cpu_percent}% | {temp_c:k10temp-Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
@ -51,7 +51,7 @@ end:
- 󰖡 {load_average:1} | {load_average:5} | {load_average:15}
- 󰥔 {uptime}
- type: volume
format: '{icon} {volume}%'
format: '{icon} {percentage}%'
max_volume: 100
icons:
volume_high: 󰕾

60
examples/test.corn Normal file
View file

@ -0,0 +1,60 @@
let {
$config_dir = "/home/jake/.config/ironbar"
$workspaces = { type = "workspaces" }
$launcher = { type = "launcher" }
$volume = {
type = "volume"
format = "{icon} {percentage}%"
max_volume = 100
icons.volume_high = "󰕾"
icons.volume_medium = "󰖀"
icons.volume_low = "󰕿"
icons.muted = "󰝟"
}
$network_manager = { type = "networkmanager" }
$clock = {
type = "clock"
// disable_popup = true
// format = "<span color='#f2777a'>%d/%m/%Y</span> <span color='#69c'>%H:%M:%S</span>"
}
$tray = { type = "tray" prefer_theme_icons = false }
// $label = { type = "label" label = "<span color='#color'>hello</span>" }
$label = { type = "label" label = "#random" }
$clipboard = { type = "clipboard" }
$notifications = {
type = "notifications"
show_count = true
icons.closed_none = "󰍥"
icons.closed_some = "󱥂"
icons.closed_dnd = "󱅯"
icons.open_none = "󰍡"
icons.open_some = "󱥁"
icons.open_dnd = "󱅮"
}
$focused = { type = "focused" }
$cairo = { type = "cairo" path = "$config_dir/clock.lua" frequency = 50 width = 300 height = 300 }
$custom = {
type = "custom"
bar = [ { type = "button" on_click = "popup:toggle" widgets = [ $focused ] } ]
popup = [ { type = "box" orientation = "v" widgets = [ $clock $cairo ] } ]
}
$mpris = { type = "music" }
} in {
// ironvar_defaults.color = "red"
position = "bottom"
icon_theme = "Paper"
start = [ $workspaces $label ]
center = [ $custom ]
end = [ $notifications $clock ]
}

30
flake.lock generated
View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1708794349,
"narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=",
"lastModified": 1713979152,
"narHash": "sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH+DcP4=",
"owner": "ipetkov",
"repo": "crane",
"rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa",
"rev": "a5eca68a2cf11adb32787fc141cddd29ac8eb79c",
"type": "github"
},
"original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
@ -58,11 +58,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709200309,
"narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=",
"lastModified": 1714314149,
"narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811",
"rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae",
"type": "github"
},
"original": {
@ -72,11 +72,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1709150264,
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
"lastModified": 1714253743,
"narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
"rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994",
"type": "github"
},
"original": {
@ -102,11 +102,11 @@
]
},
"locked": {
"lastModified": 1709172595,
"narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=",
"lastModified": 1714443211,
"narHash": "sha256-lKTA3XqRo4aVgkyTSCtpcALpGXdmkilHTtN00eRg0QU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f",
"rev": "ce35c36f58f82cee6ec959e0d44c587d64281b6f",
"type": "github"
},
"original": {

374
flake.nix
View file

@ -1,208 +1,222 @@
{
description = "Nix Flake for ironbar";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
};
outputs = {
self,
nixpkgs,
rust-overlay,
crane,
naersk,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-linux"
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
self.overlays.default
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
in {
overlays.default = final: prev: let
rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
outputs = { self, nixpkgs, rust-overlay, crane, naersk, ... }:
let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [ self.overlays.default rust-overlay.overlays.default ];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
};
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in
if builder == "crane"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
overlays.default = final: prev:
let
rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
};
packages = genSystems (
system: let
pkgs = pkgsFor system;
in
(self.overlays.default pkgs pkgs)
// {
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date="
+ (mkDate (self.lastModifiedDate or "19700101")) + "_"
+ (self.shortRev or "dirty");
in if builder == "crane" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
};
};
packages = genSystems (system:
let pkgs = pkgsFor system;
in (self.overlays.default pkgs pkgs) // {
default = self.packages.${system}.ironbar;
}
);
apps = genSystems (system: let
pkgs = pkgsFor system;
in {
default = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
});
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
];
});
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = {
config,
lib,
pkgs,
...
}: let
cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json {};
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [];
description = "The features to be used.";
};
};
config = let
pkg = cfg.package.override {features = cfg.features;};
in
lib.mkIf cfg.enable {
home.packages = [pkg];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
};
apps = genSystems (system:
let pkgs = pkgsFor system;
in rec {
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
default = ironbar;
});
devShells = genSystems (system:
let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
luajit
luajitPackages.lgi
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = { config, lib, pkgs, ... }:
let
cfg = config.programs.ironbar;
defaultIronbarPackage =
self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json { };
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ ];
description = "The features to be used.";
};
};
config = let pkg = cfg.package.override { features = cfg.features; };
in lib.mkIf cfg.enable {
home.packages = [ pkg ];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" =
lib.mkIf (cfg.style != "") { text = cfg.style; };
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = [ "graphical-session.target" ];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = with config.wayland.windowManager; [
(lib.mkIf hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf sway.systemd.enable "sway-session.target")
(lib.mkIf river.systemd.enable "river-session.target")
];
};
};
};
};
};
nixConfig = {
extra-substituters = ["https://jakestanger.cachix.org"];
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
extra-substituters = [ "https://cache.garnix.io" ];
extra-trusted-public-keys =
[ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ];
};
}

4
lua/draw.lua Normal file
View file

@ -0,0 +1,4 @@
function(id, ptr)
local cr = __lgi_core.record.new(cairo.Context, ptr)
_G['__draw_' .. id](cr)
end

4
lua/init.lua Normal file
View file

@ -0,0 +1,4 @@
local lgi = require('lgi')
cairo = lgi.cairo
__lgi_core = require('lgi.core')

View file

@ -14,6 +14,8 @@
libxkbcommon,
libpulseaudio,
openssl,
luajit,
luajitPackages,
pkg-config,
hicolor-icon-theme,
rustPlatform,
@ -23,74 +25,105 @@
builderName ? "nix",
builder ? {},
}: let
hasFeature = f: features == [ ] || builtins.elem f features;
basePkg = rec {
inherit version;
pname = "ironbar";
src = builtins.path {
name = "ironbar";
path = lib.cleanSource ../.;
};
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon libpulseaudio openssl];
propagatedBuildInputs = [
gtk3
nativeBuildInputs = [
pkg-config
wrapGAppsHook
gobject-introspection
];
buildInputs = [
gtk3
gdk-pixbuf
glib
gtk-layer-shell
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon ]
++ (if hasFeature "http" then [ openssl ] else [])
++ (if hasFeature "volume" then [ libpulseaudio ] else [])
++ (if hasFeature "cairo" then [ luajit ] else []);
propagatedBuildInputs = [ gtk3 ];
lgi = luajitPackages.lgi;
gappsWrapperArgs = ''
# Thumbnailers
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
--prefix XDG_DATA_DIRS : "${librsvg}/share"
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
# gtk-launch
--suffix PATH : "${lib.makeBinPath [ gtk3 ]}"
''
+ (if hasFeature "cairo" then ''
--prefix LUA_PATH : "./?.lua;${lgi}/share/lua/5.1/?.lua;${lgi}/share/lua/5.1/?/init.lua;${luajit}/share/lua/5.1/\?.lua;${luajit}/share/lua/5.1/?/init.lua"
--prefix LUA_CPATH : "./?.so;${lgi}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/loadall.so"
'' else "");
preFixup = ''
gappsWrapperArgs+=(
# Thumbnailers
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
--prefix XDG_DATA_DIRS : "${librsvg}/share"
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
${gappsWrapperArgs}
)
'';
passthru = {
updateScript = gnome.updateScript {
packageName = pname;
attrPath = "gnome.${pname}";
};
};
meta = with lib; {
homepage = "https://github.com/JakeStanger/ironbar";
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
description =
"Customisable gtk-layer-shell wlroots/sway bar written in rust.";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "ironbar";
};
};
flags = let
noDefault =
if features == []
then ""
else "--no-default-features";
featuresStr =
if features == []
then ""
else ''-F "${builtins.concatStringsSep "," features}"'';
in [noDefault featuresStr];
in
if builderName == "naersk"
then
builder.buildPackage (basePkg
// {
cargoOptions = old: old ++ flags;
})
else if builderName == "crane"
then
builder.buildPackage (basePkg
// {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg
// {
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})
noDefault = if features == [ ] then "" else "--no-default-features";
featuresStr = if features == [ ] then
""
else
''-F "${builtins.concatStringsSep "," features}"'';
in [ noDefault featuresStr ];
in if builderName == "naersk" then
builder.buildPackage (basePkg // { cargoBuildOptions = old: old ++ flags; })
else if builderName == "crane" then
builder.buildPackage (basePkg // {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg // {
buildNoDefaultFeatures = features != [ ];
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock { lockFile = ../Cargo.lock; };
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" =
"sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})

View file

@ -9,6 +9,9 @@ pkgs.mkShell {
gtk-layer-shell
gcc
openssl
libpulseaudio
luajit
luajitPackages.lgi
];
nativeBuildInputs = with pkgs; [

View file

@ -1,9 +1,7 @@
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
use crate::popup::Popup;
use crate::{Config, Ironbar};
use crate::Ironbar;
use color_eyre::Result;
use glib::Propagation;
use gtk::gdk::Monitor;
@ -16,7 +14,7 @@ use tracing::{debug, info};
#[derive(Debug, Clone)]
enum Inner {
New { config: Option<Config> },
New { config: Option<BarConfig> },
Loaded { popup: Rc<Popup> },
}
@ -43,7 +41,7 @@ impl Bar {
pub fn new(
app: &Application,
monitor_name: String,
config: Config,
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Self {
let window = ApplicationWindow::builder()
@ -245,7 +243,7 @@ impl Bar {
}
/// Loads the configured modules onto a bar.
fn load_modules(&self, config: Config, monitor: &Monitor) -> Result<BarLoadResult> {
fn load_modules(&self, config: BarConfig, monitor: &Monitor) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
@ -350,57 +348,10 @@ fn add_modules(
ironbar: &Rc<Ironbar>,
popup: &Rc<Popup>,
) -> Result<()> {
let orientation = info.bar_position.orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("common config to exist");
let widget_parts = create_module(
*$module,
$id,
ironbar.clone(),
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);
content.add(&container);
}};
}
let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
for config in modules {
let id = Ironbar::unique_id();
match config {
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
#[cfg(feature = "focused")]
ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Label(mut module) => add_module!(module, id),
#[cfg(feature = "launcher")]
ModuleConfig::Launcher(mut module) => add_module!(module, id),
#[cfg(feature = "music")]
ModuleConfig::Music(mut module) => add_module!(module, id),
#[cfg(feature = "networkmanager")]
ModuleConfig::Networkmanager(mut module) => add_module!(module, id),
#[cfg(feature = "notifications")]
ModuleConfig::Notifications(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
#[cfg(feature = "tray")]
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "volume")]
ModuleConfig::Volume(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}
config.create(&module_factory, content, info)?;
}
Ok(())
@ -410,7 +361,7 @@ pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: String,
config: Config,
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config, ironbar);

View file

@ -149,12 +149,27 @@ impl Client {
}
{
event_listener.add_workspace_destroy_handler(move |workspace_type| {
let _lock = lock!(lock);
debug!("Received workspace destroy: {workspace_type:?}");
let tx = tx.clone();
let lock = lock.clone();
let name = get_workspace_name(workspace_type);
send!(tx, WorkspaceUpdate::Remove(name));
event_listener.add_workspace_rename_handler(move |data| {
let _lock = lock!(lock);
send!(
tx,
WorkspaceUpdate::Rename {
id: data.workspace_id as i64,
name: data.workspace_name
}
);
});
}
{
event_listener.add_workspace_destroy_handler(move |data| {
let _lock = lock!(lock);
debug!("Received workspace destroy: {data:?}");
send!(tx, WorkspaceUpdate::Remove(data.workspace_id as i64));
});
}
@ -186,6 +201,7 @@ impl Client {
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.find_map(|w| {
if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
@ -228,6 +244,7 @@ impl WorkspaceClient for Client {
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
@ -262,7 +279,7 @@ fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
impl From<(Visibility, HWorkspace)> for Workspace {
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
Self {
id: workspace.id.to_string(),
id: workspace.id as i64,
name: workspace.name,
monitor: workspace.monitor,
visibility,

View file

@ -1,4 +1,4 @@
use crate::{await_sync, register_client};
use crate::{await_sync, register_fallible_client};
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use std::fmt::{Debug, Display, Formatter};
@ -74,7 +74,7 @@ impl Compositor {
#[derive(Debug, Clone)]
pub struct Workspace {
/// Unique identifier
pub id: String,
pub id: i64,
/// Workspace friendly name
pub name: String,
/// Name of the monitor (output) the workspace is located on
@ -119,13 +119,19 @@ pub enum WorkspaceUpdate {
/// This is re-sent to all subscribers when a new subscription is created.
Init(Vec<Workspace>),
Add(Workspace),
Remove(String),
Remove(i64),
Move(Workspace),
/// Declares focus moved from the old workspace to the new.
Focus {
old: Option<Workspace>,
new: Workspace,
},
Rename {
id: i64,
name: String,
},
/// An update was triggered by the compositor but this was not mapped by Ironbar.
///
/// This is purely used for ergonomics within the compositor clients
@ -141,4 +147,4 @@ pub trait WorkspaceClient: Debug + Send + Sync {
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}
register_client!(dyn WorkspaceClient, workspaces);
register_fallible_client!(dyn WorkspaceClient, workspaces);

View file

@ -1,7 +1,7 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn};
use color_eyre::{Report, Result};
use futures_util::StreamExt;
use futures_lite::StreamExt;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender};
@ -90,7 +90,7 @@ impl From<Node> for Workspace {
let visibility = Visibility::from(&node);
Self {
id: node.id.to_string(),
id: node.id,
name: node.name.unwrap_or_default(),
monitor: node.output.unwrap_or_default(),
visibility,
@ -103,7 +103,7 @@ impl From<swayipc_async::Workspace> for Workspace {
let visibility = Visibility::from(&workspace);
Self {
id: workspace.id.to_string(),
id: workspace.id,
name: workspace.name,
monitor: workspace.output,
visibility,
@ -141,13 +141,9 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
WorkspaceChange::Init => {
Self::Add(event.current.expect("Missing current workspace").into())
}
WorkspaceChange::Empty => Self::Remove(
event
.current
.expect("Missing current workspace")
.name
.unwrap_or_default(),
),
WorkspaceChange::Empty => {
Self::Remove(event.current.expect("Missing current workspace").id)
}
WorkspaceChange::Focus => Self::Focus {
old: event.old.map(Workspace::from),
new: Workspace::from(event.current.expect("Missing current workspace")),

41
src/clients/lua.rs Normal file
View file

@ -0,0 +1,41 @@
use mlua::Lua;
use std::ops::Deref;
use std::path::Path;
use tracing::{debug, error};
/// Wrapper around Lua instance
/// to create a singleton and handle initialization.
#[derive(Debug)]
pub struct LuaEngine {
lua: Lua,
}
impl LuaEngine {
pub fn new(config_dir: &Path) -> Self {
let lua = unsafe { Lua::unsafe_new() };
let user_init = config_dir.join("init.lua");
if user_init.exists() {
debug!("loading user init script");
if let Err(err) = lua.load(user_init).exec() {
error!("{err:?}");
}
}
debug!("loading internal init script");
if let Err(err) = lua.load(include_str!("../../lua/init.lua")).exec() {
error!("{err:?}");
}
Self { lua }
}
}
impl Deref for LuaEngine {
type Target = Lua;
fn deref(&self) -> &Self::Target {
&self.lua
}
}

View file

@ -1,15 +1,23 @@
use crate::{await_sync, Ironbar};
use color_eyre::Result;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "networkmanager")]
pub mod networkmanager;
#[cfg(feature = "notifications")]
pub mod swaync;
#[cfg(feature = "tray")]
pub mod system_tray;
pub mod tray;
#[cfg(feature = "upower")]
pub mod upower;
#[cfg(feature = "volume")]
@ -25,18 +33,24 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "networkmanager")]
networkmanager: Option<Arc<networkmanager::Client>>,
#[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "tray")]
tray: Option<Arc<system_tray::TrayEventReceiver>>,
tray: Option<Arc<tray::Client>>,
#[cfg(feature = "upower")]
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
#[cfg(feature = "volume")]
volume: Option<Arc<volume::Client>>,
}
pub type ClientResult<T> = Result<Arc<T>>;
impl Clients {
pub(crate) fn new() -> Self {
Self::default()
@ -58,12 +72,23 @@ impl Clients {
}
#[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> Arc<dyn compositor::WorkspaceClient> {
// TODO: Error handling here isn't great - should throw a user-friendly error & exit
self.workspaces
.get_or_insert_with(|| {
compositor::Compositor::create_workspace_client().expect("to be valid compositor")
})
pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
let client = match &self.workspaces {
Some(workspaces) => workspaces.clone(),
None => {
let client = compositor::Compositor::create_workspace_client()?;
self.workspaces.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
.get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
.clone()
}
@ -75,24 +100,48 @@ impl Clients {
.clone()
}
#[cfg(feature = "networkmanager")]
pub fn networkmanager(&mut self) -> ClientResult<networkmanager::Client> {
match &self.networkmanager {
Some(client) => Ok(client.clone()),
None => {
let client = networkmanager::create_client()?;
self.networkmanager = Some(client.clone());
Ok(client)
}
}
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> Arc<swaync::Client> {
self.notifications
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async { swaync::Client::new().await }))
})
.clone()
pub fn notifications(&mut self) -> ClientResult<swaync::Client> {
let client = match &self.notifications {
Some(client) => client.clone(),
None => {
let client = await_sync(async { swaync::Client::new().await })?;
let client = Arc::new(client);
self.notifications.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
self.tray
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async {
system_tray::create_client().await
}))
})
.clone()
pub fn tray(&mut self) -> ClientResult<tray::Client> {
let client = match &self.tray {
Some(client) => client.clone(),
None => {
let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
let client = await_sync(async { tray::Client::new(&service_name).await })?;
let client = Arc::new(client);
self.tray.replace(client.clone());
client
}
};
Ok(client)
}
#[cfg(feature = "upower")]
@ -119,6 +168,14 @@ pub trait ProvidesClient<T: ?Sized> {
fn provide(&self) -> Arc<T>;
}
/// Types implementing this trait
/// indicate that they provide a singleton client instance of type `T`,
/// which may fail to be created.
pub trait ProvidesFallibleClient<T: ?Sized> {
/// Returns a singleton client instance of type `T`.
fn try_provide(&self) -> ClientResult<T>;
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
@ -141,3 +198,26 @@ macro_rules! register_client {
}
};
}
/// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type.
///
/// The implementation calls `$method` (second argument)
/// on the `Clients` struct to obtain the client instance.
///
/// # Example
/// `register_client!(Client, clipboard);`
#[macro_export]
macro_rules! register_fallible_client {
($ty:ty, $method:ident) => {
impl<TSend, TReceive> $crate::clients::ProvidesFallibleClient<$ty>
for $crate::modules::WidgetContext<TSend, TReceive>
where
TSend: Clone,
{
fn try_provide(&self) -> color_eyre::Result<std::sync::Arc<$ty>> {
self.ironbar.clients.borrow_mut().$method()
}
}
};
}

View file

@ -34,14 +34,15 @@ pub struct Track {
pub cover_path: Option<String>,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub enum PlayerState {
#[default]
Stopped,
Playing,
Paused,
Stopped,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Status {
pub state: PlayerState,
pub volume_percent: Option<u8>,

View file

@ -18,6 +18,11 @@ pub struct Client {
_rx: broadcast::Receiver<PlayerUpdate>,
}
const NO_ACTIVE_PLAYER: &str = "com.github.altdesktop.playerctld.NoActivePlayer";
const NO_REPLY: &str = "org.freedesktop.DBus.Error.NoReply";
const NO_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
const NO_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
impl Client {
pub(crate) fn new() -> Self {
let (tx, rx) = broadcast::channel(32);
@ -35,44 +40,48 @@ impl Client {
// D-Bus gives no event for new players,
// so we have to keep polling the player list
loop {
let players = player_finder
.find_all()
.expect("Failed to connect to D-Bus");
// mpris-rs does not filter NoActivePlayer errors, so we have to do it ourselves
let players = player_finder.find_all().unwrap_or_else(|e| match e {
mpris::FindingError::DBusError(DBusError::TransportError(
transport_error,
)) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY) =>
{
Vec::new()
}
_ => panic!("Failed to connect to D-Bus"),
});
// Acquire the lock of current_player before players to avoid deadlock.
// There are places where we lock on current_player and players, but we always lock on current_player first.
// This is because we almost never need to lock on players without locking on current_player.
{
let mut current_player_lock = lock!(current_player);
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
if current_player_lock.is_none() {
debug!("Setting active player to '{identity}'");
current_player_lock.replace(identity.to_string());
let status = player
.get_playback_status()
.expect("Failed to connect to D-Bus");
{
let mut current_player = lock!(current_player);
if status == PlaybackStatus::Playing || current_player.is_none() {
debug!("Setting active player to '{identity}'");
current_player.replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
Self::listen_player_events(
identity.to_string(),
players_list.clone(),
current_player.clone(),
tx.clone(),
);
Self::listen_player_events(
identity.to_string(),
players_list.clone(),
current_player.clone(),
tx.clone(),
);
}
}
}
// wait 1 second before re-checking players
sleep(Duration::from_secs(1));
}
@ -111,28 +120,56 @@ impl Client {
if let Ok(player) = player_finder.find_by_name(&player_id) {
let identity = player.identity();
let handle_shutdown = |current_player_lock_option: Option<
std::sync::MutexGuard<'_, Option<String>>,
>| {
debug!("Player '{identity}' shutting down");
// Lock of player before players (see new() to make sure order is consistent)
if let Some(mut guard) = current_player_lock_option {
guard.take();
} else {
lock!(current_player).take();
}
let mut players_locked = lock!(players);
players_locked.remove(identity);
if players_locked.is_empty() {
send!(tx, PlayerUpdate::Update(Box::new(None), Status::default()));
}
};
for event in player.events()? {
trace!("Received player event from '{identity}': {event:?}");
match event {
Ok(Event::PlayerShutDown) => {
lock!(current_player).take();
lock!(players).remove(identity);
handle_shutdown(None);
break;
}
Ok(Event::Playing) => {
lock!(current_player).replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
Err(mpris::EventError::DBusError(DBusError::TransportError(
transport_error,
))) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY)
|| transport_error.name() == Some(NO_METHOD)
|| transport_error.name() == Some(NO_SERVICE) =>
{
handle_shutdown(None);
break;
}
Ok(_) => {
let current_player = lock!(current_player);
let current_player = current_player.as_ref();
if let Some(current_player) = current_player {
if current_player == identity {
let mut current_player_lock = lock!(current_player);
if matches!(event, Ok(Event::Playing)) {
current_player_lock.replace(identity.to_string());
}
if let Some(current_identity) = current_player_lock.as_ref() {
if current_identity == identity {
if let Err(err) = Self::send_update(&player, &tx) {
if let Some(DBusError::TransportError(transport_error)) =
err.downcast_ref::<DBusError>()
{
if transport_error.name() == Some(NO_SERVICE) {
handle_shutdown(Some(current_player_lock));
break;
}
}
error!("{err:?}");
}
}

View file

@ -0,0 +1,148 @@
use std::sync::Arc;
use color_eyre::Result;
use futures_signals::signal::{Mutable, MutableSignalCloned};
use tracing::error;
use zbus::{
blocking::{fdo::PropertiesProxy, Connection},
names::InterfaceName,
zvariant::{Error as ZVariantError, ObjectPath, Str},
Error as ZBusError,
};
use crate::{register_fallible_client, spawn_blocking};
const DBUS_BUS: &str = "org.freedesktop.NetworkManager";
const DBUS_PATH: &str = "/org/freedesktop/NetworkManager";
const DBUS_INTERFACE: &str = "org.freedesktop.NetworkManager";
#[derive(Debug)]
pub struct Client {
client_state: Mutable<ClientState>,
interface_name: InterfaceName<'static>,
props_proxy: PropertiesProxy<'static>,
}
#[derive(Clone, Debug)]
pub enum ClientState {
WiredConnected,
WifiConnected,
CellularConnected,
VpnConnected,
WifiDisconnected,
Offline,
Unknown,
}
impl Client {
fn new() -> Result<Self> {
let client_state = Mutable::new(ClientState::Unknown);
let dbus_connection = Connection::system()?;
let props_proxy = PropertiesProxy::builder(&dbus_connection)
.destination(DBUS_BUS)?
.path(DBUS_PATH)?
.build()?;
let interface_name = InterfaceName::from_static_str(DBUS_INTERFACE)?;
Ok(Self {
client_state,
interface_name,
props_proxy,
})
}
fn run(&self) -> Result<()> {
let props = self.props_proxy.get_all(self.interface_name.clone())?;
let mut primary_connection = props["PrimaryConnection"]
.downcast_ref::<ObjectPath>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?
.to_string();
let mut primary_connection_type = props["PrimaryConnectionType"]
.downcast_ref::<Str>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?
.to_string();
let mut wireless_enabled = *props["WirelessEnabled"]
.downcast_ref::<bool>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?;
self.client_state.set(determine_state(
&primary_connection,
&primary_connection_type,
wireless_enabled,
));
let changed_props_stream = self.props_proxy.receive_properties_changed()?;
for signal in changed_props_stream {
let args = signal.args()?;
if args.interface_name != self.interface_name {
continue;
}
let changed_props = args.changed_properties;
if let Some(new_primary_connection) = changed_props.get("PrimaryConnection") {
let new_primary_connection = new_primary_connection
.downcast_ref::<ObjectPath>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?;
primary_connection = new_primary_connection.to_string();
}
if let Some(new_primary_connection_type) = changed_props.get("PrimaryConnectionType") {
let new_primary_connection_type = new_primary_connection_type
.downcast_ref::<Str>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?;
primary_connection_type = new_primary_connection_type.to_string();
}
if let Some(new_wireless_enabled) = changed_props.get("WirelessEnabled") {
let new_wireless_enabled = new_wireless_enabled
.downcast_ref::<bool>()
.ok_or(ZBusError::Variant(ZVariantError::IncorrectType))?;
wireless_enabled = *new_wireless_enabled;
}
self.client_state.set(determine_state(
&primary_connection,
&primary_connection_type,
wireless_enabled,
));
}
Ok(())
}
pub fn subscribe(&self) -> MutableSignalCloned<ClientState> {
self.client_state.signal_cloned()
}
}
pub fn create_client() -> Result<Arc<Client>> {
let client = Arc::new(Client::new()?);
{
let client = client.clone();
spawn_blocking(move || {
if let Err(error) = client.run() {
error!("{}", error);
};
});
}
Ok(client)
}
fn determine_state(
primary_connection: &str,
primary_connection_type: &str,
wireless_enabled: bool,
) -> ClientState {
if primary_connection == "/" {
if wireless_enabled {
ClientState::WifiDisconnected
} else {
ClientState::Offline
}
} else {
match primary_connection_type {
"802-3-ethernet" | "adsl" | "pppoe" => ClientState::WiredConnected,
"802-11-olpc-mesh" | "802-11-wireless" | "wifi-p2p" => ClientState::WifiConnected,
"cdma" | "gsm" | "wimax" => ClientState::CellularConnected,
"vpn" | "wireguard" => ClientState::VpnConnected,
_ => ClientState::Unknown,
}
}
}
register_fallible_client!(Client, networkmanager);

View file

@ -1,6 +1,6 @@
mod dbus;
use crate::{register_client, send, spawn};
use crate::{register_fallible_client, send, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
@ -24,9 +24,9 @@ type GetSubscribeData = (bool, bool, u32, bool);
impl From<GetSubscribeData> for Event {
fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
Self {
count,
dnd,
cc_open,
count,
inhibited,
}
}
@ -40,15 +40,13 @@ pub struct Client {
}
impl Client {
pub async fn new() -> Self {
let dbus = Box::pin(zbus::Connection::session())
.await
.expect("failed to create connection to system bus");
pub async fn new() -> Result<Self> {
let dbus = Box::pin(zbus::Connection::session()).await?;
let proxy = SwayNcProxy::new(&dbus).await.unwrap();
let proxy = SwayNcProxy::new(&dbus).await?;
let (tx, rx) = broadcast::channel(8);
let mut stream = proxy.receive_subscribe_v2().await.unwrap();
let mut stream = proxy.receive_subscribe_v2().await?;
{
let tx = tx.clone();
@ -62,7 +60,7 @@ impl Client {
});
}
Self { proxy, tx, _rx: rx }
Ok(Self { proxy, tx, _rx: rx })
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
@ -85,4 +83,4 @@ impl Client {
}
}
register_client!(Client, notifications);
register_fallible_client!(Client, notifications);

View file

@ -1,127 +0,0 @@
use crate::{arc_mut, lock, register_client, send, spawn, Ironbar};
use color_eyre::Report;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use system_tray::message::menu::TrayMenu;
use system_tray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::StatusNotifierWatcher;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error, trace};
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
#[derive(Debug)]
pub struct TrayEventReceiver {
tx: mpsc::Sender<NotifierItemCommand>,
b_tx: broadcast::Sender<NotifierItemMessage>,
_b_rx: broadcast::Receiver<NotifierItemMessage>,
tray: Arc<Mutex<Tray>>,
}
impl TrayEventReceiver {
async fn new() -> system_tray::error::Result<Self> {
let id = format!("ironbar-{}", Ironbar::unique_id());
let (tx, rx) = mpsc::channel(16);
let (b_tx, b_rx) = broadcast::channel(64);
let tray = StatusNotifierWatcher::new(rx).await?;
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
let tray = arc_mut!(BTreeMap::new());
{
let b_tx = b_tx.clone();
let tray = tray.clone();
spawn(async move {
while let Ok(message) = host.recv().await {
trace!("Received message: {message:?}");
send!(b_tx, message.clone());
let mut tray = lock!(tray);
match message {
NotifierItemMessage::Update {
address,
item,
menu,
} => {
debug!("Adding/updating item with address '{address}'");
tray.insert(address, (item, menu));
}
NotifierItemMessage::Remove { address } => {
debug!("Removing item with address '{address}'");
tray.remove(&address);
}
}
}
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
});
}
Ok(Self {
tx,
b_tx,
_b_rx: b_rx,
tray,
})
}
pub fn subscribe(
&self,
) -> (
mpsc::Sender<NotifierItemCommand>,
broadcast::Receiver<NotifierItemMessage>,
) {
let tx = self.tx.clone();
let b_rx = self.b_tx.subscribe();
let tray = lock!(self.tray).clone();
for (address, (item, menu)) in tray {
let update = NotifierItemMessage::Update {
address,
item,
menu,
};
send!(self.b_tx, update);
}
(tx, b_rx)
}
}
/// Attempts to create a new `TrayEventReceiver` instance,
/// retrying a maximum of 10 times before panicking the thread.
pub async fn create_client() -> TrayEventReceiver {
const MAX_RETRIES: i32 = 10;
// sometimes this can fail
let mut retries = 0;
let value = loop {
retries += 1;
let tray = Box::pin(TrayEventReceiver::new()).await;
match tray {
Ok(tray) => break Some(tray),
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err(format!(
"Failed to create StatusNotifierWatcher (attempt {retries})"
))
),
}
if retries == MAX_RETRIES {
break None;
}
};
value.expect("Failed to create StatusNotifierWatcher")
}
register_client!(TrayEventReceiver, tray);

4
src/clients/tray.rs Normal file
View file

@ -0,0 +1,4 @@
use crate::register_fallible_client;
pub use system_tray::client::Client;
register_fallible_client!(Client, tray);

View file

@ -98,6 +98,7 @@ pub enum Response {
}
#[derive(Debug)]
#[allow(dead_code)]
struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>);
impl<T> From<(broadcast::Sender<T>, broadcast::Receiver<T>)> for BroadcastChannel<T> {

View file

@ -11,7 +11,7 @@ use crate::{lock, try_send, Ironbar};
use device::DataControlDevice;
use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken};
use std::cmp::min;
@ -274,7 +274,7 @@ impl DataControlDeviceHandler for Environment {
Ok(token) => {
cur_offer.token.replace(token);
}
Err(err) => error!("{err:?}"),
Err(err) => error!("Failed to insert read pipe event: {err:?}"),
}
}
}
@ -294,15 +294,15 @@ impl DataControlOfferHandler for Environment {
}
impl DataControlSourceHandler for Environment {
fn accept_mime(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_source: &ZwlrDataControlSourceV1,
mime: Option<String>,
) {
debug!("Accepted mime type: {mime:?}");
}
// fn accept_mime(
// &mut self,
// _conn: &Connection,
// _qh: &QueueHandle<Self>,
// _source: &ZwlrDataControlSourceV1,
// mime: Option<String>,
// ) {
// debug!("Accepted mime type: {mime:?}");
// }
/// Writes the current clipboard item to 'paste' it
/// upon request from a compositor client.
@ -349,11 +349,12 @@ impl DataControlSourceHandler for Environment {
.add(fd, epoll_event)
.expect("to send valid epoll operation");
let timeout = EpollTimeout::from(100u16);
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
epoll_fd
.wait(&mut events, 100)
.wait(&mut events, timeout)
.expect("Failed to wait to epoll");
match file.write(chunk) {

View file

@ -5,7 +5,7 @@ use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut;
use std::os::fd::{BorrowedFd, FromRawFd};
use std::os::fd::{AsFd, AsRawFd};
use std::sync::{Arc, Mutex};
use tracing::{trace, warn};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
@ -176,11 +176,11 @@ pub unsafe fn receive(
// create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
offer.receive(mime_type, BorrowedFd::borrow_raw(writefd));
offer.receive(mime_type, writefd.as_fd());
if let Err(err) = close(writefd) {
if let Err(err) = close(writefd.as_raw_fd()) {
warn!("Failed to close write pipe: {}", err);
}
Ok(FromRawFd::from_raw_fd(readfd))
Ok(ReadPipe::from(readfd))
}

View file

@ -10,13 +10,13 @@ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1
pub struct DataControlSourceData {}
pub trait DataControlSourceDataExt: Send + Sync {
fn data_source_data(&self) -> &DataControlSourceData;
// fn data_source_data(&self) -> &DataControlSourceData;
}
impl DataControlSourceDataExt for DataControlSourceData {
fn data_source_data(&self) -> &DataControlSourceData {
self
}
// fn data_source_data(&self) -> &DataControlSourceData {
// self
// }
}
/// Handler trait for `DataSource` events.
@ -24,13 +24,13 @@ impl DataControlSourceDataExt for DataControlSourceData {
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
pub trait DataControlSourceHandler: Sized {
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
fn accept_mime(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: Option<String>,
);
// fn accept_mime(
// &mut self,
// conn: &Connection,
// qh: &QueueHandle<Self>,
// source: &ZwlrDataControlSourceV1,
// mime: Option<String>,
// );
/// The client has requested the data for this source to be sent.
/// Send the data, then close the fd.

View file

@ -77,7 +77,6 @@ impl ToplevelHandleHandler for Environment {
match handle.info() {
Some(info) => {
trace!("Updating handle: {info:?}");
self.handles.push(handle.clone());
if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(info)));
}

View file

@ -7,26 +7,155 @@ use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
use serde::Deserialize;
use tracing::trace;
/// Common configuration options
/// which can be set on every module.
/// The following are module-level options which are present on **all** modules.
///
/// Each module also provides options specific to its type.
/// For details on those, check the relevant module documentation.
///
/// For information on the Script type, and embedding scripts in strings,
/// see [here](script).
/// For information on styling, please see the [styling guide](styling-guide).
#[derive(Debug, Default, Deserialize, Clone)]
pub struct CommonConfig {
pub class: Option<String>,
/// Sets the unique widget name,
/// allowing you to target it in CSS using `#name`.
///
/// It is best practise (although not required) to ensure that the value is
/// globally unique throughout the Ironbar instance
/// to avoid clashes.
///
/// **Default**: `null`
pub name: Option<String>,
/// Sets one or more CSS classes,
/// allowing you to target it in CSS using `.class`.
///
/// Unlike [name](#name), the `class` property is not expected to be unique.
///
/// **Default**: `null`
pub class: Option<String>,
/// Shows this text on hover.
/// Supports embedding scripts between `{{double braces}}`.
///
/// Note that full dynamic string support is not currently supported.
///
/// **Default**: `null`
pub tooltip: Option<String>,
/// Shows the module only if the dynamic boolean evaluates to true.
///
/// This allows for modules to be dynamically shown or hidden
/// based on custom events.
///
/// **Default**: `null`
pub show_if: Option<DynamicBool>,
/// The transition animation to use when showing/hiding the widget.
///
/// Note this has no effect if `show_if` is not configured.
///
/// **Valid options**: `slide_start`, `slide_end`, `crossfade`, `none`
/// <br>
/// **Default**: `slide_start`
pub transition_type: Option<TransitionType>,
/// The length in milliseconds
/// of the transition animation to use when showing/hiding the widget.
///
/// Note this has no effect if `show_if` is not configured.
///
/// **Default**: `250`
pub transition_duration: Option<u32>,
/// A [script](scripts) to run when the module is left-clicked.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
///
/// # Example
///
/// ```corn
/// { on_click_left = "echo 'event' >> log.txt" }
/// ```
pub on_click_left: Option<ScriptInput>,
/// A [script](scripts) to run when the module is right-clicked.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// /// # Example
///
/// ```corn
/// { on_click_right = "echo 'event' >> log.txt" }
/// ```
pub on_click_right: Option<ScriptInput>,
/// A [script](scripts) to run when the module is middle-clicked.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// # Example
///
/// ```corn
/// { on_click_middle = "echo 'event' >> log.txt" }
/// ```
pub on_click_middle: Option<ScriptInput>,
/// A [script](scripts) to run when the module is scrolled up on.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// # Example
///
/// ```corn
/// { on_scroll_up = "echo 'event' >> log.txt" }
/// ```
pub on_scroll_up: Option<ScriptInput>,
/// A [script](scripts) to run when the module is scrolled down on.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// # Example
///
/// ```corn
/// { on_scroll_down = "echo 'event' >> log.txt" }
/// ```
pub on_scroll_down: Option<ScriptInput>,
/// A [script](scripts) to run when the cursor begins hovering over the module.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// # Example
///
/// ```corn
/// { on_mouse_enter = "echo 'event' >> log.txt" }
/// ```
pub on_mouse_enter: Option<ScriptInput>,
/// A [script](scripts) to run when the cursor stops hovering over the module.
///
/// **Supported script types**: `oneshot`.
/// <br>
/// **Default**: `null`
/// # Example
///
/// ```corn
/// { on_mouse_exit = "echo 'event' >> log.txt" }
/// ```
pub on_mouse_exit: Option<ScriptInput>,
pub tooltip: Option<String>,
/// Prevents the popup from opening on-click for this widget.
#[serde(default)]
pub disable_popup: bool,
}
#[derive(Debug, Deserialize, Clone)]
@ -38,6 +167,34 @@ pub enum TransitionType {
SlideEnd,
}
#[derive(Debug, Default, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ModuleOrientation {
#[default]
#[serde(alias = "h")]
Horizontal,
#[serde(alias = "v")]
Vertical,
}
impl ModuleOrientation {
pub const fn to_angle(self) -> f64 {
match self {
Self::Horizontal => 0.0,
Self::Vertical => 90.0,
}
}
}
impl From<ModuleOrientation> for Orientation {
fn from(o: ModuleOrientation) -> Self {
match o {
ModuleOrientation::Horizontal => Self::Horizontal,
ModuleOrientation::Vertical => Self::Vertical,
}
}
}
impl TransitionType {
pub const fn to_revealer_transition_type(
&self,

View file

@ -1,4 +1,4 @@
use super::{BarPosition, Config, MonitorConfig};
use super::{BarConfig, BarPosition, MonitorConfig};
use color_eyre::{Help, Report};
use gtk::Orientation;
use serde::{Deserialize, Deserializer};
@ -13,11 +13,11 @@ impl<'de> Deserialize<'de> for MonitorConfig {
let content =
<serde::__private::de::Content as serde::Deserialize>::deserialize(deserializer)?;
match <Config as serde::Deserialize>::deserialize(
match <BarConfig as serde::Deserialize>::deserialize(
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
) {
Ok(config) => Ok(Self::Single(config)),
Err(outer) => match <Vec<Config> as serde::Deserialize>::deserialize(
Err(outer) => match <Vec<BarConfig> as serde::Deserialize>::deserialize(
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
) {
Ok(config) => Ok(Self::Multiple(config)),

View file

@ -2,6 +2,8 @@ mod common;
mod r#impl;
mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
@ -15,7 +17,7 @@ use crate::modules::launcher::LauncherModule;
#[cfg(feature = "music")]
use crate::modules::music::MusicModule;
#[cfg(feature = "networkmanager")]
use crate::modules::networkmanager::NetworkmanagerModule;
use crate::modules::networkmanager::NetworkManagerModule;
#[cfg(feature = "notifications")]
use crate::modules::notifications::NotificationsModule;
use crate::modules::script::ScriptModule;
@ -29,16 +31,21 @@ use crate::modules::upower::UpowerModule;
use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
use cfg_if::cfg_if;
use color_eyre::Result;
use serde::Deserialize;
use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType};
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
@ -52,7 +59,7 @@ pub enum ModuleConfig {
#[cfg(feature = "music")]
Music(Box<MusicModule>),
#[cfg(feature = "networkmanager")]
Networkmanager(Box<NetworkmanagerModule>),
NetworkManager(Box<NetworkManagerModule>),
#[cfg(feature = "notifications")]
Notifications(Box<NotificationsModule>),
Script(Box<ScriptModule>),
@ -68,10 +75,57 @@ pub enum ModuleConfig {
Workspaces(Box<WorkspacesModule>),
}
impl ModuleConfig {
pub fn create(
self,
module_factory: &AnyModuleFactory,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()> {
macro_rules! create {
($module:expr) => {
module_factory.create(*$module, container, info)
};
}
match self {
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]
Self::Clock(module) => create!(module),
Self::Custom(module) => create!(module),
#[cfg(feature = "focused")]
Self::Focused(module) => create!(module),
Self::Label(module) => create!(module),
#[cfg(feature = "launcher")]
Self::Launcher(module) => create!(module),
#[cfg(feature = "music")]
Self::Music(module) => create!(module),
#[cfg(feature = "networkmanager")]
Self::NetworkManager(module) => create!(module),
#[cfg(feature = "notifications")]
Self::Notifications(module) => create!(module),
Self::Script(module) => create!(module),
#[cfg(feature = "sys_info")]
Self::SysInfo(module) => create!(module),
#[cfg(feature = "tray")]
Self::Tray(module) => create!(module),
#[cfg(feature = "upower")]
Self::Upower(module) => create!(module),
#[cfg(feature = "volume")]
Self::Volume(module) => create!(module),
#[cfg(feature = "workspaces")]
Self::Workspaces(module) => create!(module),
}
}
}
#[derive(Debug, Clone)]
pub enum MonitorConfig {
Single(Config),
Multiple(Vec<Config>),
Single(BarConfig),
Multiple(Vec<BarConfig>),
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
@ -101,38 +155,110 @@ pub struct MarginConfig {
pub top: i32,
}
/// The following is a list of all top-level bar config options.
///
/// These options can either be written at the very top object of your config,
/// or within an object in the [monitors](#monitors) config,
/// depending on your [use-case](#2-pick-your-use-case).
///
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
pub position: BarPosition,
#[serde(default = "default_true")]
pub anchor_to_edges: bool,
#[serde(default = "default_bar_height")]
pub height: i32,
#[serde(default)]
pub margin: MarginConfig,
#[serde(default = "default_popup_gap")]
pub popup_gap: i32,
pub struct BarConfig {
/// A unique identifier for the bar, used for controlling it over IPC.
/// If not set, uses a generated integer suffix.
///
/// **Default**: `bar-n`
pub name: Option<String>,
/// The bar's position on screen.
///
/// **Valid options**: `top`, `bottom`, `left`, `right`
/// <br>
/// **Default**: `bottom`
#[serde(default)]
pub position: BarPosition,
/// Whether to anchor the bar to the edges of the screen.
/// Setting to false centers the bar.
///
/// **Default**: `true`
#[serde(default = "default_true")]
pub anchor_to_edges: bool,
/// The bar's height in pixels.
///
/// Note that GTK treats this as a target minimum,
/// and if content inside the bar is over this,
/// it will automatically expand to fit.
///
/// **Default**: `42`
#[serde(default = "default_bar_height")]
pub height: i32,
/// The margin to use on each side of the bar, in pixels.
/// Object which takes `top`, `bottom`, `left` and `right` keys.
///
/// **Default**: `0` on all sides.
///
/// # Example
///
/// The following would set a 10px margin around each edge.
///
/// ```corn
/// {
/// margin.top = 10
/// margin.bottom = 10
/// margin.left = 10
/// margin.right = 10
/// }
/// ```
#[serde(default)]
pub margin: MarginConfig,
/// The size of the gap in pixels
/// between the bar and the popup window.
///
/// **Default**: `5`
#[serde(default = "default_popup_gap")]
pub popup_gap: i32,
/// Whether the bar should be hidden when Ironbar starts.
///
/// **Default**: `false`, unless `autohide` is set.
#[serde(default)]
pub start_hidden: Option<bool>,
/// The duration in milliseconds before the bar is hidden after the cursor leaves.
/// Leave unset to disable auto-hide behaviour.
///
/// **Default**: `null`
#[serde(default)]
pub autohide: Option<u64>,
/// GTK icon theme to use.
/// The name of the GTK icon theme to use.
/// Leave unset to use the default Adwaita theme.
///
/// **Default**: `null`
pub icon_theme: Option<String>,
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
/// An array of modules to append to the start of the bar.
/// Depending on the orientation, this is either the top of the left edge.
///
/// **Default**: `[]`
pub start: Option<Vec<ModuleConfig>>,
pub center: Option<Vec<ModuleConfig>>,
pub end: Option<Vec<ModuleConfig>>,
pub monitors: Option<HashMap<String, MonitorConfig>>,
/// An array of modules to append to the center of the bar.
///
/// **Default**: `[]`
pub center: Option<Vec<ModuleConfig>>,
/// An array of modules to append to the end of the bar.
/// Depending on the orientation, this is either the bottom or right edge.
///
/// **Default**: `[]`
pub end: Option<Vec<ModuleConfig>>,
}
impl Default for Config {
impl Default for BarConfig {
fn default() -> Self {
cfg_if! {
if #[cfg(feature = "clock")] {
@ -159,20 +285,58 @@ impl Default for Config {
name: None,
start_hidden: None,
autohide: None,
popup_gap: default_popup_gap(),
icon_theme: None,
ironvar_defaults: None,
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
)]),
center,
end,
anchor_to_edges: default_true(),
monitors: None,
popup_gap: default_popup_gap(),
}
}
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct Config {
/// A map of [ironvar](ironvar) keys and values
/// to initialize Ironbar with on startup.
///
/// **Default**: `{}`
///
/// # Example
///
/// The following initializes an ironvar called `foo` set to `bar` on startup:
///
/// ```corn
/// { ironvar_defaults.foo = "bar" }
/// ```
///
/// The variable can then be immediately fetched without needing to be manually set:
///
/// ```sh
/// $ ironbar get foo
/// ok
/// bar
/// ```
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
/// The configuration for the bar.
/// Setting through this will enable a single identical bar on each monitor.
#[serde(flatten)]
pub bar: BarConfig,
/// A map of monitor names to configs.
///
/// The config values can be either:
///
/// - a single object, which denotes a single bar for that monitor,
/// - an array of multiple objects, which denotes multiple for that monitor.
///
/// Providing this option overrides the single, global `bar` option.
pub monitors: Option<HashMap<String, MonitorConfig>>,
}
const fn default_bar_height() -> i32 {
42
}

View file

@ -20,13 +20,68 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
}
}
/// Some modules provide options for truncating text.
/// This is controlled using a common `TruncateMode` type,
/// which is defined below.
///
/// The option can be configured in one of two modes.
///
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(untagged)]
pub enum TruncateMode {
/// Auto mode lets GTK decide when to ellipsize.
///
/// To use this mode, set the truncate option to a string
/// declaring the location to truncate text from and place the ellipsis.
///
/// # Example
///
/// ```corn
/// { truncate = "start" }
/// ```
///
/// **Valid options**: `start`, `middle`, `end`
/// <br>
/// **Default**: `null`
Auto(EllipsizeMode),
/// Length mode defines a fixed point at which to ellipsize.
///
/// Generally you will want to set only one of `length` or `max_length`,
/// but you can set both if required.
///
/// # Example
///
/// ```corn
/// {
/// truncate.mode = "start"
/// truncate.length = 50
/// truncate.max_length = 70
/// }
/// ```
Length {
/// The location to truncate text from and place the ellipsis.
/// **Valid options**: `start`, `middle`, `end`
/// <br>
/// **Default**: `null`
mode: EllipsizeMode,
/// The fixed width (in characters) of the widget.
///
/// The widget will be expanded to this width
/// if it would have otherwise been smaller.
///
/// Leave unset to let GTK automatically handle.
///
/// **Default**: `null`
length: Option<i32>,
/// The maximum number of characters to show
/// before truncating.
///
/// Leave unset to let GTK automatically handle.
///
/// **Default**: `null`
max_length: Option<i32>,
},
}

View file

@ -44,7 +44,7 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image));
} else {
let label = Label::new(Some(input));
let label = Label::builder().use_markup(true).label(input).build();
label.add_class("icon");
label.add_class("text-icon");

View file

@ -11,7 +11,7 @@ use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf};
#[cfg(feature = "http")]
use tokio::sync::mpsc;
use tracing::warn;
use tracing::{debug, warn};
cfg_if!(
if #[cfg(feature = "http")] {
@ -45,6 +45,7 @@ impl<'a> ImageProvider<'a> {
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
debug!("Resolved {input} --> {location:?} (size: {size})");
Some(Self { location, size })
}
@ -171,7 +172,7 @@ impl<'a> ImageProvider<'a> {
);
// Different error types makes this a bit awkward
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image))
{
Ok(Err(err)) => error!("{err:?}"),
Err(err) => error!("{err:?}"),
@ -202,7 +203,7 @@ impl<'a> ImageProvider<'a> {
_ => unreachable!(), // handled above
}?;
Self::create_and_load_surface(&pixbuf, image, scale)
Self::create_and_load_surface(&pixbuf, image)
}
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
@ -210,10 +211,13 @@ impl<'a> ImageProvider<'a> {
/// The surface is then loaded into the provided image.
///
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image, scale: i32) -> Result<()> {
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image) -> Result<()> {
let surface = unsafe {
let ptr =
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
let ptr = gdk_cairo_surface_create_from_pixbuf(
pixbuf.as_ptr(),
image.scale_factor(),
std::ptr::null_mut(),
);
Surface::from_raw_full(ptr)
}?;

View file

@ -32,6 +32,9 @@ pub enum Command {
key: Box<str>,
},
/// Gets the current value of all `ironvar`s.
List,
/// Load an additional CSS stylesheet.
/// The sheet is automatically hot-reloaded.
LoadCss {

View file

@ -129,7 +129,7 @@ impl Ipc {
ironbar.reload_config();
for output in outputs {
match crate::load_output_bars(ironbar, application, output) {
match crate::load_output_bars(ironbar, application, &output) {
Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"),
}
@ -153,6 +153,20 @@ impl Ipc {
None => Response::error("Variable not found"),
}
}
Command::List => {
let variable_manager = Ironbar::variable_manager();
let mut values = read_lock!(variable_manager)
.get_all()
.iter()
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
.collect::<Vec<_>>();
values.sort();
let value = values.join("\n");
Response::OkValue { value }
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
@ -172,7 +186,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
@ -209,7 +223,7 @@ impl Ipc {
popup.hide();
let data = popup
.cache
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)

View file

@ -46,6 +46,10 @@ impl VariableManager {
self.variables.get(key).and_then(IronVar::get)
}
pub fn get_all(&self) -> &HashMap<Box<str>, IronVar> {
&self.variables
}
/// Subscribes to an `ironvar`, creating it if it does not exist.
/// Any time the var is set, its value is sent on the channel.
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
@ -66,7 +70,7 @@ impl VariableManager {
/// Ironbar dynamic variable representation.
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
#[derive(Debug)]
struct IronVar {
pub struct IronVar {
value: Option<String>,
tx: broadcast::Sender<Option<String>>,
_rx: broadcast::Receiver<Option<String>>,
@ -82,14 +86,14 @@ impl IronVar {
/// Gets the current variable value.
/// Prefer to subscribe to changes where possible.
fn get(&self) -> Option<String> {
pub fn get(&self) -> Option<String> {
self.value.clone()
}
/// Sets the current variable value.
/// The change is broadcast to all receivers.
fn set(&mut self, value: Option<String>) {
self.value = value.clone();
self.value.clone_from(&value);
send!(self.tx, value);
}

View file

@ -1,3 +1,31 @@
/// Provides implementations of methods required by the `Module` trait
/// which cannot be included as part of the trait.
///
/// This removes the need to add the same boilerplate method definitions
/// to every module implementation.
///
/// # Usage:
///
/// ```rs
/// impl Module for ClockModule {
/// type SendMessage = DateTime<Local>;
/// type ReceiveMessage = ();
///
/// module_impl!("clock");
/// }
#[macro_export]
macro_rules! module_impl {
($name:literal) => {
fn name() -> &'static str {
$name
}
fn take_common(&mut self) -> $crate::config::CommonConfig {
self.common.take().unwrap_or_default()
}
};
}
/// Sends a message on an asynchronous `Sender` using `send()`
/// Panics if the message cannot be sent.
///
@ -181,6 +209,13 @@ macro_rules! arc_rw {
};
}
/// Wraps `val` in a new `Rc<RefCell<T>>`.
///
/// # Usage
///
/// ```rs
/// let val = rc_mut!(MyService::new())
/// ```
#[macro_export]
macro_rules! rc_mut {
($val:expr) => {

View file

@ -9,7 +9,7 @@ use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[cfg(feature = "ipc")]
use std::sync::RwLock;
use std::sync::{mpsc, Arc, OnceLock};
use std::sync::{mpsc, Arc, Mutex, OnceLock};
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>,
config_dir: PathBuf,
}
impl Ironbar {
fn new() -> Self {
let (config, config_dir) = load_config();
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
config: Rc::new(RefCell::new(load_config())),
config: Rc::new(RefCell::new(config)),
config_dir,
}
}
@ -192,7 +196,7 @@ impl Ironbar {
while let Ok(event) = rx_outputs.recv().await {
match event.event_type {
OutputEventType::New => {
match load_output_bars(&instance, &app, event.output) {
match load_output_bars(&instance, &app, &event.output) {
Ok(mut new_bars) => {
instance.bars.borrow_mut().append(&mut new_bars);
}
@ -260,7 +264,7 @@ impl Ironbar {
/// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")]
fn reload_config(&self) {
self.config.replace(load_config());
self.config.replace(load_config().0);
}
}
@ -270,20 +274,37 @@ fn start_ironbar() {
}
/// Loads the config file from disk.
fn load_config() -> Config {
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
fn load_config() -> (Config, PathBuf) {
let config_path = env::var("IRONBAR_CONFIG");
Config::default()
});
let (config, directory) = if let Ok(config_path) = config_path {
let path = PathBuf::from(config_path);
(
ConfigLoader::load(&path),
path.parent()
.map(PathBuf::from)
.ok_or_else(|| Report::msg("Specified path has no parent")),
)
} else {
let config_loader = ConfigLoader::new("ironbar");
(
config_loader.find_and_load(),
config_loader.config_dir().map_err(Report::new),
)
};
let mut config = config.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
let directory = directory
.and_then(|dir| dir.canonicalize().map_err(Report::new))
.unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
debug!("Loaded config file");
@ -297,7 +318,7 @@ fn load_config() -> Config {
}
}
config
(config, directory)
}
/// Gets the GDK `Display` instance.
@ -316,22 +337,41 @@ fn get_display() -> Display {
fn load_output_bars(
ironbar: &Rc<Ironbar>,
app: &Application,
output: OutputInfo,
output: &OutputInfo,
) -> Result<Vec<Bar>> {
// Hack to track monitor positions due to new GTK3/wlroots bug:
// https://github.com/swaywm/sway/issues/8164
// This relies on Wayland always tracking monitors in the same order as GDK.
// We also need this static to ensure hot-reloading continues to work as best we can.
static INDEX_MAP: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
let Some(monitor_name) = &output.name else {
return Err(Report::msg("Output missing monitor name"));
};
let map = INDEX_MAP.get_or_init(|| Mutex::new(vec![]));
let index = lock!(map).iter().position(|n| n == monitor_name);
let index = match index {
Some(index) => index,
None => {
lock!(map).push(monitor_name.clone());
lock!(map).len() - 1
}
};
let config = ironbar.config.borrow();
let display = get_display();
let pos = output.logical_position.unwrap_or_default();
let monitor = display
.monitor_at_point(pos.0, pos.1)
.expect("monitor to exist");
// let pos = output.logical_position.unwrap_or_default();
// let monitor = display
// .monitor_at_point(pos.0, pos.1)
// .expect("monitor to exist");
let monitor = display.monitor(index as i32).expect("monitor to exist");
let show_default_bar =
config.start.is_some() || config.center.is_some() || config.end.is_some();
config.bar.start.is_some() || config.bar.center.is_some() || config.bar.end.is_some();
let bars = match config
.monitors
@ -363,7 +403,7 @@ fn load_output_bars(
app,
&monitor,
monitor_name.to_string(),
config.clone(),
config.bar.clone(),
ironbar.clone(),
)?],
None => vec![],

215
src/modules/cairo.rs Normal file
View file

@ -0,0 +1,215 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, spawn, try_send};
use cairo::{Format, ImageSurface};
use glib::translate::IntoGlibPtr;
use glib::Propagation;
use gtk::prelude::*;
use gtk::DrawingArea;
use mlua::{Error, Function, LightUserData};
use notify::event::ModifyKind;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::time::sleep;
use tracing::{debug, error};
#[derive(Debug, Clone, Deserialize)]
pub struct CairoModule {
/// The path to the Lua script to load.
/// This can be absolute, or relative to the working directory.
///
/// The script must contain the entry `draw` function.
///
/// **Required**
path: PathBuf,
/// The number of milliseconds between each draw call.
///
/// **Default**: `200`
#[serde(default = "default_frequency")]
frequency: u64,
/// The canvas width in pixels.
///
/// **Default**: `42`
#[serde(default = "default_size")]
width: u32,
/// The canvas height in pixels.
///
/// **Default**: `42`
#[serde(default = "default_size")]
height: u32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_size() -> u32 {
42
}
const fn default_frequency() -> u64 {
200
}
impl Module<gtk::Box> for CairoModule {
type SendMessage = ();
type ReceiveMessage = ();
module_impl!("cairo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let path = self.path.to_path_buf();
let tx = context.tx.clone();
spawn(async move {
let parent = path.parent().expect("to have parent path");
let mut watcher = recommended_watcher({
let path = path.clone();
move |res: notify::Result<Event>| match res {
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
debug!("{event:?}");
if event.paths.first().is_some_and(|p| p == &path) {
try_send!(tx, ModuleUpdateEvent::Update(()));
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
_ => {}
}
})
.expect("Failed to create lua file watcher");
watcher
.watch(parent, RecursiveMode::NonRecursive)
.expect("Failed to start lua file watcher");
// avoid watcher from dropping
loop {
sleep(Duration::from_secs(1)).await;
}
});
// Lua needs to run synchronously with the GTK updates,
// so the controller does not handle the script engine.
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<gtk::Box>>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let id = context.id.to_string();
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let surface = ImageSurface::create(Format::ARgb32, self.width as i32, self.height as i32)?;
let area = DrawingArea::new();
let lua = context
.ironbar
.clients
.borrow_mut()
.lua(&context.ironbar.config_dir);
// this feels kinda dirty,
// but it keeps draw functions separate in the global scope
let script = fs::read_to_string(&self.path)?
.replace("function draw", format!("function __draw_{id}").as_str());
lua.load(&script).exec()?;
{
let lua = lua.clone();
let id = id.clone();
let path = self.path.clone();
area.connect_draw(move |_, cr| {
let function: Function = lua
.load(include_str!("../../lua/draw.lua"))
.eval()
.expect("to be valid");
if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
error!("{err}");
return Propagation::Stop;
}
let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
// mlua needs a valid return type, even if we don't return anything
if let Err(err) =
function.call::<_, Option<bool>>((id.as_str(), LightUserData(ptr)))
{
match err {
Error::RuntimeError(message) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua runtime error] {}:{message}", path.display())
}
_ => error!("{err}"),
}
return Propagation::Stop;
}
Propagation::Proceed
});
}
area.set_size_request(self.width as i32, self.height as i32);
container.add(&area);
glib::spawn_future_local(async move {
loop {
area.queue_draw();
glib::timeout_future(Duration::from_millis(self.frequency)).await;
}
});
glib_recv!(context.subscribe(), _ev => {
let res = fs::read_to_string(&self.path)
.map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
match res {
Ok(script) => {
match lua.load(&script).exec() {
Ok(_) => {},
Err(Error::SyntaxError { message, ..}) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua syntax error] {}:{message}", self.path.display())
},
Err(err) => error!("lua error: {err:?}")
}
},
Err(err) => error!("{err:?}")
}
});
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View file

@ -5,7 +5,7 @@ use crate::image::new_icon_button;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use glib::Propagation;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
@ -13,24 +13,39 @@ use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct ClipboardModule {
/// The icon to show on the bar widget button.
/// Supports [image](images) icons.
///
/// **Default**: `󰨸`
#[serde(default = "default_icon")]
icon: String,
/// The size to render the icon at.
/// Note this only applies to image-type icons.
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// The maximum number of items to keep in the history,
/// and to show in the popup.
///
/// **Default**: `10`
#[serde(default = "default_max_items")]
max_items: usize,
// -- Common --
/// See [truncate options](module-level-options#truncate-mode).
///
/// **Default**: `null`
truncate: Option<TruncateMode>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -65,9 +80,7 @@ impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent;
fn name() -> &'static str {
"clipboard"
}
module_impl!("clipboard");
fn spawn_controller(
&self,
@ -78,7 +91,7 @@ impl Module<Button> for ClipboardModule {
let max_items = self.max_items;
let tx = context.tx.clone();
let client: Arc<clipboard::Client> = context.client();
let client = context.client::<clipboard::Client>();
// listen to clipboard events
spawn(async move {
@ -137,7 +150,7 @@ impl Module<Button> for ClipboardModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -147,6 +160,7 @@ impl Module<Button> for ClipboardModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -8,29 +8,56 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc};
use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
#[derive(Debug, Deserialize, Clone)]
pub struct ClockModule {
/// Date/time format string.
/// Default: `%d/%m/%Y %H:%M`
/// The format string to use for the date/time shown on the bar.
/// Pango markup is supported.
///
/// Detail on available tokens can be found here:
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
///
/// **Default**: `%d/%m/%Y %H:%M`
#[serde(default = "default_format")]
format: String,
/// The format string to use for the date/time shown in the popup header.
/// Pango markup is supported.
///
/// Detail on available tokens can be found here:
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
///
/// **Default**: `%H:%M:%S`
#[serde(default = "default_popup_format")]
format_popup: String,
/// The locale to use when formatting dates.
///
/// Note this will not control the calendar -
/// for that you must set `LC_TIME`.
///
/// **Valid options**: See [here](https://docs.rs/pure-rust-locales/0.8.1/pure_rust_locales/enum.Locale.html#variants)
/// <br>
/// **Default**: `$LC_TIME` or `$LANG` or `'POSIX'`
#[serde(default = "default_locale")]
locale: String,
/// The orientation to display the widget contents.
/// Setting to vertical will rotate text 90 degrees.
///
/// **Valid options**: `horizontal`, `vertical`
/// <br>
/// **Default**: `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -41,6 +68,7 @@ impl Default for ClockModule {
format: default_format(),
format_popup: default_popup_format(),
locale: default_locale(),
orientation: ModuleOrientation::Horizontal,
common: Some(CommonConfig::default()),
}
}
@ -71,9 +99,7 @@ impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>;
type ReceiveMessage = ();
fn name() -> &'static str {
"clock"
}
module_impl!("clock");
fn spawn_controller(
&self,
@ -100,7 +126,7 @@ impl Module<Button> for ClockModule {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let label = Label::builder()
.angle(info.bar_position.get_angle())
.angle(self.orientation.to_angle())
.use_markup(true)
.build();
button.add(&label);
@ -120,7 +146,12 @@ impl Module<Button> for ClockModule {
});
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -130,6 +161,7 @@ impl Module<Button> for ClockModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0);

View file

@ -1,15 +1,32 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::config::ModuleOrientation;
use crate::modules::custom::WidgetConfig;
use gtk::prelude::*;
use gtk::Orientation;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct BoxWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
orientation: Option<String>,
/// Whether child widgets should be horizontally or vertically added.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br />
/// **Default**: `horizontal`
orientation: Option<ModuleOrientation>,
/// Modules and widgets to add to this box.
///
/// **Default**: `null`
widgets: Option<Vec<WidgetConfig>>,
}
@ -20,9 +37,7 @@ impl CustomWidget for BoxWidget {
let container = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
container.set_orientation(
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
);
container.set_orientation(orientation.into());
}
if let Some(widgets) = self.widgets {

View file

@ -1,19 +1,53 @@
use gtk::prelude::*;
use gtk::{Button, Label};
use gtk::{Button, Label, Orientation};
use serde::Deserialize;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
/// Widget text label. Pango markup and embedded scripts are supported.
///
/// This is a shorthand for adding a label widget to the button.
/// Ignored if `widgets` is set.
///
/// This is a [Dynamic String](dynamic-values#dynamic-string).
///
/// **Default**: `null`
label: Option<String>,
/// Command to execute. More on this [below](#commands).
///
/// **Default**: `null`
on_click: Option<String>,
/// Orientation of the button.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br />
/// **Default**: `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// Modules and widgets to add to this box.
///
/// **Default**: `null`
widgets: Option<Vec<WidgetConfig>>,
}
impl CustomWidget for ButtonWidget {
@ -23,9 +57,20 @@ impl CustomWidget for ButtonWidget {
let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label {
if let Some(widgets) = self.widgets {
let container = gtk::Box::new(Orientation::Horizontal, 0);
for widget in widgets {
widget.widget.add_to(&container, &context, widget.common);
}
button.add(&container);
} else if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
label.set_angle(self.orientation.to_angle());
button.add(&label);
dynamic_string(&text, move |string| {

View file

@ -10,9 +10,27 @@ use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ImageWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
/// Image source.
///
/// This is an [image](image) via [Dynamic String](dynamic-values#dynamic-string).
///
/// **Required**
src: String,
/// The width/height of the image.
/// Aspect ratio is preserved.
///
/// **Default**: `32`
#[serde(default = "default_size")]
size: i32,
}

View file

@ -3,15 +3,38 @@ use gtk::Label;
use serde::Deserialize;
use crate::build;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
/// Widget text label. Pango markup and embedded scripts are supported.
///
/// This is a [Dynamic String](dynamic-values#dynamic-string).
///
/// **Required**
label: String,
/// Orientation of the label.
/// Setting to vertical will rotate text 90 degrees.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br />
/// **Default**: `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
}
impl CustomWidget for LabelWidget {
@ -20,6 +43,7 @@ impl CustomWidget for LabelWidget {
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
let label = build!(self, Self::Widget);
label.set_angle(self.orientation.to_angle());
label.set_use_markup(true);
{

View file

@ -9,15 +9,16 @@ use self::image::ImageWidget;
use self::label::LabelWidget;
use self::r#box::BoxWidget;
use self::slider::SliderWidget;
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleConfig};
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
wrap_widget, AnyModuleFactory, BarModuleFactory, Module, ModuleInfo, ModuleParts, ModulePopup,
ModuleUpdateEvent, PopupButton, PopupModuleFactory, WidgetContext,
};
use crate::script::Script;
use crate::{send_async, spawn};
use color_eyre::{Report, Result};
use crate::{module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use serde::Deserialize;
@ -28,40 +29,67 @@ use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct CustomModule {
/// Widgets to add to the bar container
/// Modules and widgets to add to the bar container.
///
/// **Default**: `[]`
bar: Vec<WidgetConfig>,
/// Widgets to add to the popup container
/// Modules and widgets to add to the popup container.
///
/// **Default**: `null`
popup: Option<Vec<WidgetConfig>>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct WidgetConfig {
/// One of a custom module native Ironbar module.
#[serde(flatten)]
widget: Widget,
widget: WidgetOrModule,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
common: CommonConfig,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum WidgetOrModule {
/// A custom-module specific basic widget
Widget(Widget),
/// A native Ironbar module, such as `clock` or `focused`.
/// All widgets are supported, including their popups.
Module(ModuleConfig),
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Widget {
/// A container to place nested widgets inside.
Box(BoxWidget),
/// A text label. Pango markup is supported.
Label(LabelWidget),
/// A clickable button, which can run a command when clicked.
Button(ButtonWidget),
/// An image or icon from disk or http.
Image(ImageWidget),
/// A draggable slider.
Slider(SliderWidget),
/// A progress bar.
Progress(ProgressWidget),
}
#[derive(Clone)]
struct CustomWidgetContext<'a> {
info: &'a ModuleInfo<'a>,
tx: &'a mpsc::Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
module_factory: AnyModuleFactory,
}
trait CustomWidget {
@ -103,14 +131,16 @@ pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orient
};
}
/// Attempts to parse an `Orientation` from `String`.
/// Will accept `horizontal`, `vertical`, `h` or `v`.
/// Ignores case.
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
impl WidgetOrModule {
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) {
match self {
WidgetOrModule::Widget(widget) => widget.add_to(parent, context, common),
WidgetOrModule::Module(config) => {
if let Err(err) = config.create(&context.module_factory, parent, context.info) {
error!("{err:?}");
}
}
}
}
}
@ -151,9 +181,7 @@ impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn name() -> &'static str {
"custom"
}
module_impl!("custom");
fn spawn_controller(
&self,
@ -191,7 +219,7 @@ impl Module<gtk::Box> for CustomModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.orientation();
@ -200,10 +228,13 @@ impl Module<gtk::Box> for CustomModule {
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext {
info,
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
module_factory: BarModuleFactory::new(context.ironbar.clone(), context.popup.clone())
.into(),
};
self.bar.clone().into_iter().for_each(|widget| {
@ -212,8 +243,22 @@ impl Module<gtk::Box> for CustomModule {
.add_to(&container, &custom_context, widget.common);
});
for button in popup_buttons.borrow().iter() {
button.ensure_popup_id();
}
context.button_id = popup_buttons
.borrow()
.first()
.map_or(usize::MAX, PopupButton::popup_id);
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleParts {
@ -226,6 +271,7 @@ impl Module<gtk::Box> for CustomModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
@ -235,10 +281,17 @@ impl Module<gtk::Box> for CustomModule {
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
info,
tx: &tx,
bar_orientation: info.bar_position.orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
module_factory: PopupModuleFactory::new(
context.ironbar,
context.popup,
context.button_id,
)
.into(),
};
for widget in popup {

View file

@ -4,22 +4,59 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
orientation: Option<String>,
/// Orientation of the progress bar.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br />
/// **Default**: `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// Text label to show for the progress bar.
///
/// This is a [Dynamic String](dynamic-values#dynamic-string).
///
/// **Default**: `null`
label: Option<String>,
/// Script to run to get the progress bar value.
/// Output must be a valid percentage.
///
/// Note that this expects a numeric value between `0`-`max` as output.
///
/// **Default**: `null`
value: Option<ScriptInput>,
/// The maximum progress bar value.
///
/// **Default**: `100`
#[serde(default = "default_max")]
max: f64,
/// The progress bar length, in pixels.
/// GTK will automatically determine the size if left blank.
///
/// **Default**: `null`
length: Option<i32>,
}
@ -33,11 +70,7 @@ impl CustomWidget for ProgressWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let progress = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
progress.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
progress.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&progress, length, context.bar_orientation);

View file

@ -8,25 +8,76 @@ use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::config::ModuleOrientation;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>,
orientation: Option<String>,
/// Orientation of the slider.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br />
/// **Default**: `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// Script to run to get the slider value.
/// Output must be a valid number.
///
/// **Default**: `null`
value: Option<ScriptInput>,
/// Command to execute when the slider changes.
/// More on this [below](#slider).
///
/// Note that this will provide the floating point value as an argument.
/// If your input program requires an integer, you will need to round it.
///
/// **Default**: `null`
on_change: Option<String>,
/// Minimum slider value.
///
/// **Default**: `0`
#[serde(default = "default_min")]
min: f64,
/// Maximum slider value.
///
/// **Default**: `100`
#[serde(default = "default_max")]
max: f64,
/// If the increment to change when scrolling with the mousewheel.
/// If left blank, GTK will use the default value,
/// determined by the current environment.
///
/// **Default**: `null`
step: Option<f64>,
/// The slider length.
/// GTK will automatically determine the size if left blank.
///
/// **Default**: `null`
length: Option<i32>,
/// Whether to show the value label above the slider.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_label: bool,
}
@ -45,11 +96,7 @@ impl CustomWidget for SliderWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let scale = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
scale.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
scale.set_orientation(self.orientation.into());
if let Some(length) = self.length {
set_length(&scale, length, context.bar_orientation);

View file

@ -3,7 +3,7 @@ use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -14,18 +14,29 @@ use tracing::debug;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
/// Whether to show icon on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_icon: bool,
/// Whether to show app name on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_title: bool,
/// Icon size in pixels.
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
// -- common --
/// See [truncate options](module-level-options#truncate-mode).
///
/// **Default**: `null`
truncate: Option<TruncateMode>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -50,9 +61,7 @@ impl Module<gtk::Box> for FocusedModule {
type SendMessage = Option<(String, String)>;
type ReceiveMessage = ();
fn name() -> &'static str {
"focused"
}
module_impl!("focused");
fn spawn_controller(
&self,
@ -64,12 +73,16 @@ impl Module<gtk::Box> for FocusedModule {
let wl = context.client::<wayland::Client>();
spawn(async move {
let mut current = None;
let mut wlrx = wl.subscribe_toplevels();
let handles = wl.toplevel_info_all();
let focused = handles.into_iter().find(|info| info.focused);
if let Some(focused) = focused {
current = Some(focused.id);
try_send!(
tx,
ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id)))
@ -81,6 +94,9 @@ impl Module<gtk::Box> for FocusedModule {
ToplevelEvent::Update(info) => {
if info.focused {
debug!("Changing focus");
current = Some(info.id);
send_async!(
tx,
ModuleUpdateEvent::Update(Some((
@ -88,13 +104,16 @@ impl Module<gtk::Box> for FocusedModule {
info.app_id.clone()
)))
);
} else {
} else if info.id == current.unwrap_or_default() {
debug!("Clearing focus");
current = None;
send_async!(tx, ModuleUpdateEvent::Update(None));
}
}
ToplevelEvent::Remove(info) => {
if info.focused {
debug!("Clearing focus");
current = None;
send_async!(tx, ModuleUpdateEvent::Update(None));
}
}

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, try_send};
use crate::{glib_recv, module_impl, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -10,8 +10,13 @@ use tokio::sync::mpsc;
#[derive(Debug, Deserialize, Clone)]
pub struct LabelModule {
/// The text to show on the label.
/// This is a [Dynamic String](dynamic-values#dynamic-string).
///
/// **Required**
label: String,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -29,9 +34,7 @@ impl Module<Label> for LabelModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"label"
}
module_impl!("label");
fn spawn_controller(
&self,

View file

@ -40,7 +40,7 @@ impl Item {
let id = info.id;
if self.windows.is_empty() {
self.name = info.title.clone();
self.name.clone_from(&info.title);
}
let window = Window::from(info);
@ -59,7 +59,7 @@ impl Item {
pub fn set_window_name(&mut self, window_id: usize, name: String) {
if let Some(window) = self.windows.get_mut(&window_id) {
if let OpenState::Open { focused: true, .. } = window.open_state {
self.name = name.clone();
self.name.clone_from(&name);
}
window.name = name;

View file

@ -7,7 +7,7 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report};
use gtk::prelude::*;
use gtk::{Button, Orientation};
@ -22,17 +22,38 @@ use tracing::{debug, error, trace};
pub struct LauncherModule {
/// List of app IDs (or classes) to always show regardless of open state,
/// in the order specified.
///
/// **Default**: `null`
favorites: Option<Vec<String>>,
/// Whether to show application names on the bar.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")]
show_names: bool,
/// Whether to show application icons on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_icons: bool,
/// Size in pixels to render icon at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// Whether items should be added from right-to-left
/// instead of left-to-right.
///
/// This includes favourites.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")]
reversed: bool,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -80,9 +101,7 @@ impl Module<gtk::Box> for LauncherModule {
type SendMessage = LauncherUpdate;
type ReceiveMessage = ItemEvent;
fn name() -> &'static str {
"launcher"
}
module_impl!("launcher");
fn spawn_controller(
&self,
@ -181,13 +200,22 @@ impl Module<gtk::Box> for LauncherModule {
}?;
}
ToplevelEvent::Update(info) => {
if let Some(item) = lock!(items).get_mut(&info.app_id) {
// check if open, as updates can be sent as program closes
// if it's a focused favourite closing, it otherwise incorrectly re-focuses.
let is_open = if let Some(item) = lock!(items).get_mut(&info.app_id) {
item.set_window_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone());
}
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused))
.await?;
item.open_state.is_open()
} else {
false
};
send_update(LauncherUpdate::Focus(
info.app_id.clone(),
is_open && info.focused,
))
.await?;
send_update(LauncherUpdate::Title(
info.app_id.clone(),
info.id,
@ -340,7 +368,12 @@ impl Module<gtk::Box> for LauncherModule {
&controller_tx,
);
container.add(&button.button);
if self.reversed {
container.pack_end(&button.button, false, false, 0);
} else {
container.add(&button.button);
}
buttons.insert(item.app_id, button);
}
}
@ -349,8 +382,7 @@ impl Module<gtk::Box> for LauncherModule {
button.set_open(true);
button.set_focused(win.open_state.is_focused());
let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows += 1;
write_lock!(button.menu_state).num_windows += 1;
}
}
LauncherUpdate::RemoveItem(app_id) => {
@ -401,7 +433,7 @@ impl Module<gtk::Box> for LauncherModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
@ -414,6 +446,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250;

View file

@ -10,12 +10,14 @@ use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widge
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use crate::clients::ProvidesClient;
use crate::clients::{ClientResult, ProvidesClient, ProvidesFallibleClient};
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
@ -56,6 +58,8 @@ pub enum ModuleLocation {
Center,
Right,
}
#[derive(Clone)]
pub struct ModuleInfo<'a> {
pub app: &'a Application,
pub location: ModuleLocation,
@ -87,10 +91,16 @@ where
{
pub id: usize,
pub ironbar: Rc<Ironbar>,
pub popup: Rc<Popup>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>,
// TODO: Don't like this - need some serious refactoring to deal with it
// This is a hack to be able to pass data from module -> popup creation
// for custom widget only.
pub button_id: usize,
_update_rx: broadcast::Receiver<TSend>,
}
@ -109,6 +119,13 @@ where
ProvidesClient::provide(self)
}
pub fn try_client<T: ?Sized>(&self) -> ClientResult<T>
where
WidgetContext<TSend, TReceive>: ProvidesFallibleClient<T>,
{
ProvidesFallibleClient::try_provide(self)
}
/// Subscribes to events sent from this widget.
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe()
@ -124,6 +141,32 @@ impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
pub fn setup_identifiers(&self, common: &CommonConfig) {
if let Some(ref name) = common.name {
self.widget.set_widget_name(name);
if let Some(ref popup) = self.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
self.widget.style_context().add_class(part);
}
if let Some(ref popup) = self.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
}
}
}
}
#[derive(Debug, Clone)]
@ -152,11 +195,24 @@ impl ModulePopup for Option<gtk::Box> {
}
pub trait PopupButton {
fn ensure_popup_id(&self) -> usize;
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button,
/// or creates a new one if it does not exist.
fn ensure_popup_id(&self) -> usize {
if let Some(id) = self.try_popup_id() {
id
} else {
let id = Ironbar::unique_id();
self.set_tag("popup-id", id);
id
}
}
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
@ -203,165 +259,290 @@ where
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
<Self as Module<W>>::SendMessage: Clone,
{
None
}
fn take_common(&mut self) -> CommonConfig;
}
/// Creates a module and sets it up.
/// This setup includes widget/popup content and event channels.
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo,
popup: &Rc<Popup>,
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
pub trait ModuleFactory {
fn create<TModule, TWidget, TSend, TRev>(
&self,
mut module: TModule,
container: &gtk::Box,
info: &ModuleInfo,
) -> Result<()>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRev>,
TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static,
{
let id = Ironbar::unique_id();
let common = module.take_common();
let (tx, rx) = broadcast::channel(64);
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRev>(64);
let context = WidgetContext {
id,
ironbar,
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
};
let (tx, rx) = broadcast::channel(64);
module.spawn_controller(info, &context, controller_rx)?;
let context = WidgetContext {
id,
ironbar: self.ironbar().clone(),
popup: self.popup().clone(),
tx: ui_tx,
update_tx: tx.clone(),
controller_tx,
_update_rx: rx,
button_id: usize::MAX, // hack :(
};
let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
module.spawn_controller(info, &context, controller_rx)?;
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
let module_name = TModule::name();
let instance_name = common
.name
.clone()
.unwrap_or_else(|| module_name.to_string());
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
popup.register_content(id, instance_name, popup_content);
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{module_name}"));
self.popup()
.register_content(id, instance_name, popup_content);
}
self.setup_receiver(tx, ui_rx, module_name, id, common.disable_popup);
module_parts.setup_identifiers(&common);
let ev_container = wrap_widget(
&module_parts.widget,
common,
info.bar_position.orientation(),
);
container.add(&ev_container);
Ok(())
}
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static;
Ok(module_parts)
fn ironbar(&self) -> &Rc<Ironbar>;
fn popup(&self) -> &Rc<Popup>;
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
/// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>(
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
#[derive(Clone)]
pub struct BarModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
name: &'static str,
id: usize,
) where
TSend: Debug + Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
}
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
if popup.is_visible() {
popup.hide();
} else {
popup.show(id, button_id);
impl BarModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>) -> Self {
Self { ironbar, popup }
}
}
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
impl ModuleFactory for BarModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
has_popup_opened = true;
}
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.hide();
popup.show_at(id, geometry);
has_popup_opened = true;
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
}
}
});
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleParts<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
#[derive(Clone)]
pub struct PopupModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
button_id: usize,
}
if let Some(ref popup) = widget_parts.popup {
popup.container.set_widget_name(&format!("popup-{name}"));
impl PopupModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>, button_id: usize) -> Self {
Self {
ironbar,
popup,
button_id,
}
}
}
impl ModuleFactory for PopupModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
let popup = self.popup.clone();
let button_id = self.button_id;
glib_recv_mpsc!(rx, ev => {
match ev {
ModuleUpdateEvent::Update(update) => {
send!(tx, update);
}
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide();
} else {
popup.show(id, button_id);
}
}
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
popup.hide();
popup.show(id, button_id);
}
#[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id);
popup.hide();
popup.show_at(id, geometry);
}
ModuleUpdateEvent::ClosePopup if !disable_popup => {
debug!("Closing popup for {} [#{}]", name, id);
popup.hide();
},
_ => {}
}
});
}
fn ironbar(&self) -> &Rc<Ironbar> {
&self.ironbar
}
fn popup(&self) -> &Rc<Popup> {
&self.popup
}
}
#[derive(Clone)]
pub enum AnyModuleFactory {
Bar(BarModuleFactory),
Popup(PopupModuleFactory),
}
impl ModuleFactory for AnyModuleFactory {
fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
name: &'static str,
id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static,
{
match self {
AnyModuleFactory::Bar(bar) => bar.setup_receiver(tx, rx, name, id, disable_popup),
AnyModuleFactory::Popup(popup) => popup.setup_receiver(tx, rx, name, id, disable_popup),
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
widget_parts.widget.style_context().add_class(part);
fn ironbar(&self) -> &Rc<Ironbar> {
match self {
AnyModuleFactory::Bar(bar) => bar.ironbar(),
AnyModuleFactory::Popup(popup) => popup.ironbar(),
}
}
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
fn popup(&self) -> &Rc<Popup> {
match self {
AnyModuleFactory::Bar(bar) => bar.popup(),
AnyModuleFactory::Popup(popup) => popup.popup(),
}
}
}
impl From<BarModuleFactory> for AnyModuleFactory {
fn from(value: BarModuleFactory) -> Self {
Self::Bar(value)
}
}
impl From<PopupModuleFactory> for AnyModuleFactory {
fn from(value: PopupModuleFactory) -> Self {
Self::Popup(value)
}
}
/// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned.
pub fn wrap_widget<W: IsA<Widget>>(

View file

@ -6,34 +6,50 @@ use std::path::PathBuf;
#[derive(Debug, Deserialize, Clone)]
pub struct Icons {
/// Icon to display when playing.
///
/// **Default**: ``
#[serde(default = "default_icon_play")]
pub(crate) play: String,
/// Icon to display when paused.
///
/// **Default**: ``
#[serde(default = "default_icon_pause")]
pub(crate) pause: String,
/// Icon to display for previous button.
///
/// **Default**: `󰒮`
#[serde(default = "default_icon_prev")]
pub(crate) prev: String,
/// Icon to display for next button.
///
/// **Default**: `󰒭`
#[serde(default = "default_icon_next")]
pub(crate) next: String,
/// Icon to display under volume slider
/// Icon to display under volume slider.
///
/// **Default**: `󰕾`
#[serde(default = "default_icon_volume")]
pub(crate) volume: String,
/// Icon to display nex to track title
/// Icon to display nex to track title.
///
/// **Default**: `󰎈`
#[serde(default = "default_icon_track")]
pub(crate) track: String,
/// Icon to display nex to album name
/// Icon to display nex to album name.
///
/// **Default**: `󰀥`
#[serde(default = "default_icon_album")]
pub(crate) album: String,
/// Icon to display nex to artist name
/// Icon to display nex to artist name.
///
/// **Default**: `󰠃`
#[serde(default = "default_icon_artist")]
pub(crate) artist: String,
}
@ -73,33 +89,62 @@ pub struct MusicModule {
pub(crate) player_type: PlayerType,
/// Format of current song info to display on the bar.
///
/// Info on formatting tokens [below](#formatting-tokens).
///
/// **Default**: `{title} / {artist}`
#[serde(default = "default_format")]
pub(crate) format: String,
/// Player state icons
/// Player state icons.
///
/// See [icons](#icons).
#[serde(default)]
pub(crate) icons: Icons,
// -- MPD --
/// TCP or Unix socket address.
#[serde(default = "default_socket")]
pub(crate) host: String,
/// Path to root of music directory.
#[serde(default = "default_music_dir")]
pub(crate) music_dir: PathBuf,
/// Whether to show the play/pause status icon
/// on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
pub(crate) show_status_icon: bool,
/// Size to render the icons at, in pixels (image icons only).
///
/// **Default** `32`
#[serde(default = "default_icon_size")]
pub(crate) icon_size: i32,
/// Size to render the album art image at inside the popup, in pixels.
///
/// **Default**: `128`
#[serde(default = "default_cover_image_size")]
pub(crate) cover_image_size: i32,
// -- MPD --
/// *[MPD Only]*
/// TCP or Unix socket address of the MPD server.
/// For TCP, this should include the port number.
///
/// **Default**: `localhost:6600`
#[serde(default = "default_socket")]
pub(crate) host: String,
/// *[MPD Only]*
/// Path to root of the MPD server's music directory.
/// This is required for displaying album art.
///
/// **Default**: `$HOME/Music`
#[serde(default = "default_music_dir")]
pub(crate) music_dir: PathBuf,
// -- Common --
/// See [truncate options](module-level-options#truncate-mode).
///
/// **Default**: `null`
pub(crate) truncate: Option<TruncateMode>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}

View file

@ -22,7 +22,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
pub use self::config::MusicModule;
use self::config::PlayerType;
@ -87,9 +87,7 @@ impl Module<Button> for MusicModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = PlayerCommand;
fn name() -> &'static str {
"music"
}
module_impl!("music");
fn spawn_controller(
&self,
@ -193,6 +191,7 @@ impl Module<Button> for MusicModule {
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
let label = Label::new(None);
label.set_use_markup(true);
label.set_angle(info.bar_position.get_angle());
if let Some(truncate) = self.truncate {
@ -255,7 +254,7 @@ impl Module<Button> for MusicModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -265,6 +264,7 @@ impl Module<Button> for MusicModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;
@ -409,7 +409,7 @@ impl Module<Button> for MusicModule {
// only update art when album changes
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
prev_cover.clone_from(&new_cover);
let res = if let Some(image) = new_cover.and_then(|cover_path| {
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
}) {
@ -545,7 +545,14 @@ impl IconLabel {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label);
let mut builder = Label::builder().use_markup(true);
if let Some(label) = label {
builder = builder.label(label);
}
let label = builder.build();
icon.add_class("icon-box");
label.add_class("label");

View file

@ -1,23 +1,20 @@
use std::collections::HashMap;
use color_eyre::Result;
use futures_lite::StreamExt;
use futures_signals::signal::SignalExt;
use gtk::prelude::ContainerExt;
use gtk::{Image, Orientation};
use gtk::{Box as GtkBox, Image, Orientation};
use serde::Deserialize;
use tokio::sync::mpsc::Receiver;
use zbus::dbus_proxy;
use zbus::names::InterfaceName;
use zbus::zvariant::{ObjectPath, Value};
use crate::clients::networkmanager::{Client, ClientState};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn};
use crate::{glib_recv, module_impl, send_async, spawn};
#[derive(Debug, Deserialize, Clone)]
pub struct NetworkmanagerModule {
pub struct NetworkManagerModule {
#[serde(default = "default_icon_size")]
icon_size: i32,
@ -29,118 +26,24 @@ const fn default_icon_size() -> i32 {
24
}
#[derive(Clone, Debug)]
pub enum NetworkmanagerState {
Cellular,
Offline,
Unknown,
Wired,
Wireless,
WirelessDisconnected,
}
#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager",
default_path = "/org/freedesktop/NetworkManager"
)]
trait NetworkmanagerDBus {
#[dbus_proxy(property)]
fn active_connections(&self) -> Result<Vec<ObjectPath>>;
#[dbus_proxy(property)]
fn devices(&self) -> Result<Vec<ObjectPath>>;
#[dbus_proxy(property)]
fn networking_enabled(&self) -> Result<bool>;
#[dbus_proxy(property)]
fn primary_connection(&self) -> Result<ObjectPath>;
#[dbus_proxy(property)]
fn primary_connection_type(&self) -> Result<String>;
#[dbus_proxy(property)]
fn wireless_enabled(&self) -> Result<bool>;
}
#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.DBus.Properties",
default_path = "/org/freedesktop/NetworkManager"
)]
trait NetworkmanagerPropsDBus {
#[dbus_proxy(signal)]
fn properties_changed(
&self,
interface_name: InterfaceName<'s>,
changed_properties: HashMap<&'s str, Value<'s>>,
invalidated_properties: Vec<&'s str>,
) -> Result<()>;
}
#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager.Connection.Active"
)]
trait ActiveConnectionDBus {
#[dbus_proxy(property)]
fn connection(&self) -> Result<ObjectPath>;
#[dbus_proxy(property)]
fn default(&self) -> Result<bool>;
#[dbus_proxy(property)]
fn default6(&self) -> Result<bool>;
#[dbus_proxy(property)]
fn devices(&self) -> Result<Vec<ObjectPath>>;
#[dbus_proxy(property)]
fn id(&self) -> Result<String>;
#[dbus_proxy(property)]
fn specific_object(&self) -> Result<ObjectPath>;
#[dbus_proxy(property)]
fn type_(&self) -> Result<String>;
#[dbus_proxy(property)]
fn vpn(&self) -> Result<bool>;
}
impl Module<gtk::Box> for NetworkmanagerModule {
type SendMessage = NetworkmanagerState;
impl Module<GtkBox> for NetworkManagerModule {
type SendMessage = ClientState;
type ReceiveMessage = ();
fn name() -> &'static str {
"networkmanager"
}
fn spawn_controller(
&self,
_: &ModuleInfo,
context: &WidgetContext<NetworkmanagerState, ()>,
context: &WidgetContext<ClientState, ()>,
_: Receiver<()>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.try_client::<Client>()?;
let mut client_signal = client.subscribe().to_stream();
let widget_transmitter = context.tx.clone();
spawn(async move {
// TODO: Maybe move this into a `client` à la `upower`?
let dbus = zbus::Connection::system().await?;
let nm_proxy = NetworkmanagerDBusProxy::new(&dbus).await?;
let nm_props_proxy = NetworkmanagerPropsDBusProxy::new(&dbus).await?;
let state = get_network_state(&nm_proxy).await?;
send_async!(tx, ModuleUpdateEvent::Update(state));
let mut prop_changed_stream = nm_props_proxy.receive_properties_changed().await?;
while prop_changed_stream.next().await.is_some() {
let state = get_network_state(&nm_proxy).await?;
send_async!(tx, ModuleUpdateEvent::Update(state));
while let Some(state) = client_signal.next().await {
send_async!(widget_transmitter, ModuleUpdateEvent::Update(state));
}
Result::<()>::Ok(())
});
Ok(())
@ -148,29 +51,30 @@ impl Module<gtk::Box> for NetworkmanagerModule {
fn into_widget(
self,
context: WidgetContext<NetworkmanagerState, ()>,
context: WidgetContext<ClientState, ()>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(Orientation::Horizontal, 0);
) -> Result<ModuleParts<GtkBox>> {
let container = GtkBox::new(Orientation::Horizontal, 0);
let icon = Image::new();
icon.add_class("icon");
container.add(&icon);
let icon_theme = info.icon_theme.clone();
let initial_icon_name = "icon:content-loading-symbolic";
let initial_icon_name = "content-loading-symbolic";
ImageProvider::parse(initial_icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone()));
let rx = context.subscribe();
glib_recv!(rx, state => {
let widget_receiver = context.subscribe();
glib_recv!(widget_receiver, state => {
let icon_name = match state {
NetworkmanagerState::Cellular => "network-cellular-symbolic",
NetworkmanagerState::Offline => "network-wireless-disabled-symbolic",
NetworkmanagerState::Unknown => "dialog-question-symbolic",
NetworkmanagerState::Wired => "network-wired-symbolic",
NetworkmanagerState::Wireless => "network-wireless-symbolic",
NetworkmanagerState::WirelessDisconnected => "network-wireless-acquiring-symbolic",
ClientState::WiredConnected => "network-wired-symbolic",
ClientState::WifiConnected => "network-wireless-symbolic",
ClientState::CellularConnected => "network-cellular-symbolic",
ClientState::VpnConnected => "network-vpn-symbolic",
ClientState::WifiDisconnected => "network-wireless-acquiring-symbolic",
ClientState::Offline => "network-wireless-disabled-symbolic",
ClientState::Unknown => "dialog-question-symbolic",
};
ImageProvider::parse(icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone()));
@ -178,31 +82,6 @@ impl Module<gtk::Box> for NetworkmanagerModule {
Ok(ModuleParts::new(container, None))
}
}
async fn get_network_state(nm_proxy: &NetworkmanagerDBusProxy<'_>) -> Result<NetworkmanagerState> {
let primary_connection_path = nm_proxy.primary_connection().await?;
if primary_connection_path != "/" {
let primary_connection_type = nm_proxy.primary_connection_type().await?;
match primary_connection_type.as_str() {
"802-11-olpc-mesh" => Ok(NetworkmanagerState::Wireless),
"802-11-wireless" => Ok(NetworkmanagerState::Wireless),
"802-3-ethernet" => Ok(NetworkmanagerState::Wired),
"adsl" => Ok(NetworkmanagerState::Wired),
"cdma" => Ok(NetworkmanagerState::Cellular),
"gsm" => Ok(NetworkmanagerState::Cellular),
"pppoe" => Ok(NetworkmanagerState::Wired),
"wifi-p2p" => Ok(NetworkmanagerState::Wireless),
"wimax" => Ok(NetworkmanagerState::Cellular),
"wpan" => Ok(NetworkmanagerState::Wireless),
_ => Ok(NetworkmanagerState::Unknown),
}
} else {
let wireless_enabled = nm_proxy.wireless_enabled().await?;
if wireless_enabled {
Ok(NetworkmanagerState::WirelessDisconnected)
} else {
Ok(NetworkmanagerState::Offline)
}
}
module_impl!("networkmanager");
}

View file

@ -2,7 +2,7 @@ use crate::clients::swaync;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
use gtk::prelude::*;
use gtk::{Align, Button, Label, Overlay};
use serde::Deserialize;
@ -11,28 +11,60 @@ use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct NotificationsModule {
/// Whether to show the current notification count.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_count: bool,
/// SwayNC state icons.
///
/// See [icons](#icons).
#[serde(default)]
icons: Icons,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Clone)]
struct Icons {
/// Icon to show when the panel is closed, with no notifications.
///
/// **Default**: `󰍥`
#[serde(default = "default_icon_closed_none")]
closed_none: String,
/// Icon to show when the panel is closed, with notifications.
///
/// **Default**: `󱥂`
#[serde(default = "default_icon_closed_some")]
closed_some: String,
/// Icon to show when the panel is closed, with DnD enabled.
/// Takes higher priority than count-based icons.
///
/// **Default**: `󱅯`
#[serde(default = "default_icon_closed_dnd")]
closed_dnd: String,
/// Icon to show when the panel is open, with no notifications.
///
/// **Default**: `󰍡`
#[serde(default = "default_icon_open_none")]
open_none: String,
/// Icon to show when the panel is open, with notifications.
///
/// **Default**: `󱥁`
#[serde(default = "default_icon_open_some")]
open_some: String,
/// Icon to show when the panel is open, with DnD enabled.
/// Takes higher priority than count-based icons.
///
/// **Default**: `󱅮`
#[serde(default = "default_icon_open_dnd")]
open_dnd: String,
}
@ -75,7 +107,7 @@ fn default_icon_open_dnd() -> String {
}
impl Icons {
fn icon(&self, value: &swaync::Event) -> &str {
fn icon(&self, value: swaync::Event) -> &str {
match (value.cc_open, value.count > 0, value.dnd) {
(true, _, true) => &self.open_dnd,
(true, true, false) => &self.open_some,
@ -97,9 +129,7 @@ impl Module<Overlay> for NotificationsModule {
type SendMessage = swaync::Event;
type ReceiveMessage = UiEvent;
fn name() -> &'static str {
"notifications"
}
module_impl!("notifications");
fn spawn_controller(
&self,
@ -110,7 +140,7 @@ impl Module<Overlay> for NotificationsModule {
where
<Self as Module<Overlay>>::SendMessage: Clone,
{
let client = context.client::<swaync::Client>();
let client = context.try_client::<swaync::Client>()?;
{
let client = client.clone();
@ -174,7 +204,7 @@ impl Module<Overlay> for NotificationsModule {
let button = button.clone();
glib_recv!(context.subscribe(), ev => {
let icon = self.icons.icon(&ev);
let icon = self.icons.icon(ev);
button.set_label(icon);
label.set_label(&ev.count.to_string());

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::{glib_recv, spawn, try_send};
use crate::{glib_recv, module_impl, spawn, try_send};
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
@ -12,14 +12,29 @@ use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule {
/// Path to script to execute.
///
/// This can be an absolute path,
/// or relative to the working directory.
///
/// **Required**
cmd: String,
/// Script execution mode
/// Script execution mode.
/// See [modes](#modes) for more info.
///
/// **Valid options**: `poll`, `watch`
/// <br />
/// **Default**: `poll`
#[serde(default = "default_mode")]
mode: ScriptMode,
/// Time in milliseconds between executions.
///
/// **Default**: `5000`
#[serde(default = "default_interval")]
interval: u64,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -48,9 +63,7 @@ impl Module<Label> for ScriptModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"script"
}
module_impl!("script");
fn spawn_controller(
&self,

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn};
use crate::{glib_recv, module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
@ -15,28 +15,76 @@ use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)]
pub struct SysInfoModule {
/// List of formatting strings.
/// List of strings including formatting tokens.
/// For available tokens, see [below](#formatting-tokens).
///
/// **Required**
format: Vec<String>,
/// Number of seconds between refresh
/// Number of seconds between refresh.
///
/// This can be set as a global interval,
/// or passed as an object to customize the interval per-system.
///
/// **Default**: `5`
#[serde(default = "Interval::default")]
interval: Interval,
/// The orientation of text for the labels.
///
/// **Valid options**: `horizontal`, `vertical, `h`, `v`
/// <br>
/// **Default** : `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// The orientation by which the labels are laid out.
///
/// **Valid options**: `horizontal`, `vertical, `h`, `v`
/// <br>
/// **Default** : `horizontal`
direction: Option<ModuleOrientation>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Copy, Clone)]
pub struct Intervals {
/// The number of seconds between refreshing memory data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
memory: u64,
/// The number of seconds between refreshing CPU data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
cpu: u64,
/// The number of seconds between refreshing temperature data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
temps: u64,
/// The number of seconds between refreshing disk data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
disks: u64,
/// The number of seconds between refreshing network data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
networks: u64,
/// The number of seconds between refreshing system data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
system: u64,
}
@ -116,9 +164,7 @@ impl Module<gtk::Box> for SysInfoModule {
type SendMessage = HashMap<String, String>;
type ReceiveMessage = ();
fn name() -> &'static str {
"sysinfo"
}
module_impl!("sysinfo");
fn spawn_controller(
&self,
@ -184,11 +230,16 @@ impl Module<gtk::Box> for SysInfoModule {
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
_info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.orientation(), 10);
let layout = match self.direction {
Some(orientation) => orientation,
None => self.orientation,
};
let container = gtk::Box::new(layout.into(), 10);
let mut labels = Vec::new();
@ -196,7 +247,7 @@ impl Module<gtk::Box> for SysInfoModule {
let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item");
label.set_angle(info.bar_position.get_angle());
label.set_angle(self.orientation.to_angle());
container.add(&label);
labels.push(label);

View file

@ -1,9 +1,9 @@
use system_tray::message::menu::{MenuItem as MenuItemInfo, ToggleState};
use system_tray::menu::{MenuItem, ToggleState};
/// Diff change type and associated info.
#[derive(Debug, Clone)]
pub enum Diff {
Add(MenuItemInfo),
Add(MenuItem),
Update(i32, MenuItemDiff),
Remove(i32),
}
@ -12,7 +12,7 @@ pub enum Diff {
#[derive(Debug, Clone)]
pub struct MenuItemDiff {
/// Text of the item,
pub label: Option<String>,
pub label: Option<Option<String>>,
/// Whether the item can be activated or not.
pub enabled: Option<bool>,
/// True if the item is visible in the menu.
@ -29,7 +29,7 @@ pub struct MenuItemDiff {
}
impl MenuItemDiff {
fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self {
fn new(old: &MenuItem, new: &MenuItem) -> Self {
macro_rules! diff {
($field:ident) => {
if old.$field == new.$field {
@ -70,7 +70,7 @@ impl MenuItemDiff {
}
/// Gets a diff set between old and new state.
pub fn get_diffs(old: &[MenuItemInfo], new: &[MenuItemInfo]) -> Vec<Diff> {
pub fn get_diffs(old: &[MenuItem], new: &[MenuItem]) -> Vec<Diff> {
let mut diffs = vec![];
for new_item in new {

View file

@ -1,14 +1,16 @@
use crate::image::ImageProvider;
use crate::modules::tray::interface::TrayMenu;
use color_eyre::{Report, Result};
use glib::ffi::g_strfreev;
use glib::translate::ToGlibPtr;
use gtk::ffi::gtk_icon_theme_get_search_path;
use gtk::gdk_pixbuf::{Colorspace, InterpType};
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
use gtk::prelude::IconThemeExt;
use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image};
use gtk::{IconLookupFlags, IconTheme, Image};
use std::collections::HashSet;
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
use std::ptr;
use system_tray::message::tray::StatusNotifierItem;
/// Gets the GTK icon theme search paths by calling the FFI function.
/// Conveniently returns the result as a `HashSet`.
@ -36,40 +38,72 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
paths
}
pub fn get_image(
item: &TrayMenu,
icon_theme: &IconTheme,
size: u32,
prefer_icons: bool,
) -> Result<Image> {
if !prefer_icons && item.icon_pixmap.is_some() {
get_image_from_pixmap(item, size)
} else {
get_image_from_icon_name(item, icon_theme, size)
.or_else(|_| get_image_from_pixmap(item, size))
}
}
/// Attempts to get a GTK `Image` component
/// for the status notifier item's icon.
pub(crate) fn get_image_from_icon_name(
item: &StatusNotifierItem,
icon_theme: &IconTheme,
) -> Option<Image> {
fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
if let Some(path) = item.icon_theme_path.as_ref() {
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
icon_theme.append_search_path(path);
}
}
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
})
let icon_info = item.icon_name.as_ref().and_then(|icon_name| {
icon_theme.lookup_icon(icon_name, size as i32, IconLookupFlags::empty())
});
if let Some(icon_info) = icon_info {
let pixbuf = icon_info.load_icon()?;
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
Ok(image)
} else {
Err(Report::msg("could not find icon"))
}
}
/// Attempts to get an image from the item pixmap.
///
/// The pixmap is supplied in ARGB32 format,
/// which has 8 bits per sample and a bit stride of `4*width`.
pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image> {
/// The Pixbuf expects RGBA32 format, so some channel shuffling
/// is required.
fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
const BITS_PER_SAMPLE: i32 = 8;
let pixmap = item
.icon_pixmap
.as_ref()
.and_then(|pixmap| pixmap.first())?;
.and_then(|pixmap| pixmap.first())
.ok_or_else(|| Report::msg("Failed to get pixmap from tray icon"))?;
let bytes = glib::Bytes::from(&pixmap.pixels);
let row_stride = pixmap.width * 4; //
let mut pixels = pixmap.pixels.to_vec();
let pixbuf = gdk_pixbuf::Pixbuf::from_bytes(
for i in (0..pixels.len()).step_by(4) {
let alpha = pixels[i];
pixels[i] = pixels[i + 1];
pixels[i + 1] = pixels[i + 2];
pixels[i + 2] = pixels[i + 3];
pixels[i + 3] = alpha;
}
let row_stride = pixmap.width * 4;
let bytes = glib::Bytes::from(&pixels);
let pixbuf = Pixbuf::from_bytes(
&bytes,
Colorspace::Rgb,
true,
@ -80,7 +114,10 @@ pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image>
);
let pixbuf = pixbuf
.scale_simple(16, 16, InterpType::Bilinear)
.scale_simple(size as i32, size as i32, InterpType::Bilinear)
.unwrap_or(pixbuf);
Some(Image::from_pixbuf(Some(&pixbuf)))
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
Ok(image)
}

View file

@ -1,10 +1,12 @@
use crate::modules::tray::diff::{Diff, MenuItemDiff};
use super::diff::{Diff, MenuItemDiff};
use crate::{spawn, try_send};
use glib::Propagation;
use gtk::prelude::*;
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
use std::collections::HashMap;
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
use system_tray::message::NotifierItemCommand;
use system_tray::client::ActivateRequest;
use system_tray::item::{IconPixmap, StatusNotifierItem};
use system_tray::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
use tokio::sync::mpsc;
/// Calls a method on the underlying widget,
@ -49,37 +51,47 @@ macro_rules! call {
/// Main tray icon to show on the bar
pub(crate) struct TrayMenu {
pub(crate) widget: MenuItem,
pub widget: MenuItem,
menu_widget: Menu,
image_widget: Option<Image>,
label_widget: Option<Label>,
menu: HashMap<i32, TrayMenuItem>,
state: Vec<MenuItemInfo>,
icon_name: Option<String>,
pub title: Option<String>,
pub icon_name: Option<String>,
pub icon_theme_path: Option<String>,
pub icon_pixmap: Option<Vec<IconPixmap>>,
tx: mpsc::Sender<i32>,
}
impl TrayMenu {
pub fn new(tx: mpsc::Sender<NotifierItemCommand>, address: String, path: String) -> Self {
pub fn new(
tx: mpsc::Sender<ActivateRequest>,
address: String,
item: StatusNotifierItem,
) -> Self {
let widget = MenuItem::new();
widget.style_context().add_class("item");
let (item_tx, mut item_rx) = mpsc::channel(8);
spawn(async move {
while let Some(id) = item_rx.recv().await {
try_send!(
tx,
NotifierItemCommand::MenuItemClicked {
submenu_id: id,
menu_path: path.clone(),
notifier_address: address.clone(),
}
);
}
});
if let Some(menu) = item.menu {
spawn(async move {
while let Some(id) = item_rx.recv().await {
try_send!(
tx,
ActivateRequest {
submenu_id: id,
menu_path: menu.clone(),
address: address.clone(),
}
);
}
});
}
let menu = Menu::new();
widget.set_submenu(Some(&menu));
@ -90,7 +102,10 @@ impl TrayMenu {
image_widget: None,
label_widget: None,
state: vec![],
icon_name: None,
title: item.title,
icon_name: item.icon_name,
icon_theme_path: item.icon_theme_path,
icon_pixmap: item.icon_pixmap,
menu: HashMap::new(),
tx: item_tx,
}
@ -112,6 +127,18 @@ impl TrayMenu {
.set_label(text);
}
/// Shows the label, using its current text.
/// The image is hidden if present.
pub fn show_label(&self) {
if let Some(image) = &self.image_widget {
image.hide();
}
if let Some(label) = &self.label_widget {
label.show();
}
}
/// Updates the image, and shows it in favour of the label.
pub fn set_image(&mut self, image: &Image) {
if let Some(label) = &self.label_widget {
@ -134,6 +161,7 @@ impl TrayMenu {
let item = TrayMenuItem::new(&info, self.tx.clone());
call!(self.menu_widget, add, item.widget);
self.menu.insert(item.id, item);
// self.widget.show_all();
}
Diff::Update(id, info) => {
if let Some(item) = self.menu.get_mut(&id) {
@ -188,36 +216,61 @@ enum TrayMenuWidget {
impl TrayMenuItem {
fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self {
let mut submenu = HashMap::new();
let menu = Menu::new();
macro_rules! add_submenu {
($menu:expr, $widget:expr) => {
if !info.submenu.is_empty() {
for sub_item in &info.submenu {
let sub_item = TrayMenuItem::new(sub_item, tx.clone());
call!($menu, add, sub_item.widget);
submenu.insert(sub_item.id, sub_item);
}
$widget.set_submenu(Some(&menu));
}
};
}
let widget = match (info.menu_type, info.toggle_type) {
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
(MenuType::Standard, ToggleType::Checkmark) => {
let widget = CheckMenuItem::builder()
.label(info.label.as_str())
.visible(info.visible)
.sensitive(info.enabled)
.active(info.toggle_state == ToggleState::On)
.build();
if let Some(label) = &info.label {
widget.set_label(label);
}
add_submenu!(menu, widget);
{
let tx = tx.clone();
let id = info.id;
widget.connect_activate(move |_item| {
widget.connect_button_press_event(move |_item, _button| {
try_send!(tx, id);
Propagation::Proceed
});
}
TrayMenuWidget::Checkbox(widget)
}
(MenuType::Standard, _) => {
let builder = MenuItem::builder()
.label(&info.label)
let widget = MenuItem::builder()
.visible(info.visible)
.sensitive(info.enabled);
.sensitive(info.enabled)
.build();
let widget = builder.build();
if let Some(label) = &info.label {
widget.set_label(label);
}
add_submenu!(menu, widget);
{
let tx = tx.clone();
@ -236,7 +289,7 @@ impl TrayMenuItem {
id: info.id,
widget,
menu_widget: menu,
submenu: HashMap::new(),
submenu,
tx,
}
}
@ -247,6 +300,7 @@ impl TrayMenuItem {
/// applying the submenu diffs to any further submenu items.
fn apply_diff(&mut self, diff: MenuItemDiff) {
if let Some(label) = diff.label {
let label = label.unwrap_or_default();
match &self.widget {
TrayMenuWidget::Separator(widget) => widget.set_label(&label),
TrayMenuWidget::Standard(widget) => widget.set_label(&label),

View file

@ -2,28 +2,54 @@ mod diff;
mod icon;
mod interface;
use crate::clients::system_tray::TrayEventReceiver;
use crate::clients::tray;
use crate::config::CommonConfig;
use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, spawn};
use color_eyre::Result;
use crate::{glib_recv, lock, module_impl, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
use interface::TrayMenu;
use serde::Deserialize;
use std::collections::HashMap;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::client::Event;
use system_tray::client::{ActivateRequest, UpdateEvent};
use tokio::sync::mpsc;
use tracing::{debug, error, warn};
#[derive(Debug, Deserialize, Clone)]
pub struct TrayModule {
/// Requests that icons from the theme be used over the item-provided item.
/// Most items only provide one or the other so this will have no effect in most circumstances.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
prefer_theme_icons: bool,
/// Size in pixels to display the tray icons as.
///
/// **Default**: `16`
#[serde(default = "default_icon_size")]
icon_size: u32,
/// Direction to display the tray items.
///
/// **Valid options**: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left`
/// <br>
/// **Default**: `left_to_right` if bar is horizontal, `top_to_bottom` if bar is vertical
#[serde(default, deserialize_with = "deserialize_orientation")]
pub direction: Option<PackDirection>,
direction: Option<PackDirection>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_icon_size() -> u32 {
16
}
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
where
D: serde::Deserializer<'de>,
@ -41,12 +67,10 @@ where
}
impl Module<MenuBar> for TrayModule {
type SendMessage = NotifierItemMessage;
type ReceiveMessage = NotifierItemCommand;
type SendMessage = Event;
type ReceiveMessage = ActivateRequest;
fn name() -> &'static str {
"tray"
}
module_impl!("tray");
fn spawn_controller(
&self,
@ -56,26 +80,39 @@ impl Module<MenuBar> for TrayModule {
) -> Result<()> {
let tx = context.tx.clone();
let client = context.client::<TrayEventReceiver>();
let client = context.try_client::<tray::Client>()?;
let mut tray_rx = client.subscribe();
let (tray_tx, mut tray_rx) = client.subscribe();
let initial_items = lock!(client.items()).clone();
// listen to tray updates
spawn(async move {
while let Ok(message) = tray_rx.recv().await {
tx.send(ModuleUpdateEvent::Update(message)).await?;
for (key, (item, menu)) in initial_items {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
);
if let Some(menu) = menu.clone() {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
);
}
}
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
while let Ok(message) = tray_rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(message));
}
});
// send tray commands
spawn(async move {
while let Some(cmd) = rx.recv().await {
tray_tx.send(cmd).await?;
client.activate(cmd).await?;
}
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
Ok::<_, Report>(())
});
Ok(())
@ -106,7 +143,7 @@ impl Module<MenuBar> for TrayModule {
// listen for UI updates
glib_recv!(context.subscribe(), update =>
on_update(update, &container, &mut menus, &icon_theme, &context.controller_tx)
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons, &context.controller_tx)
);
};
@ -120,53 +157,80 @@ impl Module<MenuBar> for TrayModule {
/// Handles UI updates as callback,
/// getting the diff since the previous update and applying it to the menu.
fn on_update(
update: NotifierItemMessage,
update: Event,
container: &MenuBar,
menus: &mut HashMap<Box<str>, TrayMenu>,
icon_theme: &IconTheme,
tx: &mpsc::Sender<NotifierItemCommand>,
icon_size: u32,
prefer_icons: bool,
tx: &mpsc::Sender<ActivateRequest>,
) {
match update {
NotifierItemMessage::Update {
item,
address,
menu,
} => {
if let (Some(menu_opts), Some(menu_path)) = (menu, &item.menu) {
let submenus = menu_opts.submenus;
Event::Add(address, item) => {
debug!("Received new tray item at '{address}': {item:?}");
let mut menu_item = menus.remove(address.as_str()).unwrap_or_else(|| {
let item = TrayMenu::new(tx.clone(), address.clone(), menu_path.to_string());
container.add(&item.widget);
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
container.add(&menu_item.widget);
item
});
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
menu_item.set_image(&image);
} else {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label);
};
let label = item.title.as_ref().unwrap_or(&address);
if let Some(label_widget) = menu_item.label_widget() {
label_widget.set_label(label);
menu_item.widget.show();
menus.insert(address.into(), menu_item);
}
Event::Update(address, update) => {
debug!("Received tray update for '{address}': {update:?}");
let Some(menu_item) = menus.get_mut(address.as_str()) else {
error!("Attempted to update menu at '{address}' but could not find it");
return;
};
match update {
UpdateEvent::AttentionIcon(_icon) => {
warn!("received unimplemented NewAttentionIcon event");
}
UpdateEvent::Icon(icon) => {
if icon.as_ref() != menu_item.icon_name() {
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => menu_item.show_label(),
};
}
if item.icon_name.as_ref() != menu_item.icon_name() {
match icon::get_image_from_icon_name(&item, icon_theme)
.or_else(|| icon::get_image_from_pixmap(&item))
{
Some(image) => menu_item.set_image(&image),
None => menu_item.set_label(label),
};
menu_item.set_icon_name(icon);
}
UpdateEvent::OverlayIcon(_icon) => {
warn!("received unimplemented NewOverlayIcon event");
}
UpdateEvent::Status(_status) => {
warn!("received unimplemented NewStatus event");
}
UpdateEvent::Title(title) => {
if let Some(label_widget) = menu_item.label_widget() {
label_widget.set_label(&title.unwrap_or_default());
}
}
// UpdateEvent::Tooltip(_tooltip) => {
// warn!("received unimplemented NewAttentionIcon event");
// }
UpdateEvent::Menu(menu) => {
debug!("received new menu for '{}'", address);
let diffs = get_diffs(menu_item.state(), &submenus);
menu_item.apply_diffs(diffs);
menu_item.widget.show();
let diffs = get_diffs(menu_item.state(), &menu.submenus);
menu_item.set_state(submenus);
menu_item.set_icon_name(item.icon_name);
menus.insert(address.into(), menu_item);
menu_item.apply_diffs(diffs);
menu_item.set_state(menu.submenus);
}
}
}
NotifierItemMessage::Remove { address } => {
Event::Remove(address) => {
debug!("Removing tray item at '{address}'");
if let Some(menu) = menus.get(address.as_str()) {
container.remove(&menu.widget);
}

View file

@ -15,7 +15,7 @@ use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
@ -23,12 +23,20 @@ const MINUTE: i64 = 60;
#[derive(Debug, Deserialize, Clone)]
pub struct UpowerModule {
/// The format string to use for the widget button label.
/// For available tokens, see [below](#formatting-tokens).
///
/// **Default**: `{percentage}%`
#[serde(default = "default_format")]
format: String,
/// The size to render the icon at, in pixels.
///
/// **Default**: `24`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -54,9 +62,7 @@ impl Module<gtk::Button> for UpowerModule {
type SendMessage = UpowerProperties;
type ReceiveMessage = ();
fn name() -> &'static str {
"upower"
}
module_impl!("upower");
fn spawn_controller(
&self,
@ -183,7 +189,6 @@ impl Module<gtk::Button> for UpowerModule {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
label.set_angle(info.bar_position.get_angle());
let format = self.format.clone();
let rx = context.subscribe();
@ -199,7 +204,9 @@ impl Module<gtk::Button> for UpowerModule {
let format = format.replace("{percentage}", &properties.percentage.to_string())
.replace("{time_remaining}", &time_remaining)
.replace("{state}", battery_state_to_string(state));
let icon_name = String::from("icon:") + &properties.icon_name;
let mut icon_name = String::from("icon:");
icon_name.push_str(&properties.icon_name);
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone()));
@ -209,7 +216,7 @@ impl Module<gtk::Button> for UpowerModule {
let rx = context.subscribe();
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -219,6 +226,7 @@ impl Module<gtk::Button> for UpowerModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where

View file

@ -1,88 +1,45 @@
use crate::clients::volume::{self, Event};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, lock, send_async, spawn, try_send};
use crate::{glib_recv, lock, module_impl, send_async, spawn, try_send};
use glib::Propagation;
use gtk::pango::EllipsizeMode;
use gtk::prelude::*;
use gtk::{Button, CellRendererText, ComboBoxText, Label, Orientation, Scale, ToggleButton};
use gtk::{
Box as GtkBox, Button, CellRendererText, ComboBoxText, Image, Label, Orientation, Scale,
ToggleButton,
};
use serde::Deserialize;
use std::collections::HashMap;
use tokio::sync::mpsc;
#[derive(Debug, Clone, Deserialize)]
pub struct VolumeModule {
#[serde(default = "default_format")]
format: String,
/// Maximum value to allow volume sliders to reach.
/// Pulse supports values > 100 but this may result in distortion.
///
/// **Default**: `100`
#[serde(default = "default_max_volume")]
max_volume: f64,
#[serde(default)]
icons: Icons,
#[serde(default = "default_icon_size")]
icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_format() -> String {
String::from("{icon} {percentage}%")
}
#[derive(Debug, Clone, Deserialize)]
pub struct Icons {
#[serde(default = "default_icon_volume_high")]
volume_high: String,
#[serde(default = "default_icon_volume_medium")]
volume_medium: String,
#[serde(default = "default_icon_volume_low")]
volume_low: String,
#[serde(default = "default_icon_muted")]
muted: String,
}
impl Icons {
fn volume_icon(&self, volume_percent: f64) -> &str {
match volume_percent as u32 {
0..=33 => &self.volume_low,
34..=66 => &self.volume_medium,
67.. => &self.volume_high,
}
}
}
impl Default for Icons {
fn default() -> Self {
Self {
volume_high: default_icon_volume_high(),
volume_medium: default_icon_volume_medium(),
volume_low: default_icon_volume_low(),
muted: default_icon_muted(),
}
}
}
const fn default_max_volume() -> f64 {
100.0
}
fn default_icon_volume_high() -> String {
String::from("󰕾")
}
fn default_icon_volume_medium() -> String {
String::from("󰖀")
}
fn default_icon_volume_low() -> String {
String::from("󰕿")
}
fn default_icon_muted() -> String {
String::from("󰝟")
const fn default_icon_size() -> i32 {
24
}
#[derive(Debug, Clone)]
@ -99,9 +56,7 @@ impl Module<Button> for VolumeModule {
type SendMessage = Event;
type ReceiveMessage = Update;
fn name() -> &'static str {
"volume"
}
module_impl!("volume");
fn spawn_controller(
&self,
@ -187,28 +142,34 @@ impl Module<Button> for VolumeModule {
{
let rx = context.subscribe();
let icons = self.icons.clone();
let button = button.clone();
let icon_theme = info.icon_theme.clone();
let format = self.format.clone();
let image_icon = Image::new();
image_icon.add_class("icon");
button.set_image(Some(&image_icon));
glib_recv!(rx, event => {
match event {
Event::AddSink(sink) | Event::UpdateSink(sink) if sink.active => {
let label = format
.replace("{icon}", if sink.muted { &icons.muted } else { icons.volume_icon(sink.volume) })
.replace("{percentage}", &sink.volume.to_string())
.replace("{name}", &sink.description);
button.set_label(&label);
ImageProvider::parse(
&determine_volume_icon(sink.muted, sink.volume),
&icon_theme,
false,
self.icon_size,
).map(|provider| provider.load_into_image(image_icon.clone()));
},
_ => {}
_ => {},
}
});
}
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup(
context.controller_tx.clone(),
context.subscribe(),
context,
info,
)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
@ -218,17 +179,18 @@ impl Module<Button> for VolumeModule {
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Option<GtkBox>
where
Self: Sized,
{
let container = gtk::Box::new(Orientation::Horizontal, 10);
let container = GtkBox::new(Orientation::Horizontal, 10);
let sink_container = gtk::Box::new(Orientation::Vertical, 5);
let sink_container = GtkBox::new(Orientation::Vertical, 5);
sink_container.add_class("device-box");
let input_container = gtk::Box::new(Orientation::Vertical, 5);
let input_container = GtkBox::new(Orientation::Vertical, 5);
input_container.add_class("apps-box");
container.add(&sink_container);
@ -288,6 +250,8 @@ impl Module<Button> for VolumeModule {
let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute");
let btn_mute_icon = Image::new();
btn_mute.set_image(Some(&btn_mute_icon));
sink_container.add(&btn_mute);
{
@ -307,6 +271,7 @@ impl Module<Button> for VolumeModule {
let mut inputs = HashMap::new();
{
let icon_theme = info.icon_theme.clone();
let input_container = input_container.clone();
let mut sinks = vec![];
@ -321,7 +286,12 @@ impl Module<Button> for VolumeModule {
slider.set_value(info.volume);
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
ImageProvider::parse(
&determine_volume_icon(info.muted, info.volume),
&icon_theme,
false,
self.icon_size,
).map(|provider| provider.load_into_image(btn_mute_icon.clone()));
}
sinks.push(info);
@ -333,7 +303,12 @@ impl Module<Button> for VolumeModule {
slider.set_value(info.volume);
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
ImageProvider::parse(
&determine_volume_icon(info.muted, info.volume),
&icon_theme,
false,
self.icon_size,
).map(|provider| provider.load_into_image(btn_mute_icon.clone()));
}
}
}
@ -347,7 +322,7 @@ impl Module<Button> for VolumeModule {
Event::AddInput(info) => {
let index = info.index;
let item_container = gtk::Box::new(Orientation::Vertical, 0);
let item_container = GtkBox::new(Orientation::Vertical, 0);
item_container.add_class("app-box");
let label = Label::new(Some(&info.name));
@ -371,9 +346,16 @@ impl Module<Button> for VolumeModule {
let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute");
let btn_mute_icon = Image::new();
btn_mute.set_image(Some(&btn_mute_icon));
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
ImageProvider::parse(
&determine_volume_icon(info.muted, info.volume),
&icon_theme,
false,
self.icon_size,
).map(|provider| provider.load_into_image(btn_mute_icon.clone()));
{
let tx = tx.clone();
@ -394,7 +376,7 @@ impl Module<Button> for VolumeModule {
container: item_container,
label,
slider,
btn_mute
btn_mute_icon,
});
}
Event::UpdateInput(info) => {
@ -402,7 +384,12 @@ impl Module<Button> for VolumeModule {
ui.label.set_label(&info.name);
ui.slider.set_value(info.volume);
ui.slider.set_sensitive(info.can_set_volume);
ui.btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
ImageProvider::parse(
&determine_volume_icon(info.muted, info.volume),
&icon_theme,
false,
self.icon_size,
).map(|provider| provider.load_into_image(ui.btn_mute_icon.clone()));
}
}
Event::RemoveInput(index) => {
@ -419,8 +406,21 @@ impl Module<Button> for VolumeModule {
}
struct InputUi {
container: gtk::Box,
container: GtkBox,
label: Label,
slider: Scale,
btn_mute: ToggleButton,
btn_mute_icon: Image,
}
fn determine_volume_icon(muted: bool, volume: f64) -> String {
let icon_variant = if muted {
"muted"
} else if volume <= 33.3333 {
"low"
} else if volume <= 66.6667 {
"medium"
} else {
"high"
};
format!("audio-volume-{icon_variant}-symbolic")
}

View file

@ -1,8 +1,9 @@
use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use crate::{glib_recv, module_impl, send_async, spawn, try_send, Ironbar};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme};
@ -44,26 +45,69 @@ impl Default for Favorites {
#[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule {
/// Map of actual workspace names to custom names.
///
/// Custom names can be [images](images).
///
/// If a workspace is not present in the map,
/// it will fall back to using its actual name.
name_map: Option<HashMap<String, String>>,
/// Array of always shown workspaces, and what monitor to show on
/// Workspaces which should always be shown.
/// This can either be an array of workspace names,
/// or a map of monitor names to arrays of workspace names.
///
/// **Default**: `{}`
///
/// # Example
///
/// ```corn
/// // array format
/// {
/// type = "workspaces"
/// favorites = ["1", "2", "3"]
/// }
///
/// // map format
/// {
/// type = "workspaces"
/// favorites.DP-1 = ["1", "2", "3"]
/// favorites.DP-2 = ["4", "5", "6"]
/// }
/// ```
#[serde(default)]
favorites: Favorites,
/// List of workspace names to never show
/// A list of workspace names to never show.
///
/// This may be useful for scratchpad/special workspaces, for example.
///
/// **Default**: `[]`
#[serde(default)]
hidden: Vec<String>,
/// Whether to display buttons for all monitors.
/// Whether to display workspaces from all monitors.
/// When false, only shows workspaces on the current monitor.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
/// The method used for sorting workspaces.
/// `added` always appends to the end, `alphanumeric` sorts by number/name.
///
/// **Valid options**: `added`, `alphanumeric`
/// <br>
/// **Default**: `alphanumeric`
#[serde(default)]
sort: SortOrder,
/// The size to render icons at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -133,6 +177,15 @@ fn reorder_workspaces(container: &gtk::Box) {
}
}
fn find_btn(map: &HashMap<i64, Button>, workspace: &Workspace) -> Option<Button> {
map.get(&workspace.id)
.or_else(|| {
map.values()
.find(|btn| btn.label().unwrap_or_default() == workspace.name)
})
.cloned()
}
impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
@ -144,9 +197,7 @@ impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
fn name() -> &'static str {
"workspaces"
}
module_impl!("workspaces");
fn spawn_controller(
&self,
@ -155,7 +206,7 @@ impl Module<gtk::Box> for WorkspacesModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces();
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
@ -168,7 +219,7 @@ impl Module<gtk::Box> for WorkspacesModule {
}
});
let client = context.client::<dyn WorkspaceClient>();
let client = context.try_client::<dyn WorkspaceClient>()?;
// Change workspace focus
spawn(async move {
@ -195,7 +246,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![];
let mut button_map: HashMap<String, Button> = HashMap::new();
let mut button_map: HashMap<i64, Button> = HashMap::new();
{
let container = container.clone();
@ -215,7 +266,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let mut added = HashSet::new();
let mut add_workspace = |name: &str, visibility: Visibility| {
let mut add_workspace = |id: i64, name: &str, visibility: Visibility| {
let item = create_button(
name,
visibility,
@ -226,13 +277,13 @@ impl Module<gtk::Box> for WorkspacesModule {
);
container.add(&item);
button_map.insert(name.to_string(), item);
button_map.insert(id, item);
};
// add workspaces from client
for workspace in &workspaces {
if self.show_workspace_check(&output_name, workspace) {
add_workspace(&workspace.name, workspace.visibility);
add_workspace(workspace.id, &workspace.name, workspace.visibility);
added.insert(workspace.name.to_string());
}
}
@ -242,7 +293,11 @@ impl Module<gtk::Box> for WorkspacesModule {
fav_names.push(name.to_string());
if !added.contains(name) {
add_workspace(name, Visibility::Hidden);
// Favourites are added with the same name and ID
// as Hyprland will initialize them this way.
// Since existing workspaces are added above,
// this means there shouldn't be any issues with renaming.
add_workspace(-(Ironbar::unique_id() as i64), name, Visibility::Hidden);
added.insert(name.to_string());
}
}
@ -267,25 +322,28 @@ impl Module<gtk::Box> for WorkspacesModule {
}
}
WorkspaceUpdate::Focus { old, new } => {
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) {
if Some(new.monitor) == old.map(|w| w.monitor) {
if let Some(btn) = old.as_ref().and_then(|w| find_btn(&button_map, w)) {
if Some(new.monitor.as_str()) == old.as_ref().map(|w| w.monitor.as_str()) {
btn.style_context().remove_class("visible");
}
btn.style_context().remove_class("focused");
}
let new = button_map.get(&new.name);
if let Some(btn) = new {
let style = btn.style_context();
style.add_class("visible");
style.add_class("focused");
if let Some(btn) = find_btn(&button_map, &new) {
btn.add_class("visible");
btn.add_class("focused");
}
}
WorkspaceUpdate::Rename { id, name } => {
if let Some(btn) = button_map.get(&id) {
let name = name_map.get(&name).unwrap_or(&name);
btn.set_label(name);
}
}
WorkspaceUpdate::Add(workspace) => {
if fav_names.contains(&workspace.name) {
let btn = button_map.get(&workspace.name);
let btn = button_map.get(&workspace.id);
if let Some(btn) = btn {
btn.style_context().remove_class("inactive");
}
@ -308,7 +366,7 @@ impl Module<gtk::Box> for WorkspacesModule {
item.show();
if !name.is_empty() {
button_map.insert(name, item);
button_map.insert(workspace.id, item);
}
}
}
@ -334,9 +392,9 @@ impl Module<gtk::Box> for WorkspacesModule {
item.show();
if !name.is_empty() {
button_map.insert(name, item);
button_map.insert(workspace.id, item);
}
} else if let Some(item) = button_map.get(&workspace.name) {
} else if let Some(item) = button_map.get(&workspace.id) {
container.remove(item);
}
}
@ -344,7 +402,8 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace);
if let Some(item) = button {
if fav_names.contains(&workspace) {
if workspace < 0 {
// if fav_names.contains(&workspace) {
item.style_context().add_class("inactive");
} else {
container.remove(item);

View file

@ -12,7 +12,7 @@ use tracing::{debug, trace};
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::Ironbar;
use crate::rc_mut;
#[derive(Debug, Clone)]
pub struct PopupCacheValue {
@ -23,7 +23,8 @@ pub struct PopupCacheValue {
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub container_cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
pub button_cache: Rc<RefCell<Vec<Button>>>,
monitor: Monitor,
pos: BarPosition,
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
@ -106,10 +107,11 @@ impl Popup {
Self {
window: win,
cache: Rc::new(RefCell::new(HashMap::new())),
container_cache: rc_mut!(HashMap::new()),
button_cache: rc_mut!(vec![]),
monitor: module_info.monitor.clone(),
pos,
current_widget: Rc::new(RefCell::new(None)),
current_widget: rc_mut!(None),
}
}
@ -117,8 +119,7 @@ impl Popup {
debug!("Registered popup content for #{}", key);
for button in &content.buttons {
let id = Ironbar::unique_id();
button.set_tag("popup-id", id);
button.ensure_popup_id();
}
let orientation = self.pos.orientation();
@ -126,7 +127,8 @@ impl Popup {
let window = self.window.clone();
let current_widget = self.current_widget.clone();
let cache = self.cache.clone();
let cache = self.container_cache.clone();
let button_cache = self.button_cache.clone();
content
.container
@ -135,11 +137,9 @@ impl Popup {
trace!("Resized: {}x{}", rect.width(), rect.height());
if let Some((widget_id, button_id)) = *current_widget.borrow() {
if let Some(PopupCacheValue { content, .. }) =
cache.borrow().get(&widget_id)
{
if let Some(PopupCacheValue { .. }) = cache.borrow().get(&widget_id) {
Self::set_position(
&content.buttons,
&button_cache.borrow(),
button_id,
orientation,
&monitor,
@ -150,7 +150,11 @@ impl Popup {
}
});
self.cache
self.button_cache
.borrow_mut()
.append(&mut content.buttons.clone());
self.container_cache
.borrow_mut()
.insert(key, PopupCacheValue { name, content });
}
@ -158,16 +162,17 @@ impl Popup {
pub fn show(&self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
*self.current_widget.borrow_mut() = Some((widget_id, button_id));
content.container.style_context().add_class("popup");
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
Self::set_position(
&content.buttons,
&self.button_cache.borrow(),
button_id,
self.pos.orientation(),
&self.monitor,
@ -179,8 +184,9 @@ impl Popup {
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
content.container.style_context().add_class("popup");
if let Some(PopupCacheValue { content, .. }) = self.container_cache.borrow().get(&widget_id)
{
content.container.add_class("popup");
self.window.add(&content.container);
self.window.show();
@ -216,7 +222,7 @@ impl Popup {
}
}
/// Hides the popover
/// Hides the popup
pub fn hide(&self) {
*self.current_widget.borrow_mut() = None;
self.window.hide();

View file

@ -0,0 +1,192 @@
{
position = "bottom"
start = [
{
type = "clock"
format = "%H:%M"
}
{
type = "clock"
format = "%H:%M"
orientation = "horizontal"
}
{
type = "clock"
format = "%H:%M"
orientation = "h"
}
{
type = "clock"
format = "%H:%M"
orientation = "vertical"
}
{
type = "clock"
format = "%H:%M"
orientation = "v"
}
{
type = "custom"
bar = [
{
type = "label"
label = "label"
}
{
type = "label"
label = "label"
orientation = "horizontal"
}
{
type = "label"
label = "label"
orientation = "h"
}
{
type = "label"
label = "label"
orientation = "vertical"
}
{
type = "label"
label = "label"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "button"
label = "label"
}
{
type = "button"
label = "label"
orientation = "horizontal"
}
{
type = "button"
label = "label"
orientation = "h"
}
{
type = "button"
label = "label"
orientation = "vertical"
}
{
type = "button"
label = "label"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "progress"
value = "echo 50"
}
{
type = "progress"
value = "echo 50"
orientation = "horizontal"
}
{
type = "progress"
value = "echo 50"
orientation = "h"
}
{
type = "progress"
value = "echo 50"
orientation = "vertical"
}
{
type = "progress"
value = "echo 50"
orientation = "v"
}
]
}
{
type = "custom"
bar = [
{
type = "slider"
value = "echo 50"
length = 100
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "horizontal"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "h"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "vertical"
}
{
type = "slider"
value = "echo 50"
length = 100
orientation = "v"
}
]
}
{
type = "sys_info"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
{
type = "sys_info"
orientation = "vertical"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
{
type = "sys_info"
layout = "vertical"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}%"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"󰥔 {uptime}"
]
}
]
}

View file

@ -0,0 +1,3 @@
{
start = [ { type = "workspaces" }]
}