1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 23:01:04 +02:00

Merge branch 'master' into develop

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/clients/networkmanager.rs
#	src/modules/networkmanager.rs
#	src/modules/volume.rs
This commit is contained in:
Reinout Meliesie 2025-07-14 11:10:38 +02:00
commit c42024d48a
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
159 changed files with 13519 additions and 5655 deletions

View file

@ -2,37 +2,38 @@
name: Bug report
about: Report an issue with the bar not working as expected
title: ''
labels: bug
labels: T:Bug
assignees: ''
---
**Describe the bug**
> A clear and concise description of what the bug is.
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
**To reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
1. Add module `clock`
2. Click popup
3. Observe X is broken
**Expected behavior**
> A clear and concise description of what you expected to happen.
<!-- A clear and concise description of what you expected to happen. -->
**System information:**
- Distro: [e.g. Arch Linux, Ubuntu 22.10]
- Distro: [e.g. Arch Linux, Ubuntu 22.10]
- Compositor: [e.g. Sway]
- Ironbar version: [e.g. 0.8.0]
- Ironbar version: [e.g. 0.16.1]
**Configuration**
> Share your bar configuration and stylesheet as applicable:
<!-- Share your bar configuration and stylesheet as applicable: -->
<details><summary>Config</summary>
```
```
</details>
@ -41,10 +42,11 @@ Steps to reproduce the behavior:
```css
```
</details>
**Additional context**
> Add any other context about the problem here.
<!-- Add any other context about the problem here. -->
**Screenshots**
> If applicable, add screenshots to help explain your problem.
<!-- If applicable, add screenshots to help explain your problem. -->

View file

@ -17,5 +17,7 @@ $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} \
libinput-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libdbusmenu-gtk3-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}

View file

@ -4,8 +4,17 @@ on:
workflow_dispatch:
push:
branches: [ "master" ]
paths:
- 'src/**/*'
- 'Cargo.*'
- 'build.rs'
pull_request:
branches: [ "master" ]
paths:
- 'src/**/*'
- 'Cargo.*'
- 'build.rs'
- '.github/workflows/build.yml'
env:
CARGO_TERM_COLOR: always
@ -24,6 +33,7 @@ jobs:
clippy-base:
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
name: 'Clippy (Base features)'
steps:
- uses: actions/checkout@v4
@ -31,9 +41,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --no-default-features --features config+json
env:
@ -43,6 +50,7 @@ jobs:
clippy-all:
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
name: 'Clippy (All features)'
steps:
- uses: actions/checkout@v4
@ -50,16 +58,16 @@ jobs:
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Clippy
run: cargo clippy --all-targets --all-features
env:
RUSTFLAGS: '-W clippy::unwrap_used'
build:
name: 'Build & Test'
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
steps:
- uses: actions/checkout@v4
@ -67,9 +75,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Build
run: cargo build --verbose
@ -77,3 +82,62 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
feature-checks:
name: 'Check feature flag'
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
strategy:
matrix:
feature:
- http
- ipc
- cli
- config+all
- config+json
- config+yaml
- config+toml
- config+corn
- config+ron
- bindmode+all
- bindmode+sway
- bindmode+hyprland
- cairo
- clipboard
- clock
- custom
- focused
- keyboard+all
- keyboard+sway
- keyboard+hyprland
- label
- launcher
- menu
- music+all
- music+mpris
- music+mpd
- network_manager
- notifications
- sys_info
- script
- tray
- upower
- volume
- workspaces+all
- workspaces+sway
- workspaces+hyprland
- workspaces+niri
- schema
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Check
run: cargo check --no-default-features --features ${{ matrix.feature }}
env:
# Allow some warnings through as we'll never get it perfect
RUSTFLAGS: '-A unused-imports -A unused-variables -A unused-mut -A dead-code'

View file

@ -41,6 +41,7 @@ jobs:
publish-crate:
name: 'Publish Crate'
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
steps:
- uses: actions/checkout@v3
@ -50,9 +51,6 @@ jobs:
toolchain: stable
override: true
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Publish crate
uses: katyo/publish-crates@v1
with:
@ -62,6 +60,7 @@ jobs:
publish-schema:
name: 'Publish Schema'
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
steps:
- uses: actions/checkout@v3
@ -69,11 +68,11 @@ jobs:
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Ensure target folder
run: mkdir -p target
- name: Build schema
run: cargo build --features schema -- --print-schema > target/schema-${{ github.ref_name }}.json
run: cargo run --features schema -- --print-schema > target/schema-${{ github.ref_name }}.json
- name: Copy file via SSH
uses: appleboy/scp-action@v0.1.7

105
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,105 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '32 23 * * *'
push:
branches: [ "master" ]
paths:
- '.github/scripts/ubuntu_setup.sh'
- 'Dockerfile'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
paths:
- '.github/scripts/ubuntu_setup.sh'
- 'Dockerfile'
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}-build
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v2.2.4'
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

View file

@ -4,6 +4,11 @@ on:
workflow_dispatch:
push:
branches: [ "master" ]
paths:
- 'src/**/*'
- 'Cargo.*'
- 'build.rs'
- '.github/workflows/schema.yml'
env:
CARGO_TERM_COLOR: always
@ -13,6 +18,7 @@ jobs:
publish-schema:
name: 'Publish Schema'
runs-on: ubuntu-latest
container: ghcr.io/jakestanger/ironbar-build:master
steps:
- uses: actions/checkout@v3
@ -20,9 +26,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: ./.github/scripts/ubuntu_setup.sh
- name: Build
run: cargo build --features schema

View file

@ -3,6 +3,8 @@ name: Sync Wiki
on:
push:
branches: [ "master" ]
paths:
- 'docs/**/*'
jobs:
build:

6
.idea/git_toolbox_blame.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

View file

@ -11,7 +11,7 @@
<option name="backtrace" value="SHORT" />
<envs>
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="debug" />
<env name="IRONBAR_LOG" value="debug" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />

20
.idea/runConfigurations/Test.xml generated Normal file
View file

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="test" />
<option name="command" value="test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View file

@ -4,6 +4,116 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.16.1] - 2024-11-24
### :boom: BREAKING CHANGES
- due to [`e4e9632`](https://github.com/JakeStanger/ironbar/commit/e4e9632caab66f6a8627ffb03b2f82cd5404003f) - menu causing bar to lose focus on sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
The `direction` option has been changed to only accept `horizontal` or `vertical`
### :sparkles: New Features
- [`662ddb6`](https://github.com/JakeStanger/ironbar/commit/662ddb69464ab45546231d337f7f4f3e5efcdc98) - **tray**: image support in menu items *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e1f3b1b`](https://github.com/JakeStanger/ironbar/commit/e1f3b1bb72f3aa6562f7c7d98a0ef8d131e86600) - route gtk logging through tracing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`82a6660`](https://github.com/JakeStanger/ironbar/commit/82a6660c8568fc6fe7661a7703173c0c8cd93085) - **workspaces**: incorrectly checking focus using name_map value *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2aa55d8`](https://github.com/JakeStanger/ironbar/commit/2aa55d8d66c69ed02089811af4f3f2eaee11f2ee) - **popup**: incorrect pos when resolution changes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`d8b68fd`](https://github.com/JakeStanger/ironbar/commit/d8b68fd378b4ece7260511386115b363ed8eec2e) - **launcher**: showing xwayland menus and tooltips *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e08027f`](https://github.com/JakeStanger/ironbar/commit/e08027fe6992756f8cb4800ec19782024543b19b) - **workspaces**: prevent crash when clicking current workspace *(PR [#733](https://github.com/JakeStanger/ironbar/pull/733) by [@Leshuguita](https://github.com/Leshuguita))*
- [`b2db7b0`](https://github.com/JakeStanger/ironbar/commit/b2db7b0bb546f0fc4b642a83cbe6303213480723) - markup escape issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`d87888d`](https://github.com/JakeStanger/ironbar/commit/d87888d173b2737bc5a3ae5ff4ae192cca2a87c7) - `on_scroll` events broken on touchpad *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`80403e3`](https://github.com/JakeStanger/ironbar/commit/80403e3ca9b1261d8374baf29d6971236494929c) - not properly redrawing on style reload *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`81c48fe`](https://github.com/JakeStanger/ironbar/commit/81c48fecadb629a0f915d58f59c0523ab54af162) - **clipboard**: crash when unsupported image type *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e53a906`](https://github.com/JakeStanger/ironbar/commit/e53a9067b03de002a07f85e5302e3774e53521f2) - **tray**: cannot activate with mixed left/right click *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`55c0940`](https://github.com/JakeStanger/ironbar/commit/55c0940e1dc90069018e40254a2b079b31bc2da2) - **tray**: update `system-tray` dep to bring in a whole load of fixes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`fb17995`](https://github.com/JakeStanger/ironbar/commit/fb1799531b4638f8cd3a36ccb94425643aaa6082) - **tray**: image updates lag 1 behind *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`05530cf`](https://github.com/JakeStanger/ironbar/commit/05530cf7769a47a49a185d228afaa934d6df7575) - regression caused by [#652](https://github.com/JakeStanger/ironbar/pull/652) *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e4e9632`](https://github.com/JakeStanger/ironbar/commit/e4e9632caab66f6a8627ffb03b2f82cd5404003f) - **tray**: menu causing bar to lose focus on sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`42e25f5`](https://github.com/JakeStanger/ironbar/commit/42e25f5ef2ce9886d8fafb42aff9ced7ef183726) - **ipc**: support querying against duplicate bar names *(commit by [@zeroeightysix](https://github.com/zeroeightysix))*
- [`5aa9f37`](https://github.com/JakeStanger/ironbar/commit/5aa9f37fe4ff76d9ef61e8c0aacc110ecb9a89c3) - **tray**: menus not attaching to secondary bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f364bb6`](https://github.com/JakeStanger/ironbar/commit/f364bb64fbfaef31a55bdc3c9e4cbb6f90fcdab5) - **tray**: tray icons not disappearing on close *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ce48fc9`](https://github.com/JakeStanger/ironbar/commit/ce48fc9d0d8e603e69f01e58f3a717fc845888f7) - **tray**: prevent widget buttons from piling up *(PR [#788](https://github.com/JakeStanger/ironbar/pull/788) by [@cmeissl](https://github.com/cmeissl))*
- [`230dd8b`](https://github.com/JakeStanger/ironbar/commit/230dd8b13b024eb039613c579f940802ea5857e5) - **workspaces**: clicking currently focused workspace attempts to focus it *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`04f45cc`](https://github.com/JakeStanger/ironbar/commit/04f45ccae1498630a81edd34923c5920e864bacc) - fix some pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`486beff`](https://github.com/JakeStanger/ironbar/commit/486beff8a550e0bc757e79ea37c450cab3a810eb) - put in basic placeholders for menu icon diffs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cf38c37`](https://github.com/JakeStanger/ironbar/commit/cf38c37fe3223280bf89e50b6869e1912a8ed8bf) - **tray**: move some debug logging to trace logging *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`30de23d`](https://github.com/JakeStanger/ironbar/commit/30de23dc6487635c0e71edfb7a8780bea7ae23e1) - **tray**: switch over to `libdbusmenu-gtk3` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`56153f1`](https://github.com/JakeStanger/ironbar/commit/56153f189a6496d01cbc42cdc9086d52d69a235a) - **dynamic values**: fix missing backtick *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ff3f541`](https://github.com/JakeStanger/ironbar/commit/ff3f541183260c786d28e5c9e0a01af0f45408a9) - **tray**: fix formatting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f161429`](https://github.com/JakeStanger/ironbar/commit/f161429dfc4e3faa815a00ee9f9afe01ccc959cc) - **clock**: align table columns *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### Note to maintainers
The tray module now depends on `libdbusmenu-gtk3` , which must be present at build time and runtime.
## [v0.16.0] - 2024-08-10
### :boom: BREAKING CHANGES
- due to [`9dd7112`](https://github.com/JakeStanger/ironbar/commit/9dd711235f21d9016fec240f1be5c8d6de1596df) - improve CLI structure, add new commands *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
- `ok_value` responses will no longer print `ok` as the first line when using the CLI
- All IPC commands have changed. Namely, `type` has been changed to `command`, and bar/var related commands are now under a `subcommand`. The full spec can be found on the wiki.
- Several CLI commands are now located under the `var` and `bar` categories. Usage of any commands to get/set Ironvars or control bar visibility will need to be updated.
- The `open_popup` and `close_popup` IPC commands are now called `show_popup` and `hide_popup` respectively.
- The popup `name` argument has been renamed to `widget_name` on all IPC commands.
- The `set-visibility` CLI command now takes a `true`/`false` positional argument in place of the `-v` flag.
### :sparkles: New Features
- [`f11da3e`](https://github.com/JakeStanger/ironbar/commit/f11da3eca1b7d1bc5e1904266f285f0e28f290a0) - **music**: pango markup support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`951576c`](https://github.com/JakeStanger/ironbar/commit/951576ce3c092d187fd6d1d2ff55b7dbf6198a25) - pango markup support in image icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`36d724f`](https://github.com/JakeStanger/ironbar/commit/36d724f148ed8ebe84cbb3c3e25cd4a361d94e66) - **config**: json schema support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7413f78`](https://github.com/JakeStanger/ironbar/commit/7413f78e04fe9b532397144e49b7545547980723) - **cli**: debug flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a33e0a2`](https://github.com/JakeStanger/ironbar/commit/a33e0a241a8d5f65f7360b5c7e34a116f3f9f092) - **cli**: format flag, json output format *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9dd7112`](https://github.com/JakeStanger/ironbar/commit/9dd711235f21d9016fec240f1be5c8d6de1596df) - improve CLI structure, add new commands *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`aa45396`](https://github.com/JakeStanger/ironbar/commit/aa4539606241cfd4d7b8e5512866d30ce92e432d) - ability to set bar layer and exclusive zone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`92ae1a8`](https://github.com/JakeStanger/ironbar/commit/92ae1a8d73d68ebf51e008cd6322a9269cd10325) - **nix**: home-manager option to read style.css file *(commit by [@Alpha-Ursae-Minoris](https://github.com/Alpha-Ursae-Minoris))*
- [`6d0fe4c`](https://github.com/JakeStanger/ironbar/commit/6d0fe4c8ace9c8a4136fb65c9ff9cdb04e9b6408) - add networkmanager module *(commit by [@Zedfrigg](https://github.com/Zedfrigg))*
- [`e307e15`](https://github.com/JakeStanger/ironbar/commit/e307e15dc4462d1bdcaabff2375f5ac0c5a5df7b) - new sway-mode module *(PR [#671](https://github.com/JakeStanger/ironbar/pull/671) by [@Rodrigodd](https://github.com/Rodrigodd))*
### :bug: Bug Fixes
- [`5e7f576`](https://github.com/JakeStanger/ironbar/commit/5e7f576841f94bdfd89d401cb9a2ba1fabb45c1c) - **workspaces**: add support for hyprland rename event *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c45ea02`](https://github.com/JakeStanger/ironbar/commit/c45ea02a7d39b30fece3986044a44a64aebf5e16) - **workspaces**: regression due to [#572](https://github.com/JakeStanger/ironbar/pull/572) *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4a37429`](https://github.com/JakeStanger/ironbar/commit/4a37429634a32a2ffaeb1b591240bdb2a564cab9) - **launcher**: ghost windows in reload *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`520a94a`](https://github.com/JakeStanger/ironbar/commit/520a94abfa526c22df0bebecc42b9be8ae20881e) - all bars showing on same display due to GTK bug *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4ad4b0e`](https://github.com/JakeStanger/ironbar/commit/4ad4b0e070cc4e271251763db7210e70857d68ca) - **ipc**: regression - reload not working due to [#592](https://github.com/JakeStanger/ironbar/pull/592) *(commit by [@SerraPi](https://github.com/SerraPi))*
- [`9a39420`](https://github.com/JakeStanger/ironbar/commit/9a39420ae28b185cb38a33817f1fc91b5c4e9f55) - **launcher**: favourites staying focused when closed in hyprland *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8dda494`](https://github.com/JakeStanger/ironbar/commit/8dda49477b2ceb268b94c729aadc5986bdca8528) - **cli**: using zero exit code for error responses *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9d8a3eb`](https://github.com/JakeStanger/ironbar/commit/9d8a3eb370195321d224c0a51a6752c62404ac1b) - correctly escape pango markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`277e6b6`](https://github.com/JakeStanger/ironbar/commit/277e6b62965608ae90defa9a2170d414e09d4c59) - **notifications**: unable to click through overlay *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dbd385d`](https://github.com/JakeStanger/ironbar/commit/dbd385d225e27a7d732d60ba5a6d6f13c1184add) - **launcher**: apps with multiple windows stay focused when window closed *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`176af99`](https://github.com/JakeStanger/ironbar/commit/176af997f442833adcd7ada1919836d54530d7ef) - **music**: tokens with `&` not rendering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7e04e30`](https://github.com/JakeStanger/ironbar/commit/7e04e30171a1897de468592fe5c1f6082d12eb69) - **wayland**: exit on event dispatch error *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`461bee8`](https://github.com/JakeStanger/ironbar/commit/461bee8847590e769df186a2f24ab2ce957568f7) - **bar**: do not add start/center/end containers if empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`fb6ae42`](https://github.com/JakeStanger/ironbar/commit/fb6ae42f3bcc7ad35066e1182e617c739a8cfa8a) - crash due to clipboard fd incorrectly closed *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2bc741d`](https://github.com/JakeStanger/ironbar/commit/2bc741d197867cd5f0c391b9532b4cf9c4d378f6) - **tray**: crash when provided empty pixmap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f819aec`](https://github.com/JakeStanger/ironbar/commit/f819aec259cfe418f050c57eb51a236a95039b57) - **notifications**: client broken by recent refactor *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`45d5bf5`](https://github.com/JakeStanger/ironbar/commit/45d5bf5feb88d0854a41faa5890b56188b3e051c) - popups not accounting for monitor scaling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`474e1fe`](https://github.com/JakeStanger/ironbar/commit/474e1fe364ef70fa0afcff476034d5f307dcd54b) - **upower**: avoid panic on client init error *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`04a694e`](https://github.com/JakeStanger/ironbar/commit/04a694e2ad5998e82de8dd121cc2b486432c0a70) - fix latest clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c876904`](https://github.com/JakeStanger/ironbar/commit/c876904bda7bb51ef2d3ec1661281df75fad60be) - update `nix` crate to latest version *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dedb89b`](https://github.com/JakeStanger/ironbar/commit/dedb89bb027c4477620410d9103d64c3f2668517) - **popup**: rename `is_visible` to `visible` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a0cb01a`](https://github.com/JakeStanger/ironbar/commit/a0cb01ae5f2121581eb90f73b8f661862da12b03) - make `Ironbar#unique_id` `must_use` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b8fdd85`](https://github.com/JakeStanger/ironbar/commit/b8fdd8531e5516598f81e869b9284b8888f1d06b) - explicitly use `set_text` on non-pango labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9d12535`](https://github.com/JakeStanger/ironbar/commit/9d125353c45a7a8ce3fee43192364745a3fba931) - small tidy *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1899757`](https://github.com/JakeStanger/ironbar/commit/189975791f6424eca85fcfd76b796e5e9f9fb47f) - **mpris**: better logging, avoid panic on dbus error *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`c25440c`](https://github.com/JakeStanger/ironbar/commit/c25440cd3274cb7adf4e8a1c97b4bc88a53841b4) - update CHANGELOG.md for v0.15.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f7f991b`](https://github.com/JakeStanger/ironbar/commit/f7f991b2e68a19ff66387913b54127fd8808cc21) - **compiling**: fix wrong fedora package for pulse libs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c7743b2`](https://github.com/JakeStanger/ironbar/commit/c7743b28c68919e5bb1d8b9c53d63fb53fd3b081) - add rustdoc comments to all module options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7d19106`](https://github.com/JakeStanger/ironbar/commit/7d191065fc20e64befca64e8814aa86b2c654a7c) - add notes about nerd fonts *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9db0cbc`](https://github.com/JakeStanger/ironbar/commit/9db0cbcbdc561ba929c300cec92156c873c3c151) - **upower**: fix incorrect css selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`057fdff`](https://github.com/JakeStanger/ironbar/commit/057fdffc5f3219b60bbc1f095f88a9d8e3e8f750) - **examples**: fix incorrect cpu sensor name *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`076c2df`](https://github.com/JakeStanger/ironbar/commit/076c2df4a29bb3af2183dc1617f101e1e39d3fa4) - add fedora copr package to readme *(commit by [@victorvintorez](https://github.com/victorvintorez))*
- [`860a676`](https://github.com/JakeStanger/ironbar/commit/860a6767f144610d6c1809ddadd52e31c8d8d68d) - **upower**: add note to make clear upower is required *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.15.1] - 2024-05-05
Release to bump hyprland-rs version due to Hyprland v0.40 socket path breaking change.
@ -575,3 +685,5 @@ It also requires `lua-lgi` as a runtime dependency.
[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
[v0.16.0]: https://github.com/JakeStanger/ironbar/compare/v0.15.1...v0.16.0
[v0.16.1]: https://github.com/JakeStanger/ironbar/compare/v0.16.0...v0.16.1

3096
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "ironbar"
version = "0.16.0-pre"
edition = "2021"
version = "0.16.1"
edition = "2024"
license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar"
repository = "https://github.com/jakestanger/ironbar"
@ -10,36 +10,47 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features]
default = [
"bindmode+all",
"cli",
"cairo",
"clipboard",
"clock",
"config+all",
"custom",
"focused",
"http",
"ipc",
"keyboard+all",
"launcher",
"label",
"menu",
"music+all",
"network_manager",
"notifications",
"script",
"sys_info",
"tray",
"upower",
"volume",
"workspaces+all"
"workspaces+all",
]
cli = ["dep:clap", "ipc"]
ipc = ["dep:serde_json"]
cli = ["ipc"]
ipc = ["dep:serde_json", "dep:clap"]
http = ["dep:reqwest"]
bindmode = []
"bindmode+all" = ["bindmode+sway", "bindmode+hyprland"]
"bindmode+sway" = ["bindmode", "sway"]
"bindmode+hyprland" = ["bindmode", "hyprland"]
"config+all" = [
"config+json",
"config+yaml",
"config+toml",
"config+corn",
"config+ron",
"config+json",
"config+yaml",
"config+toml",
"config+corn",
"config+ron",
]
"config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"]
@ -49,15 +60,26 @@ http = ["dep:reqwest"]
cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["nix"]
clipboard = ["dep:rustix"]
clock = ["chrono"]
custom = []
focused = []
keyboard = ["dep:colpetto", "dep:evdev-rs", "dep:rustix", "futures-lite"]
"keyboard+all" = ["keyboard", "keyboard+sway", "keyboard+hyprland"]
"keyboard+sway" = ["keyboard", "sway"]
"keyboard+hyprland" = ["keyboard", "hyprland"]
label = []
launcher = []
music = ["regex"]
menu = []
music = ["dep:regex"]
"music+all" = ["music", "music+mpris", "music+mpd"]
"music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd-utils"]
@ -66,109 +88,113 @@ network_manager = ["futures-lite", "futures-signals", "zbus"]
notifications = ["zbus"]
sys_info = ["sysinfo", "regex"]
script = []
sys_info = ["dep:sysinfo"]
tray = ["system-tray"]
upower = ["upower_dbus", "zbus", "futures-lite"]
upower = ["zbus", "futures-lite"]
volume = ["libpulse-binding"]
workspaces = ["futures-lite"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland", "workspaces+niri"]
"workspaces+sway" = ["workspaces", "sway"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
"workspaces+niri" = ["workspaces", "niri"]
sway = ["swayipc-async", "futures-lite"]
niri = ["dep:serde_json"]
schema = ["dep:schemars"]
[dependencies]
# core
gtk = "0.18.1"
gtk-layer-shell = "0.8.1"
gtk = "0.18.2"
gtk-layer-shell = "0.8.2"
glib = "0.18.5"
tokio = { version = "1.39.2", features = [
"macros",
"rt-multi-thread",
"time",
"process",
"sync",
"io-util",
"net",
tokio = { version = "1.46.1", features = [
"macros",
"rt-multi-thread",
"time",
"process",
"sync",
"io-util",
"net",
"fs"
] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = { version = "0.2.0" , default-features = false }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing-error = { version = "0.2.1", default-features = false }
tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.3"
serde = { version = "1.0.204", features = ["derive"] }
indexmap = "2.3.0"
dirs = "5.0.1"
color-eyre = "0.6.5"
serde = { version = "1.0.219", features = ["derive"] }
indexmap = "2.10.0"
dirs = "6.0.0"
walkdir = "2.5.0"
notify = { version = "6.1.1", default-features = false }
notify = { version = "8.1.0", default-features = false }
wayland-client = "0.31.1"
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
wayland-protocols-wlr = { version = "0.3.8", features = ["client"] }
smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [
"calloop",
"calloop",
] }
universal-config = { version = "0.5.0", default-features = false }
ctrlc = "3.4.4"
cfg-if = "1.0.0"
universal-config = { version = "0.5.1", default-features = false }
ctrlc = "3.4.7"
cfg-if = "1.0.1"
# cli
clap = { version = "4.5.13", optional = true, features = ["derive"] }
# ipc
serde_json = { version = "1.0.122", optional = true }
clap = { version = "4.5.40", optional = true, features = ["derive"] }
# http
reqwest = { version = "0.12.5", default-features = false, features = ["default-tls", "http2"], optional = true }
reqwest = { version = "0.12.22", default-features = false, features = ["default-tls", "http2"], optional = true }
# cairo
lua-src = { version = "547.0.0", optional = true }
mlua = { version = "0.9.9", optional = true, features = ["luajit"] }
lua-src = { version = "548.1.1", optional = true }
mlua = { version = "0.10.5", optional = true, features = ["luajit", "send"] }
cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
# clipboard
nix = { version = "0.29.0", optional = true, features = ["event", "fs"] }
# clock
chrono = { version = "0.4.38", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
chrono = { version = "0.4.41", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
# keyboard
colpetto = { version = "0.6.0", features = ["tokio", "tracing"], optional = true }
evdev-rs = { version = "0.6.1", optional = true }
# music
mpd-utils = { version = "0.2.1", optional = true }
mpris = { version = "2.0.1", optional = true }
regex = { version = "1.11.1", default-features = false, features = [
"std",
], optional = true }
# network_manager
futures-signals = { version = "0.3.33", optional = true }
futures-signals = { version = "0.3.34", optional = true }
# sys_info
sysinfo = { version = "0.29.11", optional = true }
sysinfo = { version = "0.35.2", optional = true }
# tray
system-tray = { version = "0.2.0", optional = true }
# upower
upower_dbus = { version = "0.3.2", optional = true }
system-tray = { version = "0.7.0", features = ["dbusmenu-gtk3"], optional = true }
# volume
libpulse-binding = { version = "2.28.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.4.0-alpha.2", features = ["silent"], optional = true }
futures-util = { version = "0.3.30", optional = true }
libpulse-binding = { version = "2.30.1", optional = true }
# shared
futures-lite = { version = "2.3.0", optional = true } # network_manager, upower, workspaces
regex = { version = "1.10.6", default-features = false, features = [
"std",
], optional = true } # music, sys_info
zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower
futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces, keyboard
zbus = { version = "5.7.1", default-features = false, features = ["blocking-api", "tokio"], optional = true } # network_manager, notifications, upower
swayipc-async = { version = "2.1.0", optional = true } # workspaces, keyboard
hyprland = { version = "0.4.0-beta.2", optional = true } # workspaces, keyboard
rustix = { version = "1.0.7", default-features = false, features = ["std", "fs", "pipe", "event"], optional = true } # clipboard, input
serde_json = { version = "1.0.140", optional = true } # ipc, niri
# schema
schemars = { version = "0.8.21", optional = true }
schemars = { version = "0.8.22", optional = true }
# -- PATCH --
# temp fix for tracing-appender/time
time = "0.3.36"
[build-dependencies]
clap = { version = "4.5.40", features = ["derive"] }
clap_complete = "4.5.54"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM rust:latest
COPY .github/scripts/ubuntu_setup.sh /scripts/ubuntu_setup.sh
RUN /scripts/ubuntu_setup.sh
RUN rustup component add clippy

View file

@ -48,10 +48,10 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
## Features
- First-class support for Sway and Hyprland
- First-class support for Sway and Hyprland, and partial support for Niri
- Fully themeable with hot-loaded CSS
- Popups to show rich content
- Ability to create custom widgets, run scripts and embed dynamic content
- Ability to create custom widgets, run scripts and embed dynamic content (including via Lua)
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
- Support for multiple config languages
@ -119,8 +119,35 @@ A flake is included with the repo which can be used with Home Manager.
# And configure
programs.ironbar = {
enable = true;
config = {};
style = "";
systemd = true;
config = {
# An example:
monitors = {
DP-1 = {
anchor_to_edges = true;
position = "top";
height = 16;
start = [
{ type = "clock"; }
];
end = [
{
type = "tray";
icon_size = 16;
}
];
};
};
};
style = /* css */ ''
/* An example */
* {
font-family: Noto Sans Nerd Font, sans-serif;
font-size: 16px;
border: none;
border-radius: 0;
}
'';
package = inputs.ironbar;
features = ["feature" "another_feature"];
};

53
build.rs Normal file
View file

@ -0,0 +1,53 @@
// Importing from Ironbar modules brings in lots of things not used by the build script
// we can just globally suppress those.
#![allow(unused, dead_code)]
#[path = "src/cli.rs"]
mod cli;
#[path = "src/error.rs"]
mod error;
#[path = "src/ipc"]
mod ipc {
#[path = "commands.rs"]
mod commands;
#[path = "responses.rs"]
mod responses;
pub use commands::Command;
pub use responses::Response;
}
use clap::Command;
use clap::CommandFactory;
use clap_complete::Shell::{Bash, Fish, Zsh};
use clap_complete::generate_to;
use cli::Args;
use std::fs;
use std::path::PathBuf;
const NAME: &str = "ironbar";
fn generate_shell_completions(mut cmd: Command) -> std::io::Result<()> {
const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
let comp_dir = PathBuf::from(MANIFEST_DIR).join("target/completions");
fs::create_dir_all(&comp_dir)?;
for shell in [Bash, Fish, Zsh] {
generate_to(shell, &mut cmd, NAME, &comp_dir)?;
}
Ok(())
}
fn main() -> std::io::Result<()> {
let mut cmd = Args::command();
cmd.set_bin_name(NAME);
generate_shell_completions(cmd)?;
Ok(())
}

12
default.nix Normal file
View file

@ -0,0 +1,12 @@
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url =
lock.nodes.${nodeName}.locked.url
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
) {src = ./.;}).defaultNix

View file

@ -22,8 +22,12 @@ You also need rust; only the latest stable version is supported.
pacman -S gtk3 gtk-layer-shell
# for http support
pacman -S openssl
# for tray support
pacman -S libdbusmenu-gtk3
# for volume support
pacman -S libpulse
# for keyboard support
pacman -S libinput
# for lua/cairo support
pacman -S luajit lua51-lgi
```
@ -34,8 +38,12 @@ pacman -S luajit lua51-lgi
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
# for http support
apt install libssl-dev
# for tray support
apt install libdbusmenu-gtk3-dev
# for volume support
apt install libpulse-dev
# for keyboard support
apt install libinput-dev
# for lua/cairo support
apt install luajit-dev lua-lgi
```
@ -46,8 +54,12 @@ apt install luajit-dev lua-lgi
dnf install gtk3-devel gtk-layer-shell-devel
# for http support
dnf install openssl-devel
# for tray support
dnf install libdbusmenu-gtk3-devel
# for volume support
dnf install pulseaudio-libs-devel
# for keyboard support
dnf install libinput-devel
# for lua/cairo support
dnf install luajit-devel lua-lgi
```
@ -88,7 +100,13 @@ cargo build --release --no-default-features \
| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| custom | Enables the `custom` module. |
| focused | Enables the `focused` module. |
| keyboard | Enables the `keyboard` module without keyboard layout support. |
| keyboard+all | Enables the `keyboard` module with keyboard layout support for all compositors. |
| keyboard+sway | Enables the `keyboard` module with keyboard layout support for Sway. |
| keyboard+hyprland | Enables the `keyboard` module with keyboard layout support for Hyprland. |
| label | Enables the `label` module. |
| launcher | Enables the `launcher` module. |
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |
@ -96,15 +114,37 @@ cargo build --release --no-default-features \
| network_manager | Enables the `network_manager` module. |
| notifications | Enables the `notiications` module. |
| sys_info | Enables the `sys_info` module. |
| script | Enables the `script` module. |
| tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |
| volume | Enables the `volume` module. |
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
| workspaces+niri | Enables the `workspaces` module with support for Niri. |
| **Other** | |
| schema | Enables JSON schema support and the CLI `--print-schema` flag. |
## Shell completions
Compiling Ironbar will produce shell completions for bash, zsh and fish; these can be found in `target/completions`.
You can install these as follows:
Bash:
```shell
install -Dm644 completions/ironbar.bash /usr/share/bash-completion/completions/ironbar
```
Zsh:
```shell
install -Dm644 completions/_ironbar /usr/share/zsh/site-functions/_ironbar
```
Fish:
```shell
install -Dm644 completions/ironbar.fish /usr/share/fish/vendor_completions.d/ironbar.fish
```
## Speeding up compiling
@ -164,4 +204,4 @@ codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
```
```

View file

@ -280,10 +280,12 @@ Check [here](config) for an example config file for a fully configured bar in ea
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|--------------------|-----------------------------------------|---------|---------------------------------------------------------------|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
| Name | Type | Default | Description |
|--------------------|-----------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `icon_overrides` | `Map<string, string>` | `{}` | Map of image inputs to override names. Usually used for app IDs (or classes) to icon names, overriding the app's default icon. |
> [!TIP]
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).
@ -308,7 +310,6 @@ The following table lists each of the bar-level bar config options:
| `layer` | `background` or `bottom` or `top` or `overlay` | `top` | The layer-shell layer to place the bar on. |
| `exclusive_zone` | `boolean` | `true` unless `start_hidden` is enabled. | Whether the bar should reserve an exclusive zone around it. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `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. |
@ -350,7 +351,14 @@ For information on the `Script` type, and embedding scripts in strings, see [her
| Name | Type | Default | Description |
|-----------|----------|---------|-----------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
| `name` | `string` | `null` | The unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | 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).
#### Formatting
| Name | Type | Default | Description |
|---------------|--------------------------------------------------------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `orientation` | `horizontal` or `vertical` (shorthand: `'h'` or `'v'`) | `horizontal` or `vertical` | The direction in which the widget and its text are laid out. Some modules additionally provide a `direction` option to provide further control. |
| `justify` | `left`, `right`, `center`, `fill` | `left` | The justification (alignment) of the widget text shown on the bar. |

View file

@ -10,7 +10,8 @@ You can also view help per sub-command or command, for example using `ironbar va
The CLI supports plaintext and JSON output. Plaintext will:
- Print `ok` for empty success responses
- Print the returned body for success responses
- Print the returned body for each success response
- Some commands act on multiple objects, in which case the CLI will print one line for each body.
- Print `error` to followed by the error on the next line for error responses. This is printed to `stderr`.
Example:
@ -150,6 +151,9 @@ Each key/value pair is on its own `\n` separated newline. The key and value are
### `bar`
> [!NOTE]
> If there are multiple bars by the same name, the `bar` subcommand will act on all of them and return a `multi` response for commands that get a value.
#### `show`
Forces a bar to be shown, regardless of the current visibility state.
@ -324,6 +328,17 @@ The operation completed successfully, with response data.
}
```
### `multi`
The operation completed successfully on multiple objects, with response data.
```json
{
"type": "multi",
"values": ["lorem ipsum", "dolor sit"]
}
```
### `error`
The operation failed.
@ -335,4 +350,4 @@ Message is optional.
"type": "error",
"message": "lorem ipsum"
}
```
```

View file

@ -25,7 +25,7 @@ Dynamic booleans can use a single source of either a script or variable to contr
For scripts, you can just write these directly with no notation.
Only polling scripts are supported.
The script exit code is used, where `0` is `true` and any other code is `false.
The script exit code is used, where `0` is `true` and any other code is `false`.
For variables, use the standard `#name` notation.
An empty string, `0` and `false` are treated as false.

50
docs/GTK4.md Normal file
View file

@ -0,0 +1,50 @@
As the GTK3 and gtk-layer-shell crates are now deprecated, there is a need to move to GTK 4.
The `refactor/gtk-4` branch and PR [#112](https://github.com/JakeStanger/ironbar/pull/112) are tracking the code upgrade.
This page documents the port progress.
Assistance in the porting process is very much welcomed, no matter how small.
As many modules have not been ported, the default feature set will fail to compile.
It is therefore necessary to compile manually with `--no-default-features`, enabling only the working modules:
```shell
cargo run --no-default-features \
--features config+all,clock,cairo
```
A full list of feature flags can be found [here](Compiling#features).
## Core functionality
| Area | Status | Notes |
|-----------------|--------|-------------------------------------------------------------------------------------------------------|
| Bar | ✅ | |
| Popups | ✅ | Potential styling issues, otherwise working. |
| Theming - CSS | ✅ | |
| Theming - Icons | ⚠️ | GTK4 does not support icon theming - always uses default theme. Image scaling may be incorrect. |
| Config - Format | ❌ | Angle/justify properties have been removed from widgets and should now be controlled via CSS instead. |
| IPC | ⚠️ | Some popup-related commands not implemented. |
## Modules
| Module | Status | Notes |
|-----------------|--------|------------------------------------------------------------------------------------------------------------------------------------------|
| Bindmode | ❌ | |
| Cairo | ✅ | |
| Clipboard | ✅ | |
| Clock | ✅ | |
| Custom | ✅ | |
| Focused | ✅ | |
| Keyboard | ✅ | |
| Label | ✅ | |
| Launcher | ⚠️ | Popup behaviour may be a little strange. |
| Music | ✅ | |
| Network Manager | ❌ | |
| Notifications | ✅ | |
| Script | ✅ | |
| SysInfo | ✅ | |
| Tray | ❌ | GTK4 removes widgets required to move the tray. No `libdbusmenu-gtk4` either. will need to manually re-create menus with custom widgets. |
| UPower | ❌ | |
| Volume | ❌ | |
| Workspaces | ✅ | |

View file

@ -6,4 +6,19 @@ Any UTF-8 string is a valid value.
Reference values using `#my_variable`. These update as soon as the value changes.
You can set defaults using the `ironvar_defaults` key in your top-level config.
You can set defaults using the `ironvar_defaults` key in your top-level config.
Some modules (such as `sys_info`) expose their values over the Ironvar interface,
allowing you to build custom interfaces and integrate into scripts.
These present their values inside read-only namespaces.
Some examples below:
```shell
ironbar var list
ironbar var list sysinfo
ironbar var list sysinfo.disk_percent
ironbar var get sysinfo.disk_percent./home
ironbar var get sysinfo.disk_percent.mean
ironbar var get sysinfo.memory_percent
```

View file

@ -4,6 +4,7 @@
- [Configuration guide](configuration-guide)
- [Images](images)
- [Styling guide](styling-guide)
- [GTK 4 Port](gtk4)
# Dynamic content
@ -24,13 +25,16 @@
# Modules
- [Bindmode](bindmode)
- [Cairo](cairo)
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)
- [Focused](focused)
- [Keyboard](keyboard)
- [Label](label)
- [Launcher](launcher)
- [Menu](menu)
- [Music](music)
- [Network Manager](network-manager)
- [Notifications](notifications)

78
docs/modules/Bindmode.md Normal file
View file

@ -0,0 +1,78 @@
> [!IMPORTANT]
> This module is currently only available on Sway and Hyprland.
Displays Sway's current binding mode or [Hyprland's current submap](https://wiki.hyprland.org/Configuring/Binds/#submaps)
in a label. Nothing is displayed if no binding mode is active.
## Configuration
> Type: `bindmode`
| Name | Type | Default | Description |
| --------------------- | ------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "bindmode",
"truncate": "start"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "bindmode"
truncate = "start"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "bindmode"
truncate: "start"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "bindmode"
truncate = "start"
}
]
}
```
</details>
## Styling
| Selector | Description |
| ----------- | ---------------------- |
| `.bindmode` | Bind mode label widget |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -12,15 +12,15 @@ Supports plain text and images.
> Type: `clipboard`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string` or [image](images) | `󰨸` | Icon to show on the widget button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| Name | Type | Default | Description |
|-----------------------|------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string` or [image](images) | `󰨸` | Icon to show on the widget button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>

View file

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

View file

@ -47,6 +47,8 @@ A container to place nested widgets inside.
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this box. |
| `halign` | `'start'` or `'center'` or `'end'` or `'fill'` | `'fill'` | The horizontal alignment of the box within its parent container. |
| `valign` | `'start'` or `'center'` or `'end'` or `'fill'` | `'fill'` | The vertical alignment of the box within its parent container. |
#### Label
@ -54,10 +56,16 @@ A text label. Pango markup is supported.
> Type `label`
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the label. |
| Name | Type | Default | Description |
|-----------------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the label text. |
| `justify` | `'left'`, `'right'`, `'center'`, or `'fill'` | `'left'` | Justification (alignment) of the label text. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
#### Button
@ -65,12 +73,13 @@ A clickable button, which can run a command when clicked.
> Type `button`
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|--------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. Ignored if `widgets` is set. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this button. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the button. |
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. Ignored if `widgets` is set. |
| `widgets` | `(Module or Widget)[]` | `[]` | List of modules/widgets to add to this button. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the label text. |
| `justify` | `'left'`, `'right'`, `'center'`, or `'fill'` | `'left'` | Justification (alignment) of the label text. |
#### Image
@ -425,4 +434,4 @@ The following top-level selectors are always available:
| `.custom` | Custom widget container. |
| `.popup-custom` | Custom widget popup container. |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -10,15 +10,15 @@ Displays the title and/or icon of the currently focused window.
> Type: `focused`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| Name | Type | Default | Description |
|-----------------------|------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>

122
docs/modules/Keyboard.md Normal file
View file

@ -0,0 +1,122 @@
> [!NOTE]
> This module requires your user is in the `input` group.
> [!IMPORTANT]
> The keyboard layout feature is only available on Sway and Hyprland.
Displays the toggle state of the capslock, num lock and scroll lock keys, and the current keyboard layout.
![Screenshot of keyboard widget](https://f.jstanger.dev/github/ironbar/keys.png)
## Configuration
> Type: `keyboard`
| Name | Type | Default | Description |
| ------------------ | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `show_caps` | `boolean` | `true` | Whether to show capslock indicator. |
| `show_num` | `boolean` | `true` | Whether to show num lock indicator. |
| `show_scroll` | `boolean` | `true` | Whether to show scroll lock indicator. |
| `show_layout` | `boolean` | `true` | Whether to show the keyboard layout button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `icons.caps_on` | `string` or [image](images) | `󰪛` | Icon to show for enabled capslock indicator. |
| `icons.caps_off` | `string` or [image](images) | `''` | Icon to show for disabled capslock indicator. |
| `icons.num_on` | `string` or [image](images) | `` | Icon to show for enabled num lock indicator. |
| `icons.num_off` | `string` or [image](images) | `''` | Icon to show for disabled num lock indicator. |
| `icons.scroll_on` | `string` or [image](images) | `` | Icon to show for enabled scroll lock indicator. |
| `icons.scroll_off` | `string` or [image](images) | `''` | Icon to show for disabled scroll lock indicator. |
| `icons.layout_map` | `Map<string, string or image>` | `{}` | Map of icons or labels to show for a particular keyboard layout. Layouts use their actual name if not present in the map. |
| `seat` | `string` | `seat0` | ID of the Wayland seat to attach to. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "keyboard",
"show_scroll": false,
"icons": {
"caps_on": "󰪛",
"layout_map": {
"English (US)": "🇺🇸",
"Ukrainian": "🇺🇦"
}
}
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "keyboard"
show_scroll = false
[end.icons]
caps_on = "󰪛"
[end.icons.layout_map]
"English (US)" = "🇺🇸"
Ukrainian = "🇺🇦"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: keyboard
show_scroll: false
icons:
caps_on: 󰪛
layout_map:
"English (US)": 🇺🇸
Ukrainian: 🇺🇦
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "keyboard"
show_scroll = false
icons.caps_on = "󰪛"
icons.layout_map.'English (US)' = "🇺🇸"
icons.layout_map.Ukrainian = "🇺🇦"
}
]
}
```
</details>
## Styling
| Selector | Description |
| -------------------------- | ------------------------------------------ |
| `.keyboard` | Keys box container widget. |
| `.keyboard .key` | Individual key indicator container widget. |
| `.keyboard .key.enabled` | Key indicator where key is toggled on. |
| `.keyboard .key.caps` | Capslock key indicator. |
| `.keyboard .key.num` | Num lock key indicator. |
| `.keyboard .key.scroll` | Scroll lock key indicator. |
| `.keyboard .key.image` | Key indicator image icon. |
| `.keyboard .key.text-icon` | Key indicator textual icon. |
| `.keyboard .layout` | Keyboard layout indicator. |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -7,9 +7,14 @@ For more advanced use-cases, use [custom](custom).
> Type: `label`
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
| Name | Type | Default | Description |
|-----------------------|------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>

View file

@ -3,7 +3,8 @@
Windows-style taskbar that displays running windows, grouped by program.
Hovering over a program with multiple windows open shows a popup with each window.
Clicking an icon/popup item focuses or launches the program.
Left clicking an icon/popup item focuses the program if it has any open instances or otherwise launches a new instance of the program.
Middle clicking an icon always launches a new instance of the program.
Optionally displays a launchable set of favourites.
![Screenshot showing several open applications, including a popup showing multiple terminal windows.](https://f.jstanger.dev/github/ironbar/launcher.png)
@ -12,13 +13,24 @@ Optionally displays a launchable set of favourites.
> Type: `launcher`
| | Type | Default | Description |
|--------------|------------|---------|-----------------------------------------------------------------------------------------------------|
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
| | Type | Default | Description |
|-----------------------------|---------------------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher. |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `launch_command` | `string` | `gtk-launch {app_name}` | Command used to launch applications. |
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
| `minimize_focused` | `boolean` | `true` | Whether to minimize a focused window when its icon is clicked. Only minimizes single windows. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `end` | Location of the ellipses and where to truncate text from. Applies to application names when `show_names` is enabled. |
| `truncate.length` | `integer` | `null` | Fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | Maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `truncate_popup.mode` | `'start'` or `'middle'` or `'end'` or `off` | `middle` | Location of the ellipses and where to truncate text from. Applies to window names within a group popup. |
| `truncate_popup.length` | `integer` | `null` | Fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate_popup.max_length` | `integer` | `25` | Maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `page_size` | `integer` | `1000` | Number of items to show on a page. When the number of items is reached, controls appear which can be used to move forward/back through the list of items. |
| `icons.page_back` | `string` or [image](images) | `󰅁` | Icon to show for page back button. |
| `icons.page_forward` | `string` or [image](images) | `󰅂` | Icon to show for page forward button. |
<details>
<summary>JSON</summary>
@ -94,14 +106,17 @@ start:
## Styling
| Selector | Description |
|-------------------------------|--------------------------|
| `.launcher` | Launcher widget box |
| `.launcher .item` | App button |
| `.launcher .item.open` | App button (open app) |
| `.launcher .item.focused` | App button (focused app) |
| `.launcher .item.urgent` | App button (urgent app) |
| `.popup-launcher` | Popup container |
| `.popup-launcher .popup-item` | Window button in popup |
| Selector | Description |
|--------------------------------------|---------------------------|
| `.launcher` | Launcher widget box |
| `.launcher .item` | App button |
| `.launcher .item.open` | App button (open app) |
| `.launcher .item.focused` | App button (focused app) |
| `.launcher .item.urgent` | App button (urgent app) |
| `.launcher .pagination` | Pagination controls box |
| `.launcher .pagination .btn-back` | Pagination back button |
| `.launcher .pagination .btn-forward` | Pagination forward button |
| `.popup-launcher` | Popup container |
| `.popup-launcher .popup-item` | Window button in popup |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

158
docs/modules/Menu.md Normal file
View file

@ -0,0 +1,158 @@
Application menu that shows installed programs and optionally custom entries.
This works by reading all `.desktop` files on the system.
Clicking the menu button will open the main menu.
Clicking on any application category will open a sub-menu with any installed applications that match.
It is also possible to add custom categories and actions into the menu.
![Screenshot of open menu showing applications inside Office category](https://f.jstanger.dev/github/ironbar/menu.png)
## Configuration
| | Type | Default | Description |
|-----------------------|------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| `start` | `MenuEntry[]` | `[]` | Items to add to the start of the main menu. |
| `center` | `MenuEntry[]` | Default XDG menu | Items to add to the centre of the main menu. By default this shows a number of XDG entries that should cover all common applications. |
| `end` | `MenuEntry[]` | `[]` | Items to add to the end of the main menu. |
| `height` | `integer` | `null` | Height of the menu. Leave null to resize dynamically. |
| `width` | `integer` | `null` | Width of the menu. Leave null to resize dynamically. |
| `label` | `string` | `≡` | Label to show on the menu button on the bar. |
| `label_icon` | `string` | `null` | Icon to show on the menu button on the bar. |
| `label_icon_size` | `integer` | `16` | Size of the label_icon image. |
| `launch_command` | `string` | `gtk-launch {app_name}` | Command used to launch applications. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | Applies to popup. The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | Applies to popup. The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | Applies to popup. The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | Applies to popup. The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
### `MenuEntry`
Each entry can be one of three types:
- `xdg_entry` - Contains all applications matching the configured `categories`.
- `xdg_other` - Contains all applications not covered by `xdg_entry` categories.
- `custom` - Individual shell command entry.
| | Type | Default | Description |
|--------------|----------------------------------------|---------|----------------------------------------------------------------------------------------|
| `type` | `xdg_entry` or `xdg_other` or `custom` | | Type of the entry. |
| `label` | `string` | `''` | Label of the entry's button. |
| `icon` | `string` | `null` | Icon for the entry's button. |
| `categories` | `string[]` | `[]` | [`xfg_entry`] List of freedesktop.org categories to include in this entry's sub menu . |
| `on_click` | `string` | `''` | [`custom`] Shell command to execute when the entry's button is clicked |
### Default XDG Menu
Setting the `center` menu entries will override the default menu.
The default menu can be found in the `default` example files [here](https://github.com/jakestanger/ironbar/blob/examples/menu/).
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "menu",
"start": [
{
"type": "custom",
"label": "Terminal",
"on_click": "xterm"
}
],
"height": 440,
"width": 200,
"icon": "archlinux",
"label": null
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "memu"
height = 400
width = 200
icon = "archlinux"
[[start.start]]
type = "custom"
label = "Terminal"
on_click = "xterm"
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "menu"
start:
- type: custom
label: Terminal
on_click: xterm
height: 440
width: 200
icon: archlinux
label: null
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "menu"
start = [
{
type = "custom"
label = "Terminal"
on_click = "xterm"
}
]
height = 440
width = 200
icon = "archlinux"
label = null
}
]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------------------|-----------------------------------|
| `.menu` | Menu button |
| `.popup-menu` | Main container of the popup |
| `.popup-menu .main` | Main menu of the menu |
| `.popup-menu .main .category` | Category button |
| `.popup-menu .main .category.open` | Open category button |
| `.popup-menu .main .main-start` | Container for `start` entries |
| `.popup-menu .main .main-center` | Container for `center` entries |
| `.popup-menu .main .main-end` | Container for `end` entries |
| `.popup-menu .sub-menu` | All sub-menus |
| `.popup-menu .sub-menu .application` | Application button within submenu |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -11,29 +11,39 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
> Type: `music`
| | Type | Default | Description |
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
| `icons.prev` | `string` or [image](images) | `󰒮` | Icon to show on previous button. |
| `icons.next` | `string` or [image](images) | `󰒭` | Icon to show on next button. |
| `icons.volume` | `string` or [image](images) | `󰕾` | Icon to show under popup volume slider. |
| `icons.track` | `string` or [image](images) | `󰎈` | Icon to show next to track title. |
| `icons.album` | `string` or [image](images) | `󰀥` | Icon to show next to album name. |
| `icons.artist` | `string` or [image](images) | `󰠃` | Icon to show next to artist name. |
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
See [here](images) for information on images.
| | Type | Default | Description |
|------------------------------------|------------------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `truncate_popup_artist` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate_popup_artist.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate_popup_artist.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate_popup_artist.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `truncate_popup_album` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate_popup_album.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate_popup_album.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate_popup_album.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `truncate_popup_title` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate_popup_title.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate_popup_title.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate_popup_title.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
| `icons.prev` | `string` or [image](images) | `󰒮` | Icon to show on previous button. |
| `icons.next` | `string` or [image](images) | `󰒭` | Icon to show on next button. |
| `icons.volume` | `string` or [image](images) | `󰕾` | Icon to show under popup volume slider. |
| `icons.track` | `string` or [image](images) | `󰎈` | Icon to show next to track title. |
| `icons.album` | `string` or [image](images) | `󰀥` | Icon to show next to album name. |
| `icons.artist` | `string` or [image](images) | `󰠃` | Icon to show next to artist name. |
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
<details>
<summary>JSON</summary>
@ -170,4 +180,4 @@ and will be replaced with values from the currently playing track:
| `.popup-music .progress .slider` | Slider inside progress container |
| `.popup-music .progress .label` | Duration label inside progress container |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -17,6 +17,9 @@ Supports wired ethernet, wifi, cellular data and VPN connections among others.
|-------------|-----------|---------|-------------------------|
| `icon_size` | `integer` | `24` | Size to render icon at. |
> [!NOTE]
> This module does not support module-level [layout options](module-level-options#layout).
<details>
<summary>JSON</summary>

View file

@ -21,6 +21,8 @@ Clicking the widget opens the SwayNC panel.
| `icons.open_some` | `string` | `󱥁` | Icon to show when the panel is open, with notifications. |
| `icons.open_dnd` | `string` | `󱅮` | Icon to show when the panel is open, with DnD enabled. Takes higher priority than count-based icons. |
> [!NOTE]
> This module does not support module-level [layout options](module-level-options#layout).
<details>
<summary>JSON</summary>

View file

@ -3,6 +3,8 @@ Displays one or more labels containing system information.
Separating information across several labels allows for styling each one independently.
Pango markup is supported.
Options can be provided in a token to specify operations, units and formatting.
![Screenshot showing sys-info module with widgets for all of the types of formatting tokens](https://user-images.githubusercontent.com/5057870/196059090-4056d083-69f0-4e6f-9673-9e35dc29d9f0.png)
@ -10,17 +12,17 @@ Pango markup is supported.
> Type: `sys_info`
| Name | Type | Default | Description |
|--------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. |
| `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. |
| `interval.memory` | `integer` | `5` | Seconds between refreshing memory data |
| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data |
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. |
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). |
| Name | Type | Default | Description |
|--------------------|------------------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. |
| `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. |
| `interval.memory` | `integer` | `5` | Seconds between refreshing memory data. |
| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data. |
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data. |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data. |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. |
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). |
<details>
<summary>JSON</summary>
@ -30,12 +32,11 @@ Pango markup is supported.
"end": [
{
"format": [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰖡 {load_average_1} | {load_average_5} | {load_average_15}",
"󰥔 {uptime}"
],
"interval": {
@ -60,13 +61,12 @@ Pango markup is supported.
[[end]]
type = 'sys_info'
format = [
' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C',
' {memory_used} / {memory_total} GB ({memory_percent}%)',
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
'󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
'󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
'󰖡 {load_average:1} | {load_average:5} | {load_average:15}',
'󰥔 {uptime}',
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰖡 {load_average_1} | {load_average_5} | {load_average_15}",
"󰥔 {uptime}"
]
[end.interval]
@ -87,13 +87,12 @@ temps = 5
```yaml
end:
- format:
- ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C'
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- '󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
- '󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps'
- '󰖡 {load_average:1} | {load_average:5} | {load_average:15}'
- '󰥔 {uptime}'
- " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
- " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
- "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
- "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
- "󰖡 {load_average_1} | {load_average_5} | {load_average_15}"
- "󰥔 {uptime}"
interval:
cpu: 1
disks: 300
@ -121,12 +120,11 @@ end:
interval.networks = 3
format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}"
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
"󰖡 {load_average_1} | {load_average_5} | {load_average_15}"
"󰥔 {uptime}"
]
}
@ -138,39 +136,179 @@ end:
### Formatting Tokens
The following tokens can be used in the `format` configuration option:
The below table lists the tokens which can be used in the `format` configuration option.
More information about each of these and the additional options can be found further below.
| Token | Description |
|--------------------------|------------------------------------------------------------------------------------|
| **CPU** | |
| `{cpu_percent}` | Total CPU utilisation percentage |
| **Memory** | |
| `{memory_free}` | Memory free in GB. |
| `{memory_used}` | Memory used in GB. |
| `{memory_total}` | Memory total in GB. |
| `{memory_percent}` | Memory utilisation percentage. |
| `{swap_free}` | Swap free in GB. |
| `{swap_used}` | Swap used in GB. |
| `{swap_total}` | Swap total in GB. |
| `{swap_percent}` | Swap utilisation percentage. |
| **Temperature** | |
| `{temp_c:[sensor]}` | Temperature in degrees C. Replace `[sensor]` with the sensor label. |
| `{temp_f:[sensor]}` | Temperature in degrees F. Replace `[sensor]` with the sensor label. |
| **Disk** | |
| `{disk_free:[mount]}` | Disk free space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_used:[mount]}` | Disk used space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_total:[mount]}` | Disk total space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_percent:[mount]}` | Disk utilisation percentage. Replace `[mount]` with the disk mountpoint. |
| **Network** | |
| `{net_down:[adapter]}` | Average network download speed in Mbps. Replace `[adapter]` with the adapter name. |
| `{net_up:[adapter]}` | Average network upload speed in Mbps. Replace `[adapter]` with the adapter name. |
| **System** | |
| `{load_average:1}` | 1-minute load average. |
| `{load_average:5}` | 5-minute load average. |
| `{load_average:15}` | 15-minute load average. |
| `{uptime}` | System uptime formatted as `HH:mm`. |
| Token | Default Function | Default Unit | Default Formatting |
|--------------------------|------------------|--------------|--------------------|
| **CPU** | | | |
| `{cpu_frequency[#core]}` | `mean` | MHz | `.2` |
| `{cpu_percent[#core]}` | `mean` | % | `0<2` |
| **Memory** | | | |
| `{memory_free}` | N/A | GB | `0<4.1` |
| `{memory_available}` | N/A | GB | `0<4.1` |
| `{memory_used}` | N/A | GB | `0<4.1` |
| `{memory_total}` | N/A | GB | `0<4.1` |
| `{memory_percent}` | N/A | GB | `0<4.1` |
| `{swap_free}` | N/A | GB | `0<4.1` |
| `{swap_used}` | N/A | GB | `0<4.1` |
| `{swap_total}` | N/A | GB | `0<4.1` |
| `{swap_percent}` | N/A | GB | `0<4.1` |
| **Temperature** | | | |
| `{temp_c[#sensor]}` | `max` | °C | |
| `{temp_f[#sensor]}` | `max` | °F | |
| **Disk** | | | |
| `{disk_free[#mount]}` | `sum` | GB | |
| `{disk_used[#mount]}` | `sum` | GB | |
| `{disk_total[#mount]}` | `sum` | GB | |
| `{disk_percent[#mount]}` | `sum` | % | |
| `{disk_read[#mount]}` | `sum` | MB/s | |
| `{disk_write[#mount]}` | `sum` | MB/s | |
| **Network** | | | |
| `{net_down[#adapter]}` | `sum` | Mb/s | |
| `{net_up[#adapter]}` | `sum` | Mb/s | |
| **System** | | | |
| `{load_average_1}` | N/A | - | `.2` |
| `{load_average_5}` | N/A | - | `.2` |
| `{load_average_15}` | N/A | - | `.2` |
| `{uptime}` | N/A | ??? | ??? |
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp-Tccd1`.
#### Functions and names
Many of the tokens operate on a value set, as opposed to an individual value:
- CPU tokens operate on each physical thread.
- Temperature tokens operate on each sensor.
- Disk tokens operate on each mount.
- Network tokens operate on each adapter.
By default, these will apply a function to the full set to reduce them down to a single value.
The list of available functions is shown below:
| Function | Description |
|----------|-----------------------------------------|
| `sum` | Adds each value in the set. |
| `min` | Gets the smallest value in the set. |
| `max` | Gets the largest value in the set. |
| `mean` | Gets the mean average value of the set. |
It is also possible to get only a single value from the set by specifying a name instead of a function.
| Token category | Valid name |
|----------------|-------------------------------------------------------------------------|
| CPU | A CPU thread, eg `cpu0`, `cpu1`, ... |
| Temperature | A sensor name, eg `CPUTIN`. These line up with the output of `sensors`. |
| Disk | A disk mountpoint, eg `/`, `/home`, ... |
| Network | An adapter name, eg `eth0` or `enp30s0`. |
To specify a name or function, use a `@`. For example, to show disk percent for `/home`:
```json
"{disk_percent@/home}%"
```
To show total CPU utilization where each core represents 100% (like `htop` etc):
```json
"{cpu_percent@sum}%"
```
#### Prefixes and units
For tokens which return an appropriate unit, you can specify the SI prefix (or unit in some special cases).
The following options can be supplied:
| Name | Value |
|---------|-------|
| Kilo | `k` |
| Mega | `M` |
| Giga | `G` |
| Tera | `T` |
| Peta | `P` |
| | |
| Kibi | `ki` |
| Mebi | `Mi` |
| Gibi | `Gi` |
| Tebi | `Ti` |
| Pebi | `Pi` |
| | |
| Kilobit | `kb` |
| Megabit | `Mb` |
| Gigabit | `Gb` |
To specify a prefix or unit, use a `#`. For example, to show free total disk space in terabytes:
```json
"{disk_free#T} TB"
```
#### Formatting
To control the formatting of the resultant number,
a subset of Rust's string formatting is implemented. This includes:
- Width
- Fill/Alignment
- Precision
Formatting is specified with a `:` and MUST be the last part of a token.
##### Width
The width controls the minimum string length of the value.
Specifying just a width will left-pad the value with `0` until the value reaches the target length.
The width can be any value from `1-9`. Larger values are not supported.
For example, to render CPU usage as `045%`:
```json
"{cpu_usage:3}%"
```
##### Fill/Alignment
These options can be used to control the `width` property.
To specify the fill and alignment, prefix the width with a character and a direction.
Fill characters can be any single UTF-8 character EXCEPT 1-9. Alignment must be one of:
- `<` - Left fill
- `^` - Center fill
- `>` - Right fill
For example, to render CPU usage as ` 45%`:
```json
"{cpu_usage: <3}%"
```
##### Precision
The number of decimal places a value is shown to can be controlled using precision.
Any value is supported.
To specify precision, include a `.` followed by the value. If other options are supplied, this MUST come after.
For example, to render used disk space to 2dp:
```json
"{disk_used:.2} GB"
```
---
#### Combining Options
Each of the token options can be combined to create more complex solutions.
Putting it all together, you could show the free disk space on your `/home` partition in terabytes,
left-padded with spaces to a min width of 5, and shown to 2dp as follows:
```json
"{disk_used@/home#T: <5.2} TB"
```
## Styling

View file

@ -6,11 +6,11 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
> Type: `tray`
| Name | Type | Default | Description |
|----------------------|-----------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
| `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. |
| Name | Type | Default | Description |
|----------------------|------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | Matches bar orientation | The direction in which to pack tray icons. |
| `icon_size` | `integer` | `16` | Size in pixels to display tray icons as. |
| `prefer_theme_icons` | `bool` | `true` | Requests that icons from the theme be used over the item-provided item. Most items only provide one or the other so this will have no effect in most circumstances. |
<details>
<summary>JSON</summary>
@ -55,12 +55,10 @@ end:
```corn
{
end = [
{
end = [{
type = "tray"
direction = "top_to_bottom"
}
]
}]
}
```

View file

@ -1,5 +1,8 @@
Displays system power information such as the battery percentage, and estimated time to empty.
> [!NOTE]
> This module requires that `upower` is installed and its service running.
`TODO: ADD SCREENSHOT`
[//]: # (![Screenshot]&#40;https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png&#41;)

View file

@ -1,6 +1,7 @@
Displays the current volume level.
Clicking on the widget opens a volume mixer, which allows you to change the device output level,
the default playback device, and control application volume levels individually.
Use `truncate` option to control the display of application titles in the volume mixer.
This requires PulseAudio to function (`pipewire-pulse` is supported).
@ -10,14 +11,18 @@ This requires PulseAudio to function (`pipewire-pulse` is supported).
> Type: `volume`
| Name | Type | Default | Description |
|-----------------------|----------|------------------------|----------------------------------------------------------------------------------------------------------------|
| `format` | `string` | `{icon} {percentage}%` | Format string to use for the widget button label. |
| `max_volume` | `float` | `100` | Maximum value to allow volume sliders to reach. Pulse supports values > 100 but this may result in distortion. |
| `icons.volume_high` | `string` | `󰕾` | Icon to show for high volume levels. |
| `icons.volume_medium` | `string` | `󰖀` | Icon to show for medium volume levels. |
| `icons.volume_low` | `string` | `󰕿` | Icon to show for low volume levels. |
| `icons.muted` | `string` | `󰝟` | Icon to show for muted outputs. |
| Name | Type | Default | Description |
|-----------------------|------------------------------------------------------|------------------------|----------------------------------------------------------------------------------------------------------------|
| `format` | `string` | `{icon} {percentage}%` | Format string to use for the widget button label. |
| `max_volume` | `float` | `100` | Maximum value to allow volume sliders to reach. Pulse supports values > 100 but this may result in distortion. |
| `icons.volume_high` | `string` | `󰕾` | Icon to show for high volume levels. |
| `icons.volume_medium` | `string` | `󰖀` | Icon to show for medium volume levels. |
| `icons.volume_low` | `string` | `󰕿` | Icon to show for low volume levels. |
| `icons.muted` | `string` | `󰝟` | Icon to show for muted outputs. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>
@ -29,6 +34,7 @@ This requires PulseAudio to function (`pipewire-pulse` is supported).
"type": "volume",
"format": "{icon} {percentage}%",
"max_volume": 100,
"truncate": "middle",
"icons": {
"volume_high": "󰕾",
"volume_medium": "󰖀",
@ -51,6 +57,7 @@ This requires PulseAudio to function (`pipewire-pulse` is supported).
type = "volume"
format = "{icon} {percentage}%"
max_volume = 100
truncate = "middle"
[end.icons]
volume_high = "󰕾"
@ -69,6 +76,7 @@ end:
- type: "volume"
format: "{icon} {percentage}%"
max_volume: 100
truncate: "middle"
icons:
volume_high: "󰕾"
volume_medium: "󰖀"
@ -88,6 +96,7 @@ end:
type = "volume"
format = "{icon} {percentage}%"
max_volume = 100
truncate = "end"
icons.volume_high = "󰕾"
icons.volume_medium = "󰖀"
icons.volume_low = "󰕿"
@ -125,4 +134,4 @@ The following tokens can be used in the `format` config option:
| `.popup-volume .apps-box .app-box .slider` | Application volume slider. |
| `.popup-volume .apps-box .app-box .btn-mute` | Application volume mute toggle button. |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -1,4 +1,5 @@
> ⚠ **This module is currently only supported on Sway and Hyprland**
> [!IMPORTANT]
> This module is currently only supported on Sway, Hyprland and Niri**
Shows all current workspaces. Clicking a workspace changes focus to it.
@ -8,14 +9,14 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
| Name | Type | Default | Description |
|----------------|---------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `'added'` or `'label'` or `'name'` | `label` | The method used for sorting workspaces. `added` always appends to the end, `label` sorts by displayed value, and `name` sorts by workspace name. |
<details>
<summary>JSON</summary>
@ -98,15 +99,16 @@ end:
## Styling
| Selector | Description |
|--------------------------------|--------------------------------------|
| `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) |
| Selector | Description |
| ------------------------------ | ------------------------------------------------------- |
| `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) |
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
| `.workspaces .item .icon` | Workspace button icon (any type) |
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) |
| `.workspaces .item.urgent` | Workspace button (workspace contains urgent window) |
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open) |
| `.workspaces .item .icon` | Workspace button icon (any type) |
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) |
For more information on styling, please see the [styling guide](styling-guide).

View file

@ -55,13 +55,12 @@ let {
interval.networks = 3
format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}"
"󰥔 {uptime}"
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
"󰖡 {load_average1} | {load_average5} | {load_average15}"
"󰥔 {uptime}"
]
}

View file

@ -1,5 +1,4 @@
{
"$schema": "https://f.jstanger.dev/github/ironbar/schema.json",
"anchor_to_edges": true,
"position": "bottom",
"icon_theme": "Paper",
@ -64,12 +63,11 @@
"networks": 3
},
"format": [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰖡 {load_average1} | {load_average5} | {load_average15}",
"󰥔 {uptime}"
]
},

View file

@ -53,12 +53,11 @@ interval = 500
[[end]]
type = "sys_info"
format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
" {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰖡 {load_average1} | {load_average5} | {load_average15}",
"󰥔 {uptime}",
]

View file

@ -1,4 +1,3 @@
$schema: https://f.jstanger.dev/github/ironbar/schema.json
anchor_to_edges: true
position: bottom
icon_theme: Paper
@ -44,12 +43,11 @@ end:
disks: 300
networks: 3
format:
-  {cpu_percent}% | {temp_c:k10temp-Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
- 󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps
- 󰖡 {load_average:1} | {load_average:5} | {load_average:15}
-  {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C
-  {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)
- 󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s
- 󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps
- 󰖡 {load_average1} | {load_average5} | {load_average15}
- 󰥔 {uptime}
- type: volume
format: '{icon} {percentage}%'

102
examples/menu/default.corn Normal file
View file

@ -0,0 +1,102 @@
let {
$menu = [
{
type = "xdg_entry"
label = "Accessories"
icon = "accessories"
categories = [
"Accessibility"
"Core"
"Legacy"
"Utility"
]
}
{
type = "xdg_entry"
label = "Development"
icon = "applications-development"
categories = [
"Development"
]
}
{
type = "xdg_entry"
label = "Education"
icon = "applications-education"
categories = [
"Education"
]
}
{
type = "xdg_entry"
label = "Games"
icon = "applications-games"
categories = [
"Games"
]
}
{
type = "xdg_entry"
label = "Graphics"
icon = "applications-graphics"
categories = [
"Graphics"
]
}
{
type = "xdg_entry"
label = "Multimedia"
icon = "applications-multimedia"
categories = [
"Audio"
"Video"
"AudioVideo"
]
}
{
type = "xdg_entry"
label = "Network"
icon = "applications-internet"
categories = [
"Network"
]
}
{
type = "xdg_entry"
label = "Office"
icon = "applications-office"
categories = [
"Office"
]
}
{
type = "xdg_entry"
label = "Science"
icon = "applications-science"
categories = [
"Science"
]
}
{
type = "xdg_entry"
label = "System"
icon = "applications-system"
categories = [
"Emulator"
"System"
]
}
{ type = "xdg_other" }
{
type = "xdg_entry"
label = "Settings"
icon = "preferences-system"
categories = [
"Settings"
"Screensaver"
]
}
]
} in {
start = [ { type = "menu" center = $menu } ]
}

107
examples/menu/default.json Normal file
View file

@ -0,0 +1,107 @@
{
"start": [
{
"type": "menu",
"center": [
{
"type": "xdg_entry",
"label": "Accessories",
"icon": "accessories",
"categories": [
"Accessibility",
"Core",
"Legacy",
"Utility"
]
},
{
"type": "xdg_entry",
"label": "Development",
"icon": "applications-development",
"categories": [
"Development"
]
},
{
"type": "xdg_entry",
"label": "Education",
"icon": "applications-education",
"categories": [
"Education"
]
},
{
"type": "xdg_entry",
"label": "Games",
"icon": "applications-games",
"categories": [
"Games"
]
},
{
"type": "xdg_entry",
"label": "Graphics",
"icon": "applications-graphics",
"categories": [
"Graphics"
]
},
{
"type": "xdg_entry",
"label": "Multimedia",
"icon": "applications-multimedia",
"categories": [
"Audio",
"Video",
"AudioVideo"
]
},
{
"type": "xdg_entry",
"label": "Network",
"icon": "applications-internet",
"categories": [
"Network"
]
},
{
"type": "xdg_entry",
"label": "Office",
"icon": "applications-office",
"categories": [
"Office"
]
},
{
"type": "xdg_entry",
"label": "Science",
"icon": "applications-science",
"categories": [
"Science"
]
},
{
"type": "xdg_entry",
"label": "System",
"icon": "applications-system",
"categories": [
"Emulator",
"System"
]
},
{
"type": "xdg_other"
},
{
"type": "xdg_entry",
"label": "Settings",
"icon": "preferences-system",
"categories": [
"Settings",
"Screensaver"
]
}
]
}
]
}

View file

@ -0,0 +1,87 @@
[[start]]
type = "menu"
[[start.center]]
type = "xdg_entry"
label = "Accessories"
icon = "accessories"
categories = [
"Accessibility",
"Core",
"Legacy",
"Utility",
]
[[start.center]]
type = "xdg_entry"
label = "Development"
icon = "applications-development"
categories = ["Development"]
[[start.center]]
type = "xdg_entry"
label = "Education"
icon = "applications-education"
categories = ["Education"]
[[start.center]]
type = "xdg_entry"
label = "Games"
icon = "applications-games"
categories = ["Games"]
[[start.center]]
type = "xdg_entry"
label = "Graphics"
icon = "applications-graphics"
categories = ["Graphics"]
[[start.center]]
type = "xdg_entry"
label = "Multimedia"
icon = "applications-multimedia"
categories = [
"Audio",
"Video",
"AudioVideo",
]
[[start.center]]
type = "xdg_entry"
label = "Network"
icon = "applications-internet"
categories = ["Network"]
[[start.center]]
type = "xdg_entry"
label = "Office"
icon = "applications-office"
categories = ["Office"]
[[start.center]]
type = "xdg_entry"
label = "Science"
icon = "applications-science"
categories = ["Science"]
[[start.center]]
type = "xdg_entry"
label = "System"
icon = "applications-system"
categories = [
"Emulator",
"System",
]
[[start.center]]
type = "xdg_other"
[[start.center]]
type = "xdg_entry"
label = "Settings"
icon = "preferences-system"
categories = [
"Settings",
"Screensaver",
]

68
examples/menu/default.yml Normal file
View file

@ -0,0 +1,68 @@
start:
- type: menu
center:
- type: xdg_entry
label: Accessories
icon: accessories
categories:
- Accessibility
- Core
- Legacy
- Utility
- type: xdg_entry
label: Development
icon: applications-development
categories:
- Development
- type: xdg_entry
label: Education
icon: applications-education
categories:
- Education
- type: xdg_entry
label: Games
icon: applications-games
categories:
- Games
- type: xdg_entry
label: Graphics
icon: applications-graphics
categories:
- Graphics
- type: xdg_entry
label: Multimedia
icon: applications-multimedia
categories:
- Audio
- Video
- AudioVideo
- type: xdg_entry
label: Network
icon: applications-internet
categories:
- Network
- type: xdg_entry
label: Office
icon: applications-office
categories:
- Office
- type: xdg_entry
label: Science
icon: applications-science
categories:
- Science
- type: xdg_entry
label: System
icon: applications-system
categories:
- Emulator
- System
- type: xdg_other
- type: xdg_entry
label: Settings
icon: preferences-system
categories:
- Settings
- Screensaver

View file

@ -201,6 +201,10 @@ scale trough {
background-color: @color_bg_dark;
}
.workspaces .item.urgent {
background-color: @color_urgent;
}
.workspaces .item:hover {
box-shadow: inset 0 -3px;
}

View file

@ -1,60 +0,0 @@
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 ]
}

86
flake.lock generated
View file

@ -1,22 +1,17 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"flake-compat": {
"locked": {
"lastModified": 1722704454,
"narHash": "sha256-lcut8uZMSa80z+aWpxg+9nM8BKWtpU59rtcpMXtHd1Q=",
"owner": "ipetkov",
"repo": "crane",
"rev": "852a59f9672c3413d75bca2b3e9cb4c661cacfc3",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
@ -25,11 +20,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1745925850,
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github"
},
"original": {
@ -38,27 +33,44 @@
"type": "github"
}
},
"nix-systems": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1722640603,
"narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=",
"lastModified": 1751251929,
"narHash": "sha256-IJWIzZSkBsDzS7iS/iwSwur+xFkWqeLYC4kdf8ObtOM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "81610abc161d4021b29199aa464d6a1a521e0cc9",
"rev": "b95255df2360a45ddbb03817a68869d5cb01bf96",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1722630782,
"narHash": "sha256-hMyG9/WlUi0Ho9VkRrrez7SeNlDzLxalm9FwY7n/Noo=",
"lastModified": 1751011381,
"narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d04953086551086b44b6f3c6b7eeb26294f207da",
"rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7",
"type": "github"
},
"original": {
@ -70,30 +82,10 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-compat": "flake-compat",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1722738111,
"narHash": "sha256-cWD5pCs9AYb+512/yCx9D0Pl5KcmyuXHeJpsDw/D1vs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "27ec296d93cb4b2d03e8cbd019b1b4cde8c34280",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
"nix-systems": "nix-systems",
"nixpkgs": "nixpkgs_2"
}
}
},

280
flake.nix
View file

@ -3,224 +3,88 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-compat.url = "github:edolstra/flake-compat";
naersk.url = "github:nix-community/naersk";
nix-systems.url = "github:nix-systems/default-linux";
};
outputs = { self, nixpkgs, rust-overlay, crane, naersk, ... }:
let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [ self.overlays.default rust-overlay.overlays.default ];
outputs = {
self,
nixpkgs,
naersk,
nix-systems,
...
}: let
forAllSystems = function:
nixpkgs.lib.genAttrs (import nix-systems) (system: function nixpkgs.legacyPackages.${system});
mkDate = longDate: (nixpkgs.lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
in {
# Devshell
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
packages = builtins.attrValues {
inherit
(pkgs)
cargo
clippy
rustfmt
gtk3
gtk-layer-shell
gcc
openssl
libdbusmenu-gtk3
libpulseaudio
libinput
libevdev
luajit
;
inherit (pkgs.luajitPackages) lgi;
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
nativeBuildInputs = [
pkgs.pkg-config
];
};
});
# Packages
packages = forAllSystems (pkgs: {
ironbar = let
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
version =
props.package.version
+ "+date="
+ (mkDate (self.lastModifiedDate or "19700101"))
+ "_"
+ (self.shortRev or "dirty");
naersk' = pkgs.callPackage naersk {};
in
pkgs.callPackage ./nix/package.nix {
inherit version;
naersk = naersk';
};
in {
overlays.default = final: prev:
let
rust = mkRustToolchain final;
default = self.packages.${pkgs.hostPlatform.system}.ironbar;
});
craneLib = (crane.mkLib final).overrideToolchain rust;
# Apps
apps = forAllSystems (pkgs: {
ironbar = {
type = "app";
program = pkgs.lib.getExe self.packages.${pkgs.hostPlatform.system}.ironbar;
};
default = self.apps.ironbar;
});
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date="
+ (mkDate (self.lastModifiedDate or "19700101")) + "_"
+ (self.shortRev or "dirty");
in if builder == "crane" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk" then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
};
};
packages = genSystems (system:
let pkgs = pkgsFor system;
in (self.overlays.default pkgs pkgs) // {
default = self.packages.${system}.ironbar;
});
apps = genSystems (system:
let pkgs = pkgsFor system;
in rec {
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
default = ironbar;
});
devShells = genSystems (system:
let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
luajit
luajitPackages.lgi
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = { config, lib, pkgs, ... }:
let
cfg = config.programs.ironbar;
defaultIronbarPackage =
self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json { };
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.either (lib.types.lines) (lib.types.path);
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 != "") (
if builtins.isPath cfg.style || lib.isStorePath cfg.style then
{ source = cfg.style; }
else
{ 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")
];
};
};
};
};
homeManagerModules.default = import ./nix/module.nix self;
};
nixConfig = {
extra-substituters = [ "https://cache.garnix.io" ];
extra-trusted-public-keys =
[ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ];
extra-substituters = ["https://cache.garnix.io"];
extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="];
};
}

View file

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

96
nix/module.nix Normal file
View file

@ -0,0 +1,96 @@
self: {
config,
lib,
pkgs,
...
}: let
cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json {};
inherit
(lib)
types
mkOption
mkEnableOption
mkIf
getExe
;
in {
options.programs.ironbar = {
enable = mkEnableOption "ironbar status bar";
package = mkOption {
type = types.package;
default = defaultIronbarPackage;
apply = pkg: pkg.override {features = cfg.features;};
description = "The package for ironbar to use.";
};
systemd = mkEnableOption "systemd service for ironbar.";
style = mkOption {
type = types.either (types.lines) (types.path);
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = mkOption {
type = jsonFormat.type;
default = null;
description = "The config to pass to ironbar.";
};
features = mkOption {
type = types.listOf types.nonEmptyStr;
default = [];
description = "The features to be used.";
};
};
config = mkIf cfg.enable {
home.packages = [
cfg.package
];
xdg.configFile = {
"ironbar/config.json" = mkIf (cfg.config != null) {
onChange = "${getExe cfg.package} reload";
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = mkIf (cfg.style != "") (
if builtins.isPath cfg.style || lib.isStorePath cfg.style
then {source = cfg.style;}
else {text = cfg.style;}
);
};
systemd.user.services.ironbar = mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Documentation = "https://github.com/JakeStanger/ironbar";
PartOf = [
config.wayland.systemd.target
"tray.target"
];
After = [config.wayland.systemd.target];
ConditionEnvironment = "WAYLAND_DISPLAY";
};
Service = {
ExecReload = "${getExe cfg.package} reload";
ExecStart = "${getExe cfg.package}";
KillMode = "mixed";
Restart = "on-failure";
};
Install.WantedBy = [
config.wayland.systemd.target
"tray.target"
(mkIf config.wayland.windowManager.hyprland.enable "hyprland-session.target")
(mkIf config.wayland.windowManager.sway.enable "sway-session.target")
(mkIf config.wayland.windowManager.river.enable "river-session.target")
];
};
};
}

152
nix/package.nix Normal file
View file

@ -0,0 +1,152 @@
{
gtk3,
gdk-pixbuf,
librsvg,
webp-pixbuf-loader,
gobject-introspection,
glib-networking,
glib,
shared-mime-info,
gsettings-desktop-schemas,
wrapGAppsHook,
gtk-layer-shell,
gnome,
libxkbcommon,
libdbusmenu-gtk3,
libpulseaudio,
libinput,
libevdev,
openssl,
luajit,
luajitPackages,
pkg-config,
installShellFiles,
adwaita-icon-theme,
hicolor-icon-theme,
lib,
version ? "git",
features ? [],
naersk,
}: let
hasFeature = f: features == [] || builtins.elem f features;
flags = let
noDefault =
if features == []
then ""
else "--no-default-features";
featuresStr =
if features == []
then ""
else ''-F "${builtins.concatStringsSep "," features}"'';
in [
noDefault
featuresStr
];
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]}"
''
+ lib.optionalString (hasFeature "cairo") ''
--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"
'';
in
naersk.buildPackage {
inherit version;
pname = "ironbar";
src = let
fs = lib.fileset;
root = ../.;
nixRelated = fs.fileFilter (file: file.hasExt "nix" || file.name == "flake.lock") root;
cicdRelated = fs.unions [
(lib.path.append root "Dockerfile")
(lib.path.append root ".github")
];
ideRelated = fs.unions [
(lib.path.append root ".idea")
];
in
fs.toSource {
inherit root;
# NOTE: can possibly filter out more
fileset = fs.difference root (
fs.unions [
nixRelated
cicdRelated
ideRelated
]
);
};
nativeBuildInputs = [
pkg-config
wrapGAppsHook
gobject-introspection
installShellFiles
];
buildInputs =
[
gtk3
gdk-pixbuf
glib
gtk-layer-shell
glib-networking
shared-mime-info
adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
]
++ lib.optionals (hasFeature "http") [openssl]
++ lib.optionals (hasFeature "tray") [libdbusmenu-gtk3]
++ lib.optionals (hasFeature "volume") [libpulseaudio]
++ lib.optionals (hasFeature "cairo") [luajit]
++ lib.optionals (hasFeature "keyboard") [
libinput
libevdev
];
propagatedBuildInputs = [gtk3];
cargoBuildOptions = old: old ++ flags;
preFixup = ''
gappsWrapperArgs+=(
${gappsWrapperArgs}
)
'';
postInstall = ''
installShellCompletion --cmd ironbar \
--bash target/completions/ironbar.bash \
--fish target/completions/ironbar.fish \
--zsh target/completions/_ironbar
'';
passthru = {
updateScript = gnome.updateScript {
packageName = "ironbar";
attrPath = "gnome.ironbar";
};
};
meta = {
homepage = "https://github.com/JakeStanger/ironbar";
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "ironbar";
};
}

View file

@ -1,20 +1,12 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
cargo
clippy
rustfmt
gtk3
gtk-layer-shell
gcc
openssl
libpulseaudio
luajit
luajitPackages.lgi
];
nativeBuildInputs = with pkgs; [
pkg-config
];
}
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url =
lock.nodes.${nodeName}.locked.url
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
) {src = ./.;}).shellNix

View file

@ -1,16 +1,16 @@
use crate::Ironbar;
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
use crate::popup::Popup;
use crate::Ironbar;
use color_eyre::Result;
use glib::Propagation;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
use gtk::{Application, ApplicationWindow, Orientation, Window, WindowType};
use gtk_layer_shell::LayerShell;
use std::rc::Rc;
use std::time::Duration;
use tracing::{debug, info};
use tracing::{debug, error, info};
#[derive(Debug, Clone)]
enum Inner {
@ -22,6 +22,7 @@ enum Inner {
pub struct Bar {
name: String,
monitor_name: String,
monitor_size: (i32, i32),
position: BarPosition,
ironbar: Rc<Ironbar>,
@ -41,6 +42,7 @@ impl Bar {
pub fn new(
app: &Application,
monitor_name: String,
monitor_size: (i32, i32),
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Self {
@ -89,6 +91,7 @@ impl Bar {
Self {
name,
monitor_name,
monitor_size,
position,
ironbar,
window,
@ -146,7 +149,7 @@ impl Bar {
}
}
let load_result = self.load_modules(config, monitor)?;
let load_result = self.load_modules(config, monitor, self.monitor_size)?;
self.show(!start_hidden);
@ -243,12 +246,12 @@ impl Bar {
}
/// Loads the configured modules onto a bar.
fn load_modules(&self, config: BarConfig, monitor: &Monitor) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
fn load_modules(
&self,
config: BarConfig,
monitor: &Monitor,
output_size: (i32, i32),
) -> Result<BarLoadResult> {
let app = &self.window.application().expect("to exist");
macro_rules! info {
@ -259,13 +262,17 @@ impl Bar {
monitor,
output_name: &self.monitor_name,
location: $location,
icon_theme: &icon_theme,
}
};
}
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = Popup::new(
&self.ironbar,
&info!(ModuleLocation::Left),
output_size,
config.popup_gap,
);
let popup = Rc::new(popup);
if let Some(modules) = config.start {
@ -333,7 +340,7 @@ impl Bar {
/// Sets the window visibility status
pub fn set_visible(&self, visible: bool) {
self.window.set_visible(visible)
self.window.set_visible(visible);
}
pub fn set_exclusive(&self, exclusive: bool) {
@ -374,7 +381,10 @@ fn add_modules(
let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
for config in modules {
config.create(&module_factory, content, info)?;
let name = config.name();
if let Err(err) = config.create(&module_factory, content, info) {
error!("failed to create module {name}: {:?}", err);
}
}
Ok(())
@ -384,9 +394,10 @@ pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: String,
monitor_size: (i32, i32),
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config, ironbar);
let bar = Bar::new(app, monitor_name, monitor_size, config, ironbar);
bar.init(monitor)
}

261
src/channels.rs Normal file
View file

@ -0,0 +1,261 @@
use crate::modules::ModuleUpdateEvent;
use crate::spawn;
use smithay_client_toolkit::reexports::calloop;
use std::fmt::Debug;
use tokio::sync::{broadcast, mpsc};
pub trait SyncSenderExt<T> {
/// Asynchronously sends a message on the channel,
/// panicking if it cannot be sent.
///
/// This should be used in cases where sending should *never* fail,
/// or where failing indicates a serious bug.
fn send_expect(&self, message: T);
}
impl<T> SyncSenderExt<T> for std::sync::mpsc::Sender<T> {
#[inline]
fn send_expect(&self, message: T) {
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
}
}
impl<T> SyncSenderExt<T> for calloop::channel::Sender<T> {
#[inline]
fn send_expect(&self, message: T) {
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
}
}
impl<T: Debug> SyncSenderExt<T> for broadcast::Sender<T> {
#[inline]
fn send_expect(&self, message: T) {
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
}
}
pub trait AsyncSenderExt<T>: Sync + Send + Sized + Clone {
/// Asynchronously sends a message on the channel,
/// panicking if it cannot be sent.
///
/// This should be used in cases where sending should *never* fail,
/// or where failing indicates a serious bug.
fn send_expect(&self, message: T) -> impl Future<Output = ()> + Send;
/// Asynchronously sends a message on the channel,
/// spawning a task to allow it to be sent in the background,
/// and panicking if it cannot be sent.
///
/// Note that this function will return *before* the message is sent.
///
/// This should be used in cases where sending should *never* fail,
/// or where failing indicates a serious bug.
#[inline]
fn send_spawn(&self, message: T)
where
Self: 'static,
T: Send + 'static,
{
let tx = self.clone();
spawn(async move { tx.send_expect(message).await });
}
/// Shorthand for [`AsyncSenderExt::send_expect`]
/// when sending a [`ModuleUpdateEvent::Update`].
#[inline]
async fn send_update<U: Clone>(&self, update: U)
where
Self: AsyncSenderExt<ModuleUpdateEvent<U>>,
{
self.send_expect(ModuleUpdateEvent::Update(update)).await;
}
/// Shorthand for [`AsyncSenderExt::send_spawn`]
/// when sending a [`ModuleUpdateEvent::Update`].
#[inline]
fn send_update_spawn<U>(&self, update: U)
where
Self: AsyncSenderExt<ModuleUpdateEvent<U>> + 'static,
U: Clone + Send + 'static,
{
self.send_spawn(ModuleUpdateEvent::Update(update));
}
}
impl<T: Send> AsyncSenderExt<T> for mpsc::Sender<T> {
#[inline]
async fn send_expect(&self, message: T) {
self.send(message)
.await
.expect(crate::error::ERR_CHANNEL_SEND);
}
}
pub trait MpscReceiverExt<T> {
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
/// in a loop, passing the message to `f`.
///
/// This allows use of `GObjects` and futures in the same context.#
///
/// `deps` is a single reference, or tuple of references of clonable objects,
/// to be consumed inside the closure.
/// This avoids needing to `element.clone()` everywhere.
fn recv_glib<D, Fn>(self, deps: D, f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) + 'static;
}
impl<T: 'static> MpscReceiverExt<T> for mpsc::Receiver<T> {
fn recv_glib<D, Fn>(mut self, deps: D, mut f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) + 'static,
{
let deps = deps.clone_content();
glib::spawn_future_local(async move {
while let Some(val) = self.recv().await {
f(&deps, val);
}
});
}
}
pub trait BroadcastReceiverExt<T>
where
T: Debug + Clone + 'static,
{
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
/// in a loop, passing the message to `f`.
///
/// This allows use of `GObjects` and futures in the same context.
///
/// `deps` is a single reference, or tuple of references of clonable objects,
/// to be consumed inside the closure.
/// This avoids needing to `element.clone()` everywhere.
fn recv_glib<D, Fn>(self, deps: D, f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) + 'static;
/// Like [`BroadcastReceiverExt::recv_glib`], but the closure must return a [`Future`].
fn recv_glib_async<D, Fn, F>(self, deps: D, f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) -> F + 'static,
F: Future;
}
impl<T> BroadcastReceiverExt<T> for broadcast::Receiver<T>
where
T: Debug + Clone + 'static,
{
fn recv_glib<D, Fn>(mut self, deps: D, mut f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) + 'static,
{
let deps = deps.clone_content();
glib::spawn_future_local(async move {
loop {
match self.recv().await {
Ok(val) => f(&deps, val),
Err(broadcast::error::RecvError::Lagged(count)) => {
tracing::warn!(
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
);
}
Err(err) => {
tracing::error!("{err:?}");
break;
}
}
}
});
}
fn recv_glib_async<D, Fn, F>(mut self, deps: D, mut f: Fn)
where
D: Dependency,
D::Target: 'static,
Fn: FnMut(&D::Target, T) -> F + 'static,
F: Future,
{
let deps = deps.clone_content();
glib::spawn_future_local(async move {
loop {
match self.recv().await {
Ok(val) => {
f(&deps, val).await;
}
Err(broadcast::error::RecvError::Lagged(count)) => {
tracing::warn!(
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
);
}
Err(err) => {
tracing::error!("{err:?}");
break;
}
}
}
});
}
}
/// `recv_glib` callback dependency
/// or dependency tuple.
pub trait Dependency: Clone {
type Target;
fn clone_content(&self) -> Self::Target;
}
impl Dependency for () {
type Target = ();
fn clone_content(&self) -> Self::Target {}
}
impl<'a, T> Dependency for &'a T
where
T: Clone + 'a,
{
type Target = T;
fn clone_content(&self) -> T {
T::clone(self)
}
}
macro_rules! impl_dependency {
($($idx:tt $t:ident),+) => {
impl<'a, $($t),+> Dependency for ($(&'a $t),+)
where
$($t: Clone + 'a),+
{
type Target = ($($t),+);
fn clone_content(&self) -> Self::Target {
($(self.$idx.clone()),+)
}
}
};
}
impl_dependency!(0 T1, 1 T2);
impl_dependency!(0 T1, 1 T2, 2 T3);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10, 10 T11);
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10, 10 T11, 11 T12);

View file

@ -1,6 +1,5 @@
use crate::error::ExitCode;
use crate::ipc::commands::Command;
use crate::ipc::responses::Response;
use crate::ipc::{Command, Response};
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use std::process::exit;
@ -46,6 +45,7 @@ pub fn handle_response(response: Response, format: Format) {
Format::Plain => match response {
Response::Ok => println!("ok"),
Response::OkValue { value } => println!("{value}"),
Response::Multi { values } => println!("{}", values.join("\n")),
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
},
Format::Json => println!(

View file

@ -1,7 +1,8 @@
use super::wayland::{self, ClipboardItem};
use crate::{arc_mut, lock, register_client, spawn, try_send};
use indexmap::map::Iter;
use crate::channels::AsyncSenderExt;
use crate::{arc_mut, lock, register_client, spawn};
use indexmap::IndexMap;
use indexmap::map::Iter;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tracing::{debug, trace};
@ -46,7 +47,7 @@ impl Client {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
tx.send_spawn(ClipboardEvent::Add(item.clone()));
}
lock!(cache).insert(item, senders.len());
@ -74,16 +75,17 @@ impl Client {
let removed_id = lock!(cache)
.remove_ref_first()
.expect("Clipboard cache unexpectedly empty");
try_send!(tx, ClipboardEvent::Remove(removed_id));
tx.send_spawn(ClipboardEvent::Remove(removed_id));
}
try_send!(tx, ClipboardEvent::Add(item.clone()));
tx.send_spawn(ClipboardEvent::Add(item.clone()));
}
},
|existing_id| {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(existing_id));
tx.send_spawn(ClipboardEvent::Activate(existing_id));
}
},
);
@ -106,7 +108,7 @@ impl Client {
let iter = cache.iter();
for (_, (item, _)) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
tx.send_spawn(ClipboardEvent::Add(item.clone()));
}
}
@ -130,7 +132,7 @@ impl Client {
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(id));
tx.send_spawn(ClipboardEvent::Activate(id));
}
}
@ -140,7 +142,7 @@ impl Client {
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Remove(id));
tx.send_spawn(ClipboardEvent::Remove(id));
}
}
}

View file

@ -1,37 +1,73 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{arc_mut, lock, send, spawn_blocking};
#[cfg(feature = "bindmode+hyprland")]
use super::{BindModeClient, BindModeUpdate};
#[cfg(feature = "keyboard+hyprland")]
use super::{KeyboardLayoutClient, KeyboardLayoutUpdate};
use super::{Visibility, Workspace};
use crate::channels::SyncSenderExt;
use crate::{arc_mut, lock, spawn_blocking};
use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::ctl::switch_xkb_layout;
use hyprland::data::{Devices, Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
use hyprland::shared::{HyprDataVec, WorkspaceType};
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tracing::{debug, error, info};
use tokio::sync::broadcast::{Receiver, Sender, channel};
use tracing::{debug, error, info, warn};
#[cfg(feature = "workspaces")]
use super::WorkspaceUpdate;
#[derive(Debug)]
struct TxRx<T> {
tx: Sender<T>,
_rx: Receiver<T>,
}
impl<T: Clone> TxRx<T> {
fn new() -> Self {
let (tx, rx) = channel(16);
Self { tx, _rx: rx }
}
}
#[derive(Debug)]
pub struct Client {
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
#[cfg(feature = "workspaces+hyprland")]
workspace: TxRx<WorkspaceUpdate>,
#[cfg(feature = "keyboard+hyprland")]
keyboard_layout: TxRx<KeyboardLayoutUpdate>,
#[cfg(feature = "bindmode+hyprland")]
bindmode: TxRx<BindModeUpdate>,
}
impl Client {
pub(crate) fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16);
let instance = Self {
workspace_tx,
_workspace_rx: workspace_rx,
#[cfg(feature = "workspaces+hyprland")]
workspace: TxRx::new(),
#[cfg(feature = "keyboard+hyprland")]
keyboard_layout: TxRx::new(),
#[cfg(feature = "bindmode+hyprland")]
bindmode: TxRx::new(),
};
instance.listen_workspace_events();
instance.listen_events();
instance
}
fn listen_workspace_events(&self) {
fn listen_events(&self) {
info!("Starting Hyprland event listener");
let tx = self.workspace_tx.clone();
#[cfg(feature = "workspaces+hyprland")]
let workspace_tx = self.workspace.tx.clone();
#[cfg(feature = "keyboard+hyprland")]
let keyboard_layout_tx = self.keyboard_layout.tx.clone();
#[cfg(feature = "bindmode+hyprland")]
let bindmode_tx = self.bindmode.tx.clone();
spawn_blocking(move || {
let mut event_listener = EventListener::new();
@ -40,179 +76,319 @@ impl Client {
let lock = arc_mut!(());
// cache the active workspace since Hyprland doesn't give us the prev active
let active = Self::get_active_workspace().expect("Failed to get active workspace");
let active = arc_mut!(Some(active));
#[cfg(feature = "workspaces+hyprland")]
Self::listen_workspace_events(&workspace_tx, &mut event_listener, &lock);
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
#[cfg(feature = "keyboard+hyprland")]
Self::listen_keyboard_events(&keyboard_layout_tx, &mut event_listener, &lock);
event_listener.add_workspace_added_handler(move |workspace_type| {
let _lock = lock!(lock);
debug!("Added workspace: {workspace_type:?}");
#[cfg(feature = "bindmode+hyprland")]
Self::listen_bindmode_events(&bindmode_tx, &mut event_listener, &lock);
let workspace_name = get_workspace_name(workspace_type);
let prev_workspace = lock!(active);
if let Err(err) = event_listener.start_listener() {
error!("Failed to start listener: {err:#}");
}
});
}
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
#[cfg(feature = "workspaces+hyprland")]
fn listen_workspace_events(
tx: &Sender<WorkspaceUpdate>,
event_listener: &mut EventListener,
lock: &std::sync::Arc<std::sync::Mutex<()>>,
) {
let active = Self::get_active_workspace().map_or_else(
|err| {
error!("Failed to get active workspace: {err:#?}");
None
},
Some,
);
let active = arc_mut!(active);
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Add(workspace));
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_added_handler(move |event| {
let _lock = lock!(lock);
debug!("Added workspace: {event:?}");
let workspace_name = get_workspace_name(event.name);
let prev_workspace = lock!(active);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
match workspace {
Ok(Some(workspace)) => {
tx.send_expect(WorkspaceUpdate::Add(workspace));
}
});
}
Err(e) => error!("Failed to get workspace: {e:#}"),
_ => {}
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type| {
let _lock = lock!(lock);
event_listener.add_workspace_changed_handler(move |event| {
let _lock = lock!(lock);
let mut prev_workspace = lock!(active);
let mut prev_workspace = lock!(active);
debug!(
"Received workspace change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.id)
);
debug!(
"Received workspace change: {:?} -> {event:?}",
prev_workspace.as_ref().map(|w| &w.id)
);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
let workspace_name = get_workspace_name(event.name);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
workspace.map_or_else(
|| {
error!("Unable to locate workspace");
},
|workspace| {
// there may be another type of update so dispatch that regardless of focus change
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
},
);
});
}
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.workspace;
let mut prev_workspace = lock!(active);
debug!(
"Received active monitor change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.name)
);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some((false, workspace)) =
workspace.map(|w| (w.visibility.is_focused(), w))
{
match workspace {
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
} else {
}
Ok(None) => {
error!("Unable to locate workspace");
}
});
}
Err(e) => error!("Failed to get workspace: {e:#}"),
_ => {}
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_moved_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.workspace;
debug!("Received workspace move: {workspace_type:?}");
event_listener.add_active_monitor_changed_handler(move |event_data| {
let _lock = lock!(lock);
let Some(workspace_type) = event_data.workspace_name else {
warn!("Received active monitor change with no workspace name");
return;
};
let mut prev_workspace = lock!(active);
let mut prev_workspace = lock!(active);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
debug!(
"Received active monitor change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.name)
);
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
match workspace {
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
Ok(None) => {
error!("Unable to locate workspace");
}
Err(e) => error!("Failed to get workspace: {e:#}"),
_ => {}
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_workspace_moved_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.name;
debug!("Received workspace move: {workspace_type:?}");
let mut prev_workspace = lock!(active);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
match workspace {
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
Ok(None) => {
error!("Unable to locate workspace");
}
Err(e) => error!("Failed to get workspace: {e:#}"),
_ => {}
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_workspace_renamed_handler(move |data| {
let _lock = lock!(lock);
debug!("Received workspace rename: {data:?}");
tx.send_expect(WorkspaceUpdate::Rename {
id: data.id as i64,
name: data.name,
});
});
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_workspace_deleted_handler(move |data| {
let _lock = lock!(lock);
debug!("Received workspace destroy: {data:?}");
tx.send_expect(WorkspaceUpdate::Remove(data.id as i64));
});
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_urgent_state_changed_handler(move |address| {
let _lock = lock!(lock);
debug!("Received urgent state: {address:?}");
let clients = match hyprland::data::Clients::get() {
Ok(clients) => clients,
Err(err) => {
error!("Failed to get clients: {err}");
return;
}
};
clients.iter().find(|c| c.address == address).map_or_else(
|| {
error!("Unable to locate client");
},
|c| {
tx.send_expect(WorkspaceUpdate::Urgent {
id: c.workspace.id as i64,
urgent: true,
});
},
);
});
}
}
#[cfg(feature = "keyboard+hyprland")]
fn listen_keyboard_events(
keyboard_layout_tx: &Sender<KeyboardLayoutUpdate>,
event_listener: &mut EventListener,
lock: &std::sync::Arc<std::sync::Mutex<()>>,
) {
let tx = keyboard_layout_tx.clone();
let lock = lock.clone();
event_listener.add_layout_changed_handler(move |layout_event| {
let _lock = lock!(lock);
let layout = if layout_event.layout_name.is_empty() {
// FIXME: This field is empty due to bug in `hyprland-rs_0.4.0-alpha.3`. Which is already fixed in last betas
// The layout may be empty due to a bug in `hyprland-rs`, because of which the `layout_event` is incorrect.
//
// Instead of:
// ```
// LayoutEvent {
// keyboard_name: "keychron-keychron-c2",
// layout_name: "English (US)",
// }
// ```
//
// We get:
// ```
// LayoutEvent {
// keyboard_name: "keychron-keychron-c2,English (US)",
// layout_name: "",
// }
// ```
//
// Here we are trying to recover `layout_name` from `keyboard_name`
let layout = layout_event.keyboard_name.as_str().split(',').nth(1);
let Some(layout) = layout else {
error!(
"Failed to get layout from string: {}. The failed logic is a workaround for a bug in `hyprland 0.4.0-alpha.3`", layout_event.keyboard_name);
return;
};
layout.into()
}
else {
layout_event.layout_name
};
{
let tx = tx.clone();
let lock = lock.clone();
debug!("Received layout: {layout:?}");
tx.send_expect(KeyboardLayoutUpdate(layout));
});
}
event_listener.add_workspace_rename_handler(move |data| {
let _lock = lock!(lock);
#[cfg(feature = "bindmode+hyprland")]
fn listen_bindmode_events(
bindmode_tx: &Sender<BindModeUpdate>,
event_listener: &mut EventListener,
lock: &std::sync::Arc<std::sync::Mutex<()>>,
) {
let tx = bindmode_tx.clone();
let lock = lock.clone();
send!(
tx,
WorkspaceUpdate::Rename {
id: data.workspace_id as i64,
name: data.workspace_name
}
);
});
}
event_listener.add_sub_map_changed_handler(move |bind_mode| {
let _lock = lock!(lock);
debug!("Received bind mode: {bind_mode:?}");
{
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));
});
}
event_listener
.start_listener()
.expect("Failed to start listener");
tx.send_expect(BindModeUpdate {
name: bind_mode,
pango_markup: false,
});
});
}
/// Sends a `WorkspaceUpdate::Focus` event
/// and updates the active workspace cache.
#[cfg(feature = "workspaces+hyprland")]
fn send_focus_change(
prev_workspace: &mut Option<Workspace>,
workspace: Workspace,
tx: &Sender<WorkspaceUpdate>,
) {
send!(
tx,
WorkspaceUpdate::Focus {
old: prev_workspace.take(),
new: workspace.clone(),
}
);
tx.send_expect(WorkspaceUpdate::Focus {
old: prev_workspace.take(),
new: workspace.clone(),
});
tx.send_expect(WorkspaceUpdate::Urgent {
id: workspace.id,
urgent: false,
});
prev_workspace.replace(workspace);
}
/// Gets a workspace by name from the server, given the active workspace if known.
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.find_map(|w| {
if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
create_is_visible()(w)
}));
#[cfg(feature = "workspaces+hyprland")]
fn get_workspace(name: &str, active: Option<&Workspace>) -> Result<Option<Workspace>> {
let workspace = Workspaces::get()?.into_iter().find_map(|w| {
if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
create_is_visible()(w)
}));
Some(Workspace::from((vis, w)))
} else {
None
}
})
Some(Workspace::from((vis, w)))
} else {
None
}
});
Ok(workspace)
}
/// Gets the active workspace from the server.
@ -222,43 +398,100 @@ impl Client {
}
}
impl WorkspaceClient for Client {
fn focus(&self, id: String) -> Result<()> {
let identifier = id.parse::<i32>().map_or_else(
|_| WorkspaceIdentifierWithSpecial::Name(&id),
WorkspaceIdentifierWithSpecial::Id,
);
#[cfg(feature = "workspaces+hyprland")]
impl super::WorkspaceClient for Client {
fn focus(&self, id: i64) {
let identifier = WorkspaceIdentifierWithSpecial::Id(id as i32);
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(())
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
error!("Couldn't focus workspace '{id}': {e:#}");
}
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace_tx.subscribe();
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace.tx.subscribe();
{
let tx = self.workspace_tx.clone();
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
let is_visible = create_is_visible();
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
let is_visible = create_is_visible();
match Workspaces::get() {
Ok(workspaces) => {
let workspaces = workspaces
.into_iter()
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect();
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect();
send!(tx, WorkspaceUpdate::Init(workspaces));
self.workspace
.tx
.send_expect(WorkspaceUpdate::Init(workspaces));
}
Err(e) => {
error!("Failed to get workspaces: {e:#}");
}
}
rx
}
}
#[cfg(feature = "keyboard+hyprland")]
impl KeyboardLayoutClient for Client {
fn set_next_active(&self) {
let Ok(devices) = Devices::get() else {
error!("Failed to get devices");
return;
};
let device = devices
.keyboards
.iter()
.find(|k| k.main)
.map(|k| k.name.clone());
if let Some(device) = device {
if let Err(e) =
switch_xkb_layout::call(device, switch_xkb_layout::SwitchXKBLayoutCmdTypes::Next)
{
error!("Failed to switch keyboard layout due to Hyprland error: {e}");
}
} else {
error!("Failed to get keyboard device from hyprland");
}
}
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
let rx = self.keyboard_layout.tx.subscribe();
match Devices::get().map(|devices| {
devices
.keyboards
.iter()
.find(|k| k.main)
.map(|k| k.active_keymap.clone())
}) {
Ok(Some(layout)) => {
self.keyboard_layout
.tx
.send_expect(KeyboardLayoutUpdate(layout));
}
Ok(None) => error!("Failed to get current keyboard layout hyprland"),
Err(err) => error!("Failed to get devices: {err:#?}"),
}
rx
}
}
#[cfg(feature = "bindmode+hyprland")]
impl BindModeClient for Client {
fn subscribe(&self) -> Result<Receiver<BindModeUpdate>> {
Ok(self.bindmode.tx.subscribe())
}
}
fn get_workspace_name(name: WorkspaceType) -> String {
match name {
WorkspaceType::Regular(name) => name,

View file

@ -1,4 +1,5 @@
use crate::{await_sync, register_fallible_client};
use crate::clients::ClientResult;
use crate::register_fallible_client;
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use std::fmt::{Debug, Display, Formatter};
@ -6,16 +7,20 @@ use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::debug;
#[cfg(feature = "workspaces+hyprland")]
#[cfg(feature = "hyprland")]
pub mod hyprland;
#[cfg(feature = "workspaces+sway")]
#[cfg(feature = "niri")]
pub mod niri;
#[cfg(feature = "sway")]
pub mod sway;
pub enum Compositor {
#[cfg(feature = "workspaces+sway")]
#[cfg(feature = "sway")]
Sway,
#[cfg(feature = "workspaces+hyprland")]
#[cfg(feature = "hyprland")]
Hyprland,
#[cfg(feature = "niri")]
Niri,
Unsupported,
}
@ -25,10 +30,12 @@ impl Display for Compositor {
f,
"{}",
match self {
#[cfg(feature = "workspaces+sway")]
#[cfg(any(feature = "sway"))]
Self::Sway => "Sway",
#[cfg(feature = "workspaces+hyprland")]
#[cfg(any(feature = "hyprland"))]
Self::Hyprland => "Hyprland",
#[cfg(feature = "workspaces+niri")]
Self::Niri => "Niri",
Self::Unsupported => "Unsupported",
}
)
@ -41,32 +48,90 @@ impl Compositor {
fn get_current() -> Self {
if std::env::var("SWAYSOCK").is_ok() {
cfg_if! {
if #[cfg(feature = "workspaces+sway")] { Self::Sway }
if #[cfg(feature = "sway")] { Self::Sway }
else { tracing::error!("Not compiled with Sway support"); Self::Unsupported }
}
} else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
cfg_if! {
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
if #[cfg(feature = "hyprland")] { Self::Hyprland }
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
}
} else if std::env::var("NIRI_SOCKET").is_ok() {
cfg_if! {
if #[cfg(feature = "niri")] { Self::Niri }
else {tracing::error!("Not compiled with Niri support"); Self::Unsupported }
}
} else {
Self::Unsupported
}
}
#[cfg(feature = "bindmode")]
pub fn create_bindmode_client(
clients: &mut super::Clients,
) -> ClientResult<dyn BindModeClient + Send + Sync> {
let current = Self::get_current();
debug!("Getting keyboard_layout client for: {current}");
match current {
#[cfg(feature = "bindmode+sway")]
Self::Sway => Ok(clients.sway()?),
#[cfg(feature = "bindmode+hyprland")]
Self::Hyprland => Ok(clients.hyprland()),
#[cfg(feature = "niri")]
Self::Niri => Err(Report::msg("Unsupported compositor")
.note("Currently bindmode is only supported by Sway and Hyprland")),
Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently bindmode is only supported by Sway and Hyprland")),
#[allow(unreachable_patterns)]
_ => Err(Report::msg("Unsupported compositor")
.note("Bindmode feature is disabled for this compositor")),
}
}
#[cfg(feature = "keyboard")]
pub fn create_keyboard_layout_client(
clients: &mut super::Clients,
) -> ClientResult<dyn KeyboardLayoutClient + Send + Sync> {
let current = Self::get_current();
debug!("Getting keyboard_layout client for: {current}");
match current {
#[cfg(feature = "keyboard+sway")]
Self::Sway => Ok(clients.sway()?),
#[cfg(feature = "keyboard+hyprland")]
Self::Hyprland => Ok(clients.hyprland()),
#[cfg(feature = "niri")]
Self::Niri => Err(Report::msg("Unsupported compositor").note(
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
)),
Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
)),
#[allow(unreachable_patterns)]
_ => Err(Report::msg("Unsupported compositor")
.note("Keyboard layout feature is disabled for this compositor")),
}
}
/// Creates a new instance of
/// the workspace client for the current compositor.
pub fn create_workspace_client() -> Result<Arc<dyn WorkspaceClient + Send + Sync>> {
#[cfg(feature = "workspaces")]
pub fn create_workspace_client(
clients: &mut super::Clients,
) -> Result<Arc<dyn WorkspaceClient + Send + Sync>> {
let current = Self::get_current();
debug!("Getting workspace client for: {current}");
match current {
#[cfg(feature = "workspaces+sway")]
Self::Sway => await_sync(async { sway::Client::new().await })
.map(|client| Arc::new(client) as Arc<dyn WorkspaceClient + Send + Sync>),
Self::Sway => Ok(clients.sway()?),
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => Ok(Arc::new(hyprland::Client::new())),
Self::Hyprland => Ok(clients.hyprland()),
#[cfg(feature = "workspaces+niri")]
Self::Niri => Ok(Arc::new(niri::Client::new())),
Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently workspaces are only supported by Sway and Hyprland")),
.note("Currently workspaces are only supported by Sway, Niri and Hyprland")),
#[allow(unreachable_patterns)]
_ => Err(Report::msg("Unsupported compositor")
.note("Workspaces feature is disabled for this compositor")),
}
}
}
@ -83,29 +148,29 @@ pub struct Workspace {
pub visibility: Visibility,
}
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
/// Indicates workspace visibility.
/// Visible workspaces have a boolean flag to indicate if they are also focused.
#[derive(Debug, Copy, Clone)]
pub enum Visibility {
Visible(bool),
Visible { focused: bool },
Hidden,
}
impl Visibility {
pub fn visible() -> Self {
Self::Visible(false)
Self::Visible { focused: false }
}
pub fn focused() -> Self {
Self::Visible(true)
Self::Visible { focused: true }
}
pub fn is_visible(self) -> bool {
matches!(self, Self::Visible(_))
matches!(self, Self::Visible { .. })
}
pub fn is_focused(self) -> bool {
if let Self::Visible(focused) = self {
if let Self::Visible { focused } = self {
focused
} else {
false
@ -114,6 +179,11 @@ impl Visibility {
}
#[derive(Debug, Clone)]
#[cfg(feature = "keyboard")]
pub struct KeyboardLayoutUpdate(pub String);
#[derive(Debug, Clone)]
#[cfg(feature = "workspaces")]
pub enum WorkspaceUpdate {
/// Provides an initial list of workspaces.
/// This is re-sent to all subscribers when a new subscription is created.
@ -132,6 +202,12 @@ pub enum WorkspaceUpdate {
name: String,
},
/// The urgent state of a node changed.
Urgent {
id: i64,
urgent: bool,
},
/// An update was triggered by the compositor but this was not mapped by Ironbar.
///
/// This is purely used for ergonomics within the compositor clients
@ -139,12 +215,44 @@ pub enum WorkspaceUpdate {
Unknown,
}
pub trait WorkspaceClient: Debug + Send + Sync {
/// Requests the workspace with this name is focused.
fn focus(&self, name: String) -> Result<()>;
/// Creates a new to workspace event receiver.
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
#[derive(Clone, Debug)]
#[cfg(feature = "bindmode")]
pub struct BindModeUpdate {
/// The binding mode that became active.
pub name: String,
/// Whether the mode should be parsed as pango markup.
pub pango_markup: bool,
}
#[cfg(feature = "workspaces")]
pub trait WorkspaceClient: Debug + Send + Sync {
/// Requests the workspace with this id is focused.
fn focus(&self, id: i64);
/// Creates a new to workspace event receiver.
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}
#[cfg(feature = "workspaces")]
register_fallible_client!(dyn WorkspaceClient, workspaces);
#[cfg(feature = "keyboard")]
pub trait KeyboardLayoutClient: Debug + Send + Sync {
/// Switches to the next layout.
fn set_next_active(&self);
/// Creates a new to keyboard layout event receiver.
fn subscribe(&self) -> broadcast::Receiver<KeyboardLayoutUpdate>;
}
#[cfg(feature = "keyboard")]
register_fallible_client!(dyn KeyboardLayoutClient, keyboard_layout);
#[cfg(feature = "bindmode")]
pub trait BindModeClient: Debug + Send + Sync {
/// Add a callback for bindmode updates.
fn subscribe(&self) -> Result<broadcast::Receiver<BindModeUpdate>>;
}
#[cfg(feature = "bindmode")]
register_fallible_client!(dyn BindModeClient, bindmode);

View file

@ -0,0 +1,117 @@
/// Taken from the `niri_ipc` crate.
/// Only a relevant snippet has been extracted
/// to reduce compile times.
use crate::clients::compositor::Workspace as IronWorkspace;
use crate::{await_sync, clients::compositor::Visibility};
use color_eyre::eyre::{Result, eyre};
use core::str;
use serde::{Deserialize, Serialize};
use std::{env, path::Path};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::UnixStream,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Request {
Action(Action),
EventStream,
}
pub type Reply = Result<Response, String>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Response {
Handled,
Workspaces(Vec<Workspace>),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Action {
FocusWorkspace { reference: WorkspaceReferenceArg },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceReferenceArg {
Name(String),
Id(u64),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Workspace {
pub id: u64,
pub idx: u8,
pub name: Option<String>,
pub output: Option<String>,
pub is_active: bool,
pub is_focused: bool,
}
impl From<&Workspace> for IronWorkspace {
fn from(workspace: &Workspace) -> IronWorkspace {
// Workspaces in niri don't neccessarily have names.
// If the niri workspace has a name then it is assigned as is,
// but if it does not have a name, the monitor index is used.
Self {
id: workspace.id as i64,
name: workspace.name.clone().unwrap_or(workspace.idx.to_string()),
monitor: workspace.output.clone().unwrap_or_default(),
visibility: if workspace.is_active {
Visibility::Visible {
focused: workspace.is_focused,
}
} else {
Visibility::Hidden
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Event {
WorkspacesChanged { workspaces: Vec<Workspace> },
WorkspaceActivated { id: u64, focused: bool },
Other,
}
#[derive(Debug)]
pub struct Connection(UnixStream);
impl Connection {
pub async fn connect() -> Result<Self> {
let socket_path =
env::var_os("NIRI_SOCKET").ok_or_else(|| eyre!("NIRI_SOCKET not found!"))?;
Self::connect_to(socket_path).await
}
pub async fn connect_to(path: impl AsRef<Path>) -> Result<Self> {
let raw_stream = UnixStream::connect(path.as_ref()).await?;
let stream = raw_stream;
Ok(Self(stream))
}
pub async fn send(
&mut self,
request: Request,
) -> Result<(Reply, impl FnMut() -> Result<Event> + '_)> {
let Self(stream) = self;
let mut buf = serde_json::to_string(&request)?;
stream.write_all(buf.as_bytes()).await?;
stream.shutdown().await?;
buf.clear();
let mut reader = BufReader::new(stream);
reader.read_line(&mut buf).await?;
let reply = serde_json::from_str(&buf)?;
let events = move || {
buf.clear();
await_sync(async {
reader.read_line(&mut buf).await.unwrap_or(0);
});
let event: Event = serde_json::from_str(&buf).unwrap_or(Event::Other);
Ok(event)
};
Ok((reply, events))
}
}

View file

@ -0,0 +1,223 @@
use super::{Workspace as IronWorkspace, WorkspaceClient, WorkspaceUpdate};
use crate::channels::SyncSenderExt;
use crate::clients::compositor::Visibility;
use crate::{arc_rw, read_lock, spawn, write_lock};
use color_eyre::Report;
use connection::{Action, Connection, Event, Request, WorkspaceReferenceArg};
use std::sync::{Arc, RwLock};
use tokio::sync::broadcast;
use tracing::{debug, error, warn};
mod connection;
#[derive(Debug)]
pub struct Client {
tx: broadcast::Sender<WorkspaceUpdate>,
_rx: broadcast::Receiver<WorkspaceUpdate>,
workspaces: Arc<RwLock<Vec<IronWorkspace>>>,
}
impl Client {
pub fn new() -> Self {
let (tx, rx) = broadcast::channel(32);
let tx2 = tx.clone();
let workspace_state = arc_rw!(vec![]);
let workspace_state2 = workspace_state.clone();
spawn(async move {
let mut conn = Connection::connect().await?;
let (_, mut event_listener) = conn.send(Request::EventStream).await?;
let mut first_event = true;
loop {
let events = match event_listener() {
Ok(Event::WorkspacesChanged { workspaces }) => {
debug!("WorkspacesChanged: {:?}", workspaces);
// Niri only has a WorkspacesChanged Event and Ironbar has 4 events which have to be handled: Add, Remove, Rename and Move.
// This is handled by keeping a previous state of workspaces and comparing with the new state for changes.
let new_workspaces: Vec<IronWorkspace> = workspaces
.into_iter()
.map(|w| IronWorkspace::from(&w))
.collect();
let mut updates: Vec<WorkspaceUpdate> = vec![];
if first_event {
// Niri's WorkspacesChanged event does not initially sort workspaces by ID when first output,
// which makes sort = added meaningless. Therefore, new_workspaces are sorted by ID here to ensure a consistent addition order.
let mut new_workspaces = new_workspaces.clone();
new_workspaces.sort_by_key(|w| w.id);
updates.push(WorkspaceUpdate::Init(new_workspaces));
first_event = false;
} else {
// first pass - add/update
for workspace in &new_workspaces {
let workspace_state = read_lock!(workspace_state);
let old_workspace = workspace_state
.iter()
.find(|&w: &&IronWorkspace| w.id == workspace.id);
match old_workspace {
None => updates.push(WorkspaceUpdate::Add(workspace.clone())),
Some(old_workspace) => {
if workspace.name != old_workspace.name {
updates.push(WorkspaceUpdate::Rename {
id: workspace.id,
name: workspace.name.clone(),
});
}
if workspace.monitor != old_workspace.monitor {
updates.push(WorkspaceUpdate::Move(workspace.clone()));
}
}
}
}
// second pass - delete
for workspace in read_lock!(workspace_state).iter() {
let exists = new_workspaces.iter().any(|w| w.id == workspace.id);
if !exists {
updates.push(WorkspaceUpdate::Remove(workspace.id));
}
}
}
*write_lock!(workspace_state) = new_workspaces;
updates
}
Ok(Event::WorkspaceActivated { id, focused }) => {
debug!("WorkspaceActivated: id: {}, focused: {}", id, focused);
// workspace with id is activated, if focus is true then it is also focused
// if focused is true then focus has changed => find old focused workspace. set it to inactive and set current
//
// we use indexes here as both new/old need to be mutable
let new_index = read_lock!(workspace_state)
.iter()
.position(|w| w.id == id as i64);
if let Some(new_index) = new_index {
if focused {
let old_index = read_lock!(workspace_state)
.iter()
.position(|w| w.visibility.is_focused());
if let Some(old_index) = old_index {
write_lock!(workspace_state)[new_index].visibility =
Visibility::focused();
if read_lock!(workspace_state)[old_index].monitor
== read_lock!(workspace_state)[new_index].monitor
{
write_lock!(workspace_state)[old_index].visibility =
Visibility::Hidden;
} else {
write_lock!(workspace_state)[old_index].visibility =
Visibility::visible();
}
vec![WorkspaceUpdate::Focus {
old: Some(read_lock!(workspace_state)[old_index].clone()),
new: read_lock!(workspace_state)[new_index].clone(),
}]
} else {
write_lock!(workspace_state)[new_index].visibility =
Visibility::focused();
vec![WorkspaceUpdate::Focus {
old: None,
new: read_lock!(workspace_state)[new_index].clone(),
}]
}
} else {
// if focused is false means active workspace on a particular monitor has changed =>
// change all workspaces on monitor to inactive and change current workspace as active
write_lock!(workspace_state)[new_index].visibility =
Visibility::visible();
let old_index = read_lock!(workspace_state).iter().position(|w| {
(w.visibility.is_focused() || w.visibility.is_visible())
&& w.monitor
== read_lock!(workspace_state)[new_index].monitor
});
if let Some(old_index) = old_index {
write_lock!(workspace_state)[old_index].visibility =
Visibility::Hidden;
vec![]
} else {
vec![]
}
}
} else {
warn!("No workspace with id for new focus/visible workspace found");
vec![]
}
}
Ok(Event::Other) => {
vec![]
}
Err(err) => {
error!("{err:?}");
break;
}
};
for event in events {
tx.send_expect(event);
}
}
Ok::<(), Report>(())
});
Self {
tx: tx2,
_rx: rx,
workspaces: workspace_state2,
}
}
}
impl WorkspaceClient for Client {
fn focus(&self, id: i64) {
debug!("focusing workspace with id: {}", id);
// this does annoyingly require spawning a separate connection for every focus call
// the alternative is sticking the conn behind a mutex which could perform worse
spawn(async move {
let mut conn = Connection::connect().await?;
let command = Request::Action(Action::FocusWorkspace {
reference: WorkspaceReferenceArg::Id(id as u64),
});
if let Err(err) = conn.send(command).await {
error!("failed to send command: {err:?}");
}
Ok::<(), Report>(())
});
}
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate> {
let rx = self.tx.subscribe();
let workspaces = read_lock!(self.workspaces);
if !workspaces.is_empty() {
self.tx
.send_expect(WorkspaceUpdate::Init(workspaces.clone()));
}
rx
}
}

View file

@ -1,85 +1,66 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn};
use color_eyre::{Report, Result};
use futures_lite::StreamExt;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::sync::Mutex;
use tracing::{info, trace};
use super::{Visibility, Workspace};
use crate::channels::SyncSenderExt;
use crate::clients::sway::Client;
use crate::{await_sync, error, spawn};
use color_eyre::Report;
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{Receiver, channel};
#[derive(Debug)]
pub struct Client {
client: Arc<Mutex<Connection>>,
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
#[cfg(feature = "workspaces")]
use super::WorkspaceUpdate;
impl Client {
pub(crate) async fn new() -> Result<Self> {
// Avoid using `arc_mut!` here because we need tokio Mutex.
let client = Arc::new(Mutex::new(Connection::new().await?));
info!("Sway IPC subscription client connected");
#[cfg(feature = "workspaces+sway")]
impl super::WorkspaceClient for Client {
fn focus(&self, id: i64) {
let client = self.connection().clone();
spawn(async move {
let mut client = client.lock().await;
let (workspace_tx, workspace_rx) = channel(16);
let name = client
.get_workspaces()
.await?
.into_iter()
.find(|w| w.id == id)
.map(|w| w.name);
{
// create 2nd client as subscription takes ownership
let client = Connection::new().await?;
let workspace_tx = workspace_tx.clone();
let Some(name) = name else {
return Err(Report::msg(format!("couldn't find workspace with id {id}")));
};
spawn(async move {
let event_types = [EventType::Workspace];
let mut events = client.subscribe(event_types).await?;
if let Err(e) = client.run_command(format!("workspace {name}")).await {
return Err(Report::msg(format!(
"Couldn't focus workspace '{id}': {e:#}"
)));
}
while let Some(event) = events.next().await {
trace!("event: {:?}", event);
if let Event::Workspace(event) = event? {
let event = WorkspaceUpdate::from(*event);
if !matches!(event, WorkspaceUpdate::Unknown) {
workspace_tx.send(event)?;
}
};
}
Ok::<(), Report>(())
});
}
Ok(Self {
client,
workspace_tx,
_workspace_rx: workspace_rx,
})
}
}
impl WorkspaceClient for Client {
fn focus(&self, id: String) -> Result<()> {
await_sync(async move {
let mut client = self.client.lock().await;
client.run_command(format!("workspace {id}")).await
})?;
Ok(())
Ok(())
});
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace_tx.subscribe();
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
let (tx, rx) = channel(16);
{
let tx = self.workspace_tx.clone();
let client = self.client.clone();
let client = self.connection().clone();
await_sync(async {
let mut client = client.lock().await;
let workspaces = client.get_workspaces().await.expect("to get workspaces");
// TODO: this needs refactoring
await_sync(async {
let mut client = client.lock().await;
let workspaces = client.get_workspaces().await.expect("to get workspaces");
let event =
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
let event =
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
send!(tx, event);
});
}
tx.send_expect(event);
drop(client);
self.add_listener::<WorkspaceEvent>(move |event| {
let update = WorkspaceUpdate::from(event.clone());
tx.send_expect(update);
})
.await
.expect("to add listener");
});
rx
}
@ -135,6 +116,7 @@ impl From<&swayipc_async::Workspace> for Visibility {
}
}
#[cfg(feature = "workspaces")]
impl From<WorkspaceEvent> for WorkspaceUpdate {
fn from(event: WorkspaceEvent) -> Self {
match event.change {
@ -151,7 +133,136 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
WorkspaceChange::Move => {
Self::Move(event.current.expect("Missing current workspace").into())
}
WorkspaceChange::Rename => {
if let Some(node) = event.current {
Self::Rename {
id: node.id,
name: node.name.unwrap_or_default(),
}
} else {
Self::Unknown
}
}
WorkspaceChange::Urgent => {
if let Some(node) = event.current {
Self::Urgent {
id: node.id,
urgent: node.urgent,
}
} else {
Self::Unknown
}
}
_ => Self::Unknown,
}
}
}
#[cfg(feature = "keyboard+sway")]
use super::{KeyboardLayoutClient, KeyboardLayoutUpdate};
#[cfg(feature = "keyboard+sway")]
impl KeyboardLayoutClient for Client {
fn set_next_active(&self) {
let client = self.connection().clone();
spawn(async move {
let mut client = client.lock().await;
let inputs = client.get_inputs().await.expect("to get inputs");
if let Some(keyboard) = inputs
.into_iter()
.find(|i| i.xkb_active_layout_name.is_some())
{
if let Err(e) = client
.run_command(format!(
"input {} xkb_switch_layout next",
keyboard.identifier
))
.await
{
error!("Failed to switch keyboard layout due to Sway error: {e}");
}
} else {
error!("Failed to get keyboard identifier from Sway");
}
});
}
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
let (tx, rx) = channel(16);
let client = self.connection().clone();
await_sync(async {
let mut client = client.lock().await;
let inputs = client.get_inputs().await.expect("to get inputs");
if let Some(layout) = inputs.into_iter().find_map(|i| i.xkb_active_layout_name) {
tx.send_expect(KeyboardLayoutUpdate(layout));
} else {
error!("Failed to get keyboard layout from Sway!");
}
drop(client);
self.add_listener::<InputEvent>(move |event| {
if let Ok(layout) = KeyboardLayoutUpdate::try_from(event.clone()) {
tx.send_expect(layout);
}
})
.await
.expect("to add listener");
});
rx
}
}
#[cfg(feature = "keyboard+sway")]
impl TryFrom<InputEvent> for KeyboardLayoutUpdate {
type Error = ();
fn try_from(value: InputEvent) -> Result<Self, Self::Error> {
match value.change {
InputChange::XkbLayout => {
if let Some(layout) = value.input.xkb_active_layout_name {
Ok(KeyboardLayoutUpdate(layout))
} else {
Err(())
}
}
_ => Err(()),
}
}
}
#[cfg(feature = "bindmode+sway")]
use super::{BindModeClient, BindModeUpdate};
#[cfg(feature = "bindmode+sway")]
impl BindModeClient for Client {
fn subscribe(&self) -> Result<Receiver<BindModeUpdate>, Report> {
let (tx, rx) = channel(16);
await_sync(async {
self.add_listener::<swayipc_async::ModeEvent>(move |mode| {
tracing::trace!("mode: {:?}", mode);
// when no binding is active the bindmode is named "default", but we must display
// nothing in this case.
let name = if mode.change == "default" {
String::new()
} else {
mode.change.clone()
};
tx.send_expect(BindModeUpdate {
name,
pango_markup: mode.pango_markup,
});
})
.await
})?;
Ok(rx)
}
}

236
src/clients/libinput.rs Normal file
View file

@ -0,0 +1,236 @@
use crate::channels::SyncSenderExt;
use crate::{Ironbar, arc_rw, read_lock, spawn, write_lock};
use color_eyre::{Report, Result};
use colpetto::event::{AsRawEvent, DeviceEvent, KeyState, KeyboardEvent};
use colpetto::{DeviceCapability, Libinput};
use evdev_rs::DeviceWrapper;
use evdev_rs::enums::{EV_KEY, EV_LED, EventCode, int_to_ev_key};
use futures_lite::StreamExt;
use rustix::fs::{Mode, OFlags, open};
use rustix::io::Errno;
use std::ffi::{CStr, CString, c_int};
use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
use std::os::unix::io::OwnedFd;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::sync::broadcast;
use tokio::task::LocalSet;
use tokio::time::sleep;
use tracing::{debug, error};
#[derive(Debug, Copy, Clone)]
pub enum Key {
Caps,
Num,
Scroll,
}
impl From<Key> for EV_KEY {
fn from(value: Key) -> Self {
match value {
Key::Caps => Self::KEY_CAPSLOCK,
Key::Num => Self::KEY_NUMLOCK,
Key::Scroll => Self::KEY_SCROLLLOCK,
}
}
}
impl TryFrom<EV_KEY> for Key {
type Error = Report;
fn try_from(value: EV_KEY) -> std::result::Result<Self, Self::Error> {
match value {
EV_KEY::KEY_CAPSLOCK => Ok(Key::Caps),
EV_KEY::KEY_NUMLOCK => Ok(Key::Num),
EV_KEY::KEY_SCROLLLOCK => Ok(Key::Scroll),
_ => Err(Report::msg("provided key is not supported toggle key")),
}
}
}
impl Key {
fn get_state<P: AsRef<Path>>(self, device_path: P) -> Result<bool> {
let device = evdev_rs::Device::new_from_path(device_path)?;
match self {
Self::Caps => device.event_value(&EventCode::EV_LED(EV_LED::LED_CAPSL)),
Self::Num => device.event_value(&EventCode::EV_LED(EV_LED::LED_NUML)),
Self::Scroll => device.event_value(&EventCode::EV_LED(EV_LED::LED_SCROLLL)),
}
.map(|v| v > 0)
.ok_or_else(|| Report::msg("failed to get key status"))
}
}
#[derive(Debug, Copy, Clone)]
pub struct KeyEvent {
pub key: Key,
pub state: bool,
}
#[derive(Debug, Copy, Clone)]
pub enum Event {
Device,
Key(KeyEvent),
}
struct KeyData<P: AsRef<Path>> {
device_path: P,
key: EV_KEY,
}
impl<P: AsRef<Path>> TryFrom<KeyData<P>> for Event {
type Error = Report;
fn try_from(data: KeyData<P>) -> Result<Self> {
let key = Key::try_from(data.key)?;
key.get_state(data.device_path)
.map(|state| KeyEvent { key, state })
.map(Event::Key)
}
}
#[derive(Debug)]
pub struct Client {
tx: broadcast::Sender<Event>,
_rx: broadcast::Receiver<Event>,
seat: String,
known_devices: Arc<RwLock<Vec<PathBuf>>>,
}
impl Client {
pub fn init(seat: String) -> Arc<Self> {
let client = Arc::new(Self::new(seat));
{
let client = client.clone();
std::thread::spawn(move || {
let local = LocalSet::new();
local.spawn_local(async move {
if let Err(err) = client.run().await {
error!("{err:?}");
}
});
Ironbar::runtime().block_on(local);
});
}
client
}
fn new(seat: String) -> Self {
let (tx, rx) = broadcast::channel(4);
Self {
tx,
_rx: rx,
seat,
known_devices: arc_rw!(vec![]),
}
}
fn open_restricted(path: &CStr, flags: c_int) -> std::result::Result<RawFd, i32> {
open(path, OFlags::from_bits_retain(flags as u32), Mode::empty())
.map(IntoRawFd::into_raw_fd)
.map_err(Errno::raw_os_error)
}
fn close_restricted(fd: c_int) {
drop(unsafe { OwnedFd::from_raw_fd(fd) });
}
async fn run(&self) -> Result<()> {
let mut libinput = Libinput::with_tracing(Self::open_restricted, Self::close_restricted)?;
libinput.udev_assign_seat(CString::new(&*self.seat)?.as_c_str())?;
let mut stream = libinput.event_stream()?;
while let Some(event) = stream.try_next().await? {
match event {
colpetto::Event::Device(DeviceEvent::Added(event)) => {
let device = event.device();
if !device.has_capability(DeviceCapability::Keyboard) {
continue;
}
let name = device.name();
let Some(device) = event.device().udev_device() else {
continue;
};
if let Some(device_path) = device.devnode() {
// not all devices which report as keyboards actually are one -
// fire test event so we can figure out if it is
let caps_event: Result<Event> = KeyData {
device_path,
key: EV_KEY::KEY_CAPSLOCK,
}
.try_into();
if caps_event.is_ok() {
debug!(
"new keyboard device: {} | {}",
name.to_string_lossy(),
device_path.display()
);
write_lock!(self.known_devices).push(device_path.to_path_buf());
self.tx.send_expect(Event::Device);
}
}
}
colpetto::Event::Keyboard(KeyboardEvent::Key(event))
if event.key_state() == KeyState::Released =>
{
let Some(device) = event.device().udev_device() else {
continue;
};
let Some(
key @ (EV_KEY::KEY_CAPSLOCK | EV_KEY::KEY_NUMLOCK | EV_KEY::KEY_SCROLLLOCK),
) = int_to_ev_key(event.key())
else {
continue;
};
if let Some(device_path) = device.devnode().map(PathBuf::from) {
let tx = self.tx.clone();
// need to spawn a task to avoid blocking
spawn(async move {
// wait for kb to change
sleep(Duration::from_millis(50)).await;
let data = KeyData { device_path, key };
if let Ok(event) = data.try_into() {
tx.send_expect(event);
}
});
}
}
_ => {}
}
}
Err(Report::msg("unexpected end of stream"))
}
pub fn get_state(&self, key: Key) -> bool {
read_lock!(self.known_devices)
.iter()
.map(|device_path| key.get_state(device_path))
.filter_map(Result::ok)
.reduce(|state, curr| state || curr)
.unwrap_or_default()
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.tx.subscribe()
}
}

View file

@ -1,21 +1,33 @@
use crate::{await_sync, Ironbar};
use crate::{Ironbar, await_sync};
use color_eyre::Result;
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
#[cfg(any(
feature = "bindmode",
feature = "hyprland",
feature = "keyboard",
feature = "workspaces",
))]
pub mod compositor;
#[cfg(feature = "keyboard")]
pub mod libinput;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "network_manager")]
pub mod networkmanager;
#[cfg(feature = "sway")]
pub mod sway;
#[cfg(feature = "notifications")]
pub mod swaync;
#[cfg(feature = "sys_info")]
pub mod sysinfo;
#[cfg(feature = "tray")]
pub mod tray;
#[cfg(feature = "upower")]
@ -31,16 +43,28 @@ pub struct Clients {
wayland: Option<Arc<wayland::Client>>,
#[cfg(feature = "workspaces")]
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "sway")]
sway: Option<Arc<sway::Client>>,
#[cfg(feature = "hyprland")]
hyprland: Option<Arc<compositor::hyprland::Client>>,
#[cfg(feature = "bindmode")]
bindmode: Option<Arc<dyn compositor::BindModeClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "keyboard")]
libinput: HashMap<Box<str>, Arc<libinput::Client>>,
#[cfg(feature = "keyboard")]
keyboard_layout: Option<Arc<dyn compositor::KeyboardLayoutClient>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
music: HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "network_manager")]
network_manager: Option<Arc<networkmanager::Client>>,
#[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "sys_info")]
sys_info: Option<Arc<sysinfo::Client>>,
#[cfg(feature = "tray")]
tray: Option<Arc<tray::Client>>,
#[cfg(feature = "upower")]
@ -73,18 +97,68 @@ impl Clients {
#[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
let client = match &self.workspaces {
Some(workspaces) => workspaces.clone(),
None => {
let client = compositor::Compositor::create_workspace_client()?;
self.workspaces.replace(client.clone());
client
}
let client = if let Some(workspaces) = &self.workspaces {
workspaces.clone()
} else {
let client = compositor::Compositor::create_workspace_client(self)?;
self.workspaces.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "keyboard")]
pub fn keyboard_layout(&mut self) -> ClientResult<dyn compositor::KeyboardLayoutClient> {
let client = if let Some(keyboard_layout) = &self.keyboard_layout {
keyboard_layout.clone()
} else {
let client = compositor::Compositor::create_keyboard_layout_client(self)?;
self.keyboard_layout.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "bindmode")]
pub fn bindmode(&mut self) -> ClientResult<dyn compositor::BindModeClient> {
let client = if let Some(client) = &self.bindmode {
client.clone()
} else {
let client = compositor::Compositor::create_bindmode_client(self)?;
self.bindmode.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "sway")]
pub fn sway(&mut self) -> ClientResult<sway::Client> {
let client = if let Some(client) = &self.sway {
client.clone()
} else {
let client = await_sync(async { sway::Client::new().await })?;
let client = Arc::new(client);
self.sway.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "hyprland")]
pub fn hyprland(&mut self) -> Arc<compositor::hyprland::Client> {
if let Some(client) = &self.hyprland {
client.clone()
} else {
let client = Arc::new(compositor::hyprland::Client::new());
self.hyprland.replace(client.clone());
client
}
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
@ -92,6 +166,17 @@ impl Clients {
.clone()
}
#[cfg(feature = "keyboard")]
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
if let Some(client) = self.libinput.get(seat) {
client.clone()
} else {
let client = libinput::Client::init(seat.to_string());
self.libinput.insert(seat.into(), client.clone());
client
}
}
#[cfg(feature = "music")]
pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> {
self.music
@ -102,55 +187,68 @@ impl Clients {
#[cfg(feature = "network_manager")]
pub fn network_manager(&mut self) -> ClientResult<networkmanager::Client> {
match &self.network_manager {
Some(client) => Ok(client.clone()),
None => {
let client = networkmanager::create_client()?;
self.network_manager = Some(client.clone());
Ok(client)
}
if let Some(client) = &self.network_manager {
Ok(client.clone())
} else {
let client = await_sync(async move { networkmanager::create_client().await })?;
self.network_manager = Some(client.clone());
Ok(client)
}
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> ClientResult<swaync::Client> {
let client = match &self.notifications {
Some(client) => client.clone(),
None => {
let client = await_sync(async { swaync::Client::new().await })?;
let client = Arc::new(client);
self.notifications.replace(client.clone());
client
}
let client = if let Some(client) = &self.notifications {
client.clone()
} else {
let client = await_sync(async { swaync::Client::new().await })?;
let client = Arc::new(client);
self.notifications.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "sys_info")]
pub fn sys_info(&mut self) -> Arc<sysinfo::Client> {
self.sys_info
.get_or_insert_with(|| {
let client = Arc::new(sysinfo::Client::new());
#[cfg(feature = "ipc")]
Ironbar::variable_manager().register_namespace("sysinfo", client.clone());
client
})
.clone()
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> ClientResult<tray::Client> {
let client = match &self.tray {
Some(client) => client.clone(),
None => {
let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
let client = await_sync(async { tray::Client::new(&service_name).await })?;
let client = Arc::new(client);
self.tray.replace(client.clone());
client
}
let client = if let Some(client) = &self.tray {
client.clone()
} else {
let client = await_sync(async { tray::Client::new().await })?;
let client = Arc::new(client);
self.tray.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "upower")]
pub fn upower(&mut self) -> Arc<zbus::fdo::PropertiesProxy<'static>> {
self.upower
.get_or_insert_with(|| {
crate::await_sync(async { upower::create_display_proxy().await })
})
.clone()
pub fn upower(&mut self) -> ClientResult<zbus::fdo::PropertiesProxy<'static>> {
let client = if let Some(client) = &self.upower {
client.clone()
} else {
let client = await_sync(async { upower::create_display_proxy().await })?;
self.upower.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "volume")]

View file

@ -70,13 +70,17 @@ pub trait MusicClient: Debug + Send + Sync {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum ClientType {
#[cfg(feature = "music+mpd")]
Mpd { host: String, music_dir: PathBuf },
#[cfg(feature = "music+mpris")]
Mpris,
}
pub fn create_client(client_type: ClientType) -> Arc<dyn MusicClient> {
match client_type {
#[cfg(feature = "music+mpd")]
ClientType::Mpd { host, music_dir } => Arc::new(mpd::Client::new(host, music_dir)),
#[cfg(feature = "music+mpris")]
ClientType::Mpris => Arc::new(mpris::Client::new()),
}
}

View file

@ -1,14 +1,15 @@
use super::{
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, TICK_INTERVAL_MS, Track,
};
use crate::{await_sync, send, spawn, Ironbar};
use crate::channels::SyncSenderExt;
use crate::{Ironbar, await_sync, spawn};
use color_eyre::Report;
use color_eyre::Result;
use mpd_client::client::{ConnectionEvent, Subsystem};
use mpd_client::commands::{self, SeekMode};
use mpd_client::responses::{PlayState, Song};
use mpd_client::tag::Tag;
use mpd_utils::{mpd_client, PersistentClient};
use mpd_utils::{PersistentClient, mpd_client};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@ -97,7 +98,7 @@ impl Client {
let status = Status::from(status);
let update = PlayerUpdate::Update(Box::new(track), status);
send!(tx, update);
tx.send_expect(update);
}
Ok(())
@ -113,7 +114,7 @@ impl Client {
elapsed: status.elapsed,
});
send!(tx, update);
tx.send_expect(update);
}
}
}

View file

@ -1,6 +1,7 @@
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
use super::{MusicClient, PlayerState, PlayerUpdate, Status, TICK_INTERVAL_MS, Track};
use crate::channels::SyncSenderExt;
use crate::clients::music::ProgressTick;
use crate::{arc_mut, lock, send, spawn_blocking};
use crate::{arc_mut, lock, spawn_blocking};
use color_eyre::Result;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::cmp;
@ -47,10 +48,14 @@ impl Client {
)) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY) =>
{
Vec::new()
vec![]
}
_ => {
error!("D-Bus error getting MPRIS players: {e:?}");
vec![]
}
_ => 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.
@ -133,7 +138,7 @@ impl Client {
let mut players_locked = lock!(players);
players_locked.remove(identity);
if players_locked.is_empty() {
send!(tx, PlayerUpdate::Update(Box::new(None), Status::default()));
tx.send_expect(PlayerUpdate::Update(Box::new(None), Status::default()));
}
};
@ -208,7 +213,7 @@ impl Client {
let track = Track::from(metadata);
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
send!(tx, player_update);
tx.send_expect(player_update);
Ok(())
}
@ -238,7 +243,7 @@ impl Client {
duration: metadata.length(),
});
send!(tx, update);
tx.send_expect(update);
}
}
}
@ -286,14 +291,22 @@ impl MusicClient for Client {
fn seek(&self, duration: Duration) -> Result<()> {
if let Some(player) = Self::get_player(self) {
let pos = player.get_position().unwrap_or_default();
// if possible, use `set_position` instead of `seek` because some players have issues with seeking
// see https://github.com/JakeStanger/ironbar/issues/970
if let Ok(metadata) = player.get_metadata() {
if let Some(track_id) = metadata.track_id() {
player.set_position(track_id, &duration)?;
} else {
let pos = player.get_position().unwrap_or_default();
let duration = duration.as_micros() as i64;
let position = pos.as_micros() as i64;
let duration = duration.as_micros() as i64;
let position = pos.as_micros() as i64;
let seek = cmp::max(duration, 0) - position;
let seek = cmp::max(duration, 0) - position;
player.seek(seek)?;
player.seek(seek)?;
}
}
} else {
error!("Could not find player");
}
@ -315,7 +328,9 @@ impl MusicClient for Client {
state: PlayerState::Stopped,
volume_percent: None,
};
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
self.tx
.send_expect(PlayerUpdate::Update(Box::new(None), status));
}
rx

181
src/clients/sway.rs Normal file
View file

@ -0,0 +1,181 @@
use crate::spawn;
use color_eyre::{Report, Result};
use futures_lite::StreamExt;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType};
use tokio::sync::Mutex;
use tracing::{info, trace};
type SyncFn<T> = dyn Fn(&T) + Sync + Send;
struct TaskState {
join_handle: Option<tokio::task::JoinHandle<Result<()>>>,
// could have been a `HashMap<EventType, Vec<Box<dyn Fn(&Event) + Sync + Send>>>`, but we don't
// expect enough listeners to justify the constant overhead of a hashmap.
listeners: Arc<Vec<(EventType, Box<SyncFn<Event>>)>>,
}
pub struct Client {
connection: Arc<Mutex<Connection>>,
task_state: Mutex<TaskState>,
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("client", &"Connection")
.field("task_state", &format_args!("<...>"))
.finish()
}
}
impl Client {
pub(crate) async fn new() -> Result<Self> {
// Avoid using `arc_mut!` here because we need tokio Mutex.
let client = Arc::new(Mutex::new(Connection::new().await?));
info!("Sway IPC subscription client connected");
Ok(Self {
connection: client,
task_state: Mutex::new(TaskState {
listeners: Arc::new(Vec::new()),
join_handle: None,
}),
})
}
pub fn connection(&self) -> &Arc<Mutex<Connection>> {
&self.connection
}
pub async fn add_listener<T: SwayIpcEvent>(
&self,
f: impl Fn(&T) + Sync + Send + 'static,
) -> Result<()> {
self.add_listener_type(
T::EVENT_TYPE,
Box::new(move |event| {
let event = T::from_event(event).expect("event type mismatch");
f(event);
}),
)
.await
}
pub async fn add_listener_type(
&self,
event_type: EventType,
f: Box<SyncFn<Event>>,
) -> Result<()> {
// abort current running task
let TaskState {
join_handle,
listeners,
} = &mut *self.task_state.lock().await;
if let Some(handle) = join_handle.take() {
handle.abort();
let _ = handle.await;
}
// Only the task and self have a reference to listeners, and we just abort the task. This
// is the only reference to listeners, so we can safely get a mutable reference.
let listeners_mut = Arc::get_mut(listeners)
.ok_or_else(|| Report::msg("Failed to get mutable reference to listeners"))?;
listeners_mut.push((event_type, f));
// create new client as subscription takes ownership
let client = Connection::new().await?;
let event_types = listeners.iter().map(|(t, _)| *t).collect::<Vec<_>>();
let listeners = listeners.clone();
let handle = spawn(async move {
let mut events = client.subscribe(&event_types).await?;
while let Some(event) = events.next().await {
trace!("event: {:?}", event);
let event = event?;
let ty = sway_event_to_event_type(&event);
for (t, f) in listeners.iter() {
if *t == ty {
f(&event);
}
}
}
Ok::<(), Report>(())
});
*join_handle = Some(handle);
Ok(())
}
}
fn sway_event_to_event_type(event: &Event) -> EventType {
match event {
Event::Workspace(_) => EventType::Workspace,
Event::Mode(_) => EventType::Mode,
Event::Window(_) => EventType::Window,
Event::BarConfigUpdate(_) => EventType::BarConfigUpdate,
Event::Binding(_) => EventType::Binding,
Event::Shutdown(_) => EventType::Shutdown,
Event::Tick(_) => EventType::Tick,
Event::BarStateUpdate(_) => EventType::BarStateUpdate,
Event::Input(_) => EventType::Input,
_ => todo!(),
}
}
pub trait SwayIpcEvent {
const EVENT_TYPE: EventType;
fn from_event(e: &Event) -> Option<&Self>;
}
macro_rules! sway_ipc_event_impl {
(@ $($t:tt)*) => { $($t)* };
($t:ty, $v:expr, $($m:tt)*) => {
sway_ipc_event_impl! {@
impl SwayIpcEvent for $t {
const EVENT_TYPE: EventType = $v;
fn from_event(e: &Event) -> Option<&Self> {
match e {
$($m)* (x) => Some(x),
_ => None,
}
}
}
}
};
}
sway_ipc_event_impl!(
swayipc_async::WorkspaceEvent,
EventType::Workspace,
Event::Workspace
);
sway_ipc_event_impl!(swayipc_async::ModeEvent, EventType::Mode, Event::Mode);
sway_ipc_event_impl!(swayipc_async::WindowEvent, EventType::Window, Event::Window);
sway_ipc_event_impl!(
swayipc_async::BarConfig,
EventType::BarConfigUpdate,
Event::BarConfigUpdate
);
sway_ipc_event_impl!(
swayipc_async::BindingEvent,
EventType::Binding,
Event::Binding
);
sway_ipc_event_impl!(
swayipc_async::ShutdownEvent,
EventType::Shutdown,
Event::Shutdown
);
sway_ipc_event_impl!(swayipc_async::TickEvent, EventType::Tick, Event::Tick);
sway_ipc_event_impl!(
swayipc_async::BarStateUpdateEvent,
EventType::BarStateUpdate,
Event::BarStateUpdate
);
sway_ipc_event_impl!(swayipc_async::InputEvent, EventType::Input, Event::Input);

View file

@ -20,12 +20,14 @@
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
#[zbus::dbus_proxy(
use zbus::proxy;
#[proxy(
interface = "org.erikreider.swaync.cc",
default_service = "org.erikreider.swaync.cc",
default_path = "/org/erikreider/swaync/cc"
)]
trait SwayNc {
pub trait SwayNc {
/// AddInhibitor method
fn add_inhibitor(&self, application_id: &str) -> zbus::Result<bool>;
@ -90,11 +92,11 @@ trait SwayNc {
fn toggle_visibility(&self) -> zbus::Result<()>;
/// Subscribe signal
#[dbus_proxy(signal)]
#[zbus(signal)]
fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
/// SubscribeV2 signal
#[dbus_proxy(signal)]
#[zbus(signal)]
fn subscribe_v2(
&self,
count: u32,
@ -104,8 +106,8 @@ trait SwayNc {
) -> zbus::Result<()>;
/// Inhibited property
#[dbus_proxy(property)]
#[zbus(property)]
fn inhibited(&self) -> zbus::Result<bool>;
#[dbus_proxy(property)]
#[zbus(property)]
fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
}

View file

@ -1,6 +1,7 @@
mod dbus;
use crate::{register_fallible_client, send, spawn};
use crate::channels::SyncSenderExt;
use crate::{register_fallible_client, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
@ -54,9 +55,13 @@ impl Client {
spawn(async move {
while let Some(ev) = stream.next().await {
let ev = ev.body::<Event>().expect("to deserialize");
let ev = ev
.message()
.body()
.deserialize::<Event>()
.expect("to deserialize");
debug!("Received event: {ev:?}");
send!(tx, ev);
tx.send_expect(ev);
}
});
}

621
src/clients/sysinfo.rs Normal file
View file

@ -0,0 +1,621 @@
use crate::modules::sysinfo::Interval;
use crate::{lock, register_client};
use color_eyre::{Report, Result};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use sysinfo::{Components, Disks, LoadAvg, Networks, RefreshKind, System};
#[repr(u64)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum Prefix {
#[default]
None = 1,
Kilo = 1000,
Mega = Prefix::Kilo as u64 * 1000,
Giga = Prefix::Mega as u64 * 1000,
Tera = Prefix::Giga as u64 * 1000,
Peta = Prefix::Tera as u64 * 1000,
Kibi = 1024,
Mebi = Prefix::Kibi as u64 * 1024,
Gibi = Prefix::Mebi as u64 * 1024,
Tebi = Prefix::Gibi as u64 * 1024,
Pebi = Prefix::Tebi as u64 * 1024,
// # Units
// These are special cases
// where you'd actually want to do slightly more than a prefix alone.
// Included as part of the prefix system for simplicity.
KiloBit = 128,
MegaBit = Prefix::KiloBit as u64 * 1024,
GigaBit = Prefix::MegaBit as u64 * 1024,
}
#[derive(Debug, Clone)]
pub enum Function {
None,
Sum,
Min,
Max,
Mean,
Name(String),
}
impl FromStr for Function {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"sum" => Ok(Self::Sum),
"min" => Ok(Self::Min),
"max" => Ok(Self::Max),
"mean" => Ok(Self::Mean),
"" => Err(()),
_ => Ok(Self::Name(s.to_string())),
}
}
}
#[derive(Debug)]
pub struct ValueSet {
values: HashMap<Box<str>, Value>,
}
impl FromIterator<(Box<str>, Value)> for ValueSet {
fn from_iter<T: IntoIterator<Item = (Box<str>, Value)>>(iter: T) -> Self {
Self {
values: iter.into_iter().collect(),
}
}
}
impl ValueSet {
fn values(&self, prefix: Prefix) -> impl Iterator<Item = f64> + use<'_> {
self.values
.values()
.map(move |v| v.get(prefix))
.filter(|v| !v.is_nan())
}
pub fn apply(&self, function: &Function, prefix: Prefix) -> f64 {
match function {
Function::None => 0.0,
Function::Sum => self.sum(prefix),
Function::Min => self.min(prefix),
Function::Max => self.max(prefix),
Function::Mean => self.mean(prefix),
Function::Name(name) => self
.values
.get(&Box::from(name.as_str()))
.map(|v| v.get(prefix))
.unwrap_or_default(),
}
}
fn sum(&self, prefix: Prefix) -> f64 {
self.values(prefix).sum()
}
fn min(&self, prefix: Prefix) -> f64 {
self.values(prefix)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.unwrap_or_default()
}
fn max(&self, prefix: Prefix) -> f64 {
self.values(prefix)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.unwrap_or_default()
}
fn mean(&self, prefix: Prefix) -> f64 {
self.sum(prefix) / self.values.len() as f64
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Value {
value: f64,
prefix: Prefix,
}
impl Value {
pub fn new(value: f64) -> Self {
Self::new_with_prefix(value, Prefix::None)
}
pub fn new_with_prefix(value: f64, prefix: Prefix) -> Self {
Self { value, prefix }
}
pub fn get(self, prefix: Prefix) -> f64 {
if prefix == self.prefix {
self.value
} else {
let scale = self.prefix as u64 as f64 / prefix as u64 as f64;
self.value * scale
}
}
}
#[derive(Debug)]
pub struct Client {
system: Mutex<System>,
disks: Mutex<Disks>,
components: Mutex<Components>,
networks: Mutex<Networks>,
load_average: Mutex<LoadAvg>,
}
impl Client {
pub fn new() -> Self {
let refresh_kind = RefreshKind::everything().without_processes();
let system = System::new_with_specifics(refresh_kind);
let disks = Disks::new_with_refreshed_list();
let components = Components::new_with_refreshed_list();
let networks = Networks::new_with_refreshed_list();
let load_average = System::load_average();
Self {
system: Mutex::new(system),
disks: Mutex::new(disks),
components: Mutex::new(components),
networks: Mutex::new(networks),
load_average: Mutex::new(load_average),
}
}
pub fn refresh_cpu(&self) {
lock!(self.system).refresh_cpu_all();
}
pub fn refresh_memory(&self) {
lock!(self.system).refresh_memory();
}
pub fn refresh_network(&self) {
lock!(self.networks).refresh(true);
}
pub fn refresh_temps(&self) {
lock!(self.components).refresh(true);
}
pub fn refresh_disks(&self) {
lock!(self.disks).refresh(true);
}
pub fn refresh_load_average(&self) {
*lock!(self.load_average) = System::load_average();
}
pub fn cpu_frequency(&self) -> ValueSet {
lock!(self.system)
.cpus()
.iter()
.map(|cpu| {
(
cpu.name().into(),
Value::new_with_prefix(cpu.frequency() as f64, Prefix::Mega),
)
})
.collect()
}
pub fn cpu_percent(&self) -> ValueSet {
lock!(self.system)
.cpus()
.iter()
.map(|cpu| (cpu.name().into(), Value::new(cpu.cpu_usage() as f64)))
.collect()
}
pub fn memory_free(&self) -> Value {
Value::new(lock!(self.system).free_memory() as f64)
}
pub fn memory_available(&self) -> Value {
Value::new(lock!(self.system).available_memory() as f64)
}
pub fn memory_total(&self) -> Value {
Value::new(lock!(self.system).total_memory() as f64)
}
pub fn memory_used(&self) -> Value {
Value::new(lock!(self.system).used_memory() as f64)
}
pub fn memory_percent(&self) -> Value {
let total = lock!(self.system).total_memory() as f64;
let used = lock!(self.system).used_memory() as f64;
Value::new(used / total * 100.0)
}
pub fn swap_free(&self) -> Value {
Value::new(lock!(self.system).free_swap() as f64)
}
pub fn swap_total(&self) -> Value {
Value::new(lock!(self.system).total_swap() as f64)
}
pub fn swap_used(&self) -> Value {
Value::new(lock!(self.system).used_swap() as f64)
}
pub fn swap_percent(&self) -> Value {
let total = lock!(self.system).total_swap() as f64;
let used = lock!(self.system).used_swap() as f64;
Value::new(used / total * 100.0)
}
pub fn temp_c(&self) -> ValueSet {
lock!(self.components)
.iter()
.map(|comp| {
(
comp.label().into(),
Value::new(comp.temperature().unwrap_or_default() as f64),
)
})
.collect()
}
pub fn temp_f(&self) -> ValueSet {
lock!(self.components)
.iter()
.map(|comp| {
(
comp.label().into(),
Value::new(c_to_f(comp.temperature().unwrap_or_default() as f64)),
)
})
.collect()
}
pub fn disk_free(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.available_space() as f64),
)
})
.collect()
}
pub fn disk_total(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.total_space() as f64),
)
})
.collect()
}
pub fn disk_used(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new((disk.total_space() - disk.available_space()) as f64),
)
})
.collect()
}
pub fn disk_percent(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(
(disk.total_space() - disk.available_space()) as f64
/ disk.total_space() as f64
* 100.0,
),
)
})
.collect()
}
pub fn disk_read(&self, interval: Interval) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.usage().read_bytes as f64 / interval.disks() as f64),
)
})
.collect()
}
pub fn disk_write(&self, interval: Interval) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.usage().written_bytes as f64 / interval.disks() as f64),
)
})
.collect()
}
pub fn net_down(&self, interval: Interval) -> ValueSet {
lock!(self.networks)
.iter()
.map(|(name, net)| {
(
name.as_str().into(),
Value::new(net.received() as f64 / interval.networks() as f64),
)
})
.collect()
}
pub fn net_up(&self, interval: Interval) -> ValueSet {
lock!(self.networks)
.iter()
.map(|(name, net)| {
(
name.as_str().into(),
Value::new(net.transmitted() as f64 / interval.networks() as f64),
)
})
.collect()
}
pub fn load_average_1(&self) -> Value {
Value::new(lock!(self.load_average).one)
}
pub fn load_average_5(&self) -> Value {
Value::new(lock!(self.load_average).five)
}
pub fn load_average_15(&self) -> Value {
Value::new(lock!(self.load_average).fifteen)
}
/// Gets system uptime formatted as `HH:mm`.
pub fn uptime() -> String {
let uptime = System::uptime();
let hours = uptime / 3600;
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60)
}
}
register_client!(Client, sys_info);
const fn c_to_f(c: f64) -> f64 {
c / 5.0 * 9.0 + 32.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
CpuFrequency,
CpuPercent,
MemoryFree,
MemoryAvailable,
MemoryTotal,
MemoryUsed,
MemoryPercent,
SwapFree,
SwapTotal,
SwapUsed,
SwapPercent,
TempC,
TempF,
DiskFree,
DiskTotal,
DiskUsed,
DiskPercent,
DiskRead,
DiskWrite,
NetDown,
NetUp,
LoadAverage1,
LoadAverage5,
LoadAverage15,
Uptime,
}
impl FromStr for TokenType {
type Err = Report;
fn from_str(s: &str) -> Result<Self> {
match s {
"cpu_frequency" => Ok(Self::CpuFrequency),
"cpu_percent" => Ok(Self::CpuPercent),
"memory_free" => Ok(Self::MemoryFree),
"memory_available" => Ok(Self::MemoryAvailable),
"memory_total" => Ok(Self::MemoryTotal),
"memory_used" => Ok(Self::MemoryUsed),
"memory_percent" => Ok(Self::MemoryPercent),
"swap_free" => Ok(Self::SwapFree),
"swap_total" => Ok(Self::SwapTotal),
"swap_used" => Ok(Self::SwapUsed),
"swap_percent" => Ok(Self::SwapPercent),
"temp_c" => Ok(Self::TempC),
"temp_f" => Ok(Self::TempF),
"disk_free" => Ok(Self::DiskFree),
"disk_total" => Ok(Self::DiskTotal),
"disk_used" => Ok(Self::DiskUsed),
"disk_percent" => Ok(Self::DiskPercent),
"disk_read" => Ok(Self::DiskRead),
"disk_write" => Ok(Self::DiskWrite),
"net_down" => Ok(Self::NetDown),
"net_up" => Ok(Self::NetUp),
"load_average_1" => Ok(Self::LoadAverage1),
"load_average_5" => Ok(Self::LoadAverage5),
"load_average_15" => Ok(Self::LoadAverage15),
"uptime" => Ok(Self::Uptime),
_ => Err(Report::msg(format!("invalid token type: '{s}'"))),
}
}
}
#[cfg(feature = "ipc")]
use crate::ironvar::Namespace;
#[cfg(feature = "ipc")]
impl Namespace for Client {
fn get(&self, key: &str) -> Option<String> {
let get = |value: Value| Some(value.get(Prefix::None).to_string());
let token = TokenType::from_str(key).ok()?;
match token {
TokenType::CpuFrequency => None,
TokenType::CpuPercent => None,
TokenType::MemoryFree => get(self.memory_free()),
TokenType::MemoryAvailable => get(self.memory_available()),
TokenType::MemoryTotal => get(self.memory_total()),
TokenType::MemoryUsed => get(self.memory_used()),
TokenType::MemoryPercent => get(self.memory_percent()),
TokenType::SwapFree => get(self.swap_free()),
TokenType::SwapTotal => get(self.swap_total()),
TokenType::SwapUsed => get(self.swap_used()),
TokenType::SwapPercent => get(self.swap_percent()),
TokenType::TempC => None,
TokenType::TempF => None,
TokenType::DiskFree => None,
TokenType::DiskTotal => None,
TokenType::DiskUsed => None,
TokenType::DiskPercent => None,
TokenType::DiskRead => None,
TokenType::DiskWrite => None,
TokenType::NetDown => None,
TokenType::NetUp => None,
TokenType::LoadAverage1 => get(self.load_average_1()),
TokenType::LoadAverage5 => get(self.load_average_5()),
TokenType::LoadAverage15 => get(self.load_average_15()),
TokenType::Uptime => Some(Client::uptime()),
}
}
fn list(&self) -> Vec<String> {
vec![
"memory_free",
"memory_available",
"memory_total",
"memory_used",
"memory_percent",
"swap_free",
"swap_total",
"swap_used",
"swap_percent",
"load_average_1",
"load_average_5",
"load_average_15",
"uptime",
]
.into_iter()
.map(ToString::to_string)
.collect()
}
fn namespaces(&self) -> Vec<String> {
vec![
"cpu_frequency",
"cpu_percent",
"temp_c",
"temp_f",
"disk_free",
"disk_total",
"disk_used",
"disk_percent",
"disk_read",
"disk_write",
"net_down",
"net_up",
]
.into_iter()
.map(ToString::to_string)
.collect()
}
fn get_namespace(&self, key: &str) -> Option<Arc<dyn Namespace + Sync + Send>> {
let token = TokenType::from_str(key).ok()?;
match token {
TokenType::CpuFrequency => Some(Arc::new(self.cpu_frequency())),
TokenType::CpuPercent => Some(Arc::new(self.cpu_percent())),
TokenType::MemoryFree => None,
TokenType::MemoryAvailable => None,
TokenType::MemoryTotal => None,
TokenType::MemoryUsed => None,
TokenType::MemoryPercent => None,
TokenType::SwapFree => None,
TokenType::SwapTotal => None,
TokenType::SwapUsed => None,
TokenType::SwapPercent => None,
TokenType::TempC => Some(Arc::new(self.temp_c())),
TokenType::TempF => Some(Arc::new(self.temp_f())),
TokenType::DiskFree => Some(Arc::new(self.disk_free())),
TokenType::DiskTotal => Some(Arc::new(self.disk_total())),
TokenType::DiskUsed => Some(Arc::new(self.disk_used())),
TokenType::DiskPercent => Some(Arc::new(self.disk_percent())),
TokenType::DiskRead => Some(Arc::new(self.disk_read(Interval::All(1)))),
TokenType::DiskWrite => Some(Arc::new(self.disk_write(Interval::All(1)))),
TokenType::NetDown => Some(Arc::new(self.net_down(Interval::All(1)))),
TokenType::NetUp => Some(Arc::new(self.net_up(Interval::All(1)))),
TokenType::LoadAverage1 => None,
TokenType::LoadAverage5 => None,
TokenType::LoadAverage15 => None,
TokenType::Uptime => None,
}
}
}
#[cfg(feature = "ipc")]
impl Namespace for ValueSet {
fn get(&self, key: &str) -> Option<String> {
let function = Function::from_str(key).ok()?;
Some(self.apply(&function, Prefix::None).to_string())
}
fn list(&self) -> Vec<String> {
let mut vec = vec!["sum", "min", "max", "mean"]
.into_iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
vec.extend(self.values.keys().map(ToString::to_string));
vec
}
fn namespaces(&self) -> Vec<String> {
vec![]
}
fn get_namespace(&self, _key: &str) -> Option<Arc<dyn Namespace + Sync + Send>> {
None
}
}

View file

@ -1,35 +0,0 @@
use crate::register_client;
use std::sync::Arc;
use upower_dbus::UPowerProxy;
use zbus::fdo::PropertiesProxy;
pub async fn create_display_proxy() -> Arc<PropertiesProxy<'static>> {
let dbus = Box::pin(zbus::Connection::system())
.await
.expect("failed to create connection to system bus");
let device_proxy = UPowerProxy::new(&dbus)
.await
.expect("failed to create upower proxy");
let display_device = device_proxy
.get_display_device()
.await
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
let path = display_device.path().to_owned();
let proxy = PropertiesProxy::builder(&dbus)
.destination("org.freedesktop.UPower")
.expect("failed to set proxy destination address")
.path(path)
.expect("failed to set proxy path")
.cache_properties(zbus::CacheProperties::No)
.build()
.await
.expect("failed to build proxy");
Arc::new(proxy)
}
register_client!(PropertiesProxy<'static>, upower);

159
src/clients/upower/dbus.rs Normal file
View file

@ -0,0 +1,159 @@
/// Originally taken from `upower-dbus` crate
/// <https://github.com/pop-os/upower-dbus/blob/main/LICENSE>
// Copyright 2021 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use zbus::proxy;
use zbus::zvariant::OwnedValue;
#[derive(Debug, Copy, Clone, PartialEq, Eq, OwnedValue)]
#[repr(u32)]
pub enum BatteryState {
Unknown = 0,
Charging = 1,
Discharging = 2,
Empty = 3,
FullyCharged = 4,
PendingCharge = 5,
PendingDischarge = 6,
}
#[derive(Debug, Copy, Clone, OwnedValue)]
#[repr(u32)]
pub enum BatteryType {
Unknown = 0,
LinePower = 1,
Battery = 2,
Ups = 3,
Monitor = 4,
Mouse = 5,
Keyboard = 6,
Pda = 7,
Phone = 8,
}
#[derive(Debug, Copy, Clone, OwnedValue)]
#[repr(u32)]
pub enum BatteryLevel {
Unknown = 0,
None = 1,
Low = 3,
Critical = 4,
Normal = 6,
High = 7,
Full = 8,
}
#[proxy(
interface = "org.freedesktop.UPower.Device",
default_service = "org.freedesktop.UPower",
assume_defaults = false
)]
pub trait Device {
#[zbus(property)]
fn battery_level(&self) -> zbus::Result<BatteryLevel>;
#[zbus(property)]
fn capacity(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn energy(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn energy_empty(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn energy_full(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn energy_full_design(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn has_history(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn has_statistics(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn icon_name(&self) -> zbus::Result<String>;
#[zbus(property)]
fn is_present(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn is_rechargeable(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn luminosity(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn model(&self) -> zbus::Result<String>;
#[zbus(property)]
fn native_path(&self) -> zbus::Result<String>;
#[zbus(property)]
fn online(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn percentage(&self) -> zbus::Result<f64>;
#[zbus(property)]
fn power_supply(&self) -> zbus::Result<bool>;
fn refresh(&self) -> zbus::Result<()>;
#[zbus(property)]
fn serial(&self) -> zbus::Result<String>;
#[zbus(property)]
fn state(&self) -> zbus::Result<BatteryState>;
#[zbus(property)]
fn temperature(&self) -> zbus::Result<f64>;
#[zbus(property, name = "Type")]
fn type_(&self) -> zbus::Result<BatteryType>;
#[zbus(property)]
fn vendor(&self) -> zbus::Result<String>;
#[zbus(property)]
fn voltage(&self) -> zbus::Result<f64>;
}
#[proxy(interface = "org.freedesktop.UPower", assume_defaults = true)]
pub trait UPower {
/// EnumerateDevices method
fn enumerate_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>;
/// GetCriticalAction method
fn get_critical_action(&self) -> zbus::Result<String>;
/// GetDisplayDevice method
#[zbus(object = "Device")]
fn get_display_device(&self);
/// DeviceAdded signal
#[zbus(signal)]
fn device_added(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
/// DeviceRemoved signal
#[zbus(signal)]
fn device_removed(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
/// DaemonVersion property
#[zbus(property)]
fn daemon_version(&self) -> zbus::Result<String>;
/// LidIsClosed property
#[zbus(property)]
fn lid_is_closed(&self) -> zbus::Result<bool>;
/// LidIsPresent property
#[zbus(property)]
fn lid_is_present(&self) -> zbus::Result<bool>;
/// OnBattery property
#[zbus(property)]
fn on_battery(&self) -> zbus::Result<bool>;
}

33
src/clients/upower/mod.rs Normal file
View file

@ -0,0 +1,33 @@
mod dbus;
use crate::clients::ClientResult;
use crate::register_fallible_client;
use dbus::UPowerProxy;
use std::sync::Arc;
use zbus::fdo::PropertiesProxy;
use zbus::proxy::CacheProperties;
pub use dbus::BatteryState;
pub async fn create_display_proxy() -> ClientResult<PropertiesProxy<'static>> {
let dbus = Box::pin(zbus::Connection::system()).await?;
let device_proxy = UPowerProxy::new(&dbus).await?;
let display_device = device_proxy.get_display_device().await?;
let path = display_device.inner().path();
let proxy = PropertiesProxy::builder(&dbus)
.destination("org.freedesktop.UPower")
.expect("failed to set proxy destination address")
.path(path)
.expect("failed to set proxy path")
.cache_properties(CacheProperties::No)
.build()
.await?;
Ok(Arc::new(proxy))
}
register_fallible_client!(PropertiesProxy<'static>, upower);

View file

@ -1,7 +1,7 @@
mod sink;
mod sink_input;
use crate::{arc_mut, lock, register_client, send, spawn_blocking, APP_ID};
use crate::{APP_ID, arc_mut, lock, register_client, spawn_blocking};
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, ServerInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation};
@ -12,8 +12,9 @@ use libpulse_binding::volume::{ChannelVolumes, Volume};
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info, trace, warn};
use crate::channels::SyncSenderExt;
pub use sink::Sink;
pub use sink_input::SinkInput;
@ -230,6 +231,8 @@ fn on_event(
return;
};
trace!("server event: {facility:?}, op: {op:?}, i: {i}");
match facility {
Facility::Server => on_server_event(context, &data.sinks, &data.default_sink_name, tx),
Facility::Sink => sink::on_event(context, &data.sinks, &data.default_sink_name, tx, op, i),
@ -269,7 +272,7 @@ fn set_default_sink(
{
sink.active = true;
debug!("Set sink active: {}", sink.name);
send!(tx, Event::UpdateSink(sink.clone()));
tx.send_expect(Event::UpdateSink(sink.clone()));
} else {
warn!("Couldn't find sink: {}", default_sink_name);
}

View file

@ -1,13 +1,14 @@
use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
use crate::{lock, send};
use super::{ArcMutVec, Client, ConnectionState, Event, percent_to_volume, volume_to_percent};
use crate::channels::SyncSenderExt;
use crate::lock;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::Context;
use libpulse_binding::context::introspect::SinkInfo;
use libpulse_binding::context::subscribe::Operation;
use libpulse_binding::context::Context;
use libpulse_binding::def::SinkState;
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{Arc, Mutex, mpsc};
use tokio::sync::broadcast;
use tracing::{debug, error};
use tracing::{debug, error, instrument, trace};
#[derive(Debug, Clone)]
pub struct Sink {
@ -41,16 +42,19 @@ impl From<&SinkInfo<'_>> for Sink {
}
impl Client {
#[instrument(level = "trace")]
pub fn sinks(&self) -> Arc<Mutex<Vec<Sink>>> {
self.data.sinks.clone()
}
#[instrument(level = "trace")]
pub fn set_default_sink(&self, name: &str) {
if let ConnectionState::Connected { context, .. } = &*lock!(self.connection) {
lock!(context).set_default_sink(name, |_| {});
}
}
#[instrument(level = "trace")]
pub fn set_sink_volume(&self, name: &str, volume_percent: f64) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
let (tx, rx) = mpsc::channel();
@ -59,7 +63,7 @@ impl Client {
let ListResult::Item(info) = info else {
return;
};
send!(tx, info.volume);
tx.send_expect(info.volume);
});
let new_volume = percent_to_volume(volume_percent);
@ -73,6 +77,7 @@ impl Client {
}
}
#[instrument(level = "trace")]
pub fn set_sink_muted(&self, name: &str, muted: bool) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
introspector.set_sink_mute_by_name(name, muted, None);
@ -122,8 +127,10 @@ pub fn add(info: ListResult<&SinkInfo>, sinks: &ArcMutVec<Sink>, tx: &broadcast:
return;
};
trace!("adding {info:?}");
lock!(sinks).push(info.into());
send!(tx, Event::AddSink(info.into()));
tx.send_expect(Event::AddSink(info.into()));
}
fn update(
@ -136,6 +143,8 @@ fn update(
return;
};
trace!("updating {info:?}");
{
let mut sinks = lock!(sinks);
let Some(pos) = sinks.iter().position(|sink| sink.index == info.index) else {
@ -162,14 +171,16 @@ fn update(
}
}
send!(tx, Event::UpdateSink(sink));
tx.send_expect(Event::UpdateSink(sink));
}
fn remove(index: u32, sinks: &ArcMutVec<Sink>, tx: &broadcast::Sender<Event>) {
trace!("removing {index}");
let mut sinks = lock!(sinks);
if let Some(pos) = sinks.iter().position(|s| s.index == index) {
let info = sinks.remove(pos);
send!(tx, Event::RemoveSink(info.name));
tx.send_expect(Event::RemoveSink(info.name));
}
}

View file

@ -1,12 +1,13 @@
use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
use crate::{lock, send};
use super::{ArcMutVec, Client, ConnectionState, Event, percent_to_volume, volume_to_percent};
use crate::channels::SyncSenderExt;
use crate::lock;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::Context;
use libpulse_binding::context::introspect::SinkInputInfo;
use libpulse_binding::context::subscribe::Operation;
use libpulse_binding::context::Context;
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{Arc, Mutex, mpsc};
use tokio::sync::broadcast;
use tracing::{debug, error};
use tracing::{debug, error, instrument, trace};
#[derive(Debug, Clone)]
pub struct SinkInput {
@ -35,10 +36,12 @@ impl From<&SinkInputInfo<'_>> for SinkInput {
}
impl Client {
#[instrument(level = "trace")]
pub fn sink_inputs(&self) -> Arc<Mutex<Vec<SinkInput>>> {
self.data.sink_inputs.clone()
}
#[instrument(level = "trace")]
pub fn set_input_volume(&self, index: u32, volume_percent: f64) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
let (tx, rx) = mpsc::channel();
@ -47,7 +50,7 @@ impl Client {
let ListResult::Item(info) = info else {
return;
};
send!(tx, info.volume);
tx.send_expect(info.volume);
});
let new_volume = percent_to_volume(volume_percent);
@ -61,6 +64,7 @@ impl Client {
}
}
#[instrument(level = "trace")]
pub fn set_input_muted(&self, index: u32, muted: bool) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
introspector.set_sink_input_mute(index, muted, None);
@ -112,8 +116,10 @@ pub fn add(
return;
};
trace!("adding {info:?}");
lock!(inputs).push(info.into());
send!(tx, Event::AddInput(info.into()));
tx.send_expect(Event::AddInput(info.into()));
}
fn update(
@ -125,6 +131,8 @@ fn update(
return;
};
trace!("updating {info:?}");
{
let mut inputs = lock!(inputs);
let Some(pos) = inputs.iter().position(|input| input.index == info.index) else {
@ -135,14 +143,16 @@ fn update(
inputs[pos] = info.into();
}
send!(tx, Event::UpdateInput(info.into()));
tx.send_expect(Event::UpdateInput(info.into()));
}
fn remove(index: u32, inputs: &ArcMutVec<SinkInput>, tx: &broadcast::Sender<Event>) {
let mut inputs = lock!(inputs);
trace!("removing {index}");
if let Some(pos) = inputs.iter().position(|s| s.index == index) {
let info = inputs.remove(pos);
send!(tx, Event::RemoveInput(info.index));
tx.send_expect(Event::RemoveInput(info.index));
}
}

View file

@ -2,17 +2,18 @@ mod macros;
mod wl_output;
mod wl_seat;
use crate::error::{ExitCode, ERR_CHANNEL_RECV};
use crate::{arc_mut, lock, register_client, send, spawn, spawn_blocking};
use crate::error::{ERR_CHANNEL_RECV, ExitCode};
use crate::{arc_mut, lock, register_client, spawn, spawn_blocking};
use std::process::exit;
use std::sync::{Arc, Mutex};
use crate::channels::SyncSenderExt;
use calloop_channel::Event::Msg;
use cfg_if::cfg_if;
use color_eyre::Report;
use color_eyre::{Help, Report};
use smithay_client_toolkit::output::OutputState;
use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::reexports::calloop::channel as calloop_channel;
use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle};
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::SeatState;
@ -43,7 +44,6 @@ cfg_if! {
use self::wlr_data_control::device::DataControlDevice;
use self::wlr_data_control::manager::DataControlDeviceManagerState;
use self::wlr_data_control::source::CopyPasteSource;
use self::wlr_data_control::SelectionOfferItem;
use wayland_client::protocol::wl_seat::WlSeat;
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
@ -76,6 +76,8 @@ pub enum Request {
ToplevelInfoAll,
#[cfg(feature = "launcher")]
ToplevelFocus(usize),
#[cfg(feature = "launcher")]
ToplevelMinimize(usize),
#[cfg(feature = "clipboard")]
CopyToClipboard(ClipboardItem),
@ -150,12 +152,12 @@ impl Client {
spawn(async move {
while let Some(event) = event_rx.recv().await {
match event {
Event::Output(event) => send!(output_tx, event),
Event::Output(event) => output_tx.send_expect(event),
#[cfg(any(feature = "focused", feature = "launcher"))]
Event::Toplevel(event) => send!(toplevel_tx, event),
Event::Toplevel(event) => toplevel_tx.send_expect(event),
#[cfg(feature = "clipboard")]
Event::Clipboard(item) => send!(clipboard_tx, item),
};
Event::Clipboard(item) => clipboard_tx.send_expect(item),
}
}
});
}
@ -175,7 +177,7 @@ impl Client {
/// Sends a request to the environment event loop,
/// and returns the response.
fn send_request(&self, request: Request) -> Response {
send!(self.tx, request);
self.tx.send_expect(request);
lock!(self.rx).recv().expect(ERR_CHANNEL_RECV)
}
@ -193,7 +195,6 @@ pub struct Environment {
seat_state: SeatState,
queue_handle: QueueHandle<Self>,
loop_handle: LoopHandle<'static, Self>,
event_tx: mpsc::Sender<Event>,
response_tx: std::sync::mpsc::Sender<Response>,
@ -204,14 +205,12 @@ pub struct Environment {
// -- clipboard --
#[cfg(feature = "clipboard")]
data_control_device_manager_state: DataControlDeviceManagerState,
data_control_device_manager_state: Option<DataControlDeviceManagerState>,
#[cfg(feature = "clipboard")]
data_control_devices: Vec<DataControlDeviceEntry>,
#[cfg(feature = "clipboard")]
copy_paste_sources: Vec<CopyPasteSource>,
#[cfg(feature = "clipboard")]
selection_offers: Vec<SelectionOfferItem>,
// local state
#[cfg(feature = "clipboard")]
@ -265,12 +264,30 @@ impl Environment {
let output_state = OutputState::new(&globals, &qh);
let seat_state = SeatState::new(&globals, &qh);
#[cfg(any(feature = "focused", feature = "launcher"))]
ToplevelManagerState::bind(&globals, &qh)
.expect("to bind to wlr_foreign_toplevel_manager global");
if let Err(err) = ToplevelManagerState::bind(&globals, &qh) {
error!("{:?}",
Report::new(err)
.wrap_err("Failed to bind to wlr_foreign_toplevel_manager global")
.note("This is likely a due to the current compositor not supporting the required protocol")
.note("launcher and focused modules will not work")
);
}
#[cfg(feature = "clipboard")]
let data_control_device_manager_state = DataControlDeviceManagerState::bind(&globals, &qh)
.expect("to bind to wlr_data_control_device_manager global");
let data_control_device_manager_state = match DataControlDeviceManagerState::bind(
&globals, &qh,
) {
Ok(state) => Some(state),
Err(err) => {
error!("{:?}",
Report::new(err)
.wrap_err("Failed to bind to wlr_data_control_device global")
.note("This is likely a due to the current compositor not supporting the required protocol")
.note("clipboard module will not work")
);
None
}
};
let mut env = Self {
registry_state,
@ -279,7 +296,6 @@ impl Environment {
#[cfg(feature = "clipboard")]
data_control_device_manager_state,
queue_handle: qh,
loop_handle: loop_handle.clone(),
event_tx,
response_tx,
#[cfg(any(feature = "focused", feature = "launcher"))]
@ -290,8 +306,6 @@ impl Environment {
#[cfg(feature = "clipboard")]
copy_paste_sources: vec![],
#[cfg(feature = "clipboard")]
selection_offers: vec![],
#[cfg(feature = "clipboard")]
clipboard: arc_mut!(None),
};
@ -320,12 +334,12 @@ impl Environment {
match event {
Msg(Request::Roundtrip) => {
debug!("received roundtrip request");
send!(env.response_tx, Response::Ok);
env.response_tx.send_expect(Response::Ok);
}
#[cfg(feature = "ipc")]
Msg(Request::OutputInfoAll) => {
let infos = env.output_info_all();
send!(env.response_tx, Response::OutputInfoAll(infos));
env.response_tx.send_expect(Response::OutputInfoAll(infos));
}
#[cfg(any(feature = "focused", feature = "launcher"))]
Msg(Request::ToplevelInfoAll) => {
@ -334,31 +348,46 @@ impl Environment {
.iter()
.filter_map(ToplevelHandle::info)
.collect();
send!(env.response_tx, Response::ToplevelInfoAll(infos));
env.response_tx
.send_expect(Response::ToplevelInfoAll(infos));
}
#[cfg(feature = "launcher")]
Msg(Request::ToplevelFocus(id)) => {
let handle = env
.handles
.iter()
.find(|handle| handle.info().map_or(false, |info| info.id == id));
.find(|handle| handle.info().is_some_and(|info| info.id == id));
if let Some(handle) = handle {
let seat = env.default_seat();
handle.focus(&seat);
}
send!(env.response_tx, Response::Ok);
env.response_tx.send_expect(Response::Ok);
}
#[cfg(feature = "launcher")]
Msg(Request::ToplevelMinimize(id)) => {
let handle = env
.handles
.iter()
.find(|handle| handle.info().is_some_and(|info| info.id == id));
if let Some(handle) = handle {
handle.minimize();
}
env.response_tx.send_expect(Response::Ok);
}
#[cfg(feature = "clipboard")]
Msg(Request::CopyToClipboard(item)) => {
env.copy_to_clipboard(item);
send!(env.response_tx, Response::Ok);
env.response_tx.send_expect(Response::Ok);
}
#[cfg(feature = "clipboard")]
Msg(Request::ClipboardItem) => {
let item = lock!(env.clipboard).clone();
send!(env.response_tx, Response::ClipboardItem(item));
env.response_tx.send_expect(Response::ClipboardItem(item));
}
calloop_channel::Event::Closed => error!("request channel unexpectedly closed"),
}

View file

@ -1,5 +1,5 @@
use super::{Client, Environment, Event};
use crate::try_send;
use crate::channels::AsyncSenderExt;
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
use tokio::sync::broadcast;
use tracing::{debug, error};
@ -12,7 +12,7 @@ pub struct OutputEvent {
pub event_type: OutputEventType,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputEventType {
New,
Update,
@ -63,13 +63,10 @@ impl OutputHandler for Environment {
fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
debug!("Handler received new output");
if let Some(info) = self.output_state.info(&output) {
try_send!(
self.event_tx,
Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::New
})
);
self.event_tx.send_spawn(Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::New,
}));
} else {
error!("Output is missing information!");
}
@ -78,13 +75,10 @@ impl OutputHandler for Environment {
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
debug!("Handle received output update");
if let Some(info) = self.output_state.info(&output) {
try_send!(
self.event_tx,
Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::Update
})
);
self.event_tx.send_spawn(Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::Update,
}));
} else {
error!("Output is missing information!");
}
@ -93,13 +87,10 @@ impl OutputHandler for Environment {
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
debug!("Handle received output destruction");
if let Some(info) = self.output_state.info(&output) {
try_send!(
self.event_tx,
Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::Destroyed
})
);
self.event_tx.send_spawn(Event::Output(OutputEvent {
output: info,
event_type: OutputEventType::Destroyed,
}));
} else {
error!("Output is missing information!");
}

View file

@ -1,6 +1,6 @@
use super::Environment;
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use tracing::debug;
use tracing::{debug, error};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, QueueHandle};
@ -37,7 +37,11 @@ impl SeatHandler for Environment {
{
debug!("Adding new data control device");
// create the data device here for this seat
let data_control_device_manager = &self.data_control_device_manager_state;
let Some(data_control_device_manager) = &self.data_control_device_manager_state else {
error!("data_control_device_manager not available, cannot copy");
return;
};
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
self.data_control_devices
.push(super::DataControlDeviceEntry {

View file

@ -6,7 +6,7 @@ use crate::error::ERR_WAYLAND_DATA;
use crate::lock;
use std::sync::{Arc, Mutex};
use tracing::warn;
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, event_created_child};
use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
@ -37,7 +37,9 @@ pub trait DataControlDeviceDataExt: Send + Sync {
fn selection_mime_types(&self) -> Vec<String> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
let offer = &lock!(inner.inner).selection_offer;
lock!(offer)
.as_ref()
.map(|offer| {
let data = offer
@ -51,14 +53,14 @@ pub trait DataControlDeviceDataExt: Send + Sync {
/// Get the active selection offer if it exists.
fn selection_offer(&self) -> Option<SelectionOffer> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
.as_ref()
.and_then(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.as_selection_offer()
})
let offer = &lock!(inner.inner).selection_offer;
lock!(offer).as_ref().and_then(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.as_selection_offer()
})
}
}
@ -159,7 +161,9 @@ where
}
}
Event::Finished => {
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
warn!(
"Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues."
);
}
_ => {}
}

View file

@ -4,23 +4,29 @@ pub mod offer;
pub mod source;
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler};
use self::source::DataControlSourceHandler;
use super::{Client, Environment, Event, Request, Response};
use crate::{lock, try_send, Ironbar};
use crate::channels::AsyncSenderExt;
use crate::{Ironbar, lock, spawn};
use color_eyre::Result;
use device::DataControlDevice;
use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
use rustix::buffer::spare_capacity;
use rustix::event::epoll;
use rustix::event::epoll::CreateFlags;
use rustix::fs::Timespec;
use rustix::pipe::{fcntl_getpipe_size, fcntl_setpipe_size};
use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken};
use std::cmp::min;
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::{ErrorKind, Read, Write};
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::io::Write;
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::sync::Arc;
use std::time::Duration;
use std::{fs, io};
use tokio::io::AsyncReadExt;
use tokio::sync::broadcast;
use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle};
@ -28,12 +34,6 @@ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
#[derive(Debug)]
pub struct SelectionOfferItem {
offer: SelectionOffer,
token: Option<RegistrationToken>,
}
/// Represents a value which can be read/written
/// to/from the system clipboard and surrounding metadata.
///
@ -148,6 +148,11 @@ impl Environment {
pub fn copy_to_clipboard(&mut self, item: ClipboardItem) {
debug!("Copying item to clipboard: {item:?}");
let Some(data_control_device_manager) = &self.data_control_device_manager_state else {
error!("data_control_device_manager not available, cannot copy");
return;
};
let seat = self.default_seat();
let Some(device) = self
.data_control_devices
@ -157,9 +162,8 @@ impl Environment {
return;
};
let source = self
.data_control_device_manager_state
.create_copy_paste_source(&self.queue_handle, [INTERNAL_MIME_TYPE, &item.mime_type]);
let source = data_control_device_manager
.create_copy_paste_source(&self.queue_handle, [&item.mime_type, INTERNAL_MIME_TYPE]);
source.set_selection(&device.device);
self.copy_paste_sources.push(source);
@ -168,22 +172,20 @@ impl Environment {
}
/// Reads an offer file handle into a new `ClipboardItem`.
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
async fn read_file(
mime_type: &MimeType,
file: &mut tokio::net::unix::pipe::Receiver,
) -> io::Result<ClipboardItem> {
let mut buf = vec![];
file.read_to_end(&mut buf).await?;
let value = match mime_type.category {
MimeTypeCategory::Text => {
let mut txt = String::new();
file.read_to_string(&mut txt)?;
let txt = String::from_utf8_lossy(&buf).to_string();
ClipboardValue::Text(txt)
}
MimeTypeCategory::Image => {
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
debug!("Read bytes: {}", bytes.len());
let bytes = Bytes::from(&bytes);
let bytes = Bytes::from(&buf);
ClipboardValue::Image(bytes)
}
};
@ -214,68 +216,33 @@ impl DataControlDeviceHandler for Environment {
}
if let Some(offer) = data_device.selection_offer() {
self.selection_offers
.push(SelectionOfferItem { offer, token: None });
let cur_offer = self
.selection_offers
.last_mut()
.expect("Failed to get current offer");
// clear prev
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
lock!(self.clipboard).take();
// send an event so the clipboard module is aware it's changed
try_send!(
self.event_tx,
Event::Clipboard(ClipboardItem {
id: usize::MAX,
mime_type: String::new().into(),
value: Arc::new(ClipboardValue::Other)
})
);
self.event_tx.send_spawn(Event::Clipboard(ClipboardItem {
id: usize::MAX,
mime_type: String::new().into(),
value: Arc::new(ClipboardValue::Other),
}));
return;
};
debug!("Receiving mime type: {}", mime_type.value);
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
let offer_clone = cur_offer.offer.clone();
if let Ok(mut read_pipe) = offer.receive(mime_type.value.clone()) {
let tx = self.event_tx.clone();
let clipboard = self.clipboard.clone();
let token =
self.loop_handle
.insert_source(read_pipe, move |(), file, state| unsafe {
let item = state
.selection_offers
.iter()
.position(|o| o.offer == offer_clone)
.map(|p| state.selection_offers.remove(p))
.expect("Failed to find selection offer item");
match Self::read_file(&mime_type, file.get_mut()) {
Ok(item) => {
lock!(clipboard).replace(item.clone());
try_send!(tx, Event::Clipboard(item));
}
Err(err) => error!("{err:?}"),
}
state
.loop_handle
.remove(item.token.expect("Missing item token"));
PostAction::Remove
});
match token {
Ok(token) => {
cur_offer.token.replace(token);
spawn(async move {
match Self::read_file(&mime_type, &mut read_pipe).await {
Ok(item) => {
lock!(clipboard).replace(item.clone());
tx.send_spawn(Event::Clipboard(item));
}
Err(err) => error!("{err:?}"),
}
Err(err) => error!("Failed to insert read pipe event: {err:?}"),
}
});
}
}
}
@ -313,7 +280,7 @@ impl DataControlSourceHandler for Environment {
source: &ZwlrDataControlSourceV1,
mime: String,
write_pipe: WritePipe,
) {
) -> Result<()> {
debug!("Handler received source send request event ({mime})");
if let Some(item) = lock!(self.clipboard).clone() {
@ -330,32 +297,34 @@ impl DataControlSourceHandler for Environment {
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type")
io::Error::other("Attempted to copy unsupported mime type")
),
};
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
.expect("Failed to increase pipe size");
let pipe_size =
set_pipe_size(fd.as_fd(), bytes.len()).expect("Failed to increase pipe size");
let mut file = File::from(fd.try_clone().expect("to be able to clone"));
debug!("Writing {} bytes", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll = epoll::create(CreateFlags::CLOEXEC)?;
epoll::add(
&epoll,
fd,
epoll::EventData::new_u64(0),
epoll::EventFlags::OUT,
)?;
let epoll_fd =
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
epoll_fd
.add(fd, epoll_event)
.expect("to send valid epoll operation");
let mut events = Vec::with_capacity(16);
let timeout = EpollTimeout::from(100u16);
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
let chunk = &bytes[..min(pipe_size, bytes.len())];
epoll_fd
.wait(&mut events, timeout)
.expect("Failed to wait to epoll");
epoll::wait(
&epoll,
spare_capacity(&mut events),
Some(&Timespec::try_from(Duration::from_millis(100))?),
)?;
match file.write(chunk) {
Ok(written) => {
@ -371,9 +340,11 @@ impl DataControlSourceHandler for Environment {
debug!("Done writing");
} else {
error!("Failed to find source");
error!("Failed to find source (mime: '{mime}')");
}
}
Ok(())
}
fn cancelled(
@ -398,7 +369,7 @@ impl DataControlSourceHandler for Environment {
/// it will be clamped at this.
///
/// Returns the new size if succeeded.
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
fn set_pipe_size(fd: BorrowedFd, size: usize) -> io::Result<usize> {
// clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
.expect("Failed to find pipe-max-size virtual kernel file")
@ -408,23 +379,24 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
let size = min(size, max_pipe_size);
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
let curr_size = fcntl_getpipe_size(fd)?;
trace!("Current pipe size: {curr_size}");
let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
fcntl_setpipe_size(fd, size)?;
let res = fcntl_getpipe_size(fd)?;
trace!("New pipe size: {res}");
if res < size as i32 {
if res < size {
return Err(io::Error::last_os_error());
}
res
} else {
size as i32
size
};
Ok(new_size)

View file

@ -1,12 +1,11 @@
use super::manager::DataControlDeviceManagerState;
use crate::lock;
use nix::fcntl::OFlag;
use nix::unistd::pipe2;
use rustix::pipe::{PipeFlags, pipe_with};
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut;
use std::os::fd::AsFd;
use std::sync::{Arc, Mutex};
use tokio::net::unix::pipe::Receiver;
use tracing::trace;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
@ -36,8 +35,8 @@ impl PartialEq for SelectionOffer {
}
impl SelectionOffer {
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
unsafe { receive(&self.data_offer, mime_type) }.map_err(DataOfferError::Io)
pub fn receive(&self, mime_type: String) -> Result<Receiver, DataOfferError> {
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
}
}
@ -169,14 +168,11 @@ where
///
/// Fails if too many file descriptors were already open and a pipe
/// could not be created.
pub unsafe fn receive(
offer: &ZwlrDataControlOfferV1,
mime_type: String,
) -> std::io::Result<ReadPipe> {
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<Receiver> {
// create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
let (readfd, writefd) = pipe_with(PipeFlags::CLOEXEC)?;
offer.receive(mime_type, writefd.as_fd());
Ok(ReadPipe::from(readfd))
Receiver::from_owned_fd(readfd)
}

View file

@ -1,6 +1,8 @@
use super::device::DataControlDevice;
use super::manager::DataControlDeviceManagerState;
use color_eyre::Result;
use smithay_client_toolkit::data_device_manager::WritePipe;
use tracing::error;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
Event, ZwlrDataControlSourceV1,
@ -23,7 +25,7 @@ impl DataControlSourceDataExt for DataControlSourceData {
///
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
pub trait DataControlSourceHandler: Sized {
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
// /// This may be called multiple times, once for each accepted mime type from the destination, if any.
// fn accept_mime(
// &mut self,
// conn: &Connection,
@ -41,7 +43,7 @@ pub trait DataControlSourceHandler: Sized {
source: &ZwlrDataControlSourceV1,
mime: String,
fd: WritePipe,
);
) -> Result<()>;
/// The data source is no longer valid
/// Cleanup & destroy this resource
@ -68,7 +70,9 @@ where
) {
match event {
Event::Send { mime_type, fd } => {
state.send_request(conn, qh, source, mime_type, fd.into());
if let Err(err) = state.send_request(conn, qh, source, mime_type, fd.into()) {
error!("{err:#}");
}
}
Event::Cancelled => {
state.cancelled(conn, qh, source);

View file

@ -1,5 +1,5 @@
use super::manager::ToplevelManagerState;
use crate::{lock, Ironbar};
use crate::{Ironbar, lock};
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use tracing::trace;
@ -33,6 +33,11 @@ impl ToplevelHandle {
trace!("Activating handle");
self.handle.activate(seat);
}
pub fn minimize(&self) {
trace!("Minimizing handle");
self.handle.set_minimized();
}
}
#[derive(Debug, Default)]
@ -146,7 +151,7 @@ where
ToplevelHandle {
handle: handle.clone(),
},
)
);
}
Event::Done if !lock!(data.inner).closed => {
{

View file

@ -4,7 +4,7 @@ use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use std::marker::PhantomData;
use tracing::{debug, warn};
use wayland_client::globals::{BindError, GlobalList};
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
use wayland_client::{Connection, Dispatch, QueueHandle, event_created_child};
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
@ -67,7 +67,9 @@ where
state.toplevel(conn, qhandle);
}
Event::Finished => {
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
warn!(
"Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues."
);
}
_ => {}
}

View file

@ -4,11 +4,11 @@ pub mod manager;
use self::handle::ToplevelHandleHandler;
use self::manager::ToplevelManagerHandler;
use super::{Client, Environment, Event, Request, Response};
use crate::try_send;
use tokio::sync::broadcast;
use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle};
use crate::channels::AsyncSenderExt;
pub use handle::{ToplevelHandle, ToplevelInfo};
#[derive(Debug, Clone)]
@ -36,6 +36,15 @@ impl Client {
}
}
/// Minimizes the toplevel with the provided ID.
#[cfg(feature = "launcher")]
pub fn toplevel_minimize(&self, handle_id: usize) {
match self.send_request(Request::ToplevelMinimize(handle_id)) {
Response::Ok => (),
_ => unreachable!(),
}
}
/// Subscribes to events from toplevels.
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
self.toplevel_channel.0.subscribe()
@ -54,10 +63,16 @@ impl ToplevelHandleHandler for Environment {
match handle.info() {
Some(info) => {
if info.app_id.is_empty() {
trace!("ignoring xwayland dialog");
return;
}
trace!("Adding new handle: {info:?}");
self.handles.push(handle.clone());
if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::New(info)));
self.event_tx
.send_spawn(Event::Toplevel(ToplevelEvent::New(info)));
}
}
None => {
@ -78,7 +93,8 @@ impl ToplevelHandleHandler for Environment {
Some(info) => {
trace!("Updating handle: {info:?}");
if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(info)));
self.event_tx
.send_spawn(Event::Toplevel(ToplevelEvent::Update(info)));
}
}
None => {
@ -97,7 +113,8 @@ impl ToplevelHandleHandler for Environment {
self.handles.retain(|h| h != &handle);
if let Some(info) = handle.info() {
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Remove(info)));
self.event_tx
.send_spawn(Event::Toplevel(ToplevelEvent::Remove(info)));
}
}
}

View file

@ -1,9 +1,9 @@
use crate::dynamic_value::{dynamic_string, DynamicBool};
use crate::dynamic_value::{DynamicBool, dynamic_string};
use crate::script::{Script, ScriptInput};
use glib::Propagation;
use gtk::gdk::ScrollDirection;
use gtk::prelude::*;
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
use gtk::{EventBox, Justification, Orientation, Revealer, RevealerTransitionType};
use serde::Deserialize;
use tracing::trace;
@ -198,6 +198,28 @@ impl From<ModuleOrientation> for Orientation {
}
}
#[derive(Debug, Default, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum ModuleJustification {
#[default]
Left,
Right,
Center,
Fill,
}
impl From<ModuleJustification> for Justification {
fn from(o: ModuleJustification) -> Self {
match o {
ModuleJustification::Left => Self::Left,
ModuleJustification::Right => Self::Right,
ModuleJustification::Center => Self::Center,
ModuleJustification::Fill => Self::Fill,
}
}
}
impl TransitionType {
pub const fn to_revealer_transition_type(
&self,
@ -246,6 +268,13 @@ impl CommonConfig {
let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(),
ScrollDirection::Smooth => {
if event.scroll_deltas().unwrap_or_default().1 > 0.0 {
scroll_down_script.as_ref()
} else {
scroll_up_script.as_ref()
}
}
_ => None,
};
@ -272,8 +301,7 @@ impl CommonConfig {
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
if let Some(tooltip) = self.tooltip {
let container = container.clone();
dynamic_string(&tooltip, move |string| {
dynamic_string(&tooltip, container, move |container, string| {
container.set_tooltip_text(Some(&string));
});
}
@ -285,19 +313,15 @@ impl CommonConfig {
container.show_all();
},
|show_if| {
// need to keep clone here for the notify callback
let container = container.clone();
{
let revealer = revealer.clone();
let container = container.clone();
show_if.subscribe(move |success| {
if success {
container.show_all();
}
revealer.set_reveal_child(success);
});
}
show_if.subscribe((revealer, &container), |(revealer, container), success| {
if success {
container.show_all();
}
revealer.set_reveal_child(success);
});
revealer.connect_child_revealed_notify(move |revealer| {
if !revealer.reveals_child() {

View file

@ -37,26 +37,24 @@ impl<'de> Deserialize<'de> for MonitorConfig {
pub fn deserialize_layer<'de, D>(deserializer: D) -> Result<gtk_layer_shell::Layer, D::Error>
where
D: serde::Deserializer<'de>,
D: Deserializer<'de>,
{
use gtk_layer_shell::Layer;
let value = Option::<String>::deserialize(deserializer)?;
value
.map(|v| match v.as_str() {
"background" => Ok(Layer::Background),
"bottom" => Ok(Layer::Bottom),
"top" => Ok(Layer::Top),
"overlay" => Ok(Layer::Overlay),
_ => Err(serde::de::Error::custom("invalid value for orientation")),
})
.unwrap_or(Ok(Layer::Top))
value.map_or(Ok(Layer::Top), |v| match v.as_str() {
"background" => Ok(Layer::Background),
"bottom" => Ok(Layer::Bottom),
"top" => Ok(Layer::Top),
"overlay" => Ok(Layer::Overlay),
_ => Err(serde::de::Error::custom("invalid value for orientation")),
})
}
#[cfg(feature = "schema")]
pub fn schema_layer(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
pub fn schema_layer(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::JsonSchema;
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(gen).into();
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(generator).into();
schema.enum_values = Some(vec![
"background".into(),
"bottom".into(),
@ -79,7 +77,7 @@ impl BarPosition {
/// Gets the angle that label text should be displayed at
/// based on this position.
pub const fn get_angle(self) -> f64 {
pub const fn angle(self) -> f64 {
match self {
Self::Top | Self::Bottom => 0.0,
Self::Left => 90.0,

35
src/config/layout.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::config::{ModuleJustification, ModuleOrientation};
use crate::modules::ModuleInfo;
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LayoutConfig {
/// The orientation to display the widget contents.
/// Setting to vertical will rotate text 90 degrees.
///
/// **Valid options**: `horizontal`, `vertical`
/// <br>
/// **Default**: `horizontal`
orientation: Option<ModuleOrientation>,
/// The justification (alignment) of the widget text shown on the bar.
///
/// **Valid options**: `left`, `right`, `center`, `fill`
/// <br>
/// **Default**: `left`
#[serde(default)]
pub justify: ModuleJustification,
}
impl LayoutConfig {
pub fn orientation(&self, info: &ModuleInfo) -> gtk::Orientation {
self.orientation
.map_or(info.bar_position.orientation(), ModuleOrientation::into)
}
pub fn angle(&self, info: &ModuleInfo) -> f64 {
self.orientation
.map_or(info.bar_position.angle(), ModuleOrientation::to_angle)
}
}

View file

@ -1,25 +1,35 @@
mod common;
mod r#impl;
mod layout;
mod truncate;
#[cfg(feature = "bindmode")]
use crate::modules::bindmode::Bindmode;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
use crate::modules::clock::ClockModule;
#[cfg(feature = "custom")]
use crate::modules::custom::CustomModule;
#[cfg(feature = "focused")]
use crate::modules::focused::FocusedModule;
#[cfg(feature = "keyboard")]
use crate::modules::keyboard::KeyboardModule;
#[cfg(feature = "label")]
use crate::modules::label::LabelModule;
#[cfg(feature = "launcher")]
use crate::modules::launcher::LauncherModule;
#[cfg(feature = "menu")]
use crate::modules::menu::MenuModule;
#[cfg(feature = "music")]
use crate::modules::music::MusicModule;
#[cfg(feature = "network_manager")]
use crate::modules::networkmanager::NetworkManagerModule;
#[cfg(feature = "notifications")]
use crate::modules::notifications::NotificationsModule;
#[cfg(feature = "script")]
use crate::modules::script::ScriptModule;
#[cfg(feature = "sys_info")]
use crate::modules::sysinfo::SysInfoModule;
@ -35,37 +45,46 @@ use crate::modules::workspaces::WorkspacesModule;
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
use cfg_if::cfg_if;
use color_eyre::Result;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::Deserialize;
use std::collections::HashMap;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
pub use self::truncate::TruncateMode;
pub use self::common::{CommonConfig, ModuleJustification, ModuleOrientation, TransitionType};
pub use self::layout::LayoutConfig;
pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum ModuleConfig {
#[cfg(feature = "bindmode")]
Bindmode(Box<Bindmode>),
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
Clock(Box<ClockModule>),
#[cfg(feature = "custom")]
Custom(Box<CustomModule>),
#[cfg(feature = "focused")]
Focused(Box<FocusedModule>),
#[cfg(feature = "keyboard")]
Keyboard(Box<KeyboardModule>),
#[cfg(feature = "label")]
Label(Box<LabelModule>),
#[cfg(feature = "launcher")]
Launcher(Box<LauncherModule>),
#[cfg(feature = "menu")]
Menu(Box<MenuModule>),
#[cfg(feature = "music")]
Music(Box<MusicModule>),
#[cfg(feature = "network_manager")]
NetworkManager(Box<NetworkManagerModule>),
#[cfg(feature = "notifications")]
Notifications(Box<NotificationsModule>),
#[cfg(feature = "script")]
Script(Box<ScriptModule>),
#[cfg(feature = "sys_info")]
SysInfo(Box<SysInfoModule>),
@ -93,24 +112,33 @@ impl ModuleConfig {
}
match self {
#[cfg(feature = "bindmode")]
Self::Bindmode(module) => create!(module),
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]
Self::Clock(module) => create!(module),
#[cfg(feature = "custom")]
Self::Custom(module) => create!(module),
#[cfg(feature = "focused")]
Self::Focused(module) => create!(module),
#[cfg(feature = "keyboard")]
Self::Keyboard(module) => create!(module),
#[cfg(feature = "label")]
Self::Label(module) => create!(module),
#[cfg(feature = "launcher")]
Self::Launcher(module) => create!(module),
#[cfg(feature = "menu")]
Self::Menu(module) => create!(module),
#[cfg(feature = "music")]
Self::Music(module) => create!(module),
#[cfg(feature = "network_manager")]
Self::NetworkManager(module) => create!(module),
#[cfg(feature = "notifications")]
Self::Notifications(module) => create!(module),
#[cfg(feature = "script")]
Self::Script(module) => create!(module),
#[cfg(feature = "sys_info")]
Self::SysInfo(module) => create!(module),
@ -124,6 +152,53 @@ impl ModuleConfig {
Self::Workspaces(module) => create!(module),
}
}
pub fn name(&self) -> String {
match self {
#[cfg(feature = "bindmode")]
ModuleConfig::Bindmode(_) => "Bindmode",
#[cfg(feature = "cairo")]
ModuleConfig::Cairo(_) => "Cario",
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(_) => "Clipboard",
#[cfg(feature = "clock")]
ModuleConfig::Clock(_) => "Clock",
#[cfg(feature = "custom")]
ModuleConfig::Custom(_) => "Custom",
#[cfg(feature = "focused")]
ModuleConfig::Focused(_) => "Focused",
#[cfg(feature = "keyboard")]
ModuleConfig::Keyboard(_) => "Keyboard",
#[cfg(feature = "label")]
ModuleConfig::Label(_) => "Label",
#[cfg(feature = "launcher")]
ModuleConfig::Launcher(_) => "Launcher",
#[cfg(feature = "menu")]
ModuleConfig::Menu(_) => "Menu",
#[cfg(feature = "music")]
ModuleConfig::Music(_) => "Music",
#[cfg(feature = "network_manager")]
ModuleConfig::NetworkManager(_) => "NetworkManager",
#[cfg(feature = "notifications")]
ModuleConfig::Notifications(_) => "Notifications",
#[cfg(feature = "script")]
ModuleConfig::Script(_) => "Script",
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(_) => "SysInfo",
#[cfg(feature = "tray")]
ModuleConfig::Tray(_) => "Tray",
#[cfg(feature = "upower")]
ModuleConfig::Upower(_) => "UPower",
#[cfg(feature = "volume")]
ModuleConfig::Volume(_) => "Volume",
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(_) => "Workspaces",
// in case no modules are compiled
#[allow(unreachable_patterns)]
_ => "",
}
.to_string()
}
}
#[derive(Debug, Clone)]
@ -272,12 +347,6 @@ pub struct BarConfig {
#[serde(default)]
pub autohide: Option<u64>,
/// The name of the GTK icon theme to use.
/// Leave unset to use the default Adwaita theme.
///
/// **Default**: `null`
pub icon_theme: Option<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.
///
@ -325,10 +394,12 @@ impl Default for BarConfig {
height: default_bar_height(),
start_hidden: None,
autohide: None,
icon_theme: None,
#[cfg(feature = "label")]
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
)]),
#[cfg(not(feature = "label"))]
start: None,
center,
end,
anchor_to_edges: default_true(),
@ -376,6 +447,19 @@ pub struct Config {
///
/// Providing this option overrides the single, global `bar` option.
pub monitors: Option<HashMap<String, MonitorConfig>>,
/// The name of the GTK icon theme to use.
/// Leave unset to use the default Adwaita theme.
///
/// **Default**: `null`
pub icon_theme: Option<String>,
/// Map of app IDs (or classes) to icon names,
/// overriding the app's default icon.
///
/// **Default**: `{}`
#[serde(default)]
pub icon_overrides: HashMap<String, String>,
}
const fn default_layer() -> gtk_layer_shell::Layer {
@ -397,3 +481,7 @@ pub const fn default_false() -> bool {
pub const fn default_true() -> bool {
true
}
pub fn default_launch_command() -> String {
String::from("gtk-launch {app_name}")
}

View file

@ -1,19 +1,21 @@
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
use gtk::prelude::*;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, Copy)]
#[derive(Debug, Deserialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum EllipsizeMode {
None,
Start,
Middle,
#[default]
End,
}
impl From<EllipsizeMode> for GtkEllipsizeMode {
fn from(value: EllipsizeMode) -> Self {
match value {
EllipsizeMode::None => Self::None,
EllipsizeMode::Start => Self::Start,
EllipsizeMode::Middle => Self::Middle,
EllipsizeMode::End => Self::End,
@ -27,10 +29,23 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
///
/// The option can be configured in one of two modes.
///
/// **Default**: `Auto (end)`
///
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum TruncateMode {
/// Do not truncate content.
///
/// Setting this option may cause excessively long content to overflow other widgets,
/// shifting them off-screen.
///
/// # Example
///
/// ```corn
/// { truncate = "off" }
Off,
/// Auto mode lets GTK decide when to ellipsize.
///
/// To use this mode, set the truncate option to a string
@ -44,7 +59,7 @@ pub enum TruncateMode {
///
/// **Valid options**: `start`, `middle`, `end`
/// <br>
/// **Default**: `null`
/// **Default**: `end`
Auto(EllipsizeMode),
/// Length mode defines a fixed point at which to ellipsize.
@ -88,36 +103,34 @@ pub enum TruncateMode {
},
}
impl TruncateMode {
const fn mode(&self) -> EllipsizeMode {
match self {
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
}
impl Default for TruncateMode {
fn default() -> Self {
Self::Auto(EllipsizeMode::default())
}
}
const fn length(&self) -> Option<i32> {
impl TruncateMode {
pub const fn length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::Auto(_) | Self::Off => None,
Self::Length { length, .. } => *length,
}
}
const fn max_length(&self) -> Option<i32> {
pub const fn max_length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::Auto(_) | Self::Off => None,
Self::Length { max_length, .. } => *max_length,
}
}
}
pub fn truncate_label(&self, label: &gtk::Label) {
label.set_ellipsize(self.mode().into());
if let Some(length) = self.length() {
label.set_width_chars(length);
}
if let Some(length) = self.max_length() {
label.set_max_width_chars(length);
}
impl From<TruncateMode> for GtkEllipsizeMode {
fn from(value: TruncateMode) -> Self {
let mode = match value {
TruncateMode::Off => EllipsizeMode::None,
TruncateMode::Length { mode, .. } | TruncateMode::Auto(mode) => mode,
};
mode.into()
}
}

View file

@ -1,37 +1,302 @@
use std::collections::{HashMap, HashSet};
use crate::spawn;
use color_eyre::{Help, Report, Result};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use tracing::warn;
use std::process::{Command, Stdio};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::Mutex;
use tracing::{debug, error};
use walkdir::{DirEntry, WalkDir};
use crate::lock;
type DesktopFile = HashMap<String, Vec<String>>;
fn desktop_files() -> &'static Mutex<HashMap<PathBuf, DesktopFile>> {
static DESKTOP_FILES: OnceLock<Mutex<HashMap<PathBuf, DesktopFile>>> = OnceLock::new();
DESKTOP_FILES.get_or_init(|| Mutex::new(HashMap::new()))
#[derive(Debug, Clone)]
enum DesktopFileRef {
Unloaded(PathBuf),
Loaded(DesktopFile),
}
fn desktop_files_look_out_keys() -> &'static HashSet<&'static str> {
static DESKTOP_FILES_LOOK_OUT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
DESKTOP_FILES_LOOK_OUT_KEYS
.get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]))
impl DesktopFileRef {
async fn get(&mut self) -> Result<DesktopFile> {
match self {
DesktopFileRef::Unloaded(path) => {
let (tx, rx) = tokio::sync::oneshot::channel();
let path = path.clone();
spawn(async move { tx.send(Self::load(&path).await) });
let file = rx.await??;
*self = DesktopFileRef::Loaded(file.clone());
Ok(file)
}
DesktopFileRef::Loaded(file) => Ok(file.clone()),
}
}
async fn load(file_path: &Path) -> Result<DesktopFile> {
debug!("loading applications file: {}", file_path.display());
let file = tokio::fs::File::open(file_path).await?;
let mut desktop_file = DesktopFile::new(
file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
let mut lines = BufReader::new(file).lines();
let mut has_name = false;
let mut has_type = false;
let mut has_wm_class = false;
let mut has_exec = false;
let mut has_icon = false;
let mut has_categories = false;
let mut has_no_display = false;
while let Ok(Some(line)) = lines.next_line().await {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key {
"Name" if !has_name => {
desktop_file.name = Some(value.to_string());
has_name = true;
}
"Type" if !has_type => {
desktop_file.app_type = Some(value.to_string());
has_type = true;
}
"StartupWMClass" if !has_wm_class => {
desktop_file.startup_wm_class = Some(value.to_string());
has_wm_class = true;
}
"Exec" if !has_exec => {
desktop_file.exec = Some(value.to_string());
has_exec = true;
}
"Icon" if !has_icon => {
desktop_file.icon = Some(value.to_string());
has_icon = true;
}
"Categories" if !has_categories => {
desktop_file.categories = value.split(';').map(|s| s.to_string()).collect();
has_categories = true;
}
"NoDisplay" if !has_no_display => {
desktop_file.no_display = Some(value.parse()?);
has_no_display = true;
}
_ => {}
}
// parsing complete - don't bother with the rest of the lines
if has_name
&& has_type
&& has_wm_class
&& has_exec
&& has_icon
&& has_categories
&& has_no_display
{
break;
}
}
Ok(desktop_file)
}
}
/// Finds directories that should contain `.desktop` files
/// and exist on the filesystem.
fn find_application_dirs() -> Vec<PathBuf> {
#[derive(Debug, Clone)]
pub struct DesktopFile {
pub file_name: String,
pub name: Option<String>,
pub app_type: Option<String>,
pub startup_wm_class: Option<String>,
pub exec: Option<String>,
pub icon: Option<String>,
pub categories: Vec<String>,
pub no_display: Option<bool>,
}
impl DesktopFile {
fn new(file_name: String) -> Self {
Self {
file_name,
name: None,
app_type: None,
startup_wm_class: None,
exec: None,
icon: None,
categories: vec![],
no_display: None,
}
}
}
type FileMap = HashMap<Box<str>, DesktopFileRef>;
/// Desktop file cache and resolver.
///
/// Files are lazy-loaded as required on resolution.
#[derive(Debug, Clone)]
pub struct DesktopFiles {
files: Arc<Mutex<FileMap>>,
}
impl Default for DesktopFiles {
fn default() -> Self {
Self::new()
}
}
impl DesktopFiles {
/// Creates a new instance,
/// scanning disk to generate a list of (unloaded) file refs in the process.
pub fn new() -> Self {
let desktop_files: FileMap = dirs()
.iter()
.flat_map(|path| files(path))
.map(|file| {
(
file.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string()
.into(),
DesktopFileRef::Unloaded(file),
)
})
.collect();
debug!("resolved {} files", desktop_files.len());
Self {
files: Arc::new(Mutex::new(desktop_files)),
}
}
pub async fn get_all(&self) -> Result<Vec<DesktopFile>> {
let mut files = self.files.lock().await;
let mut res = Vec::with_capacity(files.len());
for file in files.values_mut() {
let file = file.get().await?;
res.push(file);
}
Ok(res)
}
/// Attempts to locate a applications file by file name or contents.
///
/// Input should typically be the app id, app name or icon.
pub async fn find(&self, input: &str) -> Result<Option<DesktopFile>> {
let mut res = self.find_by_file_name(input).await?;
if res.is_none() {
res = self.find_by_file_contents(input).await?;
}
debug!("found match for app_id {input}: {}", res.is_some());
Ok(res)
}
/// Checks file names for an exact or partial match of the provided input.
async fn find_by_file_name(&self, input: &str) -> Result<Option<DesktopFile>> {
let mut files = self.files.lock().await;
let mut file_ref = files
.iter_mut()
.find(|&(name, _)| name.eq_ignore_ascii_case(input));
if file_ref.is_none() {
file_ref = files.iter_mut().find(
|&(name, _)| // this will attempt to find flatpak apps that are in the format
// `com.company.app` or `com.app.something`
input
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part)),
);
}
let file_ref = file_ref.map(|(_, file)| file);
if let Some(file_ref) = file_ref {
let file = file_ref.get().await?;
Ok(Some(file))
} else {
Ok(None)
}
}
/// Checks file contents for an exact or partial match of the provided input.
async fn find_by_file_contents(&self, app_id: &str) -> Result<Option<DesktopFile>> {
let mut files = self.files.lock().await;
// first pass - check name for exact match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.name {
if name.eq_ignore_ascii_case(app_id) {
return Ok(Some(file));
}
}
}
// second pass - check name for partial match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.name {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
}
// third pass - check remaining fields for partial match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.exec {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
if let Some(name) = &file.startup_wm_class {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
if let Some(name) = &file.icon {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
}
Ok(None)
}
}
/// Gets a list of paths to all directories
/// containing `.applications` files.
fn dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/usr/share/applications"), // system installed apps
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
];
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
if let Some(xdg_dirs) = xdg_dirs {
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
let xdg_dirs = env::var("XDG_DATA_DIRS");
if let Ok(xdg_dirs) = xdg_dirs {
for mut xdg_dir in env::split_paths(&xdg_dirs) {
xdg_dir.push("applications");
dirs.push(xdg_dir);
}
@ -43,157 +308,85 @@ fn find_application_dirs() -> Vec<PathBuf> {
dirs.push(user_dir);
}
dirs.into_iter().filter(|dir| dir.exists()).collect()
dirs.into_iter().filter(|dir| dir.exists()).rev().collect()
}
/// Finds all the desktop files
fn find_desktop_files() -> Vec<PathBuf> {
let dirs = find_application_dirs();
dirs.into_iter()
.flat_map(|dir| {
WalkDir::new(dir)
.max_depth(5)
.into_iter()
.filter_map(Result::ok)
.map(DirEntry::into_path)
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
})
/// Gets a list of all `.applications` files in the provided directory.
///
/// The directory is recursed to a maximum depth of 5.
fn files(dir: &Path) -> Vec<PathBuf> {
WalkDir::new(dir)
.max_depth(5)
.into_iter()
.filter_map(Result::ok)
.map(DirEntry::into_path)
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
.collect()
}
/// Attempts to locate a `.desktop` file for an app id
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
// this is necessary to invalidate the cache
let files = find_desktop_files();
find_desktop_file_by_filename(app_id, &files)
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
/// Starts a `.desktop` file with the provided formatted command.
pub fn open_program(file_name: &str, str: &str) {
let expanded = str.replace("{app_name}", file_name);
let launch_command_parts: Vec<&str> = expanded.split_whitespace().collect();
if let Err(err) = Command::new(&launch_command_parts[0])
.args(&launch_command_parts[1..])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run launch command.")
.suggestion("Perhaps the applications file is invalid?")
);
}
}
/// Finds the correct desktop file using a simple condition check
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let with_names = files
.iter()
.map(|f| {
(
f,
f.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase(),
)
})
.collect::<Vec<_>>();
#[cfg(test)]
mod tests {
use super::*;
with_names
.iter()
// first pass - check for exact match
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
// second pass - check for substring
.or_else(|| {
with_names.iter().find(|(_, name)| {
// this will attempt to find flatpak apps that are in the format
// `com.company.app` or `com.app.something`
app_id
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part))
})
})
.map(|(file, _)| file.into())
}
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let app_id = &app_id.to_lowercase();
let mut desktop_files_cache = lock!(desktop_files());
let files = files
.iter()
.filter_map(|file| {
let parsed_desktop_file = parse_desktop_file(file)?;
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
Some((file.clone(), parsed_desktop_file))
})
.collect::<Vec<_>>();
let file = files
.iter()
// first pass - check name key for exact match
.find(|(_, desktop_file)| {
desktop_file
.get("Name")
.is_some_and(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
})
// second pass - check name key for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file.get("Name").is_some_and(|names| {
names
.iter()
.any(|name| name.to_lowercase().contains(app_id))
})
})
})
// third pass - check all keys for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file
.values()
.flatten()
.any(|value| value.to_lowercase().contains(app_id))
})
});
file.map(|(path, _)| path).cloned()
}
/// Parses a desktop file into a hashmap of keys/vector(values).
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
let Ok(file) = fs::read_to_string(path) else {
warn!("Couldn't Open File: {}", path.display());
return None;
};
let mut desktop_file: DesktopFile = DesktopFile::new();
file.lines()
.filter_map(|line| {
let (key, value) = line.split_once('=')?;
let key = key.trim();
let value = value.trim();
if desktop_files_look_out_keys().contains(key) {
Some((key, value))
} else {
None
}
})
.for_each(|(key, value)| {
desktop_file
.entry(key.to_string())
.or_default()
.push(value.to_string());
});
Some(desktop_file)
}
/// Attempts to get the icon name from the app's `.desktop` file.
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
let path = find_desktop_file(app_id)?;
let mut desktop_files_cache = lock!(desktop_files());
let desktop_file = match desktop_files_cache.get(&path) {
Some(desktop_file) => desktop_file,
_ => desktop_files_cache
.entry(path.clone())
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
};
let mut icons = desktop_file.get("Icon").into_iter().flatten();
icons.next().map(std::string::ToString::to_string)
fn setup() {
unsafe {
let pwd = env::current_dir().unwrap();
env::set_var("XDG_DATA_DIRS", format!("{}/test-configs", pwd.display()));
}
}
#[tokio::test]
async fn find_by_filename() {
setup();
let desktop_files = DesktopFiles::new();
let file = desktop_files.find_by_file_name("firefox").await.unwrap();
assert!(file.is_some());
assert_eq!(file.unwrap().file_name, "firefox.desktop");
}
#[tokio::test]
async fn find_by_file_contents() {
setup();
let desktop_files = DesktopFiles::new();
let file = desktop_files.find_by_file_contents("427520").await.unwrap();
assert!(file.is_some());
assert_eq!(file.unwrap().file_name, "Factorio.desktop");
}
#[tokio::test]
async fn parser() {
let mut file_ref =
DesktopFileRef::Unloaded(PathBuf::from("test-configs/applications/firefox.desktop"));
let file = file_ref.get().await.unwrap();
assert_eq!(file.name, Some("Firefox".to_string()));
assert_eq!(file.icon, Some("firefox".to_string()));
assert_eq!(file.exec, Some("/usr/lib/firefox/firefox %u".to_string()));
assert_eq!(file.startup_wm_class, Some("firefox".to_string()));
assert_eq!(file.app_type, Some("Application".to_string()));
}
}

View file

@ -1,7 +1,8 @@
use crate::script::Script;
use crate::{glib_recv_mpsc, spawn, try_send};
#[cfg(feature = "ipc")]
use crate::{send_async, Ironbar};
use crate::Ironbar;
use crate::channels::{AsyncSenderExt, Dependency, MpscReceiverExt};
use crate::script::Script;
use crate::spawn;
use cfg_if::cfg_if;
use serde::Deserialize;
use tokio::sync::mpsc;
@ -18,9 +19,11 @@ pub enum DynamicBool {
}
impl DynamicBool {
pub fn subscribe<F>(self, mut f: F)
pub fn subscribe<D, F>(self, deps: D, f: F)
where
F: FnMut(bool) + 'static,
D: Dependency,
D::Target: Clone + 'static,
F: FnMut(&D::Target, bool) + 'static,
{
let value = match self {
Self::Unknown(input) => {
@ -42,14 +45,14 @@ impl DynamicBool {
let (tx, rx) = mpsc::channel(32);
glib_recv_mpsc!(rx, val => f(val));
rx.recv_glib(deps, f);
spawn(async move {
match value {
DynamicBool::Script(script) => {
script
.run(None, |_, success| {
try_send!(tx, success);
tx.send_spawn(success);
})
.await;
}
@ -58,11 +61,11 @@ impl DynamicBool {
let variable_manager = Ironbar::variable_manager();
let variable_name = variable[1..].into(); // remove hash
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
let mut rx = variable_manager.subscribe(variable_name);
while let Ok(value) = rx.recv().await {
let has_value = value.is_some_and(|s| is_truthy(&s));
send_async!(tx, has_value);
tx.send_expect(has_value).await;
}
}
DynamicBool::Unknown(_) => unreachable!(),

View file

@ -1,7 +1,8 @@
use crate::script::{OutputStream, Script};
#[cfg(feature = "ipc")]
use crate::Ironbar;
use crate::{arc_mut, glib_recv_mpsc, lock, spawn, try_send};
use crate::channels::{AsyncSenderExt, Dependency, MpscReceiverExt};
use crate::script::{OutputStream, Script};
use crate::{arc_mut, lock, spawn};
use tokio::sync::mpsc;
/// A segment of a dynamic string,
@ -22,12 +23,14 @@ enum DynamicStringSegment {
///
/// ```rs
/// dynamic_string(&text, move |string| {
/// label.set_markup(&string);
/// label.set_label_escaped(&string);
/// });
/// ```
pub fn dynamic_string<F>(input: &str, mut f: F)
pub fn dynamic_string<D, F>(input: &str, deps: D, f: F)
where
F: FnMut(String) + 'static,
D: Dependency,
D::Target: Clone + 'static,
F: FnMut(&D::Target, String) + 'static,
{
let (tokens, is_static) = parse_input(input);
@ -55,7 +58,7 @@ where
let _: String = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join("");
try_send!(tx, string);
tx.send_spawn(string);
}
})
.await;
@ -71,7 +74,7 @@ where
spawn(async move {
let variable_manager = Ironbar::variable_manager();
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
let mut rx = variable_manager.subscribe(name);
while let Ok(value) = rx.recv().await {
if let Some(value) = value {
@ -80,7 +83,7 @@ where
let _: String = std::mem::replace(&mut label_parts[i], value);
let string = label_parts.join("");
try_send!(tx, string);
tx.send_spawn(string);
}
}
});
@ -88,12 +91,12 @@ where
}
}
glib_recv_mpsc!(rx , val => f(val));
rx.recv_glib(deps, f);
// initialize
if is_static {
let label_parts = lock!(label_parts).join("");
try_send!(tx, label_parts);
tx.send_spawn(label_parts);
}
}

View file

@ -1,6 +1,8 @@
use glib::IsA;
use crate::config::TruncateMode;
use glib::{IsA, markup_escape_text};
use gtk::pango::EllipsizeMode;
use gtk::prelude::*;
use gtk::{Orientation, Widget};
use gtk::{Label, Orientation, Widget};
/// Represents a widget's size
/// and location relative to the bar's start edge.
@ -18,6 +20,8 @@ pub struct WidgetGeometry {
pub trait IronbarGtkExt {
/// Adds a new CSS class to the widget.
fn add_class(&self, class: &str);
/// Removes a CSS class from the widget
fn remove_class(&self, class: &str);
/// Gets the geometry for the widget
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
@ -32,6 +36,10 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
self.style_context().add_class(class);
}
fn remove_class(&self, class: &str) {
self.style_context().remove_class(class);
}
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
let allocation = self.allocation();
@ -75,3 +83,36 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
unsafe { self.set_data(key, value) }
}
}
pub trait IronbarLabelExt {
/// Sets the label value to the provided string.
///
/// If the label does not contain markup `span` tags,
/// the text is escaped to avoid issues with special characters (ie `&`).
/// Otherwise, the text is used verbatim, and it is up to the user to escape.
fn set_label_escaped(&self, label: &str);
fn truncate(&self, mode: TruncateMode);
}
impl IronbarLabelExt for Label {
fn set_label_escaped(&self, label: &str) {
if label.contains("<span") {
self.set_label(label);
} else {
self.set_label(&markup_escape_text(label));
}
}
fn truncate(&self, mode: TruncateMode) {
self.set_ellipsize(<TruncateMode as Into<EllipsizeMode>>::into(mode));
if let Some(length) = mode.length() {
self.set_width_chars(length);
}
if let Some(length) = mode.max_length() {
self.set_max_width_chars(length);
}
}
}

Some files were not shown because too many files have changed in this diff Show more