1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 18:51:04 +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: Cache dependencies
- name: Install build deps - name: Install build deps
run: | run: ./.github/scripts/ubuntu_setup.sh
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- name: Clippy - name: Clippy
run: cargo clippy --no-default-features --features config+json run: cargo clippy --no-default-features --features config+json
@ -53,9 +51,7 @@ jobs:
name: Cache dependencies name: Cache dependencies
- name: Install build deps - name: Install build deps
run: | run: ./.github/scripts/ubuntu_setup.sh
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- name: Clippy - name: Clippy
run: cargo clippy --all-targets --all-features run: cargo clippy --all-targets --all-features
@ -72,9 +68,7 @@ jobs:
name: Cache dependencies name: Cache dependencies
- name: Install build deps - name: Install build deps
run: | run: ./.github/scripts/ubuntu_setup.sh
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose

View file

@ -18,9 +18,7 @@ jobs:
override: true override: true
- name: Install build deps - name: Install build deps
run: | run: ./.github/scripts/ubuntu_setup.sh
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libpulse-dev
- name: Update CHANGELOG - name: Update CHANGELOG
id: changelog id: changelog

1
.gitignore vendored
View file

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

View file

@ -1,15 +1,15 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <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$" /> <option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="false" />
<option name="channel" value="DEFAULT" /> <option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" /> <option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" /> <option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" /> <option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" /> <option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" /> <option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />
<method v="2"> <method v="2">

View file

@ -2,18 +2,17 @@
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package ironbar --bin ironbar" /> <option name="command" value="run --package ironbar --bin ironbar" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" /> <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="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" /> <option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" /> <option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" /> <option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" /> <option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" /> <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="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />
<method v="2"> <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/), 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). 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 ## [v0.14.1] - 2024-02-10
### :bug: Bug Fixes ### :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))* - [`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))* - [`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 ### :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))* - [`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))* - [`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))* - [`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))* - [`18b4784`](https://github.com/JakeStanger/ironbar/commit/18b47844f94067bbf029cf4b6b94153a742d6af1) - **wayland**: simplify task spawning code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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))* - [`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))* - [`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 ### :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))* - [`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))* - [`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))* - [`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))* - [`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 ### :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))* - [`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))* - [`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))* - [`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))* - [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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))* - [`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))* - [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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))* - [`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))* - [`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 ### :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))* - [`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))* - [`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))* - [`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))* - [`0d7ab54`](https://github.com/JakeStanger/ironbar/commit/0d7ab541604691455ed39c73e039ac0635307bc8) - remove redundant clone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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 ### :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))* - [`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))* - [`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))* - [`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))* - [`bc625b9`](https://github.com/JakeStanger/ironbar/commit/bc625b929b8644ce92f275b5d98cdf74b93fe067) - clippy & fmt *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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))* - [`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))* - [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes ### :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))* - [`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))* - [`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))* - [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@ -505,3 +573,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0 [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.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] [package]
name = "ironbar" name = "ironbar"
version = "0.15.0-pre" version = "0.16.0-pre"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar" description = "Customisable GTK Layer Shell wlroots/sway bar"
@ -11,6 +11,7 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features] [features]
default = [ default = [
"cli", "cli",
"cairo",
"clipboard", "clipboard",
"clock", "clock",
"config+all", "config+all",
@ -46,6 +47,8 @@ http = ["dep:reqwest"]
"config+corn" = ["universal-config/corn"] "config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"] "config+ron" = ["universal-config/ron"]
cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["nix"] clipboard = ["nix"]
clock = ["chrono"] clock = ["chrono"]
@ -59,7 +62,7 @@ music = ["regex"]
"music+mpris" = ["music", "mpris"] "music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd-utils"] "music+mpd" = ["music", "mpd-utils"]
networkmanager = ["futures-lite", "zbus"] networkmanager = ["futures-lite", "futures-signals", "zbus"]
notifications = ["zbus"] notifications = ["zbus"]
@ -71,7 +74,7 @@ upower = ["upower_dbus", "zbus", "futures-lite"]
volume = ["libpulse-binding"] volume = ["libpulse-binding"]
workspaces = ["futures-util"] workspaces = ["futures-lite"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"] "workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+hyprland" = ["workspaces", "hyprland"] "workspaces+hyprland" = ["workspaces", "hyprland"]
@ -81,7 +84,7 @@ workspaces = ["futures-util"]
gtk = "0.18.1" gtk = "0.18.1"
gtk-layer-shell = "0.8.0" gtk-layer-shell = "0.8.0"
glib = "0.18.5" glib = "0.18.5"
tokio = { version = "1.36.0", features = [ tokio = { version = "1.37.0", features = [
"macros", "macros",
"rt-multi-thread", "rt-multi-thread",
"time", "time",
@ -92,65 +95,71 @@ tokio = { version = "1.36.0", features = [
] } ] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 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" tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2" color-eyre = "0.6.3"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
indexmap = "2.2.5" indexmap = "2.2.6"
dirs = "5.0.1" dirs = "5.0.1"
walkdir = "2.5.0" walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false } notify = { version = "6.1.1", default-features = false }
wayland-client = "0.31.1" wayland-client = "0.31.1"
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [ smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [
"calloop", "calloop",
] } ] }
universal-config = { version = "0.4.3", default_features = false } universal-config = { version = "0.5.0", default_features = false }
ctrlc = "3.4.2" ctrlc = "3.4.2"
cfg-if = "1.0.0" cfg-if = "1.0.0"
# cli # cli
clap = { version = "4.5.2", optional = true, features = ["derive"] } clap = { version = "4.5.4", optional = true, features = ["derive"] }
# ipc # ipc
serde_json = { version = "1.0.114", optional = true } serde_json = { version = "1.0.117", optional = true }
# http # 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 # clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] } nix = { version = "0.29.0", optional = true, features = ["event", "fs"] }
# clock # 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 # music
mpd-utils = { version = "0.2.0", optional = true } mpd-utils = { version = "0.2.1", optional = true }
mpris = { version = "2.0.1", optional = true } mpris = { version = "2.0.1", optional = true }
# networkmanager
futures-signals = { version = "0.3.33", optional = true }
# sys_info # sys_info
sysinfo = { version = "0.29.11", optional = true } sysinfo = { version = "0.29.11", optional = true }
# tray # tray
system-tray = { version = "0.1.5", optional = true } system-tray = { version = "0.2.0", optional = true }
# upower # upower
upower_dbus = { version = "0.3.2", optional = true } upower_dbus = { version = "0.3.2", optional = true }
# volume # volume
libpulse-binding = { version = "2.28.1", optional = true } libpulse-binding = { version = "2.28.1", optional = true }
# libpulse-glib-binding = { version = "2.27.1", optional = true }
# workspaces # workspaces
swayipc-async = { version = "2.0.1", optional = true } 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 } futures-util = { version = "0.3.30", optional = true }
# shared # 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", "std",
], optional = true } # music, sys_info ], optional = true } # music, sys_info
futures-lite = { version = "2.2.0", optional = true } # networkmanager, upower zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # networkmanager, notifications, upower
zbus = { version = "3.15.2", 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 ## 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 ### Cargo
[crate](https://crates.io/crates/ironbar) [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. CI builds are automatically cached by Garnix.
You can use their binary cache by following the steps [here](https://garnix.io/docs/caching). 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 ### Source
[repo](https://github.com/jakestanger/ironbar) [repo](https://github.com/jakestanger/ironbar)

View file

@ -9,6 +9,8 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar 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 ## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed. 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 pacman -S openssl
# for volume support # for volume support
pacman -S libpulse pacman -S libpulse
# for lua/cairo support
pacman -S luajit lua51-lgi
``` ```
### Ubuntu/Debian ### Ubuntu/Debian
@ -32,6 +36,8 @@ apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
apt install libssl-dev apt install libssl-dev
# for volume support # for volume support
apt install libpulse-dev apt install libpulse-dev
# for lua/cairo support
apt install luajit-dev lua-lgi
``` ```
### Fedora ### Fedora
@ -41,7 +47,9 @@ dnf install gtk3-devel gtk-layer-shell-devel
# for http support # for http support
dnf install openssl-devel dnf install openssl-devel
# for volume support # for volume support
dnf install libpulseaudio-devel dnf install pulseaudio-libs-devel
# for lua/cairo support
dnf install luajit-devel lua-lgi
``` ```
## Features ## 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. 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. 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. As of `v0.15.0`, compiling with no features is about 50% faster.
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all. 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. 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. 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+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). | | config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | | | **Modules** | |
| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. | | clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. | | clock | Enables the `clock` module. |
| focused | Enables the `focused` 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+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. | | music+mpris | Enables the `music` module with MPRIS support. |
| music+mpd | Enables the `music` module with MPD support. | | music+mpd | Enables the `music` module with MPD support. |
| notifications | Enables the `notiications` module. |
| sys_info | Enables the `sys_info` module. | | sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. | | tray | Enables the `tray` module. |
| upower | Enables the `upower` 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) the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples)
and the example pages in the sidebar. 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 ## 1. Create config file
The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`. The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`.
@ -272,7 +274,22 @@ 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: The following table lists each of the top-level bar config options:
| Name | Type | Default | Description | | 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. | | `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. | | `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. | | `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
@ -283,7 +300,6 @@ The following table lists each of the top-level bar config options:
| `margin.left` | `integer` | `0` | The margin on the left 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 | | `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. | | `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. | | `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. | | `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. | | `start` | `Module[]` | `[]` | Array of left or top modules. |
@ -292,6 +308,8 @@ The following table lists each of the top-level bar config options:
### 3.2 Module-level options ### 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. 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. For details on available modules and each of their config options, check the sidebar.
@ -300,7 +318,7 @@ For information on the `Script` type, and embedding scripts in strings, see [her
#### Events #### Events
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------| |-------------------|--------------------|---------|------------------------------------------------------------|
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. | | `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_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_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
@ -311,17 +329,17 @@ For information on the `Script` type, and embedding scripts in strings, see [her
#### Visibility #### Visibility
| Name | Type | Default | Description | | 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. | | `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_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. | | `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 #### Appearance
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-----------|--------------------|---------|-----------------------------------------------------------------------------------| |-----------|----------|---------|-----------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. | | `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`. | | `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`. | | `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |

View file

@ -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` ### `load_css`
Loads an additional CSS stylesheet, with hot-reloading enabled. Loads an additional CSS stylesheet, with hot-reloading enabled.

View file

@ -20,9 +20,11 @@
## Custom ## Custom
- [Power Menu](power-menu) - [Power Menu](power-menu)
- [Weather](weather)
# Modules # Modules
- [Cairo](cairo)
- [Clipboard](clipboard) - [Clipboard](clipboard)
- [Clock](clock) - [Clock](clock)
- [Custom](custom) - [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" type = "clipboard"
max_items = 3 max_items = 3
[[end.truncate]] [end.truncate]
mode = "end" mode = "end"
length = 50 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` | `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. | | `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). | | `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> > 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. 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, 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. 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. 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. 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` ### `Widget`
There are many widget types, each with their own config options. 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 | | Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------| |---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. | | `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 #### Label
@ -47,6 +57,7 @@ A text label. Pango markup is supported.
| Name | Type | Default | Description | | Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------| |---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. | | `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 #### Button
@ -55,9 +66,11 @@ A clickable button, which can run a command when clicked.
> Type `button` > Type `button`
| Name | Type | Default | Description | | Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------| |------------|-------------------------------------------------|---------|--------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. | | `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). | | `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 #### Image
@ -197,6 +210,7 @@ to help get your head around what's going on:
<button class="power-btn" label="" on_click="!reboot" /> <button class="power-btn" label="" on_click="!reboot" />
</box> </box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" /> <label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
<clock disable_popup="true" />
</box> </box>
</popup> </popup>
</custom> </custom>
@ -252,6 +266,10 @@ to help get your head around what's going on:
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}", "label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime", "name": "uptime",
"type": "label" "type": "label"
},
{
"type": "clock",
"disable_popup": true
} }
] ]
} }
@ -309,6 +327,10 @@ type = 'button'
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}''' label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime' name = 'uptime'
type = 'label' type = 'label'
[[end.popup.widgets]]
type = 'clock'
disable_popup = true
``` ```
</details> </details>
@ -345,6 +367,8 @@ end:
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}' - label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime name: uptime
type: label type: label
- type: clock
disable_popup: true
type: custom type: custom
``` ```
@ -370,6 +394,7 @@ let {
] ]
} }
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" } { 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_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. | | `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | | `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>
@ -32,7 +32,8 @@ Optionally displays a launchable set of favourites.
"discord" "discord"
], ],
"show_names": false, "show_names": false,
"show_icons": true "show_icons": true,
"reversed": false
} }
] ]
} }
@ -51,6 +52,7 @@ type = "launcher"
favorites = ["firefox", "discord"] favorites = ["firefox", "discord"]
show_names = false show_names = false
show_icons = true show_icons = true
reversed = false
``` ```
</details> </details>
@ -66,6 +68,7 @@ start:
- discord - discord
show_names: false show_names: false
show_icons: true show_icons: true
reversed: false
``` ```
</details> </details>
@ -81,7 +84,7 @@ start:
favorites = [ "firefox" "discord" ] favorites = [ "firefox" "discord" ]
show_names = false show_names = false
show_icons = true show_icons = true
reversed = false
} }
] ]
} }

View file

@ -69,7 +69,7 @@ format = "{title} / {artist}"
music_dir = "/home/jake/Music" music_dir = "/home/jake/Music"
truncate = "end" truncate = "end"
[[start.icons]] [start.icons]
play = "" play = ""
pause = "" 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 ## Configuration
@ -21,7 +27,6 @@ Displays network connectivity information. Requires NetworkManager.
] ]
} }
``` ```
</details> </details>
<details> <details>
@ -32,7 +37,6 @@ Displays network connectivity information. Requires NetworkManager.
type = "networkmanager" type = "networkmanager"
icon_size = 32 icon_size = 32
``` ```
</details> </details>
<details> <details>
@ -43,7 +47,6 @@ end:
- type: "networkmanager" - type: "networkmanager"
icon_size: 32 icon_size: 32
``` ```
</details> </details>
<details> <details>
@ -59,7 +62,6 @@ end:
] ]
} }
``` ```
</details> </details>
## Styling ## Styling

View file

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

View file

@ -19,6 +19,8 @@ Pango markup is supported.
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data | | `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data | | `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
| `interval.network` | `integer` | `5` | Seconds between refreshing network 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> <details>
<summary>JSON</summary> <summary>JSON</summary>

View file

@ -6,10 +6,11 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
> Type: `tray` > Type: `tray`
| Name | Type | Default | Description | | 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` | | `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> <details>
<summary>JSON</summary> <summary>JSON</summary>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ end:
disks: 300 disks: 300
networks: 3 networks: 3
format: format:
-  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C -  {cpu_percent}% | {temp_c:k10temp-Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%) -  {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)' - '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%) - 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
@ -51,7 +51,7 @@ end:
- 󰖡 {load_average:1} | {load_average:5} | {load_average:15} - 󰖡 {load_average:1} | {load_average:5} | {load_average:15}
- 󰥔 {uptime} - 󰥔 {uptime}
- type: volume - type: volume
format: '{icon} {volume}%' format: '{icon} {percentage}%'
max_volume: 100 max_volume: 100
icons: icons:
volume_high: 󰕾 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": { "locked": {
"lastModified": 1708794349, "lastModified": 1713979152,
"narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=", "narHash": "sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH+DcP4=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa", "rev": "a5eca68a2cf11adb32787fc141cddd29ac8eb79c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1698420672, "lastModified": 1713520724,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", "narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9", "rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -58,11 +58,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1709200309, "lastModified": 1714314149,
"narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=", "narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811", "rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -72,11 +72,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1709150264, "lastModified": 1714253743,
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=", "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08", "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -102,11 +102,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1709172595, "lastModified": 1714443211,
"narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=", "narHash": "sha256-lKTA3XqRo4aVgkyTSCtpcALpGXdmkilHTtN00eRg0QU=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f", "rev": "ce35c36f58f82cee6ec959e0d44c587d64281b6f",
"type": "github" "type": "github"
}, },
"original": { "original": {

132
flake.nix
View file

@ -1,47 +1,47 @@
{ {
description = "Nix Flake for ironbar"; description = "Nix Flake for ironbar";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
crane = { crane = {
url = "github:ipetkov/crane"; url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
naersk.url = "github:nix-community/naersk"; naersk.url = "github:nix-community/naersk";
}; };
outputs = {
self, outputs = { self, nixpkgs, rust-overlay, crane, naersk, ... }:
nixpkgs, let
rust-overlay,
crane,
naersk,
...
}: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-linux" genSystems = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ];
"x86_64-linux"
];
pkgsFor = system: pkgsFor = system:
import nixpkgs { import nixpkgs {
inherit system; inherit system;
overlays = [ overlays = [ self.overlays.default rust-overlay.overlays.default ];
self.overlays.default
rust-overlay.overlays.default
];
}; };
mkRustToolchain = pkgs: mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override { pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
}; };
in { in {
overlays.default = final: prev: let overlays.default = final: prev:
let
rust = mkRustToolchain final; rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust; craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk { naersk' = prev.callPackage naersk {
cargo = rust; cargo = rust;
rustc = rust; rustc = rust;
@ -51,27 +51,30 @@
cargo = rust; cargo = rust;
rustc = rust; rustc = rust;
}; };
props = builtins.fromTOML (builtins.readFile ./Cargo.toml); props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate: (lib.concatStringsSep "-" [
mkDate = longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate) (builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate) (builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate) (builtins.substring 6 2 longDate)
]); ]);
builder = "naersk"; builder = "naersk";
in { in {
ironbar = let ironbar = let
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); version = props.package.version + "+date="
in + (mkDate (self.lastModifiedDate or "19700101")) + "_"
if builder == "crane" + (self.shortRev or "dirty");
then in if builder == "crane" then
prev.callPackage ./nix/default.nix { prev.callPackage ./nix/default.nix {
inherit version; inherit version;
inherit rustPlatform; inherit rustPlatform;
builderName = builder; builderName = builder;
builder = craneLib; builder = craneLib;
} }
else if builder == "naersk" else if builder == "naersk" then
then
prev.callPackage ./nix/default.nix { prev.callPackage ./nix/default.nix {
inherit version; inherit version;
inherit rustPlatform; inherit rustPlatform;
@ -85,30 +88,29 @@
builderName = builder; builderName = builder;
}; };
}; };
packages = genSystems (
system: let packages = genSystems (system:
pkgs = pkgsFor system; let pkgs = pkgsFor system;
in in (self.overlays.default pkgs pkgs) // {
(self.overlays.default pkgs pkgs)
// {
default = self.packages.${system}.ironbar; default = self.packages.${system}.ironbar;
} });
);
apps = genSystems (system: let apps = genSystems (system:
pkgs = pkgsFor system; let pkgs = pkgsFor system;
in { in rec {
default = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
ironbar = { ironbar = {
type = "app"; type = "app";
program = "${pkgs.ironbar}/bin/ironbar"; program = "${pkgs.ironbar}/bin/ironbar";
}; };
default = ironbar;
}); });
devShells = genSystems (system: let
devShells = genSystems (system:
let
pkgs = pkgsFor system; pkgs = pkgsFor system;
rust = mkRustToolchain pkgs; rust = mkRustToolchain pkgs;
in { in {
default = pkgs.mkShell { default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
@ -128,81 +130,93 @@
gsettings-desktop-schemas gsettings-desktop-schemas
libxkbcommon libxkbcommon
libpulseaudio libpulseaudio
luajit
luajitPackages.lgi
]; ];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
}; };
}); });
homeManagerModules.default = {
config, homeManagerModules.default = { config, lib, pkgs, ... }:
lib, let
pkgs,
...
}: let
cfg = config.programs.ironbar; cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default; defaultIronbarPackage =
self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json { }; jsonFormat = pkgs.formats.json { };
in { in {
options.programs.ironbar = { options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar"; enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption { package = lib.mkOption {
type = with lib.types; package; type = with lib.types; package;
default = defaultIronbarPackage; default = defaultIronbarPackage;
description = "The package for ironbar to use."; description = "The package for ironbar to use.";
}; };
systemd = lib.mkOption { systemd = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = pkgs.stdenv.isLinux; default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar."; description = "Whether to enable to systemd service for ironbar.";
}; };
style = lib.mkOption { style = lib.mkOption {
type = lib.types.lines; type = lib.types.lines;
default = ""; default = "";
description = "The stylesheet to apply to ironbar."; description = "The stylesheet to apply to ironbar.";
}; };
config = lib.mkOption { config = lib.mkOption {
type = jsonFormat.type; type = jsonFormat.type;
default = { }; default = { };
description = "The config to pass to ironbar."; description = "The config to pass to ironbar.";
}; };
features = lib.mkOption { features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr; type = lib.types.listOf lib.types.nonEmptyStr;
default = [ ]; default = [ ];
description = "The features to be used."; description = "The features to be used.";
}; };
}; };
config = let config = let pkg = cfg.package.override { features = cfg.features; };
pkg = cfg.package.override {features = cfg.features;}; in lib.mkIf cfg.enable {
in
lib.mkIf cfg.enable {
home.packages = [ pkg ]; home.packages = [ pkg ];
xdg.configFile = { xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") { "ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config; source = jsonFormat.generate "ironbar-config" cfg.config;
}; };
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style; "ironbar/style.css" =
}; lib.mkIf (cfg.style != "") { text = cfg.style; };
}; };
systemd.user.services.ironbar = lib.mkIf cfg.systemd { systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = { Unit = {
Description = "Systemd service for Ironbar"; Description = "Systemd service for Ironbar";
Requires = [ "graphical-session.target" ]; Requires = [ "graphical-session.target" ];
}; };
Service = { Service = {
Type = "simple"; Type = "simple";
ExecStart = "${pkg}/bin/ironbar"; ExecStart = "${pkg}/bin/ironbar";
}; };
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target") Install.WantedBy = with config.wayland.windowManager; [
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target") (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 = { nixConfig = {
extra-substituters = ["https://jakestanger.cachix.org"]; extra-substituters = [ "https://cache.garnix.io" ];
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="]; 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, libxkbcommon,
libpulseaudio, libpulseaudio,
openssl, openssl,
luajit,
luajitPackages,
pkg-config, pkg-config,
hicolor-icon-theme, hicolor-icon-theme,
rustPlatform, rustPlatform,
@ -23,74 +25,105 @@
builderName ? "nix", builderName ? "nix",
builder ? {}, builder ? {},
}: let }: let
hasFeature = f: features == [ ] || builtins.elem f features;
basePkg = rec { basePkg = rec {
inherit version; inherit version;
pname = "ironbar"; pname = "ironbar";
src = builtins.path { src = builtins.path {
name = "ironbar"; name = "ironbar";
path = lib.cleanSource ../.; 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]; nativeBuildInputs = [
propagatedBuildInputs = [ pkg-config
gtk3 wrapGAppsHook
gobject-introspection
]; ];
preFixup = ''
gappsWrapperArgs+=( 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 # Thumbnailers
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share" --prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
--prefix XDG_DATA_DIRS : "${librsvg}/share" --prefix XDG_DATA_DIRS : "${librsvg}/share"
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share" --prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
--prefix XDG_DATA_DIRS : "${shared-mime-info}/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+=(
${gappsWrapperArgs}
) )
''; '';
passthru = { passthru = {
updateScript = gnome.updateScript { updateScript = gnome.updateScript {
packageName = pname; packageName = pname;
attrPath = "gnome.${pname}"; attrPath = "gnome.${pname}";
}; };
}; };
meta = with lib; { meta = with lib; {
homepage = "https://github.com/JakeStanger/ironbar"; 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; license = licenses.mit;
platforms = platforms.linux; platforms = platforms.linux;
mainProgram = "ironbar"; mainProgram = "ironbar";
}; };
}; };
flags = let flags = let
noDefault = noDefault = if features == [ ] then "" else "--no-default-features";
if features == []
then "" featuresStr = if features == [ ] then
else "--no-default-features"; ""
featuresStr = else
if features == [] ''-F "${builtins.concatStringsSep "," features}"'';
then ""
else ''-F "${builtins.concatStringsSep "," features}"'';
in [ noDefault featuresStr ]; in [ noDefault featuresStr ];
in in if builderName == "naersk" then
if builderName == "naersk" builder.buildPackage (basePkg // { cargoBuildOptions = old: old ++ flags; })
then else if builderName == "crane" then
builder.buildPackage (basePkg builder.buildPackage (basePkg // {
// {
cargoOptions = old: old ++ flags;
})
else if builderName == "crane"
then
builder.buildPackage (basePkg
// {
cargoExtraArgs = builtins.concatStringsSep " " flags; cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false; doCheck = false;
}) })
else else
rustPlatform.buildRustPackage (basePkg rustPlatform.buildRustPackage (basePkg // {
// { buildNoDefaultFeatures = features != [ ];
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features; buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock { lockFile = ../Cargo.lock; }; cargoDeps = rustPlatform.importCargoLock { lockFile = ../Cargo.lock; };
cargoLock.lockFile = ../Cargo.lock; cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc="; cargoLock.outputHashes."stray-0.1.3" =
"sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
}) })

View file

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

View file

@ -1,9 +1,7 @@
use crate::config::{BarPosition, MarginConfig, ModuleConfig}; use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{ use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{Config, Ironbar}; use crate::Ironbar;
use color_eyre::Result; use color_eyre::Result;
use glib::Propagation; use glib::Propagation;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
@ -16,7 +14,7 @@ use tracing::{debug, info};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Inner { enum Inner {
New { config: Option<Config> }, New { config: Option<BarConfig> },
Loaded { popup: Rc<Popup> }, Loaded { popup: Rc<Popup> },
} }
@ -43,7 +41,7 @@ impl Bar {
pub fn new( pub fn new(
app: &Application, app: &Application,
monitor_name: String, monitor_name: String,
config: Config, config: BarConfig,
ironbar: Rc<Ironbar>, ironbar: Rc<Ironbar>,
) -> Self { ) -> Self {
let window = ApplicationWindow::builder() let window = ApplicationWindow::builder()
@ -245,7 +243,7 @@ impl Bar {
} }
/// Loads the configured modules onto a 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(); let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme { if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme)); icon_theme.set_custom_theme(Some(theme));
@ -350,57 +348,10 @@ fn add_modules(
ironbar: &Rc<Ironbar>, ironbar: &Rc<Ironbar>,
popup: &Rc<Popup>, popup: &Rc<Popup>,
) -> Result<()> { ) -> Result<()> {
let orientation = info.bar_position.orientation(); let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
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);
}};
}
for config in modules { for config in modules {
let id = Ironbar::unique_id(); config.create(&module_factory, content, info)?;
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),
}
} }
Ok(()) Ok(())
@ -410,7 +361,7 @@ pub fn create_bar(
app: &Application, app: &Application,
monitor: &Monitor, monitor: &Monitor,
monitor_name: String, monitor_name: String,
config: Config, config: BarConfig,
ironbar: Rc<Ironbar>, ironbar: Rc<Ironbar>,
) -> Result<Bar> { ) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config, ironbar); 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 tx = tx.clone();
let _lock = lock!(lock); let lock = lock.clone();
debug!("Received workspace destroy: {workspace_type:?}");
let name = get_workspace_name(workspace_type); event_listener.add_workspace_rename_handler(move |data| {
send!(tx, WorkspaceUpdate::Remove(name)); 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> { fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
Workspaces::get() Workspaces::get()
.expect("Failed to get workspaces") .expect("Failed to get workspaces")
.into_iter()
.find_map(|w| { .find_map(|w| {
if w.name == name { if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| { 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() let workspaces = Workspaces::get()
.expect("Failed to get workspaces") .expect("Failed to get workspaces")
.into_iter()
.map(|w| { .map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible)); 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 { impl From<(Visibility, HWorkspace)> for Workspace {
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self { fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
Self { Self {
id: workspace.id.to_string(), id: workspace.id as i64,
name: workspace.name, name: workspace.name,
monitor: workspace.monitor, monitor: workspace.monitor,
visibility, 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 cfg_if::cfg_if;
use color_eyre::{Help, Report, Result}; use color_eyre::{Help, Report, Result};
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
@ -74,7 +74,7 @@ impl Compositor {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Workspace { pub struct Workspace {
/// Unique identifier /// Unique identifier
pub id: String, pub id: i64,
/// Workspace friendly name /// Workspace friendly name
pub name: String, pub name: String,
/// Name of the monitor (output) the workspace is located on /// 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. /// This is re-sent to all subscribers when a new subscription is created.
Init(Vec<Workspace>), Init(Vec<Workspace>),
Add(Workspace), Add(Workspace),
Remove(String), Remove(i64),
Move(Workspace), Move(Workspace),
/// Declares focus moved from the old workspace to the new. /// Declares focus moved from the old workspace to the new.
Focus { Focus {
old: Option<Workspace>, old: Option<Workspace>,
new: Workspace, new: Workspace,
}, },
Rename {
id: i64,
name: String,
},
/// An update was triggered by the compositor but this was not mapped by Ironbar. /// An update was triggered by the compositor but this was not mapped by Ironbar.
/// ///
/// This is purely used for ergonomics within the compositor clients /// 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>; 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 super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn}; use crate::{await_sync, send, spawn};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use futures_util::StreamExt; use futures_lite::StreamExt;
use std::sync::Arc; use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
@ -90,7 +90,7 @@ impl From<Node> for Workspace {
let visibility = Visibility::from(&node); let visibility = Visibility::from(&node);
Self { Self {
id: node.id.to_string(), id: node.id,
name: node.name.unwrap_or_default(), name: node.name.unwrap_or_default(),
monitor: node.output.unwrap_or_default(), monitor: node.output.unwrap_or_default(),
visibility, visibility,
@ -103,7 +103,7 @@ impl From<swayipc_async::Workspace> for Workspace {
let visibility = Visibility::from(&workspace); let visibility = Visibility::from(&workspace);
Self { Self {
id: workspace.id.to_string(), id: workspace.id,
name: workspace.name, name: workspace.name,
monitor: workspace.output, monitor: workspace.output,
visibility, visibility,
@ -141,13 +141,9 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
WorkspaceChange::Init => { WorkspaceChange::Init => {
Self::Add(event.current.expect("Missing current workspace").into()) Self::Add(event.current.expect("Missing current workspace").into())
} }
WorkspaceChange::Empty => Self::Remove( WorkspaceChange::Empty => {
event Self::Remove(event.current.expect("Missing current workspace").id)
.current }
.expect("Missing current workspace")
.name
.unwrap_or_default(),
),
WorkspaceChange::Focus => Self::Focus { WorkspaceChange::Focus => Self::Focus {
old: event.old.map(Workspace::from), old: event.old.map(Workspace::from),
new: Workspace::from(event.current.expect("Missing current workspace")), 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; use std::sync::Arc;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub mod compositor; pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")] #[cfg(feature = "music")]
pub mod music; pub mod music;
#[cfg(feature = "networkmanager")]
pub mod networkmanager;
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
pub mod swaync; pub mod swaync;
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub mod system_tray; pub mod tray;
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
pub mod upower; pub mod upower;
#[cfg(feature = "volume")] #[cfg(feature = "volume")]
@ -25,18 +33,24 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>, workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>, clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")] #[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>, music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "networkmanager")]
networkmanager: Option<Arc<networkmanager::Client>>,
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>, notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
tray: Option<Arc<system_tray::TrayEventReceiver>>, tray: Option<Arc<tray::Client>>,
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>, upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
#[cfg(feature = "volume")] #[cfg(feature = "volume")]
volume: Option<Arc<volume::Client>>, volume: Option<Arc<volume::Client>>,
} }
pub type ClientResult<T> = Result<Arc<T>>;
impl Clients { impl Clients {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self::default() Self::default()
@ -58,12 +72,23 @@ impl Clients {
} }
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> Arc<dyn compositor::WorkspaceClient> { pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
// TODO: Error handling here isn't great - should throw a user-friendly error & exit let client = match &self.workspaces {
self.workspaces Some(workspaces) => workspaces.clone(),
.get_or_insert_with(|| { None => {
compositor::Compositor::create_workspace_client().expect("to be valid compositor") 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() .clone()
} }
@ -75,24 +100,48 @@ impl Clients {
.clone() .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")] #[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> Arc<swaync::Client> { pub fn notifications(&mut self) -> ClientResult<swaync::Client> {
self.notifications let client = match &self.notifications {
.get_or_insert_with(|| { Some(client) => client.clone(),
Arc::new(crate::await_sync(async { swaync::Client::new().await })) None => {
}) let client = await_sync(async { swaync::Client::new().await })?;
.clone() let client = Arc::new(client);
self.notifications.replace(client.clone());
client
}
};
Ok(client)
} }
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> { pub fn tray(&mut self) -> ClientResult<tray::Client> {
self.tray let client = match &self.tray {
.get_or_insert_with(|| { Some(client) => client.clone(),
Arc::new(crate::await_sync(async { None => {
system_tray::create_client().await let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
}))
}) let client = await_sync(async { tray::Client::new(&service_name).await })?;
.clone() let client = Arc::new(client);
self.tray.replace(client.clone());
client
}
};
Ok(client)
} }
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
@ -119,6 +168,14 @@ pub trait ProvidesClient<T: ?Sized> {
fn provide(&self) -> Arc<T>; 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` /// Generates a `ProvidesClient` impl block on `WidgetContext`
/// for the provided `$ty` (first argument) client type. /// 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>, pub cover_path: Option<String>,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
pub enum PlayerState { pub enum PlayerState {
#[default]
Stopped,
Playing, Playing,
Paused, Paused,
Stopped,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
pub struct Status { pub struct Status {
pub state: PlayerState, pub state: PlayerState,
pub volume_percent: Option<u8>, pub volume_percent: Option<u8>,

View file

@ -18,6 +18,11 @@ pub struct Client {
_rx: broadcast::Receiver<PlayerUpdate>, _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 { impl Client {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let (tx, rx) = broadcast::channel(32); let (tx, rx) = broadcast::channel(32);
@ -35,34 +40,38 @@ impl Client {
// D-Bus gives no event for new players, // D-Bus gives no event for new players,
// so we have to keep polling the player list // so we have to keep polling the player list
loop { loop {
let players = player_finder // mpris-rs does not filter NoActivePlayer errors, so we have to do it ourselves
.find_all() let players = player_finder.find_all().unwrap_or_else(|e| match e {
.expect("Failed to connect to D-Bus"); 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); let mut players_list_val = lock!(players_list);
for player in players { for player in players {
let identity = player.identity(); let identity = player.identity();
if !players_list_val.contains(identity) { if current_player_lock.is_none() {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(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}'"); debug!("Setting active player to '{identity}'");
current_player_lock.replace(identity.to_string());
current_player.replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) { if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}"); error!("{err:?}");
} }
} }
} if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
Self::listen_player_events( Self::listen_player_events(
identity.to_string(), identity.to_string(),
@ -72,7 +81,7 @@ impl Client {
); );
} }
} }
}
// wait 1 second before re-checking players // wait 1 second before re-checking players
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
} }
@ -111,28 +120,56 @@ impl Client {
if let Ok(player) = player_finder.find_by_name(&player_id) { if let Ok(player) = player_finder.find_by_name(&player_id) {
let identity = player.identity(); 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()? { for event in player.events()? {
trace!("Received player event from '{identity}': {event:?}"); trace!("Received player event from '{identity}': {event:?}");
match event { match event {
Ok(Event::PlayerShutDown) => { Ok(Event::PlayerShutDown) => {
lock!(current_player).take(); handle_shutdown(None);
lock!(players).remove(identity);
break; break;
} }
Ok(Event::Playing) => { Err(mpris::EventError::DBusError(DBusError::TransportError(
lock!(current_player).replace(identity.to_string()); transport_error,
))) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
if let Err(err) = Self::send_update(&player, &tx) { || transport_error.name() == Some(NO_REPLY)
error!("{err:?}"); || transport_error.name() == Some(NO_METHOD)
} || transport_error.name() == Some(NO_SERVICE) =>
{
handle_shutdown(None);
break;
} }
Ok(_) => { Ok(_) => {
let current_player = lock!(current_player); let mut current_player_lock = lock!(current_player);
let current_player = current_player.as_ref(); if matches!(event, Ok(Event::Playing)) {
if let Some(current_player) = current_player { current_player_lock.replace(identity.to_string());
if current_player == identity { }
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 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:?}"); 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; mod dbus;
use crate::{register_client, send, spawn}; use crate::{register_fallible_client, send, spawn};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use dbus::SwayNcProxy; use dbus::SwayNcProxy;
use serde::Deserialize; use serde::Deserialize;
@ -24,9 +24,9 @@ type GetSubscribeData = (bool, bool, u32, bool);
impl From<GetSubscribeData> for Event { impl From<GetSubscribeData> for Event {
fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self { fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
Self { Self {
count,
dnd, dnd,
cc_open, cc_open,
count,
inhibited, inhibited,
} }
} }
@ -40,15 +40,13 @@ pub struct Client {
} }
impl Client { impl Client {
pub async fn new() -> Self { pub async fn new() -> Result<Self> {
let dbus = Box::pin(zbus::Connection::session()) let dbus = Box::pin(zbus::Connection::session()).await?;
.await
.expect("failed to create connection to system bus");
let proxy = SwayNcProxy::new(&dbus).await.unwrap(); let proxy = SwayNcProxy::new(&dbus).await?;
let (tx, rx) = broadcast::channel(8); 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(); 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> { 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)] #[derive(Debug)]
#[allow(dead_code)]
struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>); struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>);
impl<T> From<(broadcast::Sender<T>, broadcast::Receiver<T>)> for BroadcastChannel<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 device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ}; 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::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken}; use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken};
use std::cmp::min; use std::cmp::min;
@ -274,7 +274,7 @@ impl DataControlDeviceHandler for Environment {
Ok(token) => { Ok(token) => {
cur_offer.token.replace(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 { impl DataControlSourceHandler for Environment {
fn accept_mime( // fn accept_mime(
&mut self, // &mut self,
_conn: &Connection, // _conn: &Connection,
_qh: &QueueHandle<Self>, // _qh: &QueueHandle<Self>,
_source: &ZwlrDataControlSourceV1, // _source: &ZwlrDataControlSourceV1,
mime: Option<String>, // mime: Option<String>,
) { // ) {
debug!("Accepted mime type: {mime:?}"); // debug!("Accepted mime type: {mime:?}");
} // }
/// Writes the current clipboard item to 'paste' it /// Writes the current clipboard item to 'paste' it
/// upon request from a compositor client. /// upon request from a compositor client.
@ -349,11 +349,12 @@ impl DataControlSourceHandler for Environment {
.add(fd, epoll_event) .add(fd, epoll_event)
.expect("to send valid epoll operation"); .expect("to send valid epoll operation");
let timeout = EpollTimeout::from(100u16);
while !bytes.is_empty() { while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())]; let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
epoll_fd epoll_fd
.wait(&mut events, 100) .wait(&mut events, timeout)
.expect("Failed to wait to epoll"); .expect("Failed to wait to epoll");
match file.write(chunk) { 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::data_offer::DataOfferError;
use smithay_client_toolkit::data_device_manager::ReadPipe; use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::os::fd::{BorrowedFd, FromRawFd}; use std::os::fd::{AsFd, AsRawFd};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::{trace, warn}; use tracing::{trace, warn};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
@ -176,11 +176,11 @@ pub unsafe fn receive(
// create a pipe // create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?; 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); 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 struct DataControlSourceData {}
pub trait DataControlSourceDataExt: Send + Sync { pub trait DataControlSourceDataExt: Send + Sync {
fn data_source_data(&self) -> &DataControlSourceData; // fn data_source_data(&self) -> &DataControlSourceData;
} }
impl DataControlSourceDataExt for DataControlSourceData { impl DataControlSourceDataExt for DataControlSourceData {
fn data_source_data(&self) -> &DataControlSourceData { // fn data_source_data(&self) -> &DataControlSourceData {
self // self
} // }
} }
/// Handler trait for `DataSource` events. /// 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. /// The functions defined in this trait are called as `DataSource` events are received from the compositor.
pub trait DataControlSourceHandler: Sized { pub trait DataControlSourceHandler: Sized {
/// This may be called multiple times, once for each accepted mime type from the destination, if any. /// This may be called multiple times, once for each accepted mime type from the destination, if any.
fn accept_mime( // fn accept_mime(
&mut self, // &mut self,
conn: &Connection, // conn: &Connection,
qh: &QueueHandle<Self>, // qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1, // source: &ZwlrDataControlSourceV1,
mime: Option<String>, // mime: Option<String>,
); // );
/// The client has requested the data for this source to be sent. /// The client has requested the data for this source to be sent.
/// Send the data, then close the fd. /// Send the data, then close the fd.

View file

@ -77,7 +77,6 @@ impl ToplevelHandleHandler for Environment {
match handle.info() { match handle.info() {
Some(info) => { Some(info) => {
trace!("Updating handle: {info:?}"); trace!("Updating handle: {info:?}");
self.handles.push(handle.clone());
if let Some(info) = handle.info() { if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(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 serde::Deserialize;
use tracing::trace; use tracing::trace;
/// Common configuration options /// The following are module-level options which are present on **all** modules.
/// which can be set on every module. ///
/// 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)] #[derive(Debug, Default, Deserialize, Clone)]
pub struct CommonConfig { 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>, 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>, 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>, 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>, 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>, 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>, 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>, 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>, 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>, 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>, 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 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)] #[derive(Debug, Deserialize, Clone)]
@ -38,6 +167,34 @@ pub enum TransitionType {
SlideEnd, 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 { impl TransitionType {
pub const fn to_revealer_transition_type( pub const fn to_revealer_transition_type(
&self, &self,

View file

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

View file

@ -2,6 +2,8 @@ mod common;
mod r#impl; mod r#impl;
mod truncate; mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule; use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
@ -15,7 +17,7 @@ use crate::modules::launcher::LauncherModule;
#[cfg(feature = "music")] #[cfg(feature = "music")]
use crate::modules::music::MusicModule; use crate::modules::music::MusicModule;
#[cfg(feature = "networkmanager")] #[cfg(feature = "networkmanager")]
use crate::modules::networkmanager::NetworkmanagerModule; use crate::modules::networkmanager::NetworkManagerModule;
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
use crate::modules::notifications::NotificationsModule; use crate::modules::notifications::NotificationsModule;
use crate::modules::script::ScriptModule; use crate::modules::script::ScriptModule;
@ -29,16 +31,21 @@ use crate::modules::upower::UpowerModule;
use crate::modules::volume::VolumeModule; use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule; use crate::modules::workspaces::WorkspacesModule;
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::Result;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType}; pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
pub use self::truncate::TruncateMode; pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig { pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>), Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
@ -52,7 +59,7 @@ pub enum ModuleConfig {
#[cfg(feature = "music")] #[cfg(feature = "music")]
Music(Box<MusicModule>), Music(Box<MusicModule>),
#[cfg(feature = "networkmanager")] #[cfg(feature = "networkmanager")]
Networkmanager(Box<NetworkmanagerModule>), NetworkManager(Box<NetworkManagerModule>),
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
Notifications(Box<NotificationsModule>), Notifications(Box<NotificationsModule>),
Script(Box<ScriptModule>), Script(Box<ScriptModule>),
@ -68,10 +75,57 @@ pub enum ModuleConfig {
Workspaces(Box<WorkspacesModule>), 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)] #[derive(Debug, Clone)]
pub enum MonitorConfig { pub enum MonitorConfig {
Single(Config), Single(BarConfig),
Multiple(Vec<Config>), Multiple(Vec<BarConfig>),
} }
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
@ -101,38 +155,110 @@ pub struct MarginConfig {
pub top: i32, 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)] #[derive(Debug, Deserialize, Clone)]
pub struct Config { pub struct BarConfig {
#[serde(default)] /// A unique identifier for the bar, used for controlling it over IPC.
pub position: BarPosition, /// If not set, uses a generated integer suffix.
#[serde(default = "default_true")] ///
pub anchor_to_edges: bool, /// **Default**: `bar-n`
#[serde(default = "default_bar_height")]
pub height: i32,
#[serde(default)]
pub margin: MarginConfig,
#[serde(default = "default_popup_gap")]
pub popup_gap: i32,
pub name: Option<String>, 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)] #[serde(default)]
pub start_hidden: Option<bool>, 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)] #[serde(default)]
pub autohide: Option<u64>, 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 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 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 { fn default() -> Self {
cfg_if! { cfg_if! {
if #[cfg(feature = "clock")] { if #[cfg(feature = "clock")] {
@ -159,20 +285,58 @@ impl Default for Config {
name: None, name: None,
start_hidden: None, start_hidden: None,
autohide: None, autohide: None,
popup_gap: default_popup_gap(),
icon_theme: None, icon_theme: None,
ironvar_defaults: None,
start: Some(vec![ModuleConfig::Label( start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(), LabelModule::new(" Using default config".to_string()).into(),
)]), )]),
center, center,
end, end,
anchor_to_edges: default_true(), 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 { const fn default_bar_height() -> i32 {
42 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)] #[derive(Debug, Deserialize, Clone, Copy)]
#[serde(untagged)] #[serde(untagged)]
pub enum TruncateMode { 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), 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 { Length {
/// The location to truncate text from and place the ellipsis.
/// **Valid options**: `start`, `middle`, `end`
/// <br>
/// **Default**: `null`
mode: EllipsizeMode, 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>, 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>, 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) ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image)); .map(|provider| provider.load_into_image(image));
} else { } else {
let label = Label::new(Some(input)); let label = Label::builder().use_markup(true).label(input).build();
label.add_class("icon"); label.add_class("icon");
label.add_class("text-icon"); label.add_class("text-icon");

View file

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

View file

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

View file

@ -129,7 +129,7 @@ impl Ipc {
ironbar.reload_config(); ironbar.reload_config();
for output in outputs { 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), Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
} }
@ -153,6 +153,20 @@ impl Ipc {
None => Response::error("Variable not found"), 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 } => { Command::LoadCss { path } => {
if path.exists() { if path.exists() {
load_css(path); load_css(path);
@ -172,7 +186,7 @@ impl Ipc {
popup.hide(); popup.hide();
let data = popup let data = popup
.cache .container_cache
.borrow() .borrow()
.iter() .iter()
.find(|(_, value)| value.name == name) .find(|(_, value)| value.name == name)
@ -209,7 +223,7 @@ impl Ipc {
popup.hide(); popup.hide();
let data = popup let data = popup
.cache .container_cache
.borrow() .borrow()
.iter() .iter()
.find(|(_, value)| value.name == name) .find(|(_, value)| value.name == name)

View file

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

View file

@ -9,7 +9,7 @@ use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::{mpsc, Arc, OnceLock}; use std::sync::{mpsc, Arc, Mutex, OnceLock};
use cfg_if::cfg_if; use cfg_if::cfg_if;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>, bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>, clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>, config: Rc<RefCell<Config>>,
config_dir: PathBuf,
} }
impl Ironbar { impl Ironbar {
fn new() -> Self { fn new() -> Self {
let (config, config_dir) = load_config();
Self { Self {
bars: Rc::new(RefCell::new(vec![])), bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())), 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 { while let Ok(event) = rx_outputs.recv().await {
match event.event_type { match event.event_type {
OutputEventType::New => { OutputEventType::New => {
match load_output_bars(&instance, &app, event.output) { match load_output_bars(&instance, &app, &event.output) {
Ok(mut new_bars) => { Ok(mut new_bars) => {
instance.bars.borrow_mut().append(&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. /// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
fn reload_config(&self) { fn reload_config(&self) {
self.config.replace(load_config()); self.config.replace(load_config().0);
} }
} }
@ -270,13 +274,26 @@ fn start_ironbar() {
} }
/// Loads the config file from disk. /// Loads the config file from disk.
fn load_config() -> Config { fn load_config() -> (Config, PathBuf) {
let mut config = env::var("IRONBAR_CONFIG") let config_path = env::var("IRONBAR_CONFIG");
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(), let (config, directory) = if let Ok(config_path) = config_path {
ConfigLoader::load, let path = PathBuf::from(config_path);
(
ConfigLoader::load(&path),
path.parent()
.map(PathBuf::from)
.ok_or_else(|| Report::msg("Specified path has no parent")),
) )
.unwrap_or_else(|err| { } 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); error!("Failed to load config: {}", err);
warn!("Falling back to the default config"); 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!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
@ -285,6 +302,10 @@ fn load_config() -> Config {
Config::default() 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"); debug!("Loaded config file");
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
@ -297,7 +318,7 @@ fn load_config() -> Config {
} }
} }
config (config, directory)
} }
/// Gets the GDK `Display` instance. /// Gets the GDK `Display` instance.
@ -316,22 +337,41 @@ fn get_display() -> Display {
fn load_output_bars( fn load_output_bars(
ironbar: &Rc<Ironbar>, ironbar: &Rc<Ironbar>,
app: &Application, app: &Application,
output: OutputInfo, output: &OutputInfo,
) -> Result<Vec<Bar>> { ) -> 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 { let Some(monitor_name) = &output.name else {
return Err(Report::msg("Output missing monitor name")); 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 config = ironbar.config.borrow();
let display = get_display(); let display = get_display();
let pos = output.logical_position.unwrap_or_default(); // let pos = output.logical_position.unwrap_or_default();
let monitor = display // let monitor = display
.monitor_at_point(pos.0, pos.1) // .monitor_at_point(pos.0, pos.1)
.expect("monitor to exist"); // .expect("monitor to exist");
let monitor = display.monitor(index as i32).expect("monitor to exist");
let show_default_bar = 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 let bars = match config
.monitors .monitors
@ -363,7 +403,7 @@ fn load_output_bars(
app, app,
&monitor, &monitor,
monitor_name.to_string(), monitor_name.to_string(),
config.clone(), config.bar.clone(),
ironbar.clone(), ironbar.clone(),
)?], )?],
None => vec![], 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::{ use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, 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 glib::Propagation;
use gtk::gdk_pixbuf::Pixbuf; use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream}; use gtk::gio::{Cancellable, MemoryInputStream};
@ -13,24 +13,39 @@ use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget}; use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error}; use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ClipboardModule { pub struct ClipboardModule {
/// The icon to show on the bar widget button.
/// Supports [image](images) icons.
///
/// **Default**: `󰨸`
#[serde(default = "default_icon")] #[serde(default = "default_icon")]
icon: String, icon: String,
/// The size to render the icon at.
/// Note this only applies to image-type icons.
///
/// **Default**: `32`
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, 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")] #[serde(default = "default_max_items")]
max_items: usize, max_items: usize,
// -- Common -- // -- Common --
/// See [truncate options](module-level-options#truncate-mode).
///
/// **Default**: `null`
truncate: Option<TruncateMode>, truncate: Option<TruncateMode>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
@ -65,9 +80,7 @@ impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent; type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent; type ReceiveMessage = UIEvent;
fn name() -> &'static str { module_impl!("clipboard");
"clipboard"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -78,7 +91,7 @@ impl Module<Button> for ClipboardModule {
let max_items = self.max_items; let max_items = self.max_items;
let tx = context.tx.clone(); let tx = context.tx.clone();
let client: Arc<clipboard::Client> = context.client(); let client = context.client::<clipboard::Client>();
// listen to clipboard events // listen to clipboard events
spawn(async move { spawn(async move {
@ -137,7 +150,7 @@ impl Module<Button> for ClipboardModule {
let rx = context.subscribe(); let rx = context.subscribe();
let popup = self let popup = self
.into_popup(context.controller_tx, rx, info) .into_popup(context.controller_tx.clone(), rx, context, info)
.into_popup_parts(vec![&button]); .into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleParts::new(button, popup))
@ -147,6 +160,7 @@ impl Module<Button> for ClipboardModule {
self, self,
tx: mpsc::Sender<Self::ReceiveMessage>, tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where

View file

@ -8,29 +8,56 @@ use serde::Deserialize;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tokio::time::sleep; use tokio::time::sleep;
use crate::config::CommonConfig; use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{ use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, 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)] #[derive(Debug, Deserialize, Clone)]
pub struct ClockModule { pub struct ClockModule {
/// Date/time format string. /// The format string to use for the date/time shown on the bar.
/// Default: `%d/%m/%Y %H:%M` /// Pango markup is supported.
/// ///
/// Detail on available tokens can be found here: /// Detail on available tokens can be found here:
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> /// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
///
/// **Default**: `%d/%m/%Y %H:%M`
#[serde(default = "default_format")] #[serde(default = "default_format")]
format: String, 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")] #[serde(default = "default_popup_format")]
format_popup: String, 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")] #[serde(default = "default_locale")]
locale: String, 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)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
@ -41,6 +68,7 @@ impl Default for ClockModule {
format: default_format(), format: default_format(),
format_popup: default_popup_format(), format_popup: default_popup_format(),
locale: default_locale(), locale: default_locale(),
orientation: ModuleOrientation::Horizontal,
common: Some(CommonConfig::default()), common: Some(CommonConfig::default()),
} }
} }
@ -71,9 +99,7 @@ impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>; type SendMessage = DateTime<Local>;
type ReceiveMessage = (); type ReceiveMessage = ();
fn name() -> &'static str { module_impl!("clock");
"clock"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -100,7 +126,7 @@ impl Module<Button> for ClockModule {
) -> Result<ModuleParts<Button>> { ) -> Result<ModuleParts<Button>> {
let button = Button::new(); let button = Button::new();
let label = Label::builder() let label = Label::builder()
.angle(info.bar_position.get_angle()) .angle(self.orientation.to_angle())
.use_markup(true) .use_markup(true)
.build(); .build();
button.add(&label); button.add(&label);
@ -120,7 +146,12 @@ impl Module<Button> for ClockModule {
}); });
let popup = self 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]); .into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleParts::new(button, popup))
@ -130,6 +161,7 @@ impl Module<Button> for ClockModule {
self, self,
_tx: mpsc::Sender<Self::ReceiveMessage>, _tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0); 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::build;
use crate::config::ModuleOrientation;
use crate::modules::custom::WidgetConfig; use crate::modules::custom::WidgetConfig;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Orientation;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct BoxWidget { pub struct BoxWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>, name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>, 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>>, widgets: Option<Vec<WidgetConfig>>,
} }
@ -20,9 +37,7 @@ impl CustomWidget for BoxWidget {
let container = build!(self, Self::Widget); let container = build!(self, Self::Widget);
if let Some(orientation) = self.orientation { if let Some(orientation) = self.orientation {
container.set_orientation( container.set_orientation(orientation.into());
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
);
} }
if let Some(widgets) = self.widgets { if let Some(widgets) = self.widgets {

View file

@ -1,19 +1,53 @@
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Label}; use gtk::{Button, Label, Orientation};
use serde::Deserialize; use serde::Deserialize;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton; use crate::modules::PopupButton;
use crate::{build, try_send}; use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent}; use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget { pub struct ButtonWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>, name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>, 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>, label: Option<String>,
/// Command to execute. More on this [below](#commands).
///
/// **Default**: `null`
on_click: Option<String>, 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 { impl CustomWidget for ButtonWidget {
@ -23,9 +57,20 @@ impl CustomWidget for ButtonWidget {
let button = build!(self, Self::Widget); let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone()); 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); let label = Label::new(None);
label.set_use_markup(true); label.set_use_markup(true);
label.set_angle(self.orientation.to_angle());
button.add(&label); button.add(&label);
dynamic_string(&text, move |string| { dynamic_string(&text, move |string| {

View file

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

View file

@ -3,15 +3,38 @@ use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use crate::build; use crate::build;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext}; use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget { pub struct LabelWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>, name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>, 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, 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 { impl CustomWidget for LabelWidget {
@ -20,6 +43,7 @@ impl CustomWidget for LabelWidget {
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget { fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
let label = build!(self, Self::Widget); let label = build!(self, Self::Widget);
label.set_angle(self.orientation.to_angle());
label.set_use_markup(true); label.set_use_markup(true);
{ {

View file

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

View file

@ -4,22 +4,59 @@ use serde::Deserialize;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::error; use tracing::error;
use crate::config::ModuleOrientation;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length; use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput}; use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send}; use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext}; use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget { pub struct ProgressWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>, name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>, 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>, 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>, value: Option<ScriptInput>,
/// The maximum progress bar value.
///
/// **Default**: `100`
#[serde(default = "default_max")] #[serde(default = "default_max")]
max: f64, max: f64,
/// The progress bar length, in pixels.
/// GTK will automatically determine the size if left blank.
///
/// **Default**: `null`
length: Option<i32>, length: Option<i32>,
} }
@ -33,11 +70,7 @@ impl CustomWidget for ProgressWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let progress = build!(self, Self::Widget); let progress = build!(self, Self::Widget);
if let Some(orientation) = self.orientation { progress.set_orientation(self.orientation.into());
progress.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
if let Some(length) = self.length { if let Some(length) = self.length {
set_length(&progress, length, context.bar_orientation); set_length(&progress, length, context.bar_orientation);

View file

@ -8,25 +8,76 @@ use serde::Deserialize;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::error; use tracing::error;
use crate::config::ModuleOrientation;
use crate::modules::custom::set_length; use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput}; use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send}; 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)] #[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget { pub struct SliderWidget {
/// Widget name.
///
/// **Default**: `null`
name: Option<String>, name: Option<String>,
/// Widget class name.
///
/// **Default**: `null`
class: Option<String>, 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>, 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>, on_change: Option<String>,
/// Minimum slider value.
///
/// **Default**: `0`
#[serde(default = "default_min")] #[serde(default = "default_min")]
min: f64, min: f64,
/// Maximum slider value.
///
/// **Default**: `100`
#[serde(default = "default_max")] #[serde(default = "default_max")]
max: f64, 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>, step: Option<f64>,
/// The slider length.
/// GTK will automatically determine the size if left blank.
///
/// **Default**: `null`
length: Option<i32>, length: Option<i32>,
/// Whether to show the value label above the slider.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_label: bool, show_label: bool,
} }
@ -45,11 +96,7 @@ impl CustomWidget for SliderWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let scale = build!(self, Self::Widget); let scale = build!(self, Self::Widget);
if let Some(orientation) = self.orientation { scale.set_orientation(self.orientation.into());
scale.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
if let Some(length) = self.length { if let Some(length) = self.length {
set_length(&scale, length, context.bar_orientation); 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::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; 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 color_eyre::Result;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
@ -14,18 +14,29 @@ use tracing::debug;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule { pub struct FocusedModule {
/// Whether to show icon on the bar. /// Whether to show icon on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_icon: bool, show_icon: bool,
/// Whether to show app name on the bar. /// Whether to show app name on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_title: bool, show_title: bool,
/// Icon size in pixels. /// Icon size in pixels.
///
/// **Default**: `32`
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, icon_size: i32,
// -- common --
/// See [truncate options](module-level-options#truncate-mode).
///
/// **Default**: `null`
truncate: Option<TruncateMode>, truncate: Option<TruncateMode>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
@ -50,9 +61,7 @@ impl Module<gtk::Box> for FocusedModule {
type SendMessage = Option<(String, String)>; type SendMessage = Option<(String, String)>;
type ReceiveMessage = (); type ReceiveMessage = ();
fn name() -> &'static str { module_impl!("focused");
"focused"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -64,12 +73,16 @@ impl Module<gtk::Box> for FocusedModule {
let wl = context.client::<wayland::Client>(); let wl = context.client::<wayland::Client>();
spawn(async move { spawn(async move {
let mut current = None;
let mut wlrx = wl.subscribe_toplevels(); let mut wlrx = wl.subscribe_toplevels();
let handles = wl.toplevel_info_all(); let handles = wl.toplevel_info_all();
let focused = handles.into_iter().find(|info| info.focused); let focused = handles.into_iter().find(|info| info.focused);
if let Some(focused) = focused { if let Some(focused) = focused {
current = Some(focused.id);
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id))) ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id)))
@ -81,6 +94,9 @@ impl Module<gtk::Box> for FocusedModule {
ToplevelEvent::Update(info) => { ToplevelEvent::Update(info) => {
if info.focused { if info.focused {
debug!("Changing focus"); debug!("Changing focus");
current = Some(info.id);
send_async!( send_async!(
tx, tx,
ModuleUpdateEvent::Update(Some(( ModuleUpdateEvent::Update(Some((
@ -88,13 +104,16 @@ impl Module<gtk::Box> for FocusedModule {
info.app_id.clone() info.app_id.clone()
))) )))
); );
} else { } else if info.id == current.unwrap_or_default() {
debug!("Clearing focus");
current = None;
send_async!(tx, ModuleUpdateEvent::Update(None)); send_async!(tx, ModuleUpdateEvent::Update(None));
} }
} }
ToplevelEvent::Remove(info) => { ToplevelEvent::Remove(info) => {
if info.focused { if info.focused {
debug!("Clearing focus"); debug!("Clearing focus");
current = None;
send_async!(tx, ModuleUpdateEvent::Update(None)); send_async!(tx, ModuleUpdateEvent::Update(None));
} }
} }

View file

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

View file

@ -40,7 +40,7 @@ impl Item {
let id = info.id; let id = info.id;
if self.windows.is_empty() { if self.windows.is_empty() {
self.name = info.title.clone(); self.name.clone_from(&info.title);
} }
let window = Window::from(info); let window = Window::from(info);
@ -59,7 +59,7 @@ impl Item {
pub fn set_window_name(&mut self, window_id: usize, name: String) { pub fn set_window_name(&mut self, window_id: usize, name: String) {
if let Some(window) = self.windows.get_mut(&window_id) { if let Some(window) = self.windows.get_mut(&window_id) {
if let OpenState::Open { focused: true, .. } = window.open_state { if let OpenState::Open { focused: true, .. } = window.open_state {
self.name = name.clone(); self.name.clone_from(&name);
} }
window.name = 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::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file; 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 color_eyre::{Help, Report};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
@ -22,17 +22,38 @@ use tracing::{debug, error, trace};
pub struct LauncherModule { pub struct LauncherModule {
/// List of app IDs (or classes) to always show regardless of open state, /// List of app IDs (or classes) to always show regardless of open state,
/// in the order specified. /// in the order specified.
///
/// **Default**: `null`
favorites: Option<Vec<String>>, favorites: Option<Vec<String>>,
/// Whether to show application names on the bar. /// Whether to show application names on the bar.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")] #[serde(default = "crate::config::default_false")]
show_names: bool, show_names: bool,
/// Whether to show application icons on the bar. /// Whether to show application icons on the bar.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_icons: bool, show_icons: bool,
/// Size in pixels to render icon at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, 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)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
@ -80,9 +101,7 @@ impl Module<gtk::Box> for LauncherModule {
type SendMessage = LauncherUpdate; type SendMessage = LauncherUpdate;
type ReceiveMessage = ItemEvent; type ReceiveMessage = ItemEvent;
fn name() -> &'static str { module_impl!("launcher");
"launcher"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -181,12 +200,21 @@ impl Module<gtk::Box> for LauncherModule {
}?; }?;
} }
ToplevelEvent::Update(info) => { 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_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone()); item.set_window_name(info.id, info.title.clone());
}
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused)) item.open_state.is_open()
} else {
false
};
send_update(LauncherUpdate::Focus(
info.app_id.clone(),
is_open && info.focused,
))
.await?; .await?;
send_update(LauncherUpdate::Title( send_update(LauncherUpdate::Title(
info.app_id.clone(), info.app_id.clone(),
@ -340,7 +368,12 @@ impl Module<gtk::Box> for LauncherModule {
&controller_tx, &controller_tx,
); );
if self.reversed {
container.pack_end(&button.button, false, false, 0);
} else {
container.add(&button.button); container.add(&button.button);
}
buttons.insert(item.app_id, button); buttons.insert(item.app_id, button);
} }
} }
@ -349,8 +382,7 @@ impl Module<gtk::Box> for LauncherModule {
button.set_open(true); button.set_open(true);
button.set_focused(win.open_state.is_focused()); button.set_focused(win.open_state.is_focused());
let mut menu_state = write_lock!(button.menu_state); write_lock!(button.menu_state).num_windows += 1;
menu_state.num_windows += 1;
} }
} }
LauncherUpdate::RemoveItem(app_id) => { LauncherUpdate::RemoveItem(app_id) => {
@ -401,7 +433,7 @@ impl Module<gtk::Box> for LauncherModule {
let rx = context.subscribe(); let rx = context.subscribe();
let popup = self 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 .into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts { Ok(ModuleParts {
@ -414,6 +446,7 @@ impl Module<gtk::Box> for LauncherModule {
self, self,
controller_tx: mpsc::Sender<Self::ReceiveMessage>, controller_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250; 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 tokio::sync::{broadcast, mpsc};
use tracing::debug; use tracing::debug;
use crate::clients::ProvidesClient; use crate::clients::{ClientResult, ProvidesClient, ProvidesFallibleClient};
use crate::config::{BarPosition, CommonConfig, TransitionType}; use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry}; use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar}; use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
/// Displays the current date and time. /// Displays the current date and time.
@ -56,6 +58,8 @@ pub enum ModuleLocation {
Center, Center,
Right, Right,
} }
#[derive(Clone)]
pub struct ModuleInfo<'a> { pub struct ModuleInfo<'a> {
pub app: &'a Application, pub app: &'a Application,
pub location: ModuleLocation, pub location: ModuleLocation,
@ -87,10 +91,16 @@ where
{ {
pub id: usize, pub id: usize,
pub ironbar: Rc<Ironbar>, pub ironbar: Rc<Ironbar>,
pub popup: Rc<Popup>,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>, pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>, pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>, 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>, _update_rx: broadcast::Receiver<TSend>,
} }
@ -109,6 +119,13 @@ where
ProvidesClient::provide(self) 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. /// Subscribes to events sent from this widget.
pub fn subscribe(&self) -> broadcast::Receiver<TSend> { pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe() self.update_tx.subscribe()
@ -124,6 +141,32 @@ impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self { fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup } 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)] #[derive(Debug, Clone)]
@ -152,11 +195,24 @@ impl ModulePopup for Option<gtk::Box> {
} }
pub trait PopupButton { pub trait PopupButton {
fn ensure_popup_id(&self) -> usize;
fn try_popup_id(&self) -> Option<usize>; fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize; fn popup_id(&self) -> usize;
} }
impl PopupButton for Button { 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. /// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button. /// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> { fn try_popup_id(&self) -> Option<usize> {
@ -203,48 +259,57 @@ where
self, self,
_tx: mpsc::Sender<Self::ReceiveMessage>, _tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>, _rx: broadcast::Receiver<Self::SendMessage>,
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where
Self: Sized, Self: Sized,
<Self as Module<W>>::SendMessage: Clone,
{ {
None None
} }
fn take_common(&mut self) -> CommonConfig;
} }
/// Creates a module and sets it up. pub trait ModuleFactory {
/// This setup includes widget/popup content and event channels. fn create<TModule, TWidget, TSend, TRev>(
pub fn create_module<TModule, TWidget, TSend, TRec>( &self,
module: TModule, mut module: TModule,
id: usize, container: &gtk::Box,
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Rc<Popup>, ) -> Result<()>
) -> Result<ModuleParts<TWidget>>
where where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>, TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRev>,
TWidget: IsA<Widget>, TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static, TSend: Debug + Clone + Send + 'static,
{ {
let id = Ironbar::unique_id();
let common = module.take_common();
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64); let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64); let (controller_tx, controller_rx) = mpsc::channel::<TRev>(64);
let (tx, rx) = broadcast::channel(64); let (tx, rx) = broadcast::channel(64);
let context = WidgetContext { let context = WidgetContext {
id, id,
ironbar, ironbar: self.ironbar().clone(),
popup: self.popup().clone(),
tx: ui_tx, tx: ui_tx,
update_tx: tx.clone(), update_tx: tx.clone(),
controller_tx, controller_tx,
_update_rx: rx, _update_rx: rx,
button_id: usize::MAX, // hack :(
}; };
module.spawn_controller(info, &context, controller_rx)?; module.spawn_controller(info, &context, controller_rx)?;
let module_name = TModule::name(); let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string()); let instance_name = common
.name
.clone()
.unwrap_or_else(|| module_name.to_string());
let module_parts = module.into_widget(context, info)?; let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget"); module_parts.widget.add_class("widget");
@ -256,110 +321,226 @@ where
.style_context() .style_context()
.add_class(&format!("popup-{module_name}")); .add_class(&format!("popup-{module_name}"));
popup.register_content(id, instance_name, popup_content); self.popup()
.register_content(id, instance_name, popup_content);
} }
setup_receiver(tx, ui_rx, popup.clone(), module_name, id); self.setup_receiver(tx, ui_rx, module_name, id, common.disable_popup);
Ok(module_parts) module_parts.setup_identifiers(&common);
let ev_container = wrap_widget(
&module_parts.widget,
common,
info.bar_position.orientation(),
);
container.add(&ev_container);
Ok(())
} }
/// 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>( fn setup_receiver<TSend>(
&self,
tx: broadcast::Sender<TSend>, tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>, rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
popup: Rc<Popup>,
name: &'static str, name: &'static str,
id: usize, id: usize,
disable_popup: bool,
) where
TSend: Debug + Clone + Send + 'static;
fn ironbar(&self) -> &Rc<Ironbar>;
fn popup(&self) -> &Rc<Popup>;
}
#[derive(Clone)]
pub struct BarModuleFactory {
ironbar: Rc<Ironbar>,
popup: Rc<Popup>,
}
impl BarModuleFactory {
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>) -> Self {
Self { ironbar, popup }
}
}
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 ) where
TSend: Debug + Clone + Send + 'static, TSend: Debug + Clone + Send + 'static,
{ {
// some rare cases can cause the popup to incorrectly calculate its size on first open. let popup = self.popup.clone();
// 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 => { glib_recv_mpsc!(rx, ev => {
match ev { match ev {
ModuleUpdateEvent::Update(update) => { ModuleUpdateEvent::Update(update) => {
send!(tx, update); send!(tx, update);
} }
ModuleUpdateEvent::TogglePopup(button_id) => { ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
debug!("Toggling popup for {} [#{}]", name, id); debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() { if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide(); popup.hide();
} else { } else {
popup.show(id, button_id); popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show(id, button_id);
has_popup_opened = true;
} }
} }
} ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
ModuleUpdateEvent::OpenPopup(button_id) => { debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
debug!("Opening popup for {} [#{}]", name, id);
popup.hide(); popup.hide();
popup.show(id, button_id); popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show(id, button_id);
has_popup_opened = true;
}
} }
#[cfg(feature = "launcher")] #[cfg(feature = "launcher")]
ModuleUpdateEvent::OpenPopupAt(geometry) => { ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
debug!("Opening popup for {} [#{}]", name, id); debug!("Opening popup for {} [#{}]", name, id);
popup.hide(); popup.hide();
popup.show_at(id, geometry); popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_at(id, geometry);
has_popup_opened = true;
} }
} ModuleUpdateEvent::ClosePopup if !disable_popup => {
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id); debug!("Closing popup for {} [#{}]", name, id);
popup.hide(); popup.hide();
} },
_ => {}
} }
}); });
} }
pub fn set_widget_identifiers<TWidget: IsA<Widget>>( fn ironbar(&self) -> &Rc<Ironbar> {
widget_parts: &ModuleParts<TWidget>, &self.ironbar
common: &CommonConfig, }
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup { fn popup(&self) -> &Rc<Popup> {
popup.container.set_widget_name(&format!("popup-{name}")); &self.popup
} }
} }
if let Some(ref class) = common.class { #[derive(Clone)]
// gtk counts classes with spaces as the same class pub struct PopupModuleFactory {
for part in class.split(' ') { ironbar: Rc<Ironbar>,
widget_parts.widget.style_context().add_class(part); popup: Rc<Popup>,
button_id: usize,
} }
if let Some(ref popup) = widget_parts.popup { impl PopupModuleFactory {
for part in class.split(' ') { pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>, button_id: usize) -> Self {
popup Self {
.container ironbar,
.style_context() popup,
.add_class(&format!("popup-{part}")); 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),
}
}
fn ironbar(&self) -> &Rc<Ironbar> {
match self {
AnyModuleFactory::Bar(bar) => bar.ironbar(),
AnyModuleFactory::Popup(popup) => popup.ironbar(),
}
}
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`. /// Takes a widget and adds it into a new `gtk::EventBox`.

View file

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

View file

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

View file

@ -1,23 +1,20 @@
use std::collections::HashMap;
use color_eyre::Result; use color_eyre::Result;
use futures_lite::StreamExt; use futures_lite::StreamExt;
use futures_signals::signal::SignalExt;
use gtk::prelude::ContainerExt; use gtk::prelude::ContainerExt;
use gtk::{Image, Orientation}; use gtk::{Box as GtkBox, Image, Orientation};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc::Receiver; 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::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; 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)] #[derive(Debug, Deserialize, Clone)]
pub struct NetworkmanagerModule { pub struct NetworkManagerModule {
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, icon_size: i32,
@ -29,118 +26,24 @@ const fn default_icon_size() -> i32 {
24 24
} }
#[derive(Clone, Debug)] impl Module<GtkBox> for NetworkManagerModule {
pub enum NetworkmanagerState { type SendMessage = ClientState;
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;
type ReceiveMessage = (); type ReceiveMessage = ();
fn name() -> &'static str {
"networkmanager"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
_: &ModuleInfo, _: &ModuleInfo,
context: &WidgetContext<NetworkmanagerState, ()>, context: &WidgetContext<ClientState, ()>,
_: Receiver<()>, _: Receiver<()>,
) -> Result<()> { ) -> 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 { spawn(async move {
// TODO: Maybe move this into a `client` à la `upower`? while let Some(state) = client_signal.next().await {
let dbus = zbus::Connection::system().await?; send_async!(widget_transmitter, ModuleUpdateEvent::Update(state));
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));
} }
Result::<()>::Ok(())
}); });
Ok(()) Ok(())
@ -148,29 +51,30 @@ impl Module<gtk::Box> for NetworkmanagerModule {
fn into_widget( fn into_widget(
self, self,
context: WidgetContext<NetworkmanagerState, ()>, context: WidgetContext<ClientState, ()>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleParts<GtkBox>> {
let container = gtk::Box::new(Orientation::Horizontal, 0); let container = GtkBox::new(Orientation::Horizontal, 0);
let icon = Image::new(); let icon = Image::new();
icon.add_class("icon"); icon.add_class("icon");
container.add(&icon); container.add(&icon);
let icon_theme = info.icon_theme.clone(); 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) ImageProvider::parse(initial_icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone())); .map(|provider| provider.load_into_image(icon.clone()));
let rx = context.subscribe(); let widget_receiver = context.subscribe();
glib_recv!(rx, state => { glib_recv!(widget_receiver, state => {
let icon_name = match state { let icon_name = match state {
NetworkmanagerState::Cellular => "network-cellular-symbolic", ClientState::WiredConnected => "network-wired-symbolic",
NetworkmanagerState::Offline => "network-wireless-disabled-symbolic", ClientState::WifiConnected => "network-wireless-symbolic",
NetworkmanagerState::Unknown => "dialog-question-symbolic", ClientState::CellularConnected => "network-cellular-symbolic",
NetworkmanagerState::Wired => "network-wired-symbolic", ClientState::VpnConnected => "network-vpn-symbolic",
NetworkmanagerState::Wireless => "network-wireless-symbolic", ClientState::WifiDisconnected => "network-wireless-acquiring-symbolic",
NetworkmanagerState::WirelessDisconnected => "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) ImageProvider::parse(icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone())); .map(|provider| provider.load_into_image(icon.clone()));
@ -178,31 +82,6 @@ impl Module<gtk::Box> for NetworkmanagerModule {
Ok(ModuleParts::new(container, None)) Ok(ModuleParts::new(container, None))
} }
}
async fn get_network_state(nm_proxy: &NetworkmanagerDBusProxy<'_>) -> Result<NetworkmanagerState> { module_impl!("networkmanager");
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)
}
}
} }

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use crate::config::CommonConfig; use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; 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 color_eyre::Result;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
@ -15,28 +15,76 @@ use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct SysInfoModule { pub struct SysInfoModule {
/// List of formatting strings. /// List of strings including formatting tokens.
/// For available tokens, see [below](#formatting-tokens).
///
/// **Required**
format: Vec<String>, 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")] #[serde(default = "Interval::default")]
interval: Interval, 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)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
#[derive(Debug, Deserialize, Copy, Clone)] #[derive(Debug, Deserialize, Copy, Clone)]
pub struct Intervals { pub struct Intervals {
/// The number of seconds between refreshing memory data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
memory: u64, memory: u64,
/// The number of seconds between refreshing CPU data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
cpu: u64, cpu: u64,
/// The number of seconds between refreshing temperature data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
temps: u64, temps: u64,
/// The number of seconds between refreshing disk data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
disks: u64, disks: u64,
/// The number of seconds between refreshing network data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
networks: u64, networks: u64,
/// The number of seconds between refreshing system data.
///
/// **Default**: `5`
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
system: u64, system: u64,
} }
@ -116,9 +164,7 @@ impl Module<gtk::Box> for SysInfoModule {
type SendMessage = HashMap<String, String>; type SendMessage = HashMap<String, String>;
type ReceiveMessage = (); type ReceiveMessage = ();
fn name() -> &'static str { module_impl!("sysinfo");
"sysinfo"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -184,11 +230,16 @@ impl Module<gtk::Box> for SysInfoModule {
fn into_widget( fn into_widget(
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, _info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?; 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(); 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(); let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item"); label.add_class("item");
label.set_angle(info.bar_position.get_angle()); label.set_angle(self.orientation.to_angle());
container.add(&label); container.add(&label);
labels.push(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. /// Diff change type and associated info.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Diff { pub enum Diff {
Add(MenuItemInfo), Add(MenuItem),
Update(i32, MenuItemDiff), Update(i32, MenuItemDiff),
Remove(i32), Remove(i32),
} }
@ -12,7 +12,7 @@ pub enum Diff {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MenuItemDiff { pub struct MenuItemDiff {
/// Text of the item, /// Text of the item,
pub label: Option<String>, pub label: Option<Option<String>>,
/// Whether the item can be activated or not. /// Whether the item can be activated or not.
pub enabled: Option<bool>, pub enabled: Option<bool>,
/// True if the item is visible in the menu. /// True if the item is visible in the menu.
@ -29,7 +29,7 @@ pub struct MenuItemDiff {
} }
impl MenuItemDiff { impl MenuItemDiff {
fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self { fn new(old: &MenuItem, new: &MenuItem) -> Self {
macro_rules! diff { macro_rules! diff {
($field:ident) => { ($field:ident) => {
if old.$field == new.$field { if old.$field == new.$field {
@ -70,7 +70,7 @@ impl MenuItemDiff {
} }
/// Gets a diff set between old and new state. /// 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![]; let mut diffs = vec![];
for new_item in new { 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::ffi::g_strfreev;
use glib::translate::ToGlibPtr; use glib::translate::ToGlibPtr;
use gtk::ffi::gtk_icon_theme_get_search_path; 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::prelude::IconThemeExt;
use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image}; use gtk::{IconLookupFlags, IconTheme, Image};
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::CStr; use std::ffi::CStr;
use std::os::raw::{c_char, c_int}; use std::os::raw::{c_char, c_int};
use std::ptr; use std::ptr;
use system_tray::message::tray::StatusNotifierItem;
/// Gets the GTK icon theme search paths by calling the FFI function. /// Gets the GTK icon theme search paths by calling the FFI function.
/// Conveniently returns the result as a `HashSet`. /// Conveniently returns the result as a `HashSet`.
@ -36,40 +38,72 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
paths 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 /// Attempts to get a GTK `Image` component
/// for the status notifier item's icon. /// for the status notifier item's icon.
pub(crate) fn get_image_from_icon_name( fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
item: &StatusNotifierItem,
icon_theme: &IconTheme,
) -> Option<Image> {
if let Some(path) = item.icon_theme_path.as_ref() { if let Some(path) = item.icon_theme_path.as_ref() {
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) { if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
icon_theme.append_search_path(path); icon_theme.append_search_path(path);
} }
} }
item.icon_name.as_ref().and_then(|icon_name| { let icon_info = item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); icon_theme.lookup_icon(icon_name, size as i32, IconLookupFlags::empty())
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) });
})
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. /// Attempts to get an image from the item pixmap.
/// ///
/// The pixmap is supplied in ARGB32 format, /// The pixmap is supplied in ARGB32 format,
/// which has 8 bits per sample and a bit stride of `4*width`. /// 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; const BITS_PER_SAMPLE: i32 = 8;
let pixmap = item let pixmap = item
.icon_pixmap .icon_pixmap
.as_ref() .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 mut pixels = pixmap.pixels.to_vec();
let row_stride = pixmap.width * 4; //
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, &bytes,
Colorspace::Rgb, Colorspace::Rgb,
true, true,
@ -80,7 +114,10 @@ pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image>
); );
let pixbuf = pixbuf let pixbuf = pixbuf
.scale_simple(16, 16, InterpType::Bilinear) .scale_simple(size as i32, size as i32, InterpType::Bilinear)
.unwrap_or(pixbuf); .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 crate::{spawn, try_send};
use glib::Propagation;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem}; use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
use std::collections::HashMap; use std::collections::HashMap;
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType}; use system_tray::client::ActivateRequest;
use system_tray::message::NotifierItemCommand; use system_tray::item::{IconPixmap, StatusNotifierItem};
use system_tray::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
use tokio::sync::mpsc; use tokio::sync::mpsc;
/// Calls a method on the underlying widget, /// Calls a method on the underlying widget,
@ -49,37 +51,47 @@ macro_rules! call {
/// Main tray icon to show on the bar /// Main tray icon to show on the bar
pub(crate) struct TrayMenu { pub(crate) struct TrayMenu {
pub(crate) widget: MenuItem, pub widget: MenuItem,
menu_widget: Menu, menu_widget: Menu,
image_widget: Option<Image>, image_widget: Option<Image>,
label_widget: Option<Label>, label_widget: Option<Label>,
menu: HashMap<i32, TrayMenuItem>, menu: HashMap<i32, TrayMenuItem>,
state: Vec<MenuItemInfo>, 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>, tx: mpsc::Sender<i32>,
} }
impl TrayMenu { 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(); let widget = MenuItem::new();
widget.style_context().add_class("item"); widget.style_context().add_class("item");
let (item_tx, mut item_rx) = mpsc::channel(8); let (item_tx, mut item_rx) = mpsc::channel(8);
if let Some(menu) = item.menu {
spawn(async move { spawn(async move {
while let Some(id) = item_rx.recv().await { while let Some(id) = item_rx.recv().await {
try_send!( try_send!(
tx, tx,
NotifierItemCommand::MenuItemClicked { ActivateRequest {
submenu_id: id, submenu_id: id,
menu_path: path.clone(), menu_path: menu.clone(),
notifier_address: address.clone(), address: address.clone(),
} }
); );
} }
}); });
}
let menu = Menu::new(); let menu = Menu::new();
widget.set_submenu(Some(&menu)); widget.set_submenu(Some(&menu));
@ -90,7 +102,10 @@ impl TrayMenu {
image_widget: None, image_widget: None,
label_widget: None, label_widget: None,
state: vec![], 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(), menu: HashMap::new(),
tx: item_tx, tx: item_tx,
} }
@ -112,6 +127,18 @@ impl TrayMenu {
.set_label(text); .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. /// Updates the image, and shows it in favour of the label.
pub fn set_image(&mut self, image: &Image) { pub fn set_image(&mut self, image: &Image) {
if let Some(label) = &self.label_widget { if let Some(label) = &self.label_widget {
@ -134,6 +161,7 @@ impl TrayMenu {
let item = TrayMenuItem::new(&info, self.tx.clone()); let item = TrayMenuItem::new(&info, self.tx.clone());
call!(self.menu_widget, add, item.widget); call!(self.menu_widget, add, item.widget);
self.menu.insert(item.id, item); self.menu.insert(item.id, item);
// self.widget.show_all();
} }
Diff::Update(id, info) => { Diff::Update(id, info) => {
if let Some(item) = self.menu.get_mut(&id) { if let Some(item) = self.menu.get_mut(&id) {
@ -188,36 +216,61 @@ enum TrayMenuWidget {
impl TrayMenuItem { impl TrayMenuItem {
fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self { fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self {
let mut submenu = HashMap::new();
let menu = Menu::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) { let widget = match (info.menu_type, info.toggle_type) {
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()), (MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
(MenuType::Standard, ToggleType::Checkmark) => { (MenuType::Standard, ToggleType::Checkmark) => {
let widget = CheckMenuItem::builder() let widget = CheckMenuItem::builder()
.label(info.label.as_str())
.visible(info.visible) .visible(info.visible)
.sensitive(info.enabled) .sensitive(info.enabled)
.active(info.toggle_state == ToggleState::On) .active(info.toggle_state == ToggleState::On)
.build(); .build();
if let Some(label) = &info.label {
widget.set_label(label);
}
add_submenu!(menu, widget);
{ {
let tx = tx.clone(); let tx = tx.clone();
let id = info.id; let id = info.id;
widget.connect_activate(move |_item| { widget.connect_button_press_event(move |_item, _button| {
try_send!(tx, id); try_send!(tx, id);
Propagation::Proceed
}); });
} }
TrayMenuWidget::Checkbox(widget) TrayMenuWidget::Checkbox(widget)
} }
(MenuType::Standard, _) => { (MenuType::Standard, _) => {
let builder = MenuItem::builder() let widget = MenuItem::builder()
.label(&info.label)
.visible(info.visible) .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(); let tx = tx.clone();
@ -236,7 +289,7 @@ impl TrayMenuItem {
id: info.id, id: info.id,
widget, widget,
menu_widget: menu, menu_widget: menu,
submenu: HashMap::new(), submenu,
tx, tx,
} }
} }
@ -247,6 +300,7 @@ impl TrayMenuItem {
/// applying the submenu diffs to any further submenu items. /// applying the submenu diffs to any further submenu items.
fn apply_diff(&mut self, diff: MenuItemDiff) { fn apply_diff(&mut self, diff: MenuItemDiff) {
if let Some(label) = diff.label { if let Some(label) = diff.label {
let label = label.unwrap_or_default();
match &self.widget { match &self.widget {
TrayMenuWidget::Separator(widget) => widget.set_label(&label), TrayMenuWidget::Separator(widget) => widget.set_label(&label),
TrayMenuWidget::Standard(widget) => widget.set_label(&label), TrayMenuWidget::Standard(widget) => widget.set_label(&label),

View file

@ -2,28 +2,54 @@ mod diff;
mod icon; mod icon;
mod interface; mod interface;
use crate::clients::system_tray::TrayEventReceiver; use crate::clients::tray;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::tray::diff::get_diffs; use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, spawn}; use crate::{glib_recv, lock, module_impl, send_async, spawn};
use color_eyre::Result; use color_eyre::{Report, Result};
use gtk::{prelude::*, PackDirection}; use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar}; use gtk::{IconTheme, MenuBar};
use interface::TrayMenu; use interface::TrayMenu;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; 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 tokio::sync::mpsc;
use tracing::{debug, error, warn};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct TrayModule { 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")] #[serde(default, deserialize_with = "deserialize_orientation")]
pub direction: Option<PackDirection>, direction: Option<PackDirection>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
const fn default_icon_size() -> u32 {
16
}
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error> fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -41,12 +67,10 @@ where
} }
impl Module<MenuBar> for TrayModule { impl Module<MenuBar> for TrayModule {
type SendMessage = NotifierItemMessage; type SendMessage = Event;
type ReceiveMessage = NotifierItemCommand; type ReceiveMessage = ActivateRequest;
fn name() -> &'static str { module_impl!("tray");
"tray"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -56,26 +80,39 @@ impl Module<MenuBar> for TrayModule {
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone(); 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 // listen to tray updates
spawn(async move { spawn(async move {
while let Ok(message) = tray_rx.recv().await { for (key, (item, menu)) in initial_items {
tx.send(ModuleUpdateEvent::Update(message)).await?; 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 // send tray commands
spawn(async move { spawn(async move {
while let Some(cmd) = rx.recv().await { while let Some(cmd) = rx.recv().await {
tray_tx.send(cmd).await?; client.activate(cmd).await?;
} }
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(()) Ok::<_, Report>(())
}); });
Ok(()) Ok(())
@ -106,7 +143,7 @@ impl Module<MenuBar> for TrayModule {
// listen for UI updates // listen for UI updates
glib_recv!(context.subscribe(), update => 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, /// Handles UI updates as callback,
/// getting the diff since the previous update and applying it to the menu. /// getting the diff since the previous update and applying it to the menu.
fn on_update( fn on_update(
update: NotifierItemMessage, update: Event,
container: &MenuBar, container: &MenuBar,
menus: &mut HashMap<Box<str>, TrayMenu>, menus: &mut HashMap<Box<str>, TrayMenu>,
icon_theme: &IconTheme, icon_theme: &IconTheme,
tx: &mpsc::Sender<NotifierItemCommand>, icon_size: u32,
prefer_icons: bool,
tx: &mpsc::Sender<ActivateRequest>,
) { ) {
match update { match update {
NotifierItemMessage::Update { Event::Add(address, item) => {
item, debug!("Received new tray item at '{address}': {item:?}");
address,
menu,
} => {
if let (Some(menu_opts), Some(menu_path)) = (menu, &item.menu) {
let submenus = menu_opts.submenus;
let mut menu_item = menus.remove(address.as_str()).unwrap_or_else(|| { let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
let item = TrayMenu::new(tx.clone(), address.clone(), menu_path.to_string()); container.add(&menu_item.widget);
container.add(&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); menu_item.widget.show();
if let Some(label_widget) = menu_item.label_widget() { menus.insert(address.into(), menu_item);
label_widget.set_label(label);
} }
Event::Update(address, update) => {
debug!("Received tray update for '{address}': {update:?}");
if item.icon_name.as_ref() != menu_item.icon_name() { let Some(menu_item) = menus.get_mut(address.as_str()) else {
match icon::get_image_from_icon_name(&item, icon_theme) error!("Attempted to update menu at '{address}' but could not find it");
.or_else(|| icon::get_image_from_pixmap(&item)) return;
{ };
Some(image) => menu_item.set_image(&image),
None => menu_item.set_label(label), 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(),
}; };
} }
let diffs = get_diffs(menu_item.state(), &submenus); 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(), &menu.submenus);
menu_item.apply_diffs(diffs); menu_item.apply_diffs(diffs);
menu_item.widget.show(); menu_item.set_state(menu.submenus);
menu_item.set_state(submenus);
menu_item.set_icon_name(item.icon_name);
menus.insert(address.into(), menu_item);
} }
} }
NotifierItemMessage::Remove { address } => { }
Event::Remove(address) => {
debug!("Removing tray item at '{address}'");
if let Some(menu) = menus.get(address.as_str()) { if let Some(menu) = menus.get(address.as_str()) {
container.remove(&menu.widget); container.remove(&menu.widget);
} }

View file

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

View file

@ -1,88 +1,45 @@
use crate::clients::volume::{self, Event}; use crate::clients::volume::{self, Event};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{ use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, 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 glib::Propagation;
use gtk::pango::EllipsizeMode; use gtk::pango::EllipsizeMode;
use gtk::prelude::*; 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 serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct VolumeModule { pub struct VolumeModule {
#[serde(default = "default_format")] /// Maximum value to allow volume sliders to reach.
format: String, /// Pulse supports values > 100 but this may result in distortion.
///
/// **Default**: `100`
#[serde(default = "default_max_volume")] #[serde(default = "default_max_volume")]
max_volume: f64, max_volume: f64,
#[serde(default)] #[serde(default = "default_icon_size")]
icons: Icons, icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, 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 { const fn default_max_volume() -> f64 {
100.0 100.0
} }
fn default_icon_volume_high() -> String { const fn default_icon_size() -> i32 {
String::from("󰕾") 24
}
fn default_icon_volume_medium() -> String {
String::from("󰖀")
}
fn default_icon_volume_low() -> String {
String::from("󰕿")
}
fn default_icon_muted() -> String {
String::from("󰝟")
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -99,9 +56,7 @@ impl Module<Button> for VolumeModule {
type SendMessage = Event; type SendMessage = Event;
type ReceiveMessage = Update; type ReceiveMessage = Update;
fn name() -> &'static str { module_impl!("volume");
"volume"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -187,28 +142,34 @@ impl Module<Button> for VolumeModule {
{ {
let rx = context.subscribe(); let rx = context.subscribe();
let icons = self.icons.clone(); let icon_theme = info.icon_theme.clone();
let button = button.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 => { glib_recv!(rx, event => {
match event { match event {
Event::AddSink(sink) | Event::UpdateSink(sink) if sink.active => { Event::AddSink(sink) | Event::UpdateSink(sink) if sink.active => {
let label = format ImageProvider::parse(
.replace("{icon}", if sink.muted { &icons.muted } else { icons.volume_icon(sink.volume) }) &determine_volume_icon(sink.muted, sink.volume),
.replace("{percentage}", &sink.volume.to_string()) &icon_theme,
.replace("{name}", &sink.description); false,
self.icon_size,
button.set_label(&label); ).map(|provider| provider.load_into_image(image_icon.clone()));
}, },
_ => {} _ => {},
} }
}); });
} }
let popup = self 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]); .into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleParts::new(button, popup))
@ -218,17 +179,18 @@ impl Module<Button> for VolumeModule {
self, self,
tx: mpsc::Sender<Self::ReceiveMessage>, tx: mpsc::Sender<Self::ReceiveMessage>,
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>, rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
) -> Option<gtk::Box> info: &ModuleInfo,
) -> Option<GtkBox>
where where
Self: Sized, 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"); 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"); input_container.add_class("apps-box");
container.add(&sink_container); container.add(&sink_container);
@ -288,6 +250,8 @@ impl Module<Button> for VolumeModule {
let btn_mute = ToggleButton::new(); let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute"); 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); sink_container.add(&btn_mute);
{ {
@ -307,6 +271,7 @@ impl Module<Button> for VolumeModule {
let mut inputs = HashMap::new(); let mut inputs = HashMap::new();
{ {
let icon_theme = info.icon_theme.clone();
let input_container = input_container.clone(); let input_container = input_container.clone();
let mut sinks = vec![]; let mut sinks = vec![];
@ -321,7 +286,12 @@ impl Module<Button> for VolumeModule {
slider.set_value(info.volume); slider.set_value(info.volume);
btn_mute.set_active(info.muted); 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); sinks.push(info);
@ -333,7 +303,12 @@ impl Module<Button> for VolumeModule {
slider.set_value(info.volume); slider.set_value(info.volume);
btn_mute.set_active(info.muted); 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) => { Event::AddInput(info) => {
let index = info.index; 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"); item_container.add_class("app-box");
let label = Label::new(Some(&info.name)); let label = Label::new(Some(&info.name));
@ -371,9 +346,16 @@ impl Module<Button> for VolumeModule {
let btn_mute = ToggleButton::new(); let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute"); 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_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(); let tx = tx.clone();
@ -394,7 +376,7 @@ impl Module<Button> for VolumeModule {
container: item_container, container: item_container,
label, label,
slider, slider,
btn_mute btn_mute_icon,
}); });
} }
Event::UpdateInput(info) => { Event::UpdateInput(info) => {
@ -402,7 +384,12 @@ impl Module<Button> for VolumeModule {
ui.label.set_label(&info.name); ui.label.set_label(&info.name);
ui.slider.set_value(info.volume); ui.slider.set_value(info.volume);
ui.slider.set_sensitive(info.can_set_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) => { Event::RemoveInput(index) => {
@ -419,8 +406,21 @@ impl Module<Button> for VolumeModule {
} }
struct InputUi { struct InputUi {
container: gtk::Box, container: GtkBox,
label: Label, label: Label,
slider: Scale, 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::clients::compositor::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; 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 color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme}; use gtk::{Button, IconTheme};
@ -44,26 +45,69 @@ impl Default for Favorites {
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule { pub struct WorkspacesModule {
/// Map of actual workspace names to custom names. /// 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>>, 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)] #[serde(default)]
favorites: Favorites, 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)] #[serde(default)]
hidden: Vec<String>, 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")] #[serde(default = "crate::config::default_false")]
all_monitors: bool, 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)] #[serde(default)]
sort: SortOrder, sort: SortOrder,
/// The size to render icons at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, 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 { impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool { fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.visibility.is_focused() || !self.hidden.contains(&work.name)) (work.visibility.is_focused() || !self.hidden.contains(&work.name))
@ -144,9 +197,7 @@ impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate; type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String; type ReceiveMessage = String;
fn name() -> &'static str { module_impl!("workspaces");
"workspaces"
}
fn spawn_controller( fn spawn_controller(
&self, &self,
@ -155,7 +206,7 @@ impl Module<gtk::Box> for WorkspacesModule {
mut rx: Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let tx = context.tx.clone(); let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces(); let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events // Subscribe & send events
spawn(async move { spawn(async move {
let mut srx = client.subscribe_workspace_change(); 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 // Change workspace focus
spawn(async move { spawn(async move {
@ -195,7 +246,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let favs = self.favorites.clone(); let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![]; 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(); let container = container.clone();
@ -215,7 +266,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let mut added = HashSet::new(); 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( let item = create_button(
name, name,
visibility, visibility,
@ -226,13 +277,13 @@ impl Module<gtk::Box> for WorkspacesModule {
); );
container.add(&item); container.add(&item);
button_map.insert(name.to_string(), item); button_map.insert(id, item);
}; };
// add workspaces from client // add workspaces from client
for workspace in &workspaces { for workspace in &workspaces {
if self.show_workspace_check(&output_name, workspace) { 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()); added.insert(workspace.name.to_string());
} }
} }
@ -242,7 +293,11 @@ impl Module<gtk::Box> for WorkspacesModule {
fav_names.push(name.to_string()); fav_names.push(name.to_string());
if !added.contains(name) { 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()); added.insert(name.to_string());
} }
} }
@ -267,25 +322,28 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
WorkspaceUpdate::Focus { old, new } => { WorkspaceUpdate::Focus { old, new } => {
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) { if let Some(btn) = old.as_ref().and_then(|w| find_btn(&button_map, w)) {
if Some(new.monitor) == old.map(|w| w.monitor) { 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("visible");
} }
btn.style_context().remove_class("focused"); btn.style_context().remove_class("focused");
} }
let new = button_map.get(&new.name); if let Some(btn) = find_btn(&button_map, &new) {
if let Some(btn) = new { btn.add_class("visible");
let style = btn.style_context(); btn.add_class("focused");
}
style.add_class("visible"); }
style.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) => { WorkspaceUpdate::Add(workspace) => {
if fav_names.contains(&workspace.name) { 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 { if let Some(btn) = btn {
btn.style_context().remove_class("inactive"); btn.style_context().remove_class("inactive");
} }
@ -308,7 +366,7 @@ impl Module<gtk::Box> for WorkspacesModule {
item.show(); item.show();
if !name.is_empty() { 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(); item.show();
if !name.is_empty() { 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); container.remove(item);
} }
} }
@ -344,7 +402,8 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Remove(workspace) => { WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace); let button = button_map.get(&workspace);
if let Some(item) = button { if let Some(item) = button {
if fav_names.contains(&workspace) { if workspace < 0 {
// if fav_names.contains(&workspace) {
item.style_context().add_class("inactive"); item.style_context().add_class("inactive");
} else { } else {
container.remove(item); container.remove(item);

View file

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