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:
commit
96d0d9e85b
100 changed files with 4647 additions and 1780 deletions
21
.github/scripts/ubuntu_setup.sh
vendored
Executable file
21
.github/scripts/ubuntu_setup.sh
vendored
Executable 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
68
.github/workflows/binary.yml
vendored
Normal 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
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
.direnv/
|
||||||
|
|
6
.idea/runConfigurations/Clippy__Strict_.xml
generated
6
.idea/runConfigurations/Clippy__Strict_.xml
generated
|
@ -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 -- -W clippy::pedantic -W clippy::unwrap_used -A clippy::cast_possible_wrap -A clippy::cast_possible_truncation -A clippy::cast_sign_loss -A clippy::cast_precision_loss -A clippy::cast_lossless -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">
|
||||||
|
|
11
.idea/runConfigurations/Run.xml
generated
11
.idea/runConfigurations/Run.xml
generated
|
@ -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">
|
||||||
|
|
92
CHANGELOG.md
92
CHANGELOG.md
|
@ -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
731
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
53
Cargo.toml
53
Cargo.toml
|
@ -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
7
Cross.toml
Normal 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"
|
14
README.md
14
README.md
|
@ -57,6 +57,12 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
[](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)
|
||||||
|
|
|
@ -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. |
|
||||||
|
|
|
@ -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`.
|
||||||
|
@ -271,27 +273,43 @@ Check [here](config) for an example config file for a fully configured bar in ea
|
||||||
|
|
||||||
The following table lists each of the top-level bar config options:
|
The following table lists each of the top-level bar config options:
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| 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. |
|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
|
||||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
|
||||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
> [!TIP]
|
||||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).
|
||||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
|
||||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
> [!Note]
|
||||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
> All bar-level options listed in the below section can also be defined at the top-level.
|
||||||
| `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. |
|
# 3.2 Bar-level options
|
||||||
| `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. |
|
The following table lists each of the bar-level bar config options:
|
||||||
| `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. |
|
| Name | Type | Default | Description |
|
||||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
|-------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
|
||||||
|
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||||
|
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||||
|
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||||
|
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||||
|
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||||
|
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||||
|
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||||
|
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||||
|
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||||
|
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
|
||||||
|
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
|
||||||
|
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||||
|
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||||
|
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||||
|
|
||||||
### 3.2 Module-level options
|
### 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.
|
||||||
|
|
||||||
|
@ -299,31 +317,31 @@ 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. |
|
||||||
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
|
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
|
||||||
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
|
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
|
||||||
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
|
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
|
||||||
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
|
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
|
||||||
|
|
||||||
#### 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`. |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
For more information on styling, please see the [styling guide](styling-guide).
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
468
docs/examples/custom/Weather.md
Normal file
468
docs/examples/custom/Weather.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
215
docs/modules/Cairo.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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).
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -54,10 +65,12 @@ 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. |
|
||||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this button. |
|
||||||
|
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||||
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the button. |
|
||||||
|
|
||||||
#### Image
|
#### 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 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = ""
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -9,57 +15,53 @@ Displays network connectivity information. Requires NetworkManager.
|
||||||
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"end": [
|
|
||||||
{
|
|
||||||
"type": "networkmanager",
|
|
||||||
"icon_size": 32
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": [
|
||||||
|
{
|
||||||
|
"type": "networkmanager",
|
||||||
|
"icon_size": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>TOML</summary>
|
<summary>TOML</summary>
|
||||||
|
|
||||||
```toml
|
|
||||||
[[end]]
|
|
||||||
type = "networkmanager"
|
|
||||||
icon_size = 32
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[end]]
|
||||||
|
type = "networkmanager"
|
||||||
|
icon_size = 32
|
||||||
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>YAML</summary>
|
<summary>YAML</summary>
|
||||||
|
|
||||||
```yaml
|
|
||||||
end:
|
|
||||||
- type: "networkmanager"
|
|
||||||
icon_size: 32
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
end:
|
||||||
|
- type: "networkmanager"
|
||||||
|
icon_size: 32
|
||||||
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Corn</summary>
|
<summary>Corn</summary>
|
||||||
|
|
||||||
```corn
|
|
||||||
{
|
|
||||||
end = [
|
|
||||||
{
|
|
||||||
type = "networkmanager"
|
|
||||||
icon_size = 32
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```corn
|
||||||
|
{
|
||||||
|
end = [
|
||||||
|
{
|
||||||
|
type = "networkmanager"
|
||||||
|
icon_size = 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -54,12 +55,12 @@ end:
|
||||||
|
|
||||||
```corn
|
```corn
|
||||||
{
|
{
|
||||||
end = [
|
end = [
|
||||||
{
|
{
|
||||||
type = "tray"
|
type = "tray"
|
||||||
direction = "top_to_bottom"
|
direction = "top_to_bottom"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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. |
|
||||||
|
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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": "",
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
60
examples/test.corn
Normal 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
30
flake.lock
generated
|
@ -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": {
|
||||||
|
|
374
flake.nix
374
flake.nix
|
@ -1,208 +1,222 @@
|
||||||
{
|
{
|
||||||
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,
|
|
||||||
nixpkgs,
|
|
||||||
rust-overlay,
|
|
||||||
crane,
|
|
||||||
naersk,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (nixpkgs) lib;
|
|
||||||
genSystems = lib.genAttrs [
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-linux"
|
|
||||||
];
|
|
||||||
pkgsFor = system:
|
|
||||||
import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
|
|
||||||
overlays = [
|
outputs = { self, nixpkgs, rust-overlay, crane, naersk, ... }:
|
||||||
self.overlays.default
|
let
|
||||||
rust-overlay.overlays.default
|
inherit (nixpkgs) lib;
|
||||||
];
|
|
||||||
};
|
genSystems = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ];
|
||||||
mkRustToolchain = pkgs:
|
|
||||||
pkgs.rust-bin.stable.latest.default.override {
|
pkgsFor = system:
|
||||||
extensions = ["rust-src"];
|
import nixpkgs {
|
||||||
};
|
inherit system;
|
||||||
in {
|
|
||||||
overlays.default = final: prev: let
|
overlays = [ self.overlays.default rust-overlay.overlays.default ];
|
||||||
rust = mkRustToolchain final;
|
};
|
||||||
craneLib = (crane.mkLib final).overrideToolchain rust;
|
|
||||||
naersk' = prev.callPackage naersk {
|
mkRustToolchain = pkgs:
|
||||||
cargo = rust;
|
pkgs.rust-bin.stable.latest.default.override {
|
||||||
rustc = rust;
|
extensions = [ "rust-src" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
rustPlatform = prev.makeRustPlatform {
|
|
||||||
cargo = rust;
|
|
||||||
rustc = rust;
|
|
||||||
};
|
|
||||||
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
|
||||||
mkDate = longDate: (lib.concatStringsSep "-" [
|
|
||||||
(builtins.substring 0 4 longDate)
|
|
||||||
(builtins.substring 4 2 longDate)
|
|
||||||
(builtins.substring 6 2 longDate)
|
|
||||||
]);
|
|
||||||
builder = "naersk";
|
|
||||||
in {
|
in {
|
||||||
ironbar = let
|
overlays.default = final: prev:
|
||||||
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
let
|
||||||
in
|
rust = mkRustToolchain final;
|
||||||
if builder == "crane"
|
|
||||||
then
|
craneLib = (crane.mkLib final).overrideToolchain rust;
|
||||||
prev.callPackage ./nix/default.nix {
|
|
||||||
inherit version;
|
naersk' = prev.callPackage naersk {
|
||||||
inherit rustPlatform;
|
cargo = rust;
|
||||||
builderName = builder;
|
rustc = rust;
|
||||||
builder = craneLib;
|
|
||||||
}
|
|
||||||
else if builder == "naersk"
|
|
||||||
then
|
|
||||||
prev.callPackage ./nix/default.nix {
|
|
||||||
inherit version;
|
|
||||||
inherit rustPlatform;
|
|
||||||
builderName = builder;
|
|
||||||
builder = naersk';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
prev.callPackage ./nix/default.nix {
|
|
||||||
inherit version;
|
|
||||||
inherit rustPlatform;
|
|
||||||
builderName = builder;
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
packages = genSystems (
|
rustPlatform = prev.makeRustPlatform {
|
||||||
system: let
|
cargo = rust;
|
||||||
pkgs = pkgsFor system;
|
rustc = rust;
|
||||||
in
|
};
|
||||||
(self.overlays.default pkgs pkgs)
|
|
||||||
// {
|
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
|
|
||||||
|
mkDate = longDate:
|
||||||
|
(lib.concatStringsSep "-" [
|
||||||
|
(builtins.substring 0 4 longDate)
|
||||||
|
(builtins.substring 4 2 longDate)
|
||||||
|
(builtins.substring 6 2 longDate)
|
||||||
|
]);
|
||||||
|
|
||||||
|
builder = "naersk";
|
||||||
|
in {
|
||||||
|
ironbar = let
|
||||||
|
version = props.package.version + "+date="
|
||||||
|
+ (mkDate (self.lastModifiedDate or "19700101")) + "_"
|
||||||
|
+ (self.shortRev or "dirty");
|
||||||
|
in if builder == "crane" then
|
||||||
|
prev.callPackage ./nix/default.nix {
|
||||||
|
inherit version;
|
||||||
|
inherit rustPlatform;
|
||||||
|
builderName = builder;
|
||||||
|
builder = craneLib;
|
||||||
|
}
|
||||||
|
else if builder == "naersk" then
|
||||||
|
prev.callPackage ./nix/default.nix {
|
||||||
|
inherit version;
|
||||||
|
inherit rustPlatform;
|
||||||
|
builderName = builder;
|
||||||
|
builder = naersk';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
prev.callPackage ./nix/default.nix {
|
||||||
|
inherit version;
|
||||||
|
inherit rustPlatform;
|
||||||
|
builderName = builder;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = genSystems (system:
|
||||||
|
let pkgs = pkgsFor system;
|
||||||
|
in (self.overlays.default pkgs pkgs) // {
|
||||||
default = self.packages.${system}.ironbar;
|
default = self.packages.${system}.ironbar;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
apps = genSystems (system: let
|
|
||||||
pkgs = pkgsFor system;
|
|
||||||
in {
|
|
||||||
default = {
|
|
||||||
type = "app";
|
|
||||||
program = "${pkgs.ironbar}/bin/ironbar";
|
|
||||||
};
|
|
||||||
ironbar = {
|
|
||||||
type = "app";
|
|
||||||
program = "${pkgs.ironbar}/bin/ironbar";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
devShells = genSystems (system: let
|
|
||||||
pkgs = pkgsFor system;
|
|
||||||
rust = mkRustToolchain pkgs;
|
|
||||||
in {
|
|
||||||
default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
rust
|
|
||||||
rust-analyzer-unwrapped
|
|
||||||
gcc
|
|
||||||
gtk3
|
|
||||||
gtk-layer-shell
|
|
||||||
pkg-config
|
|
||||||
openssl
|
|
||||||
gdk-pixbuf
|
|
||||||
glib
|
|
||||||
glib-networking
|
|
||||||
shared-mime-info
|
|
||||||
gnome.adwaita-icon-theme
|
|
||||||
hicolor-icon-theme
|
|
||||||
gsettings-desktop-schemas
|
|
||||||
libxkbcommon
|
|
||||||
libpulseaudio
|
|
||||||
];
|
|
||||||
|
|
||||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
apps = genSystems (system:
|
||||||
};
|
let pkgs = pkgsFor system;
|
||||||
});
|
in rec {
|
||||||
homeManagerModules.default = {
|
ironbar = {
|
||||||
config,
|
type = "app";
|
||||||
lib,
|
program = "${pkgs.ironbar}/bin/ironbar";
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
cfg = config.programs.ironbar;
|
|
||||||
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
|
|
||||||
jsonFormat = pkgs.formats.json {};
|
|
||||||
in {
|
|
||||||
options.programs.ironbar = {
|
|
||||||
enable = lib.mkEnableOption "ironbar status bar";
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = with lib.types; package;
|
|
||||||
default = defaultIronbarPackage;
|
|
||||||
description = "The package for ironbar to use.";
|
|
||||||
};
|
|
||||||
systemd = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = pkgs.stdenv.isLinux;
|
|
||||||
description = "Whether to enable to systemd service for ironbar.";
|
|
||||||
};
|
|
||||||
style = lib.mkOption {
|
|
||||||
type = lib.types.lines;
|
|
||||||
default = "";
|
|
||||||
description = "The stylesheet to apply to ironbar.";
|
|
||||||
};
|
|
||||||
config = lib.mkOption {
|
|
||||||
type = jsonFormat.type;
|
|
||||||
default = {};
|
|
||||||
description = "The config to pass to ironbar.";
|
|
||||||
};
|
|
||||||
features = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.nonEmptyStr;
|
|
||||||
default = [];
|
|
||||||
description = "The features to be used.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config = let
|
|
||||||
pkg = cfg.package.override {features = cfg.features;};
|
|
||||||
in
|
|
||||||
lib.mkIf cfg.enable {
|
|
||||||
home.packages = [pkg];
|
|
||||||
xdg.configFile = {
|
|
||||||
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
|
||||||
source = jsonFormat.generate "ironbar-config" cfg.config;
|
|
||||||
};
|
|
||||||
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
|
|
||||||
text = cfg.style;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
|
|
||||||
Unit = {
|
default = ironbar;
|
||||||
Description = "Systemd service for Ironbar";
|
});
|
||||||
Requires = ["graphical-session.target"];
|
|
||||||
};
|
devShells = genSystems (system:
|
||||||
Service = {
|
let
|
||||||
Type = "simple";
|
pkgs = pkgsFor system;
|
||||||
ExecStart = "${pkg}/bin/ironbar";
|
rust = mkRustToolchain pkgs;
|
||||||
};
|
|
||||||
Install.WantedBy = [
|
in {
|
||||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
default = pkgs.mkShell {
|
||||||
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
|
packages = with pkgs; [
|
||||||
|
rust
|
||||||
|
rust-analyzer-unwrapped
|
||||||
|
gcc
|
||||||
|
gtk3
|
||||||
|
gtk-layer-shell
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
glib-networking
|
||||||
|
shared-mime-info
|
||||||
|
gnome.adwaita-icon-theme
|
||||||
|
hicolor-icon-theme
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
libxkbcommon
|
||||||
|
libpulseaudio
|
||||||
|
luajit
|
||||||
|
luajitPackages.lgi
|
||||||
];
|
];
|
||||||
|
|
||||||
|
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
homeManagerModules.default = { config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.programs.ironbar;
|
||||||
|
defaultIronbarPackage =
|
||||||
|
self.packages.${pkgs.hostPlatform.system}.default;
|
||||||
|
jsonFormat = pkgs.formats.json { };
|
||||||
|
in {
|
||||||
|
options.programs.ironbar = {
|
||||||
|
enable = lib.mkEnableOption "ironbar status bar";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = with lib.types; package;
|
||||||
|
default = defaultIronbarPackage;
|
||||||
|
description = "The package for ironbar to use.";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = pkgs.stdenv.isLinux;
|
||||||
|
description = "Whether to enable to systemd service for ironbar.";
|
||||||
|
};
|
||||||
|
|
||||||
|
style = lib.mkOption {
|
||||||
|
type = lib.types.lines;
|
||||||
|
default = "";
|
||||||
|
description = "The stylesheet to apply to ironbar.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkOption {
|
||||||
|
type = jsonFormat.type;
|
||||||
|
default = { };
|
||||||
|
description = "The config to pass to ironbar.";
|
||||||
|
};
|
||||||
|
|
||||||
|
features = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.nonEmptyStr;
|
||||||
|
default = [ ];
|
||||||
|
description = "The features to be used.";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
config = let pkg = cfg.package.override { features = cfg.features; };
|
||||||
|
in lib.mkIf cfg.enable {
|
||||||
|
home.packages = [ pkg ];
|
||||||
|
|
||||||
|
xdg.configFile = {
|
||||||
|
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
||||||
|
source = jsonFormat.generate "ironbar-config" cfg.config;
|
||||||
|
};
|
||||||
|
|
||||||
|
"ironbar/style.css" =
|
||||||
|
lib.mkIf (cfg.style != "") { text = cfg.style; };
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
|
||||||
|
Unit = {
|
||||||
|
Description = "Systemd service for Ironbar";
|
||||||
|
Requires = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
Service = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = "${pkg}/bin/ironbar";
|
||||||
|
};
|
||||||
|
|
||||||
|
Install.WantedBy = with config.wayland.windowManager; [
|
||||||
|
(lib.mkIf hyprland.systemd.enable "hyprland-session.target")
|
||||||
|
(lib.mkIf sway.systemd.enable "sway-session.target")
|
||||||
|
(lib.mkIf river.systemd.enable "river-session.target")
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
nixConfig = {
|
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
4
lua/draw.lua
Normal 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
4
lua/init.lua
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
local lgi = require('lgi')
|
||||||
|
cairo = lgi.cairo
|
||||||
|
|
||||||
|
__lgi_core = require('lgi.core')
|
123
nix/default.nix
123
nix/default.nix
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
gtk3
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
gtk-layer-shell
|
||||||
|
glib-networking
|
||||||
|
shared-mime-info
|
||||||
|
gnome.adwaita-icon-theme
|
||||||
|
hicolor-icon-theme
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
libxkbcommon ]
|
||||||
|
++ (if hasFeature "http" then [ openssl ] else [])
|
||||||
|
++ (if hasFeature "volume" then [ libpulseaudio ] else [])
|
||||||
|
++ (if hasFeature "cairo" then [ luajit ] else []);
|
||||||
|
|
||||||
|
propagatedBuildInputs = [ gtk3 ];
|
||||||
|
|
||||||
|
lgi = luajitPackages.lgi;
|
||||||
|
|
||||||
|
gappsWrapperArgs = ''
|
||||||
|
# Thumbnailers
|
||||||
|
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||||
|
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||||
|
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||||
|
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||||
|
|
||||||
|
# gtk-launch
|
||||||
|
--suffix PATH : "${lib.makeBinPath [ gtk3 ]}"
|
||||||
|
''
|
||||||
|
+ (if hasFeature "cairo" then ''
|
||||||
|
--prefix LUA_PATH : "./?.lua;${lgi}/share/lua/5.1/?.lua;${lgi}/share/lua/5.1/?/init.lua;${luajit}/share/lua/5.1/\?.lua;${luajit}/share/lua/5.1/?/init.lua"
|
||||||
|
--prefix LUA_CPATH : "./?.so;${lgi}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/loadall.so"
|
||||||
|
'' else "");
|
||||||
|
|
||||||
preFixup = ''
|
preFixup = ''
|
||||||
gappsWrapperArgs+=(
|
gappsWrapperArgs+=(
|
||||||
# Thumbnailers
|
${gappsWrapperArgs}
|
||||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
|
||||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
|
||||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
|
||||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
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 if builderName == "naersk" then
|
||||||
in
|
builder.buildPackage (basePkg // { cargoBuildOptions = old: old ++ flags; })
|
||||||
if builderName == "naersk"
|
else if builderName == "crane" then
|
||||||
then
|
builder.buildPackage (basePkg // {
|
||||||
builder.buildPackage (basePkg
|
cargoExtraArgs = builtins.concatStringsSep " " flags;
|
||||||
// {
|
doCheck = false;
|
||||||
cargoOptions = old: old ++ flags;
|
})
|
||||||
})
|
else
|
||||||
else if builderName == "crane"
|
rustPlatform.buildRustPackage (basePkg // {
|
||||||
then
|
buildNoDefaultFeatures = features != [ ];
|
||||||
builder.buildPackage (basePkg
|
|
||||||
// {
|
buildFeatures = features;
|
||||||
cargoExtraArgs = builtins.concatStringsSep " " flags;
|
cargoDeps = rustPlatform.importCargoLock { lockFile = ../Cargo.lock; };
|
||||||
doCheck = false;
|
cargoLock.lockFile = ../Cargo.lock;
|
||||||
})
|
cargoLock.outputHashes."stray-0.1.3" =
|
||||||
else
|
"sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||||
rustPlatform.buildRustPackage (basePkg
|
})
|
||||||
// {
|
|
||||||
buildNoDefaultFeatures =
|
|
||||||
if features == []
|
|
||||||
then false
|
|
||||||
else true;
|
|
||||||
buildFeatures = features;
|
|
||||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
|
||||||
cargoLock.lockFile = ../Cargo.lock;
|
|
||||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
|
||||||
})
|
|
||||||
|
|
|
@ -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; [
|
||||||
|
|
67
src/bar.rs
67
src/bar.rs
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
41
src/clients/lua.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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,44 +40,48 @@ 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}'");
|
debug!("Setting active player to '{identity}'");
|
||||||
players_list_val.insert(identity.to_string());
|
current_player_lock.replace(identity.to_string());
|
||||||
|
|
||||||
let status = player
|
if let Err(err) = Self::send_update(&player, &tx) {
|
||||||
.get_playback_status()
|
error!("{err:?}");
|
||||||
.expect("Failed to connect to D-Bus");
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut current_player = lock!(current_player);
|
|
||||||
|
|
||||||
if status == PlaybackStatus::Playing || current_player.is_none() {
|
|
||||||
debug!("Setting active player to '{identity}'");
|
|
||||||
|
|
||||||
current_player.replace(identity.to_string());
|
|
||||||
if let Err(err) = Self::send_update(&player, &tx) {
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !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(),
|
||||||
players_list.clone(),
|
players_list.clone(),
|
||||||
current_player.clone(),
|
current_player.clone(),
|
||||||
tx.clone(),
|
tx.clone(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
148
src/clients/networkmanager.rs
Normal file
148
src/clients/networkmanager.rs
Normal 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);
|
|
@ -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);
|
||||||
|
|
|
@ -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
4
src/clients/tray.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
use crate::register_fallible_client;
|
||||||
|
pub use system_tray::client::Client;
|
||||||
|
|
||||||
|
register_fallible_client!(Client, tray);
|
|
@ -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> {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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: >k::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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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: >k::Image, scale: i32) -> Result<()> {
|
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::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)
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
90
src/main.rs
90
src/main.rs
|
@ -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,20 +274,37 @@ 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(),
|
|
||||||
ConfigLoader::load,
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
error!("Failed to load config: {}", err);
|
|
||||||
warn!("Falling back to the default config");
|
|
||||||
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
|
|
||||||
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
|
|
||||||
|
|
||||||
Config::default()
|
let (config, directory) = if let Ok(config_path) = config_path {
|
||||||
});
|
let path = PathBuf::from(config_path);
|
||||||
|
(
|
||||||
|
ConfigLoader::load(&path),
|
||||||
|
path.parent()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok_or_else(|| Report::msg("Specified path has no parent")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let config_loader = ConfigLoader::new("ironbar");
|
||||||
|
(
|
||||||
|
config_loader.find_and_load(),
|
||||||
|
config_loader.config_dir().map_err(Report::new),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = config.unwrap_or_else(|err| {
|
||||||
|
error!("Failed to load config: {}", err);
|
||||||
|
warn!("Falling back to the default config");
|
||||||
|
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
|
||||||
|
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
|
||||||
|
|
||||||
|
Config::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let directory = directory
|
||||||
|
.and_then(|dir| dir.canonicalize().map_err(Report::new))
|
||||||
|
.unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
|
||||||
|
|
||||||
debug!("Loaded config file");
|
debug!("Loaded config file");
|
||||||
|
|
||||||
|
@ -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
215
src/modules/cairo.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -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: >k::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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,13 +200,22 @@ 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()
|
||||||
.await?;
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Focus(
|
||||||
|
info.app_id.clone(),
|
||||||
|
is_open && info.focused,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
send_update(LauncherUpdate::Title(
|
send_update(LauncherUpdate::Title(
|
||||||
info.app_id.clone(),
|
info.app_id.clone(),
|
||||||
info.id,
|
info.id,
|
||||||
|
@ -340,7 +368,12 @@ impl Module<gtk::Box> for LauncherModule {
|
||||||
&controller_tx,
|
&controller_tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
container.add(&button.button);
|
if self.reversed {
|
||||||
|
container.pack_end(&button.button, false, false, 0);
|
||||||
|
} else {
|
||||||
|
container.add(&button.button);
|
||||||
|
}
|
||||||
|
|
||||||
buttons.insert(item.app_id, button);
|
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;
|
||||||
|
|
|
@ -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,165 +259,290 @@ 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: >k::Box,
|
||||||
ironbar: Rc<Ironbar>,
|
info: &ModuleInfo,
|
||||||
name: Option<String>,
|
) -> Result<()>
|
||||||
info: &ModuleInfo,
|
where
|
||||||
popup: &Rc<Popup>,
|
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRev>,
|
||||||
) -> Result<ModuleParts<TWidget>>
|
TWidget: IsA<Widget>,
|
||||||
where
|
TSend: Debug + Clone + Send + 'static,
|
||||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
{
|
||||||
TWidget: IsA<Widget>,
|
let id = Ironbar::unique_id();
|
||||||
TSend: Debug + Clone + Send + 'static,
|
let common = module.take_common();
|
||||||
{
|
|
||||||
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
|
|
||||||
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
|
|
||||||
|
|
||||||
let (tx, rx) = broadcast::channel(64);
|
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
|
||||||
|
let (controller_tx, controller_rx) = mpsc::channel::<TRev>(64);
|
||||||
|
|
||||||
let context = WidgetContext {
|
let (tx, rx) = broadcast::channel(64);
|
||||||
id,
|
|
||||||
ironbar,
|
|
||||||
tx: ui_tx,
|
|
||||||
update_tx: tx.clone(),
|
|
||||||
controller_tx,
|
|
||||||
_update_rx: rx,
|
|
||||||
};
|
|
||||||
|
|
||||||
module.spawn_controller(info, &context, controller_rx)?;
|
let context = WidgetContext {
|
||||||
|
id,
|
||||||
|
ironbar: self.ironbar().clone(),
|
||||||
|
popup: self.popup().clone(),
|
||||||
|
tx: ui_tx,
|
||||||
|
update_tx: tx.clone(),
|
||||||
|
controller_tx,
|
||||||
|
_update_rx: rx,
|
||||||
|
button_id: usize::MAX, // hack :(
|
||||||
|
};
|
||||||
|
|
||||||
let module_name = TModule::name();
|
module.spawn_controller(info, &context, controller_rx)?;
|
||||||
let instance_name = name.unwrap_or_else(|| module_name.to_string());
|
|
||||||
|
|
||||||
let module_parts = module.into_widget(context, info)?;
|
let module_name = TModule::name();
|
||||||
module_parts.widget.add_class("widget");
|
let instance_name = common
|
||||||
module_parts.widget.add_class(module_name);
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| module_name.to_string());
|
||||||
|
|
||||||
if let Some(popup_content) = module_parts.popup.clone() {
|
let module_parts = module.into_widget(context, info)?;
|
||||||
popup_content
|
module_parts.widget.add_class("widget");
|
||||||
.container
|
module_parts.widget.add_class(module_name);
|
||||||
.style_context()
|
|
||||||
.add_class(&format!("popup-{module_name}"));
|
|
||||||
|
|
||||||
popup.register_content(id, instance_name, popup_content);
|
if let Some(popup_content) = module_parts.popup.clone() {
|
||||||
|
popup_content
|
||||||
|
.container
|
||||||
|
.style_context()
|
||||||
|
.add_class(&format!("popup-{module_name}"));
|
||||||
|
|
||||||
|
self.popup()
|
||||||
|
.register_content(id, instance_name, popup_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setup_receiver(tx, ui_rx, module_name, id, common.disable_popup);
|
||||||
|
|
||||||
|
module_parts.setup_identifiers(&common);
|
||||||
|
|
||||||
|
let ev_container = wrap_widget(
|
||||||
|
&module_parts.widget,
|
||||||
|
common,
|
||||||
|
info.bar_position.orientation(),
|
||||||
|
);
|
||||||
|
container.add(&ev_container);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
|
fn setup_receiver<TSend>(
|
||||||
|
&self,
|
||||||
|
tx: broadcast::Sender<TSend>,
|
||||||
|
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||||
|
name: &'static str,
|
||||||
|
id: usize,
|
||||||
|
disable_popup: bool,
|
||||||
|
) where
|
||||||
|
TSend: Debug + Clone + Send + 'static;
|
||||||
|
|
||||||
Ok(module_parts)
|
fn ironbar(&self) -> &Rc<Ironbar>;
|
||||||
|
fn popup(&self) -> &Rc<Popup>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up the bridge channel receiver
|
#[derive(Clone)]
|
||||||
/// to pick up events from the controller, widget or popup.
|
pub struct BarModuleFactory {
|
||||||
///
|
ironbar: Rc<Ironbar>,
|
||||||
/// Handles opening/closing popups
|
|
||||||
/// and communicating update messages between controllers and widgets/popups.
|
|
||||||
fn setup_receiver<TSend>(
|
|
||||||
tx: broadcast::Sender<TSend>,
|
|
||||||
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
|
||||||
popup: Rc<Popup>,
|
popup: Rc<Popup>,
|
||||||
name: &'static str,
|
}
|
||||||
id: usize,
|
|
||||||
) where
|
|
||||||
TSend: Debug + Clone + Send + 'static,
|
|
||||||
{
|
|
||||||
// some rare cases can cause the popup to incorrectly calculate its size on first open.
|
|
||||||
// we can fix that by just force re-rendering it on its first open.
|
|
||||||
let mut has_popup_opened = false;
|
|
||||||
|
|
||||||
glib_recv_mpsc!(rx, ev => {
|
impl BarModuleFactory {
|
||||||
match ev {
|
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>) -> Self {
|
||||||
ModuleUpdateEvent::Update(update) => {
|
Self { ironbar, popup }
|
||||||
send!(tx, update);
|
}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::TogglePopup(button_id) => {
|
|
||||||
debug!("Toggling popup for {} [#{}]", name, id);
|
|
||||||
if popup.is_visible() {
|
|
||||||
popup.hide();
|
|
||||||
} else {
|
|
||||||
popup.show(id, button_id);
|
|
||||||
|
|
||||||
// force re-render on initial open to try and fix size issue
|
impl ModuleFactory for BarModuleFactory {
|
||||||
if !has_popup_opened {
|
fn setup_receiver<TSend>(
|
||||||
|
&self,
|
||||||
|
tx: broadcast::Sender<TSend>,
|
||||||
|
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||||
|
name: &'static str,
|
||||||
|
id: usize,
|
||||||
|
disable_popup: bool,
|
||||||
|
) where
|
||||||
|
TSend: Debug + Clone + Send + 'static,
|
||||||
|
{
|
||||||
|
let popup = self.popup.clone();
|
||||||
|
glib_recv_mpsc!(rx, ev => {
|
||||||
|
match ev {
|
||||||
|
ModuleUpdateEvent::Update(update) => {
|
||||||
|
send!(tx, update);
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
|
||||||
|
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
|
||||||
|
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
|
||||||
|
popup.hide();
|
||||||
|
} else {
|
||||||
popup.show(id, button_id);
|
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);
|
|
||||||
|
|
||||||
// force re-render on initial open to try and fix size issue
|
|
||||||
if !has_popup_opened {
|
|
||||||
popup.show(id, button_id);
|
popup.show(id, button_id);
|
||||||
has_popup_opened = true;
|
|
||||||
}
|
}
|
||||||
}
|
#[cfg(feature = "launcher")]
|
||||||
#[cfg(feature = "launcher")]
|
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||||
ModuleUpdateEvent::OpenPopupAt(geometry) => {
|
debug!("Opening popup for {} [#{}]", name, id);
|
||||||
debug!("Opening popup for {} [#{}]", name, id);
|
|
||||||
|
|
||||||
popup.hide();
|
popup.hide();
|
||||||
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);
|
popup.show_at(id, geometry);
|
||||||
has_popup_opened = true;
|
|
||||||
}
|
}
|
||||||
|
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||||
|
debug!("Closing popup for {} [#{}]", name, id);
|
||||||
|
popup.hide();
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::ClosePopup => {
|
});
|
||||||
debug!("Closing popup for {} [#{}]", name, id);
|
}
|
||||||
popup.hide();
|
|
||||||
}
|
fn ironbar(&self) -> &Rc<Ironbar> {
|
||||||
}
|
&self.ironbar
|
||||||
});
|
}
|
||||||
|
|
||||||
|
fn popup(&self) -> &Rc<Popup> {
|
||||||
|
&self.popup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
|
#[derive(Clone)]
|
||||||
widget_parts: &ModuleParts<TWidget>,
|
pub struct PopupModuleFactory {
|
||||||
common: &CommonConfig,
|
ironbar: Rc<Ironbar>,
|
||||||
) {
|
popup: Rc<Popup>,
|
||||||
if let Some(ref name) = common.name {
|
button_id: usize,
|
||||||
widget_parts.widget.set_widget_name(name);
|
}
|
||||||
|
|
||||||
if let Some(ref popup) = widget_parts.popup {
|
impl PopupModuleFactory {
|
||||||
popup.container.set_widget_name(&format!("popup-{name}"));
|
pub fn new(ironbar: Rc<Ironbar>, popup: Rc<Popup>, button_id: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
ironbar,
|
||||||
|
popup,
|
||||||
|
button_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleFactory for PopupModuleFactory {
|
||||||
|
fn setup_receiver<TSend>(
|
||||||
|
&self,
|
||||||
|
tx: broadcast::Sender<TSend>,
|
||||||
|
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||||
|
name: &'static str,
|
||||||
|
id: usize,
|
||||||
|
disable_popup: bool,
|
||||||
|
) where
|
||||||
|
TSend: Debug + Clone + Send + 'static,
|
||||||
|
{
|
||||||
|
let popup = self.popup.clone();
|
||||||
|
let button_id = self.button_id;
|
||||||
|
glib_recv_mpsc!(rx, ev => {
|
||||||
|
match ev {
|
||||||
|
ModuleUpdateEvent::Update(update) => {
|
||||||
|
send!(tx, update);
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
|
||||||
|
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
|
||||||
|
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id {
|
||||||
|
popup.hide();
|
||||||
|
} else {
|
||||||
|
popup.show(id, button_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
|
||||||
|
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
|
||||||
|
popup.hide();
|
||||||
|
popup.show(id, button_id);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "launcher")]
|
||||||
|
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||||
|
debug!("Opening popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
|
popup.hide();
|
||||||
|
popup.show_at(id, geometry);
|
||||||
|
}
|
||||||
|
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||||
|
debug!("Closing popup for {} [#{}]", name, id);
|
||||||
|
popup.hide();
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ironbar(&self) -> &Rc<Ironbar> {
|
||||||
|
&self.ironbar
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popup(&self) -> &Rc<Popup> {
|
||||||
|
&self.popup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum AnyModuleFactory {
|
||||||
|
Bar(BarModuleFactory),
|
||||||
|
Popup(PopupModuleFactory),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleFactory for AnyModuleFactory {
|
||||||
|
fn setup_receiver<TSend>(
|
||||||
|
&self,
|
||||||
|
tx: broadcast::Sender<TSend>,
|
||||||
|
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||||
|
name: &'static str,
|
||||||
|
id: usize,
|
||||||
|
disable_popup: bool,
|
||||||
|
) where
|
||||||
|
TSend: Debug + Clone + Send + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
AnyModuleFactory::Bar(bar) => bar.setup_receiver(tx, rx, name, id, disable_popup),
|
||||||
|
AnyModuleFactory::Popup(popup) => popup.setup_receiver(tx, rx, name, id, disable_popup),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref class) = common.class {
|
fn ironbar(&self) -> &Rc<Ironbar> {
|
||||||
// gtk counts classes with spaces as the same class
|
match self {
|
||||||
for part in class.split(' ') {
|
AnyModuleFactory::Bar(bar) => bar.ironbar(),
|
||||||
widget_parts.widget.style_context().add_class(part);
|
AnyModuleFactory::Popup(popup) => popup.ironbar(),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref popup) = widget_parts.popup {
|
fn popup(&self) -> &Rc<Popup> {
|
||||||
for part in class.split(' ') {
|
match self {
|
||||||
popup
|
AnyModuleFactory::Bar(bar) => bar.popup(),
|
||||||
.container
|
AnyModuleFactory::Popup(popup) => popup.popup(),
|
||||||
.style_context()
|
|
||||||
.add_class(&format!("popup-{part}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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`.
|
||||||
/// The event box container is returned.
|
/// The event box container is returned.
|
||||||
pub fn wrap_widget<W: IsA<Widget>>(
|
pub fn wrap_widget<W: IsA<Widget>>(
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
spawn(async move {
|
if let Some(menu) = item.menu {
|
||||||
while let Some(id) = item_rx.recv().await {
|
spawn(async move {
|
||||||
try_send!(
|
while let Some(id) = item_rx.recv().await {
|
||||||
tx,
|
try_send!(
|
||||||
NotifierItemCommand::MenuItemClicked {
|
tx,
|
||||||
submenu_id: id,
|
ActivateRequest {
|
||||||
menu_path: path.clone(),
|
submenu_id: id,
|
||||||
notifier_address: address.clone(),
|
menu_path: menu.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),
|
||||||
|
|
|
@ -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:?}");
|
||||||
|
|
||||||
|
let Some(menu_item) = menus.get_mut(address.as_str()) else {
|
||||||
|
error!("Attempted to update menu at '{address}' but could not find it");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match update {
|
||||||
|
UpdateEvent::AttentionIcon(_icon) => {
|
||||||
|
warn!("received unimplemented NewAttentionIcon event");
|
||||||
}
|
}
|
||||||
|
UpdateEvent::Icon(icon) => {
|
||||||
|
if icon.as_ref() != menu_item.icon_name() {
|
||||||
|
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
|
||||||
|
Ok(image) => menu_item.set_image(&image),
|
||||||
|
Err(_) => menu_item.show_label(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if item.icon_name.as_ref() != menu_item.icon_name() {
|
menu_item.set_icon_name(icon);
|
||||||
match icon::get_image_from_icon_name(&item, icon_theme)
|
|
||||||
.or_else(|| icon::get_image_from_pixmap(&item))
|
|
||||||
{
|
|
||||||
Some(image) => menu_item.set_image(&image),
|
|
||||||
None => menu_item.set_label(label),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
UpdateEvent::OverlayIcon(_icon) => {
|
||||||
|
warn!("received unimplemented NewOverlayIcon event");
|
||||||
|
}
|
||||||
|
UpdateEvent::Status(_status) => {
|
||||||
|
warn!("received unimplemented NewStatus event");
|
||||||
|
}
|
||||||
|
UpdateEvent::Title(title) => {
|
||||||
|
if let Some(label_widget) = menu_item.label_widget() {
|
||||||
|
label_widget.set_label(&title.unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// UpdateEvent::Tooltip(_tooltip) => {
|
||||||
|
// warn!("received unimplemented NewAttentionIcon event");
|
||||||
|
// }
|
||||||
|
UpdateEvent::Menu(menu) => {
|
||||||
|
debug!("received new menu for '{}'", address);
|
||||||
|
|
||||||
let diffs = get_diffs(menu_item.state(), &submenus);
|
let diffs = get_diffs(menu_item.state(), &menu.submenus);
|
||||||
menu_item.apply_diffs(diffs);
|
|
||||||
menu_item.widget.show();
|
|
||||||
|
|
||||||
menu_item.set_state(submenus);
|
menu_item.apply_diffs(diffs);
|
||||||
menu_item.set_icon_name(item.icon_name);
|
menu_item.set_state(menu.submenus);
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: >k::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);
|
||||||
|
|
42
src/popup.rs
42
src/popup.rs
|
@ -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();
|
||||||
|
|
192
test-configs/orientation.corn
Normal file
192
test-configs/orientation.corn
Normal 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}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
test-configs/workspaces.corn
Normal file
3
test-configs/workspaces.corn
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
start = [ { type = "workspaces" }]
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue