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:
commit
c42024d48a
159 changed files with 13519 additions and 5655 deletions
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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. -->
|
||||
|
|
2
.github/scripts/ubuntu_setup.sh
vendored
2
.github/scripts/ubuntu_setup.sh
vendored
|
@ -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}
|
||||
|
|
82
.github/workflows/build.yml
vendored
82
.github/workflows/build.yml
vendored
|
@ -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'
|
11
.github/workflows/deploy.yml
vendored
11
.github/workflows/deploy.yml
vendored
|
@ -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
105
.github/workflows/docker.yml
vendored
Normal 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}
|
9
.github/workflows/schema.yml
vendored
9
.github/workflows/schema.yml
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/wiki.yml
vendored
2
.github/workflows/wiki.yml
vendored
|
@ -3,6 +3,8 @@ name: Sync Wiki
|
|||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'docs/**/*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal 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>
|
2
.idea/runConfigurations/Run__Live_Config_.xml
generated
2
.idea/runConfigurations/Run__Live_Config_.xml
generated
|
@ -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
20
.idea/runConfigurations/Test.xml
generated
Normal 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>
|
112
CHANGELOG.md
112
CHANGELOG.md
|
@ -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
3096
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
168
Cargo.toml
168
Cargo.toml
|
@ -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
6
Dockerfile
Normal 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
|
35
README.md
35
README.md
|
@ -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
53
build.rs
Normal 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
12
default.nix
Normal 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
|
|
@ -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"
|
||||
```
|
||||
```
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
|
@ -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
50
docs/GTK4.md
Normal 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 | ✅ | |
|
|
@ -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
|
||||
```
|
|
@ -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
78
docs/modules/Bindmode.md
Normal 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).
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
122
docs/modules/Keyboard.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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).
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
||||

|
||||
|
@ -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
158
docs/modules/Menu.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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).
|
|
@ -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).
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
||||
[//]: # ()
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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}"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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}"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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}",
|
||||
]
|
||||
|
||||
|
|
|
@ -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
102
examples/menu/default.corn
Normal 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
107
examples/menu/default.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
87
examples/menu/default.toml
Normal file
87
examples/menu/default.toml
Normal 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
68
examples/menu/default.yml
Normal 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
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
86
flake.lock
generated
|
@ -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
280
flake.nix
|
@ -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="];
|
||||
};
|
||||
}
|
||||
|
|
129
nix/default.nix
129
nix/default.nix
|
@ -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
96
nix/module.nix
Normal 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
152
nix/package.nix
Normal 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";
|
||||
};
|
||||
}
|
32
shell.nix
32
shell.nix
|
@ -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
|
||||
|
|
41
src/bar.rs
41
src/bar.rs
|
@ -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
261
src/channels.rs
Normal 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);
|
|
@ -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!(
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
117
src/clients/compositor/niri/connection.rs
Normal file
117
src/clients/compositor/niri/connection.rs
Normal 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))
|
||||
}
|
||||
}
|
223
src/clients/compositor/niri/mod.rs
Normal file
223
src/clients/compositor/niri/mod.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
236
src/clients/libinput.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
181
src/clients/sway.rs
Normal 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);
|
|
@ -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<()>;
|
||||
}
|
||||
|
|
|
@ -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
621
src/clients/sysinfo.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
159
src/clients/upower/dbus.rs
Normal 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
33
src/clients/upower/mod.rs
Normal 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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
{
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
35
src/config/layout.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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: >k::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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue