Compare commits
1 Commits
master
...
refactor/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c73585324c |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "cargo" # See documentation for possible values
|
|
||||||
directory: "/" # Location of package manifests
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -22,9 +22,6 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
name: Cache dependencies
|
|
||||||
|
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -73,6 +70,4 @@ jobs:
|
|||||||
name: jakestanger
|
name: jakestanger
|
||||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||||
|
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
|
|
||||||
- run: nix build --print-build-logs
|
- run: nix build --print-build-logs
|
||||||
154
CHANGELOG.md
154
CHANGELOG.md
@@ -4,157 +4,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [v0.13.0] - 2023-08-16
|
|
||||||
### :sparkles: New Features
|
|
||||||
- [`c3e9654`](https://github.com/JakeStanger/ironbar/commit/c3e9654cd3dfcea4276f5b114112b7541dd847fd) - **upower**: icon size option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`f5bdc5a`](https://github.com/JakeStanger/ironbar/commit/f5bdc5a0272fefca4c91336699ea63913cdf3c08) - ipc server and cli *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ded50cc`](https://github.com/JakeStanger/ironbar/commit/ded50cca6f01f08a8e44257394fdde634d421e8e) - support for 'ironvar' dynamic variables *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`c6319b7`](https://github.com/JakeStanger/ironbar/commit/c6319b78fd3992ad43327e90b6326ab653238f2e) - **ipc**: support for injecting additional stylesheets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`27f920d`](https://github.com/JakeStanger/ironbar/commit/27f920d01217bedba279003291ad48c1aaa56bb0) - **launcher**: slightly improve focus logic when clicking item with multiple windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`bd90167`](https://github.com/JakeStanger/ironbar/commit/bd90167f4ea90cb97984b9f3b5e6f65b375d0c4a) - **clock**: format option for popup header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`12053f1`](https://github.com/JakeStanger/ironbar/commit/12053f111a6f05a59e33396b9042821b98b9bc5c) - **music**: progress/seek bar in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7d3bb02`](https://github.com/JakeStanger/ironbar/commit/7d3bb02b4612f278bcc8a268a48c61b239c63e82) - **ipc**: reload config command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`b310ea7`](https://github.com/JakeStanger/ironbar/commit/b310ea76362bcdf10e187d6b00cd2b8ed2870c41) - **clock**: localization support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`738b9e3`](https://github.com/JakeStanger/ironbar/commit/738b9e3da714c9b998030e9f60b9b6f50c62ec76) - **config**: use default fallback with config instructions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`2ccb263`](https://github.com/JakeStanger/ironbar/commit/2ccb2633c6c4d7f6eb264a6c49951853b728c9f3) - IPC for get_visible, set_visible, new bar `name` config option *(commit by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja))*
|
|
||||||
- [`b7ee794`](https://github.com/JakeStanger/ironbar/commit/b7ee794bfc86730e7921c8a930cf8d8bb44474ad) - **ipc**: commands for opening/closing popups *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ef443e6`](https://github.com/JakeStanger/ironbar/commit/ef443e6978949479388129760dabc3f8930c0b0f) - **image resolver**: add fallback image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`9f65cf2`](https://github.com/JakeStanger/ironbar/commit/9f65cf293d9527a2c536847f0005957421a9be33) - **workspaces**: add `favorites` and `hidden` options *(commit by [@yavko](https://github.com/yavko))*
|
|
||||||
- [`19c684e`](https://github.com/JakeStanger/ironbar/commit/19c684e49facb57e3e2acf9cafecf177c2e1bfbf) - **nix**: automatic development environment with direnv *(commit by [@yavko](https://github.com/yavko))*
|
|
||||||
|
|
||||||
### :bug: Bug Fixes
|
|
||||||
- [`6db7742`](https://github.com/JakeStanger/ironbar/commit/6db7742e068dc03d67dbf35e0d9db27f900392fe) - crash on startup introduced by recent refactors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`f78c7f9`](https://github.com/JakeStanger/ironbar/commit/f78c7f9b981c98676e855ff6a63e33a51927c709) - not resolving flatpak application icons *(commit by [@body20002](https://github.com/body20002))*
|
|
||||||
- [`1759945`](https://github.com/JakeStanger/ironbar/commit/1759945912e376581e5fcd5ed2916f89e2090f2b) - **music**: correctly show/hide popup elements based on player capabilities *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`a9ac29d`](https://github.com/JakeStanger/ironbar/commit/a9ac29d8857256d13e14104db235117e3c752972) - clipboard partially behind wrong feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`c711dd8`](https://github.com/JakeStanger/ironbar/commit/c711dd858554140bcfb6df515a43b40ee2baee67) - failing to resolve icons with home_manager *(commit by [@christoph00](https://github.com/christoph00))*
|
|
||||||
- [`1a272e0`](https://github.com/JakeStanger/ironbar/commit/1a272e00fbeca4b5e39b527ffed439bc51fd4080) - **label**: not using markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`4ca17d1`](https://github.com/JakeStanger/ironbar/commit/4ca17d1337acfbbc21c04058d97f689a1cce60a6) - **launcher**: incorrectly resolving some applications *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`eee2182`](https://github.com/JakeStanger/ironbar/commit/eee2182ab90fdc22cd05da9417cbee17e4c67088) - **ipc**: command/response casing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`c582bc3`](https://github.com/JakeStanger/ironbar/commit/c582bc33905702a9ebe323e6dfa9413485f48fb7) - **cli**: `set-visible` command causing panic *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`87dd764`](https://github.com/JakeStanger/ironbar/commit/87dd7646fc9223ac7e67842934f3bd104b4eea80) - **launcher**: not clearing focused state when closing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`6f57ad4`](https://github.com/JakeStanger/ironbar/commit/6f57ad47ac30348c0ae2b7dba35d5ffdbf96f72d) - **launcher**: not setting focus state when opening favourite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`2367faa`](https://github.com/JakeStanger/ironbar/commit/2367faab0440327620052de845c6a0d3032f2f05) - **image**: using fallback in places it shouldn't *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7f6fef6`](https://github.com/JakeStanger/ironbar/commit/7f6fef6338d7a8c909f3224b32426dfc2aacc295) - **image**: matching desktop file names too eagerly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`89ec06f`](https://github.com/JakeStanger/ironbar/commit/89ec06fc7b252052f96e45f5d0f6d6504878a13a) - **music**: hide album art widget when no image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`2902331`](https://github.com/JakeStanger/ironbar/commit/2902331af00f2e52fdea06964fbd89d72fe2ebbb) - **dynamic string**: incorrectly handling strings containing multipoint utf-8 chars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`901a86c`](https://github.com/JakeStanger/ironbar/commit/901a86caa491648268f0618e17a25b978552db0c) - **custom**: crash when clicking non-popup button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`54f0f23`](https://github.com/JakeStanger/ironbar/commit/54f0f232f208602699e5021942c3d0f3947ca6de) - **launcher**: popup not closing when hover leaves widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :recycle: Refactors
|
|
||||||
- [`d121dc3`](https://github.com/JakeStanger/ironbar/commit/d121dc3d1e9468a67deb528c35fc3897c3840f77) - fix unused var warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`cc181a8`](https://github.com/JakeStanger/ironbar/commit/cc181a8b6d0ac1cccd4ed2f9f420c138ed5383d2) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7016f7f`](https://github.com/JakeStanger/ironbar/commit/7016f7f79e7e29a3318b535ba224aa78bd91688a) - use new smart pointer macros throughout codebase *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`06251e2`](https://github.com/JakeStanger/ironbar/commit/06251e293e8f56e1897fed80335f114fdea57183) - fix new pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :memo: Documentation Changes
|
|
||||||
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`3d94987`](https://github.com/JakeStanger/ironbar/commit/3d949874de90b0e5c06cb62726629133d0ea76e3) - **ipc**: add link to luajit library *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.12.1] - 2023-06-18
|
|
||||||
### :boom: BREAKING CHANGES
|
|
||||||
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
|
||||||
|
|
||||||
Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.
|
|
||||||
|
|
||||||
|
|
||||||
### :bug: Bug Fixes
|
|
||||||
- [`31a57ae`](https://github.com/JakeStanger/ironbar/commit/31a57ae637fa5918f163c8b191916867395912f3) - scripts don't work while running ironbar under a systemd service *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`f82f897`](https://github.com/JakeStanger/ironbar/commit/f82f897982e87906e2c9156d4115013bc8e99763) - **upower**: popup always empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`9012fee`](https://github.com/JakeStanger/ironbar/commit/9012feee4f9b60b2c22a956de732847892331222) - **image**: still blurry on hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`0e65f93`](https://github.com/JakeStanger/ironbar/commit/0e65f93a230cb5ab010b43962fd2e829945c291b) - excess popup windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`87ca399`](https://github.com/JakeStanger/ironbar/commit/87ca399220e5d48eefe2f295d1dba1b9452c4472) - poor error handling for missing images *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`22b630a`](https://github.com/JakeStanger/ironbar/commit/22b630a10b9836531a8b03eb904e6f9fcf839fe6) - broken nerd font icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`48d6af0`](https://github.com/JakeStanger/ironbar/commit/48d6af0281f460d3ed3745a2ffb2b61848430ecb) - **music**: showing when no mpris player found *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`b9740cb`](https://github.com/JakeStanger/ironbar/commit/b9740cba8f2fa9dfa18a57345027283610f6487e) - upower icon too large *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`a6b6866`](https://github.com/JakeStanger/ironbar/commit/a6b686624b750863aa1c26ca4f1688dfa8c81a61) - **upower**: icon outside button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`a5ecb36`](https://github.com/JakeStanger/ironbar/commit/a5ecb363fdb2eb3ab543ad56c55c186414500469) - popups occasionally getting jumbled with multiple bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ac34c05`](https://github.com/JakeStanger/ironbar/commit/ac34c05d2ecb07fd871ed03ef6ee545dc2e6743d) - **focused**: empty icon rendered when `show_icon = false` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`de3aa5d`](https://github.com/JakeStanger/ironbar/commit/de3aa5d7b10e0bf6d5ff3a39b009ff53a3316a5e) - **focused**: previous icon does not clear if new icon fails to load *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`de98cf3`](https://github.com/JakeStanger/ironbar/commit/de98cf3daee816a0ff72d1f6ba6bc0e15ec53fca) - **tray**: (maybe?) sometimes bus name is taken *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :memo: Documentation Changes
|
|
||||||
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ff315ff`](https://github.com/JakeStanger/ironbar/commit/ff315ff5dbd545d8b72b6aa10087c940cb8a5eee) - **music**: fix incorrect type for `host`/`music_dir` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`bd144e8`](https://github.com/JakeStanger/ironbar/commit/bd144e87a8f6668c877d42697ebbedbe5a374c3d) - **readme**: make prettier *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`242b70e`](https://github.com/JakeStanger/ironbar/commit/242b70ed3988b85455b0dbbcb3243b31f89d2ee1) - **contributing**: enforce conventional commits *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`96d36c4`](https://github.com/JakeStanger/ironbar/commit/96d36c43d43ba2f9e9d9441ae01c0743cc56f627) - add missing icon/image selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.12.0] - 2023-05-06
|
|
||||||
### :boom: BREAKING CHANGES
|
|
||||||
- due to [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
|
||||||
|
|
||||||
To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.
|
|
||||||
|
|
||||||
|
|
||||||
### :sparkles: New Features
|
|
||||||
- [`6c62286`](https://github.com/JakeStanger/ironbar/commit/6c622864b388548eaaa595f41993606cc151d585) - new label module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`cac064f`](https://github.com/JakeStanger/ironbar/commit/cac064f4795e9f418cc0820f04944f91121c426a) - ability to configure popup gap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`dfe1964`](https://github.com/JakeStanger/ironbar/commit/dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d) - **custom**: slider widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`72b14b6`](https://github.com/JakeStanger/ironbar/commit/72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2) - **custom**: progress bar widget. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`a9d1233`](https://github.com/JakeStanger/ironbar/commit/a9d12339097cbe0fef1628460ef538319a048223) - **custom**: support dynamic strings on buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`3d308ab`](https://github.com/JakeStanger/ironbar/commit/3d308ab572a39ada2501ddc1b822e50e1f8a8363) - **custom**: support dynamic string in image source *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`4a09b70`](https://github.com/JakeStanger/ironbar/commit/4a09b70854dad33bf890a3fe766f854d9195e786) - **custom**: support common options in widgets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`83f44fd`](https://github.com/JakeStanger/ironbar/commit/83f44fd92fe74b45fcdfc242fb90fc932dd2b00b) - wrap modules in a revealer to support animated show/hide *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`1fa0c0e`](https://github.com/JakeStanger/ironbar/commit/1fa0c0e9774c302727d414f5aef999ab71a7acb8) - **custom**: support mouse wheel on slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`2da28b9`](https://github.com/JakeStanger/ironbar/commit/2da28b9bf5790adfc46c58b6f6d5fdd13cc17195) - ability to configure image icon sizes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`033d0f7`](https://github.com/JakeStanger/ironbar/commit/033d0f7e6e450b3f2d62d9a75210d52611cf346d) - **custom**: option to toggle slider label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`76e2b7b`](https://github.com/JakeStanger/ironbar/commit/76e2b7ba3e788f273039d74635881ddb96264258) - **music**: option to hide status icon on widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ad3c171`](https://github.com/JakeStanger/ironbar/commit/ad3c171ecacaebf10408c2583ed7361ed029075e) - implement upower module *(commit by [@p00f](https://github.com/p00f))*
|
|
||||||
- [`2a155b9`](https://github.com/JakeStanger/ironbar/commit/2a155b9aa8a3634908512d9b83680925962d478f) - **music**: add css selector for button contents *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`c1ea5fa`](https://github.com/JakeStanger/ironbar/commit/c1ea5fad7ec308895f0454b6de05a3177563626c) - **logging**: include line numbers *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :bug: Bug Fixes
|
|
||||||
- [`9109453`](https://github.com/JakeStanger/ironbar/commit/910945306c3261190a16300da2ed28efb945a6ac) - **dynamic string**: parser issue related to incorrectly matching braces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7355db7`](https://github.com/JakeStanger/ironbar/commit/7355db74ec9118c2cb46899534a3adac8d7165d9) - **image**: http provider not handling non-success codes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`a87d8d5`](https://github.com/JakeStanger/ironbar/commit/a87d8d5c3071a1d8ab149deae17d261ae97368ea) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`15a9d8d`](https://github.com/JakeStanger/ironbar/commit/15a9d8d42c9319a7062e6a90086e0c1c3323f5d8) - **script**: parser incorrectly handling colons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`68bc823`](https://github.com/JakeStanger/ironbar/commit/68bc8230ddf3352cc0de9f8cc770632744c22747) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`b038e76`](https://github.com/JakeStanger/ironbar/commit/b038e7671af4bfa41060adf724deb8c6151fac1f) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7926bb0`](https://github.com/JakeStanger/ironbar/commit/7926bb07eb181edaf6da2f11a7dc00f8be2240eb) - **nix**: Fix `nix run` support *(commit by [@yavko](https://github.com/yavko))*
|
|
||||||
- [`2c88c99`](https://github.com/JakeStanger/ironbar/commit/2c88c99cb605d312e2d76d620f502c7e7cd8866e) - **dynamic string**: crash when last segment is static and a single char *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`338f5a0`](https://github.com/JakeStanger/ironbar/commit/338f5a0e1b58dc9b52caee61d6a9748cf13153c5) - **nix**: Attempt to fix image blurriness *(commit by [@yavko](https://github.com/yavko))*
|
|
||||||
- [`db0868a`](https://github.com/JakeStanger/ironbar/commit/db0868a3fc0734daa61067e377018c692599ebff) - **image**: not scaling icons for hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`14b6c1a`](https://github.com/JakeStanger/ironbar/commit/14b6c1a69f28836ed9e3b74eeb97a42ea60ffc27) - bars duplicate when starting second instance *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`98aaaa0`](https://github.com/JakeStanger/ironbar/commit/98aaaa0d1407681b3d790c933c4972b8122f8007) - fallback to default icon theme for notifier items *(commit by [@oknozor](https://github.com/oknozor))*
|
|
||||||
- [`735f5cc`](https://github.com/JakeStanger/ironbar/commit/735f5cc9f1518c256785d42f3d21ed5c68b11711) - **launcher**: crash when focusing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`e1abadc`](https://github.com/JakeStanger/ironbar/commit/e1abadcf39a2d39078e75179a167e9277ee5e550) - **clipboard**: copying large images filling write pipe *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :recycle: Refactors
|
|
||||||
- [`2ab06f0`](https://github.com/JakeStanger/ironbar/commit/2ab06f044ec300628d6648852d395889b6752b76) - **custom**: split into enum with separate file per widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`3613aef`](https://github.com/JakeStanger/ironbar/commit/3613aef5c5a4051b5a44e33342c0eaaab3d4a690) - **custom**: reduce a lot of repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`c214f65`](https://github.com/JakeStanger/ironbar/commit/c214f65ecb86a0da6559025203701661924f65bb) - fix strict clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`27d11de`](https://github.com/JakeStanger/ironbar/commit/27d11de6616c410422d7abd579d09b3abc02f43a) - **config**: split common code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`6fd69d6`](https://github.com/JakeStanger/ironbar/commit/6fd69d657c6224bc47c9b3cb5affcf74b63a6aa6) - move module creation code to module module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`e63509a`](https://github.com/JakeStanger/ironbar/commit/e63509a3a7673ea41b4c937089a1cf6d2362fed3) - fix a few new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`7f46cb4`](https://github.com/JakeStanger/ironbar/commit/7f46cb49767bd722be8d42999a9ba69887efcd40) - **wayland**: update to 0.30.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
### :memo: Documentation Changes
|
|
||||||
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`dd7c9f3`](https://github.com/JakeStanger/ironbar/commit/dd7c9f30db6e4e1ede4d57255122b359636b8f58) - add transition module-level options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`610c352`](https://github.com/JakeStanger/ironbar/commit/610c3528af98b8c6b02af7ce5c07190776522c3a) - add missing link to upower page *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`ea9f7ca`](https://github.com/JakeStanger/ironbar/commit/ea9f7caaf7a35eebd603ce2854672d5af2901018) - add missing `upower` feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`618b7ef`](https://github.com/JakeStanger/ironbar/commit/618b7ef5520de6f3796b66e42422a36c5a191ab0) - improve example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`139bc5d`](https://github.com/JakeStanger/ironbar/commit/139bc5d23f7f887b7b65d50adc21fa6679ea291e) - **compiling**: improve requirements list *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
- [`cf32870`](https://github.com/JakeStanger/ironbar/commit/cf32870f8a380c305a436593950c3da524a2296f) - **compiling**: add ron feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.11.0] - 2023-04-01
|
## [v0.11.0] - 2023-04-01
|
||||||
### :boom: BREAKING CHANGES
|
### :boom: BREAKING CHANGES
|
||||||
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||||
@@ -423,6 +272,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
|
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
|
||||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
||||||
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
|
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
|
||||||
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
|
|
||||||
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
|
|
||||||
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0
|
|
||||||
@@ -4,8 +4,7 @@ I welcome contributions of any kind with open arms. That said, please do stick t
|
|||||||
- Fix any `cargo clippy` warnings, using at least the default configuration.
|
- Fix any `cargo clippy` warnings, using at least the default configuration.
|
||||||
- Make sure your code is formatted using `cargo fmt`.
|
- Make sure your code is formatted using `cargo fmt`.
|
||||||
- Keep any documentation up to date.
|
- Keep any documentation up to date.
|
||||||
- Please use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||||
This ensures your contributions are automatically included in the changelog.
|
|
||||||
|
|
||||||
|
|
||||||
- For PRs:
|
- For PRs:
|
||||||
|
|||||||
1452
Cargo.lock
generated
1452
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
103
Cargo.toml
103
Cargo.toml
@@ -1,17 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ironbar"
|
name = "ironbar"
|
||||||
version = "0.14.0-pre"
|
version = "0.11.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||||
repository = "https://github.com/jakestanger/ironbar"
|
|
||||||
categories = ["gui"]
|
|
||||||
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
"cli",
|
|
||||||
"ipc",
|
|
||||||
"http",
|
"http",
|
||||||
"config+all",
|
"config+all",
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@@ -22,24 +17,14 @@ default = [
|
|||||||
"upower",
|
"upower",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
|
|
||||||
cli = ["dep:clap", "ipc"]
|
|
||||||
ipc = ["dep:serde_json"]
|
|
||||||
|
|
||||||
http = ["dep:reqwest"]
|
http = ["dep:reqwest"]
|
||||||
|
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||||
|
|
||||||
"config+all" = [
|
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
|
||||||
"config+json",
|
|
||||||
"config+yaml",
|
|
||||||
"config+toml",
|
|
||||||
"config+corn",
|
|
||||||
"config+ron",
|
|
||||||
]
|
|
||||||
"config+json" = ["universal-config/json"]
|
"config+json" = ["universal-config/json"]
|
||||||
"config+yaml" = ["universal-config/yaml"]
|
"config+yaml" = ["universal-config/yaml"]
|
||||||
"config+toml" = ["universal-config/toml"]
|
"config+toml" = ["universal-config/toml"]
|
||||||
"config+corn" = ["universal-config/corn"]
|
"config+corn" = ["universal-config/corn"]
|
||||||
"config+ron" = ["universal-config/ron"]
|
|
||||||
|
|
||||||
clipboard = ["nix"]
|
clipboard = ["nix"]
|
||||||
|
|
||||||
@@ -52,9 +37,7 @@ music = ["regex"]
|
|||||||
|
|
||||||
sys_info = ["sysinfo", "regex"]
|
sys_info = ["sysinfo", "regex"]
|
||||||
|
|
||||||
tray = ["system-tray"]
|
tray = ["stray"]
|
||||||
|
|
||||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
|
||||||
|
|
||||||
workspaces = ["futures-util"]
|
workspaces = ["futures-util"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||||
@@ -63,78 +46,58 @@ workspaces = ["futures-util"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# core
|
# core
|
||||||
gtk = "0.18.1"
|
gtk = { package = "gtk4", version = "0.6.6" }
|
||||||
gtk-layer-shell = "0.8.0"
|
gtk-layer-shell = { package = "gtk4-layer-shell", version = "0.0.3" }
|
||||||
glib = "0.18.5"
|
glib = "0.17.9"
|
||||||
tokio = { version = "1.35.1", features = [
|
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
||||||
"macros",
|
tracing = "0.1.37"
|
||||||
"rt-multi-thread",
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
"time",
|
|
||||||
"process",
|
|
||||||
"sync",
|
|
||||||
"io-util",
|
|
||||||
"net",
|
|
||||||
] }
|
|
||||||
tracing = "0.1.40"
|
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.2"
|
||||||
strip-ansi-escapes = "0.2.0"
|
strip-ansi-escapes = "0.1.1"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.141", features = ["derive"] }
|
||||||
indexmap = "2.1.0"
|
indexmap = "1.9.1"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0.0"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.3.2"
|
||||||
notify = { version = "6.1.1", default-features = false }
|
notify = { version = "5.0.0", default-features = false }
|
||||||
wayland-client = "0.31.1"
|
wayland-client = "0.29.5"
|
||||||
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
|
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||||
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
|
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
||||||
smithay-client-toolkit = { version = "0.18.0", default-features = false, features = [
|
universal-config = { version = "0.3.0", default_features = false }
|
||||||
"calloop",
|
|
||||||
] }
|
|
||||||
universal-config = { version = "0.4.3", default_features = false }
|
|
||||||
ctrlc = "3.4.2"
|
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
# cli
|
|
||||||
clap = { version = "4.4.12", optional = true, features = ["derive"] }
|
|
||||||
|
|
||||||
# ipc
|
|
||||||
serde_json = { version = "1.0.109", optional = true }
|
|
||||||
|
|
||||||
# http
|
# http
|
||||||
reqwest = { version = "0.11.23", optional = true }
|
reqwest = { version = "0.11.14", optional = true }
|
||||||
|
|
||||||
# clipboard
|
# clipboard
|
||||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
nix = { version = "0.26.2", optional = true }
|
||||||
|
|
||||||
# clock
|
# clock
|
||||||
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
|
chrono = { version = "0.4.19", optional = true }
|
||||||
|
|
||||||
# music
|
# music
|
||||||
mpd_client = { version = "1.3.0", optional = true }
|
mpd_client = { version = "1.0.0", optional = true }
|
||||||
mpris = { version = "2.0.1", optional = true }
|
mpris = { version = "2.0.0", optional = true }
|
||||||
|
|
||||||
# sys_info
|
# sys_info
|
||||||
sysinfo = { version = "0.29.11", optional = true }
|
sysinfo = { version = "0.28.4", optional = true }
|
||||||
|
|
||||||
# tray
|
# tray
|
||||||
system-tray = { version = "0.1.4", optional = true }
|
stray = { version = "0.1.3", optional = true }
|
||||||
|
|
||||||
# upower
|
# upower
|
||||||
upower_dbus = { version = "0.3.2", optional = true }
|
upower_dbus = { version = "0.3.2", optional = true }
|
||||||
futures-lite = { version = "2.1.0", optional = true }
|
futures-lite = { version = "1.12.0", optional = true }
|
||||||
zbus = { version = "3.14.1", optional = true }
|
zbus = { version = "3.11.0", optional = true }
|
||||||
|
|
||||||
# workspaces
|
# workspaces
|
||||||
swayipc-async = { version = "2.0.1", optional = true }
|
swayipc-async = { version = "2.0.1", optional = true }
|
||||||
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
|
hyprland = { version = "0.3.1", optional = true }
|
||||||
futures-util = { version = "0.3.30", optional = true }
|
futures-util = { version = "0.3.21", optional = true }
|
||||||
|
|
||||||
# shared
|
# shared
|
||||||
regex = { version = "1.10.2", default-features = false, features = [
|
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
|
||||||
"std",
|
|
||||||
], optional = true } # music, sys_info
|
|
||||||
|
|||||||
133
README.md
133
README.md
@@ -1,94 +1,49 @@
|
|||||||
<h1 align="center" >--- Ironbar ---</h1>
|
# Ironbar
|
||||||
|
|
||||||
<div align="center">
|
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
|
||||||
<a href="https://github.com/JakeStanger/ironbar/releases">
|
It uses GTK3 and gtk-layer-shell.
|
||||||
<img src="https://img.shields.io/crates/v/ironbar?label=version&style=for-the-badge" alt="Current version" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/JakeStanger/ironbar/actions/workflows/build.yml">
|
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/jakestanger/ironbar/build.yml?style=for-the-badge" alt="Build status" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/JakeStanger/ironbar/issues">
|
|
||||||
<img src="https://img.shields.io/github/issues/jakestanger/ironbar?style=for-the-badge" alt="Open issues" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/JakeStanger/ironbar/blob/master/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/jakestanger/ironbar?style=for-the-badge" alt="License" />
|
|
||||||
</a>
|
|
||||||
<a href="https://crates.io/crates/ironbar">
|
|
||||||
<img src="https://img.shields.io/crates/d/ironbar?label=crates.io%20downloads&style=for-the-badge" alt="Crates.io downloads" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
The bar can be styled to your liking using CSS and hot-loads style changes.
|
||||||
|
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||||
<div align="center">
|
|
||||||
A customisable and feature-rich GTK bar for wlroots compositors, written in Rust.
|
|
||||||
|
|
||||||
Ironbar is designed to support anything from a lightweight bar to a full desktop panel with ease.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
[Wiki](https://github.com/JakeStanger/ironbar/wiki)
|
|
||||||
|
|
|
||||||
[Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide)
|
|
||||||
|
|
|
||||||
[Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide)
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
✨ Looking for a starting point, or want to show off? Head to [Show and tell](https://github.com/JakeStanger/ironbar/discussions/categories/show-and-tell) ✨
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- First-class support for Sway and Hyprland
|
- First-class support for Sway and Hyprland, but should (mostly) work on any wlroots compositor.
|
||||||
- Fully themeable with hot-loaded CSS
|
- Fully themeable with CSS and hot-loaded styles.
|
||||||
- Popups to show rich content
|
- Support for multiple configuration languages.
|
||||||
- Ability to create custom widgets, run scripts and embed dynamic content
|
- Popups used by widgets to show rich content and controls on click.
|
||||||
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
|
||||||
- Support for multiple config languages
|
- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
|
|
||||||
[crate](https://crates.io/crates/ironbar)
|
|
||||||
|
|
||||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install ironbar
|
cargo install ironbar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch Linux
|
[crate](https://crates.io/crates/ironbar)
|
||||||
|
|
||||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
### Arch Linux
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yay -S ironbar-git
|
yay -S ironbar-git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nix
|
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||||
|
|
||||||
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
|
### Nix Flake
|
||||||
|
|
||||||
```sh
|
A flake is included with the repo which can be used with home-manager.
|
||||||
nix-shell -p ironbar
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Flake
|
#### Example
|
||||||
|
|
||||||
A flake is included with the repo which can be used with Home Manager.
|
Here is an example nix flake that uses Ironbar.
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example usage</summary>
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
@@ -125,14 +80,13 @@ A flake is included with the repo which can be used with Home Manager.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
#### Binary Caching
|
||||||
|
|
||||||
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
|
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`
|
||||||
|
in case you don't want to compile Ironbar.
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
[repo](https://github.com/jakestanger/ironbar)
|
|
||||||
|
|
||||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -146,36 +100,53 @@ install target/release/ironbar ~/.local/bin/ironbar
|
|||||||
By default, all features are enabled.
|
By default, all features are enabled.
|
||||||
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
||||||
|
|
||||||
|
[repo](https://github.com/jakestanger/ironbar)
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
|
All of the above installation methods provide a binary called `ironbar`.
|
||||||
See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
|
|
||||||
|
|
||||||
Ironbar can be launched using the `ironbar` binary.
|
|
||||||
|
|
||||||
Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
|
|
||||||
|
|
||||||
|
You can set the `IRONBAR_LOG` or `IRONBAR_FILE_LOG` environment variables to
|
||||||
|
`error`, `warn`, `info`, `debug` or `trace` to configure the log output level.
|
||||||
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||||
|
|
||||||
File output can be found at `~/.local/share/ironbar/error.log`.
|
File output can be found at `~/.local/share/ironbar/error.log`.
|
||||||
|
|
||||||
## Status
|
## Configuration
|
||||||
|
|
||||||
Ironbar is an **alpha** project.
|
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||||
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
|
and options for scaling complexity: you can use a single config across all monitors,
|
||||||
|
or configure different/multiple bars per monitor.
|
||||||
|
|
||||||
If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
|
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
|
||||||
|
|
||||||
I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
|
## Styling
|
||||||
|
|
||||||
|
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the
|
||||||
|
file.
|
||||||
|
|
||||||
|
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
|
This project is in alpha, but should be usable.
|
||||||
|
Everything that is implemented works and should be documented.
|
||||||
|
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
|
||||||
|
|
||||||
|
There is currently room for lots more modules, and lots more configuration options for the existing modules.
|
||||||
|
The current configuration schema is not set in stone and breaking changes could come along at any point;
|
||||||
|
until the project matures I am more interested in ease of use than backwards compatibility.
|
||||||
|
|
||||||
|
A few bugs do exist, and I am sure there are plenty more to be found.
|
||||||
|
|
||||||
|
The project will be *actively developed* as I am using it on my daily driver.
|
||||||
|
Bugs will be fixed, features will be added, code will be refactored.
|
||||||
|
|
||||||
## Contribution Guidelines
|
## Contribution Guidelines
|
||||||
|
|
||||||
All are welcome, but I ask a few basic things to help make things easier. Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md) for details.
|
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
||||||
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
||||||
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
||||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
|
||||||
|
|||||||
@@ -12,30 +12,23 @@ install target/release/ironbar ~/.local/bin/ironbar
|
|||||||
## Build requirements
|
## Build requirements
|
||||||
|
|
||||||
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
|
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
|
||||||
You also need rust; only the latest stable version is supported.
|
|
||||||
|
|
||||||
### Arch
|
### Arch
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pacman -S gtk3 gtk-layer-shell
|
pacman -S gtk3 gtk-layer-shell
|
||||||
# for http support
|
|
||||||
pacman -S openssl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ubuntu/Debian
|
### Ubuntu/Debian
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||||
# for http support
|
|
||||||
apt install libssl-dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora
|
### Fedora
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
dnf install gtk3-devel gtk-layer-shell-devel
|
dnf install gtk3 gtk-layer-shell
|
||||||
# for http support
|
|
||||||
dnf install openssl-devel
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -62,14 +55,11 @@ cargo build --release --no-default-features \
|
|||||||
|---------------------|-----------------------------------------------------------------------------------|
|
|---------------------|-----------------------------------------------------------------------------------|
|
||||||
| **Core** | |
|
| **Core** | |
|
||||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||||
| ipc | Enables the IPC server. |
|
|
||||||
| cli | Enables the CLI. Will also enable `ipc`. |
|
|
||||||
| config+all | Enables support for all configuration languages. |
|
| config+all | Enables support for all configuration languages. |
|
||||||
| config+json | Enables configuration support for JSON. |
|
| config+json | Enables configuration support for JSON. |
|
||||||
| config+yaml | Enables configuration support for YAML. |
|
| config+yaml | Enables configuration support for YAML. |
|
||||||
| config+toml | Enables configuration support for TOML. |
|
| config+toml | Enables configuration support for TOML. |
|
||||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
|
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
|
||||||
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
|
|
||||||
| **Modules** | |
|
| **Modules** | |
|
||||||
| clipboard | Enables the `clipboard` module. |
|
| clipboard | Enables the `clipboard` module. |
|
||||||
| clock | Enables the `clock` module. |
|
| clock | Enables the `clock` module. |
|
||||||
@@ -78,7 +68,6 @@ cargo build --release --no-default-features \
|
|||||||
| music+mpd | Enables the `music` module with MPD support. |
|
| music+mpd | Enables the `music` module with MPD support. |
|
||||||
| sys_info | Enables the `sys_info` module. |
|
| sys_info | Enables the `sys_info` module. |
|
||||||
| tray | Enables the `tray` module. |
|
| tray | Enables the `tray` module. |
|
||||||
| upower | Enables the `upower` module. |
|
|
||||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||||
|
|||||||
@@ -95,11 +95,7 @@ Create a map/object called `monitors` inside the top-level object.
|
|||||||
Each of the map's keys should be an output name,
|
Each of the map's keys should be an output name,
|
||||||
and each value should be an object containing the bar config.
|
and each value should be an object containing the bar config.
|
||||||
|
|
||||||
You can still define a top-level "default" config to use for unspecified monitors.
|
To find your output names, run `wayland-info | grep wl_output -A1`.
|
||||||
Alternatively, leave the top-level `start`, `center` and `end` keys null to hide bars on unspecified monitors.
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> To find your output names, run `wayland-info | grep wl_output -A1`.
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -271,24 +267,20 @@ Check [here](config) for an example config file for a fully configured bar in ea
|
|||||||
|
|
||||||
The following table lists each of the top-level bar config options:
|
The following table lists each of the top-level bar config options:
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|--------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
|
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
||||||
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
|
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||||
| `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. |
|
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||||
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
|
|
||||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
|
||||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
|
||||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
|
||||||
|
|
||||||
### 3.2 Module-level options
|
### 3.2 Module-level options
|
||||||
|
|
||||||
@@ -314,16 +306,13 @@ For information on the `Script` type, and embedding scripts in strings, see [her
|
|||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||||
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
| `transition_duration` | `Integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||||
|
|
||||||
#### Appearance
|
#### Other
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
|
||||||
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
|
|
||||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
|
||||||
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
|
|
||||||
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
| Name | Type | Default | Description |
|
||||||
|
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
Ironbar includes a simple IPC server which can be used to control it programmatically at runtime.
|
|
||||||
|
|
||||||
It also includes a command line interface, which can be used for interacting with the IPC server.
|
|
||||||
|
|
||||||
# CLI
|
|
||||||
|
|
||||||
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
|
|
||||||
You can also view help per-command, for example using `ironbar set --help`.
|
|
||||||
|
|
||||||
Responses are handled by writing their type to stdout, followed by any value starting on the next line.
|
|
||||||
Error responses are written to stderr in the same format.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ ironbar set subject world
|
|
||||||
ok
|
|
||||||
|
|
||||||
$ ironbar get subject
|
|
||||||
ok
|
|
||||||
world
|
|
||||||
```
|
|
||||||
|
|
||||||
# IPC
|
|
||||||
|
|
||||||
The server listens on a Unix socket.
|
|
||||||
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
|
|
||||||
|
|
||||||
Commands and responses are sent as JSON objects, denoted by their `type` key.
|
|
||||||
|
|
||||||
The message buffer is currently limited to `1024` bytes.
|
|
||||||
Particularly large messages will be truncated or cause an error.
|
|
||||||
|
|
||||||
The full spec can be found below.
|
|
||||||
|
|
||||||
## Libraries
|
|
||||||
|
|
||||||
- [Luajit](https://github.com/A-Cloud-Ninja/ironbar-ipc-luajit) - Maintained by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja)
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `ping`
|
|
||||||
|
|
||||||
Sends a ping request to the IPC.
|
|
||||||
|
|
||||||
Responds with `ok`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ping"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `inspect`
|
|
||||||
|
|
||||||
Opens the GTK inspector window.
|
|
||||||
|
|
||||||
Responds with `ok`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "inspect"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `reload`
|
|
||||||
|
|
||||||
Restarts the bars, reloading the config in the process.
|
|
||||||
|
|
||||||
The IPC server and main GTK application are untouched.
|
|
||||||
|
|
||||||
Responds with `ok`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "reload"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `get`
|
|
||||||
|
|
||||||
Gets an [ironvar](ironvars) value.
|
|
||||||
|
|
||||||
Responds with `ok_value` if the value exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "get",
|
|
||||||
"key": "foo"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `set`
|
|
||||||
|
|
||||||
Sets an [ironvar](ironvars) value.
|
|
||||||
|
|
||||||
Responds with `ok`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "set",
|
|
||||||
"key": "foo",
|
|
||||||
"value": "bar"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `load_css`
|
|
||||||
|
|
||||||
Loads an additional CSS stylesheet, with hot-reloading enabled.
|
|
||||||
|
|
||||||
Responds with `ok` if the stylesheet exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "load_css",
|
|
||||||
"path": "/path/to/style.css"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `set_visible`
|
|
||||||
|
|
||||||
Sets a bar's visibility.
|
|
||||||
|
|
||||||
Responds with `ok` if the bar exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "set_visible",
|
|
||||||
"bar_name": "bar-123",
|
|
||||||
"visible": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `get_visible`
|
|
||||||
|
|
||||||
Gets a bar's visibility.
|
|
||||||
|
|
||||||
Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "get_visible",
|
|
||||||
"bar_name": "bar-123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `toggle_popup`
|
|
||||||
|
|
||||||
Toggles the open/closed state for a module's popup.
|
|
||||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
|
||||||
|
|
||||||
Responds with `ok` if the popup exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "toggle_popup",
|
|
||||||
"bar_name": "bar-123",
|
|
||||||
"name": "clock"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `open_popup`
|
|
||||||
|
|
||||||
Sets a module's popup open, regardless of its current state.
|
|
||||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
|
||||||
|
|
||||||
Responds with `ok` if the popup exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "open_popup",
|
|
||||||
"bar_name": "bar-123",
|
|
||||||
"name": "clock"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `close_popup`
|
|
||||||
|
|
||||||
Sets the popup on a bar closed, regardless of which module it is open for.
|
|
||||||
|
|
||||||
Responds with `ok` if the popup exists, otherwise `error`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "close_popup",
|
|
||||||
"bar_name": "bar-123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Responses
|
|
||||||
|
|
||||||
### `ok`
|
|
||||||
|
|
||||||
The operation completed successfully, with no response data.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ok"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ok_value`
|
|
||||||
|
|
||||||
The operation completed successfully, with response data.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ok_value",
|
|
||||||
"value": "lorem ipsum"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `error`
|
|
||||||
|
|
||||||
The operation failed.
|
|
||||||
|
|
||||||
Message is optional.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"message": "lorem ipsum"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
In some configuration locations, Ironbar supports dynamic values,
|
|
||||||
meaning you can inject content into the bar from an external source.
|
|
||||||
|
|
||||||
Currently two dynamic content sources are supported - [scripts](scripts) (via shorthand syntax) and [ironvars](ironvars).
|
|
||||||
|
|
||||||
## Dynamic String
|
|
||||||
|
|
||||||
Dynamic strings can contain any mixture of static string elements, scripts and variables.
|
|
||||||
|
|
||||||
Scripts should be placed inside `{{double braces}}`. Both polling and watching scripts are supported.
|
|
||||||
|
|
||||||
Variables use the standard `#name` syntax. Variables cannot be placed inside scripts.
|
|
||||||
|
|
||||||
To use a literal hash, use `##`. This is only necessary outside of scripts.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
label = "{{cat greeting.txt}}, #subject"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dynamic Boolean
|
|
||||||
|
|
||||||
Dynamic booleans can use a single source of either a script or variable to control a true/false value.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
For variables, use the standard `#name` notation.
|
|
||||||
An empty string, `0` and `false` are treated as false.
|
|
||||||
Any other value is true.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
show_if = "exit 0" # script
|
|
||||||
show_if = "#show_module" # variable
|
|
||||||
```
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Ironvars are runtime variables that can be referenced in several places in your config,
|
|
||||||
then set using the IPC server (such as via the CLI) using the `set` command.
|
|
||||||
|
|
||||||
Any UTF-8 string *without whitespace* is a valid key.
|
|
||||||
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.
|
|
||||||
@@ -4,46 +4,17 @@ To style the bar, create a file at `~/.config/ironbar/style.css`.
|
|||||||
|
|
||||||
Style changes are hot-loaded so there is no need to reload the bar.
|
Style changes are hot-loaded so there is no need to reload the bar.
|
||||||
|
|
||||||
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html),
|
A reminder: since the bar is GTK-based, it uses GTK's implementation of CSS,
|
||||||
which only includes a subset of the full web spec (plus a few non-standard properties).
|
which only includes a subset of the full web spec (plus a few non-standard properties).
|
||||||
|
|
||||||
The below table describes the selectors provided by the bar itself.
|
The below table describes the selectors provided by the bar itself.
|
||||||
Information on styling individual modules can be found on their pages in the sidebar.
|
Information on styling individual modules can be found on their pages in the sidebar.
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|---------------------|--------------------------------------------|
|
|----------------|-------------------------------------------|
|
||||||
| `.background` | Top-level window. |
|
| `.background` | Top-level window |
|
||||||
| `#bar` | Bar root box. |
|
| `#bar` | Bar root box |
|
||||||
| `#bar #start` | Bar left or top modules container box. |
|
| `#bar #start` | Bar left or top modules container box |
|
||||||
| `#bar #center` | Bar center modules container box. |
|
| `#bar #center` | Bar center modules container box |
|
||||||
| `#bar #end` | Bar right or bottom modules container box. |
|
| `#bar #end` | Bar right or bottom modules container box |
|
||||||
| `.container` | All of the above. |
|
| `.container` | All of the above |
|
||||||
| `.widget-container` | The `EventBox` wrapping any widget. |
|
|
||||||
| `.widget` | Any widget. |
|
|
||||||
| `.popup` | Any popup box. |
|
|
||||||
|
|
||||||
Every Ironbar widget can be selected using a `kebab-case` class name matching its name.
|
|
||||||
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
|
|
||||||
|
|
||||||
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
|
|
||||||
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
|
|
||||||
|
|
||||||
You can also target all GTK widgets of a certain type directly using their name. For example, `label` will select all labels, and `button:hover` will select the hover state on *all* buttons.
|
|
||||||
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If an entry takes no effect you might have to use a more specific selector.
|
|
||||||
> For example, attempting to set text size on `.popup-clipboard .item` will likely have no effect.
|
|
||||||
> Instead, you can target the more specific `.popup-clipboard .item label`.
|
|
||||||
|
|
||||||
Running `ironbar inspect` can be used to find out how to address an element.
|
|
||||||
|
|
||||||
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
|
|
||||||
|
|
||||||
```css
|
|
||||||
@define-color color_bg #2d2d2d;
|
|
||||||
|
|
||||||
box, menubar {
|
|
||||||
background-color: @color_bg;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -2,16 +2,10 @@
|
|||||||
|
|
||||||
- [Compiling from source](compiling)
|
- [Compiling from source](compiling)
|
||||||
- [Configuration guide](configuration-guide)
|
- [Configuration guide](configuration-guide)
|
||||||
|
- [Scripts](scripts)
|
||||||
- [Images](images)
|
- [Images](images)
|
||||||
- [Styling guide](styling-guide)
|
- [Styling guide](styling-guide)
|
||||||
|
|
||||||
# Dynamic content
|
|
||||||
|
|
||||||
- [Controlling Ironbar](controlling-ironbar)
|
|
||||||
- [Dynamic values](dynamic-values)
|
|
||||||
- [Scripts](scripts)
|
|
||||||
- [Ironvars](ironvars)
|
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
- [Config](config)
|
- [Config](config)
|
||||||
@@ -33,5 +27,4 @@
|
|||||||
- [Script](script)
|
- [Script](script)
|
||||||
- [Sys_Info](sys-info)
|
- [Sys_Info](sys-info)
|
||||||
- [Tray](tray)
|
- [Tray](tray)
|
||||||
- [Upower](upower)
|
|
||||||
- [Workspaces](workspaces)
|
- [Workspaces](workspaces)
|
||||||
@@ -9,15 +9,17 @@ Supports plain text and images.
|
|||||||
|
|
||||||
> Type: `clipboard`
|
> Type: `clipboard`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
| `icon` | `string/image` | `` | Icon to show on the widget button. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
| `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` | `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.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.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.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
|
See [here](images) for information on images.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -82,16 +84,11 @@ end:
|
|||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|--------------------------------------|------------------------------------------------------|
|
|--------------------------------------|------------------------------------------------------|
|
||||||
| `.clipboard` | Clipboard widget. |
|
| `#clipboard` | Clipboard widget. |
|
||||||
| `.clipboard .btn` | Clipboard widget button. |
|
| `#clipboard .btn` | Clipboard widget button. |
|
||||||
| `.clipboard .btn .icon` | Clipboard widget button icon (any type). |
|
| `#popup-clipboard` | Clipboard popup box. |
|
||||||
| `.clipboard .btn .text-icon` | Clipboard widget button icon (textual only). |
|
| `#popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||||
| `.clipboard .btn .image` | Clipboard widget button icon (image only). |
|
| `#popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||||
| `.popup-clipboard` | Clipboard popup box. |
|
| `#popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
||||||
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
| `#popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
||||||
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
| `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
||||||
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
|
||||||
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
|
||||||
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -8,13 +8,9 @@ Clicking on the widget opens a popup with the time and a calendar.
|
|||||||
|
|
||||||
> Type: `clock`
|
> Type: `clock`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
|
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. |
|
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
|
||||||
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. |
|
|
||||||
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
|
|
||||||
|
|
||||||
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -75,9 +71,7 @@ end:
|
|||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|--------------------------------|------------------------------------------------------------------------------------|
|
|--------------------------------|------------------------------------------------------------------------------------|
|
||||||
| `.clock` | Clock widget button |
|
| `#clock` | Clock widget button |
|
||||||
| `.popup-clock` | Clock popup box |
|
| `#popup-clock` | Clock popup box |
|
||||||
| `.popup-clock .calendar-clock` | Clock inside the popup |
|
| `#popup-clock #calendar-clock` | Clock inside the popup |
|
||||||
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
||||||
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||||
|
|
||||||
If you only intend to run a single script, prefer the [script](script) module,
|
|
||||||
or [label](label) if you only need a single text label.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -21,11 +18,11 @@ You can think of these like HTML elements and their attributes.
|
|||||||
Every widget has the following options available; `type` is mandatory.
|
Every widget has the following options available; `type` is mandatory.
|
||||||
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
|
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
||||||
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
|
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
||||||
| `name` | `string` | `null` | Widget name. |
|
| `name` | `string` | `null` | Widget name. |
|
||||||
| `class` | `string` | `null` | Widget class name. |
|
| `class` | `string` | `null` | Widget class name. |
|
||||||
|
|
||||||
#### Box
|
#### Box
|
||||||
|
|
||||||
@@ -33,20 +30,20 @@ A container to place nested widgets inside.
|
|||||||
|
|
||||||
> Type: `box`
|
> Type: `box`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
|
|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------|
|
||||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
|
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. |
|
||||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||||
|
|
||||||
#### Label
|
#### Label
|
||||||
|
|
||||||
A text label. Pango markup is supported.
|
A text label. Pango markup and embedded scripts are supported.
|
||||||
|
|
||||||
> Type `label`
|
> Type `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
|---------|----------|--------------|---------------------------------------------------------------------|
|
||||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
|
|
||||||
#### Button
|
#### Button
|
||||||
|
|
||||||
@@ -54,10 +51,10 @@ A clickable button, which can run a command when clicked.
|
|||||||
|
|
||||||
> Type `button`
|
> Type `button`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
|------------|--------------------|--------------|---------------------------------------------------------------------|
|
||||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||||
|
|
||||||
#### Image
|
#### Image
|
||||||
|
|
||||||
@@ -65,10 +62,10 @@ An image or icon from disk or http.
|
|||||||
|
|
||||||
> Type `image`
|
> Type `image`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
|--------|-----------|---------|---------------------------------------------------------------------------------------------|
|
||||||
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||||
|
|
||||||
#### Slider
|
#### Slider
|
||||||
|
|
||||||
@@ -79,16 +76,18 @@ A draggable slider.
|
|||||||
Note that `on_change` will provide the **floating point** value as an argument.
|
Note that `on_change` will provide the **floating point** value as an argument.
|
||||||
If your input program requires an integer, you will need to round it.
|
If your input program requires an integer, you will need to round it.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
||||||
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||||
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
||||||
| `min` | `float` | `0` | Minimum slider value. |
|
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
||||||
| `max` | `float` | `100` | Maximum slider value. |
|
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
||||||
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
| `min` | `float` | `0` | Minimum slider value. |
|
||||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
| `max` | `float` | `100` | Maximum slider value. |
|
||||||
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
||||||
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
|
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
||||||
|
|
||||||
The example slider widget below shows a volume control for MPC,
|
The example slider widget below shows a volume control for MPC,
|
||||||
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
||||||
@@ -116,12 +115,14 @@ A progress bar.
|
|||||||
|
|
||||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
||||||
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||||
| `max` | `float` | `100` | Maximum progress bar value. |
|
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
||||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
||||||
|
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||||
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
|
|
||||||
The example below shows progress for the current playing song in MPD,
|
The example below shows progress for the current playing song in MPD,
|
||||||
and displays the elapsed/length timestamps as a label above:
|
and displays the elapsed/length timestamps as a label above:
|
||||||
@@ -391,13 +392,10 @@ let {
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
Since the widgets are all custom, you can use their `name` and `class` attributes, then target them using `#name` and `.class`.
|
Since the widgets are all custom, you can use the `name` and `class` attributes, then target them using `#name` and `.class`.
|
||||||
|
|
||||||
The following top-level selectors are always available:
|
The following top-level selector is always available:
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-----------------|--------------------------------|
|
|-----------|-------------------------|
|
||||||
| `.custom` | Custom widget container. |
|
| `#custom` | Custom widget container |
|
||||||
| `.popup-custom` | Custom widget popup container. |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -7,15 +7,15 @@ Displays the title and/or icon of the currently focused window.
|
|||||||
|
|
||||||
> Type: `focused`
|
> Type: `focused`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
||||||
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
||||||
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
|
| `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` | `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.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.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.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -86,10 +86,8 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-------------------|--------------------|
|
|--------------------------|--------------------|
|
||||||
| `.focused` | Focused widget box |
|
| `#focused` | Focused widget box |
|
||||||
| `.focused .icon` | App icon |
|
| `#focused #icon` | App icon |
|
||||||
| `.focused .label` | App name |
|
| `#focused #label` | App name |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
Displays custom text, with markup support.
|
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
|
||||||
|
|
||||||
If you only intend to run a single script, prefer the [script](script) module.
|
|
||||||
For more advanced use-cases, use [custom](custom).
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
> Type: `label`
|
> Type: `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|-------------------------------------------------|---------|------------------------|
|
|---------|----------|---------|-----------------------------------------|
|
||||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
| `label` | `string` | `null` | Text, optionally with embedded scripts. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -68,8 +65,6 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|----------|------------------------------------------------------------------------------------|
|
|--------------------------------|------------------------------------------------------------------------------------|
|
||||||
| `.label` | Label widget |
|
| `#label` | Label widget |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -90,12 +90,10 @@ start:
|
|||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-------------------------------|--------------------------|
|
|-------------------------------|--------------------------|
|
||||||
| `.launcher` | Launcher widget box |
|
| `#launcher` | Launcher widget box |
|
||||||
| `.launcher .item` | App button |
|
| `#launcher .item` | App button |
|
||||||
| `.launcher .item.open` | App button (open app) |
|
| `#launcher .item.open` | App button (open app) |
|
||||||
| `.launcher .item.focused` | App button (focused app) |
|
| `#launcher .item.focused` | App button (focused app) |
|
||||||
| `.launcher .item.urgent` | App button (urgent app) |
|
| `#launcher .item.urgent` | App button (urgent app) |
|
||||||
| `.popup-launcher` | Popup container |
|
| `#launcher-popup` | Popup container |
|
||||||
| `.popup-launcher .popup-item` | Window button in popup |
|
| `#launcher-popup .popup-item` | Window button in popup |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
|||||||
|
|
||||||
> Type: `music`
|
> Type: `music`
|
||||||
|
|
||||||
| | Type | Default | Description |
|
| | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
| `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. |
|
| `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` | `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.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.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.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.play` | `string/image` | `` | Icon to show when playing. |
|
||||||
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
||||||
| `icons.prev` | `string` or [image](images) | `` | Icon to show on previous button. |
|
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
||||||
| `icons.next` | `string` or [image](images) | `` | Icon to show on next button. |
|
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
|
||||||
| `icons.volume` | `string` or [image](images) | `` | Icon to show under popup volume slider. |
|
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
|
||||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
|
||||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
|
||||||
| `icons.artist` | `string` or [image](images) | `` | Icon to show next to artist name. |
|
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
|
||||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
| `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). |
|
| `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. |
|
| `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. |
|
| `host` | `string/image` | `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. |
|
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||||
|
|
||||||
See [here](images) for information on images.
|
See [here](images) for information on images.
|
||||||
|
|
||||||
@@ -128,46 +128,30 @@ and will be replaced with values from the currently playing track:
|
|||||||
| `{track}` | Track number |
|
| `{track}` | Track number |
|
||||||
| `{disc}` | Disc number |
|
| `{disc}` | Disc number |
|
||||||
| `{genre}` | Genre |
|
| `{genre}` | Genre |
|
||||||
|
| `{duration}` | Duration in `mm:ss` |
|
||||||
|
| `{elapsed}` | Time elapsed in `mm:ss` |
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|---------------------------------------------|-------------------------------------------------------|
|
|-------------------------------------|------------------------------------------|
|
||||||
| `.music` | Tray widget button |
|
| `#music` | Tray widget button |
|
||||||
| `.music .contents` | Tray widget button contents box |
|
| `#popup-music` | Popup box |
|
||||||
| `.music .contents .icon` | Tray widget button icon (any type) |
|
| `#popup-music #album-art` | Album art image inside popup box |
|
||||||
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
|
| `#popup-music #title` | Track title container inside popup box |
|
||||||
| `.music .contents .image` | Tray widget button icon (image only) |
|
| `#popup-music #title .icon` | Track title icon label inside popup box |
|
||||||
| `.popup-music` | Popup box |
|
| `#popup-music #title .label` | Track title label inside popup box |
|
||||||
| `.popup-music .album-art` | Album art image inside popup box |
|
| `#popup-music #album` | Track album container inside popup box |
|
||||||
| `.popup-music .title` | Track title container inside popup box |
|
| `#popup-music #album .icon` | Track album icon label inside popup box |
|
||||||
| `.popup-music .title .icon-box` | Track title icon container inside popup box |
|
| `#popup-music #album .label` | Track album label inside popup box |
|
||||||
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
|
| `#popup-music #artist` | Track artist container inside popup box |
|
||||||
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
|
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
|
||||||
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
|
| `#popup-music #artist .label` | Track artist label inside popup box |
|
||||||
| `.popup-music .title .label` | Track title label inside popup box |
|
| `#popup-music #controls` | Controls container inside popup box |
|
||||||
| `.popup-music .album` | Track album container inside popup box |
|
| `#popup-music #controls #btn-prev` | Previous button inside popup box |
|
||||||
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
|
| `#popup-music #controls #btn-play` | Play button inside popup box |
|
||||||
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
|
| `#popup-music #controls #btn-pause` | Pause button inside popup box |
|
||||||
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
|
| `#popup-music #controls #btn-next` | Next button inside popup box |
|
||||||
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
|
| `#popup-music #volume` | Volume container inside popup box |
|
||||||
| `.popup-music .album .label` | Track album label inside popup box |
|
| `#popup-music #volume #slider` | Volume slider popup box |
|
||||||
| `.popup-music .artist` | Track artist container inside popup box |
|
| `#popup-music #volume .icon` | Volume icon label inside popup box |
|
||||||
| `.popup-music .artist .icon-box` | Track artist icon container inside popup box |
|
|
||||||
| `.popup-music .artist .icon-box .icon` | Track artist icon inside its container (any type) |
|
|
||||||
| `.popup-music .artist .icon-box .text-icon` | Track artist icon inside its container (textual only) |
|
|
||||||
| `.popup-music .artist .icon-box .image` | Track artist icon inside its container (image only) |
|
|
||||||
| `.popup-music .artist .label` | Track artist label inside popup box |
|
|
||||||
| `.popup-music .controls` | Controls container inside popup box |
|
|
||||||
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
|
|
||||||
| `.popup-music .controls .btn-play` | Play button inside popup box |
|
|
||||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
|
||||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
|
||||||
| `.popup-music .volume` | Volume container inside popup box |
|
|
||||||
| `.popup-music .volume .slider` | Slider inside volume container |
|
|
||||||
| `.popup-music .volume .icon` | Icon inside volume container |
|
|
||||||
| `.popup-music .progress` | Progress (seek) bar container |
|
|
||||||
| `.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).
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
Executes a script and shows the result of `stdout` on a label.
|
Executes a script and shows the result of `stdout` on a label.
|
||||||
Pango markup is supported.
|
Pango markup is supported.
|
||||||
|
|
||||||
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
|
|
||||||
For more advanced use-cases, use [custom](custom).
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
> Type: `script`
|
> Type: `script`
|
||||||
@@ -85,8 +82,6 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-----------|---------------------|
|
|---------------|---------------------|
|
||||||
| `.script` | Script widget label |
|
| `#script` | Script widget label |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -28,13 +28,13 @@ Pango markup is supported.
|
|||||||
"end": [
|
"end": [
|
||||||
{
|
{
|
||||||
"format": [
|
"format": [
|
||||||
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||||
" {uptime}"
|
" {uptime}"
|
||||||
],
|
],
|
||||||
"interval": {
|
"interval": {
|
||||||
"cpu": 1,
|
"cpu": 1,
|
||||||
@@ -58,13 +58,13 @@ Pango markup is supported.
|
|||||||
[[end]]
|
[[end]]
|
||||||
type = 'sys_info'
|
type = 'sys_info'
|
||||||
format = [
|
format = [
|
||||||
' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C',
|
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||||
' {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||||
' {load_average:1} | {load_average:5} | {load_average:15}',
|
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||||
' {uptime}',
|
' {uptime}',
|
||||||
]
|
]
|
||||||
|
|
||||||
[end.interval]
|
[end.interval]
|
||||||
@@ -85,13 +85,13 @@ temps = 5
|
|||||||
```yaml
|
```yaml
|
||||||
end:
|
end:
|
||||||
- format:
|
- format:
|
||||||
- ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C'
|
- ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C'
|
||||||
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
|
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
|
||||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||||
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
|
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
|
||||||
- ' {net_down:enp39s0} / {net_up:enp39s0} Mbps'
|
- '李 {net_down:enp39s0} / {net_up:enp39s0} Mbps'
|
||||||
- ' {load_average:1} | {load_average:5} | {load_average:15}'
|
- '猪 {load_average:1} | {load_average:5} | {load_average:15}'
|
||||||
- ' {uptime}'
|
- ' {uptime}'
|
||||||
interval:
|
interval:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
disks: 300
|
disks: 300
|
||||||
@@ -119,13 +119,13 @@ end:
|
|||||||
interval.networks = 3
|
interval.networks = 3
|
||||||
|
|
||||||
format = [
|
format = [
|
||||||
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C"
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}"
|
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||||
" {uptime}"
|
" {uptime}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -168,13 +168,9 @@ The following tokens can be used in the `format` configuration option:
|
|||||||
| `{load_average:15}` | 15-minute load average. |
|
| `{load_average:15}` | 15-minute load average. |
|
||||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
||||||
|
|
||||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp-Tccd1`.
|
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|------------------|------------------------------|
|
|------------------|------------------------------|
|
||||||
| `.sysinfo` | Sysinfo widget box |
|
| `#sysinfo` | Sysinfo widget box |
|
||||||
| `.sysinfo .item` | Individual information label |
|
| `#sysinfo #item` | Individual information label |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -60,7 +60,5 @@ end:
|
|||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|---------------|------------------|
|
|---------------|------------------|
|
||||||
| `.tray` | Tray widget box |
|
| `#tray` | Tray widget box |
|
||||||
| `.tray .item` | Tray icon button |
|
| `#tray .item` | Tray icon button |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ Displays system power information such as the battery percentage, and estimated
|
|||||||
|
|
||||||
> Type: `upower`
|
> Type: `upower`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-------------|-----------|-----------------|---------------------------------------------------|
|
|----------|----------|-----------------|---------------------------------------------------|
|
||||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||||
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -71,14 +70,11 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|---------------------------------|--------------------------------|
|
|---------------------------------|-----------------------------|
|
||||||
| `.upower` | Upower widget container. |
|
| `#upower` | Upower widget container. |
|
||||||
| `.upower .button` | Upower widget button. |
|
| `#upower #icon` | Upower widget battery icon. |
|
||||||
| `.upower .button .contents` | Upower widget button contents. |
|
| `#upower #button` | Upower widget button. |
|
||||||
| `.upower .button .icon` | Upower widget battery icon. |
|
| `#upower #button #label` | Upower widget button label. |
|
||||||
| `.upower .button .label` | Upower widget button label. |
|
| `#popup-upower` | Clock popup box. |
|
||||||
| `.popup-upower` | Upower popup box. |
|
| `#popup-upower #upower-details` | Label inside the popup. |
|
||||||
| `.popup-upower .upower-details` | Label inside the popup. |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
|||||||
|
|
||||||
> Type: `workspaces`
|
> Type: `workspaces`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| 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. |
|
| `name_map` | `Map<string, string/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. |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
|
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||||
| `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. |
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -30,7 +28,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
|||||||
"2": "",
|
"2": "",
|
||||||
"3": ""
|
"3": ""
|
||||||
},
|
},
|
||||||
"favorites": ["1", "2", "3"],
|
|
||||||
"all_monitors": false
|
"all_monitors": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -46,7 +43,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
|||||||
[[end]]
|
[[end]]
|
||||||
type = "workspaces"
|
type = "workspaces"
|
||||||
all_monitors = false
|
all_monitors = false
|
||||||
favorites = ["1", "2", "3"]
|
|
||||||
|
|
||||||
[[end.name_map]]
|
[[end.name_map]]
|
||||||
1 = ""
|
1 = ""
|
||||||
@@ -67,10 +63,6 @@ end:
|
|||||||
1: ""
|
1: ""
|
||||||
2: ""
|
2: ""
|
||||||
3: ""
|
3: ""
|
||||||
favorites:
|
|
||||||
- "1"
|
|
||||||
- "2"
|
|
||||||
- "3"
|
|
||||||
all_monitors: false
|
all_monitors: false
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,7 +79,6 @@ end:
|
|||||||
name_map.1 = ""
|
name_map.1 = ""
|
||||||
name_map.2 = ""
|
name_map.2 = ""
|
||||||
name_map.3 = ""
|
name_map.3 = ""
|
||||||
favorites = [ "1" "2" "3" ]
|
|
||||||
all_monitors = false
|
all_monitors = false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -98,15 +89,8 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|--------------------------------|--------------------------------------|
|
|-----------------------------|--------------------------------------|
|
||||||
| `.workspaces` | Workspaces widget box |
|
| `#workspaces` | Workspaces widget box |
|
||||||
| `.workspaces .item` | Workspace button |
|
| `#workspaces .item` | Workspace button |
|
||||||
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
| `#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) |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
||||||
@@ -3,7 +3,7 @@ let {
|
|||||||
type = "workspaces"
|
type = "workspaces"
|
||||||
all_monitors = false
|
all_monitors = false
|
||||||
name_map = {
|
name_map = {
|
||||||
1 = ""
|
1 = "ﭮ"
|
||||||
2 = "icon:firefox"
|
2 = "icon:firefox"
|
||||||
3 = ""
|
3 = ""
|
||||||
Games = "icon:steam"
|
Games = "icon:steam"
|
||||||
@@ -15,7 +15,7 @@ let {
|
|||||||
|
|
||||||
$launcher = {
|
$launcher = {
|
||||||
type = "launcher"
|
type = "launcher"
|
||||||
favorites = ["firefox" "discord" "steam"]
|
favorites = ["firefox" "discord" "Steam"]
|
||||||
show_names = false
|
show_names = false
|
||||||
show_icons = true
|
show_icons = true
|
||||||
}
|
}
|
||||||
@@ -46,10 +46,10 @@ let {
|
|||||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}"
|
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||||
" {uptime}"
|
" {uptime}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ let {
|
|||||||
|
|
||||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||||
|
|
||||||
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
|
$label = { type = "label" label = "random num: {{500:echo $RANDOM}}" }
|
||||||
|
|
||||||
// -- begin custom --
|
// -- begin custom --
|
||||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||||
" {uptime}"
|
" {uptime}"
|
||||||
],
|
],
|
||||||
"interval": {
|
"interval": {
|
||||||
"cpu": 1,
|
"cpu": 1,
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
{
|
{
|
||||||
"all_monitors": false,
|
"all_monitors": false,
|
||||||
"name_map": {
|
"name_map": {
|
||||||
"1": "",
|
"1": "ﭮ",
|
||||||
"2": "icon:firefox",
|
"2": "icon:firefox",
|
||||||
"3": "",
|
"3": "",
|
||||||
"Code": "",
|
"Code": "",
|
||||||
@@ -121,14 +121,14 @@
|
|||||||
"favorites": [
|
"favorites": [
|
||||||
"firefox",
|
"firefox",
|
||||||
"discord",
|
"discord",
|
||||||
"steam"
|
"Steam"
|
||||||
],
|
],
|
||||||
"show_icons": true,
|
"show_icons": true,
|
||||||
"show_names": false,
|
"show_names": false,
|
||||||
"type": "launcher"
|
"type": "launcher"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "random num: {{500:echo FIXME}}",
|
"label": "random num: {{500:echo $RANDOM}}",
|
||||||
"type": "label"
|
"type": "label"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
anchor_to_edges = true
|
anchor_to_edges = true
|
||||||
icon_theme = "Paper"
|
icon_theme = 'Paper'
|
||||||
position = "bottom"
|
position = 'bottom'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
music_dir = "/home/jake/Music"
|
music_dir = '/home/jake/Music'
|
||||||
player_type = "mpd"
|
player_type = 'mpd'
|
||||||
type = "music"
|
type = 'music'
|
||||||
|
|
||||||
[end.truncate]
|
[end.truncate]
|
||||||
max_length = 100
|
max_length = 100
|
||||||
mode = "end"
|
mode = 'end'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
host = "chloe:6600"
|
host = 'chloe:6600'
|
||||||
player_type = "mpd"
|
player_type = 'mpd'
|
||||||
truncate = "end"
|
truncate = 'end'
|
||||||
type = "music"
|
type = 'music'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
cmd = "/home/jake/bin/phone-battery"
|
cmd = '/home/jake/bin/phone-battery'
|
||||||
type = "script"
|
type = 'script'
|
||||||
|
|
||||||
[end.show_if]
|
[end.show_if]
|
||||||
cmd = "/home/jake/bin/phone-connected"
|
cmd = '/home/jake/bin/phone-connected'
|
||||||
interval = 500
|
interval = 500
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
|
type = 'sys_info'
|
||||||
format = [
|
format = [
|
||||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||||
" {uptime}",
|
' {uptime}',
|
||||||
]
|
]
|
||||||
type = "sys_info"
|
|
||||||
|
|
||||||
[end.interval]
|
[end.interval]
|
||||||
cpu = 1
|
cpu = 1
|
||||||
@@ -46,77 +46,77 @@ temps = 5
|
|||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
max_items = 3
|
max_items = 3
|
||||||
type = "clipboard"
|
type = 'clipboard'
|
||||||
|
|
||||||
[end.truncate]
|
[end.truncate]
|
||||||
length = 50
|
length = 50
|
||||||
mode = "end"
|
mode = 'end'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
class = "power-menu"
|
class = 'power-menu'
|
||||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||||
type = "custom"
|
type = 'custom'
|
||||||
|
|
||||||
[[end.bar]]
|
[[end.bar]]
|
||||||
label = ""
|
label = ''
|
||||||
name = "power-btn"
|
name = 'power-btn'
|
||||||
on_click = "popup:toggle"
|
on_click = 'popup:toggle'
|
||||||
type = "button"
|
type = 'button'
|
||||||
|
|
||||||
[[end.popup]]
|
[[end.popup]]
|
||||||
orientation = "vertical"
|
orientation = 'vertical'
|
||||||
type = "box"
|
type = 'box'
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
label = "Power menu"
|
label = 'Power menu'
|
||||||
name = "header"
|
name = 'header'
|
||||||
type = "label"
|
type = 'label'
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
type = "box"
|
type = 'box'
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
class = "power-btn"
|
class = 'power-btn'
|
||||||
label = "<span font-size='40pt'></span>"
|
label = '''<span font-size='40pt'></span>'''
|
||||||
on_click = "!shutdown now"
|
on_click = '!shutdown now'
|
||||||
type = "button"
|
type = 'button'
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
class = "power-btn"
|
class = 'power-btn'
|
||||||
label = "<span font-size='40pt'></span>"
|
label = '''<span font-size='40pt'></span>'''
|
||||||
on_click = "!reboot"
|
on_click = '!reboot'
|
||||||
type = "button"
|
type = 'button'
|
||||||
|
|
||||||
[[end.popup.widgets]]
|
[[end.popup.widgets]]
|
||||||
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||||
name = "uptime"
|
name = 'uptime'
|
||||||
type = "label"
|
type = 'label'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
type = "clock"
|
type = 'clock'
|
||||||
|
|
||||||
[[start]]
|
[[start]]
|
||||||
all_monitors = false
|
all_monitors = false
|
||||||
type = "workspaces"
|
type = 'workspaces'
|
||||||
|
|
||||||
[start.name_map]
|
[start.name_map]
|
||||||
1 = ""
|
1 = 'ﭮ'
|
||||||
2 = "icon:firefox"
|
2 = 'icon:firefox'
|
||||||
3 = ""
|
3 = ''
|
||||||
Code = ""
|
Code = ''
|
||||||
Games = "icon:steam"
|
Games = 'icon:steam'
|
||||||
|
|
||||||
[[start]]
|
[[start]]
|
||||||
favorites = [
|
|
||||||
"firefox",
|
|
||||||
"discord",
|
|
||||||
"steam",
|
|
||||||
]
|
|
||||||
show_icons = true
|
show_icons = true
|
||||||
show_names = false
|
show_names = false
|
||||||
type = "launcher"
|
type = 'launcher'
|
||||||
|
favorites = [
|
||||||
|
'firefox',
|
||||||
|
'discord',
|
||||||
|
'Steam',
|
||||||
|
]
|
||||||
|
|
||||||
[[start]]
|
[[start]]
|
||||||
label = "random num: {{500:echo FIXME}}"
|
label = 'random num: {{500:echo $RANDOM}}'
|
||||||
type = "label"
|
type = 'label'
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +1,87 @@
|
|||||||
anchor_to_edges: true
|
anchor_to_edges: true
|
||||||
end:
|
end:
|
||||||
- music_dir: /home/jake/Music
|
- music_dir: /home/jake/Music
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
truncate:
|
truncate:
|
||||||
max_length: 100
|
max_length: 100
|
||||||
mode: end
|
mode: end
|
||||||
type: music
|
type: music
|
||||||
- host: chloe:6600
|
- host: chloe:6600
|
||||||
player_type: mpd
|
player_type: mpd
|
||||||
truncate: end
|
truncate: end
|
||||||
type: music
|
type: music
|
||||||
- cmd: /home/jake/bin/phone-battery
|
- cmd: /home/jake/bin/phone-battery
|
||||||
show_if:
|
show_if:
|
||||||
cmd: /home/jake/bin/phone-connected
|
cmd: /home/jake/bin/phone-connected
|
||||||
interval: 500
|
interval: 500
|
||||||
type: script
|
type: script
|
||||||
- format:
|
- format:
|
||||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||||
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
||||||
- {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
- 李 {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||||
- {load_average:1} | {load_average:5} | {load_average:15}
|
- 猪 {load_average:1} | {load_average:5} | {load_average:15}
|
||||||
- {uptime}
|
- {uptime}
|
||||||
interval:
|
interval:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
disks: 300
|
disks: 300
|
||||||
memory: 30
|
memory: 30
|
||||||
networks: 3
|
networks: 3
|
||||||
temps: 5
|
temps: 5
|
||||||
type: sys_info
|
type: sys_info
|
||||||
- max_items: 3
|
- max_items: 3
|
||||||
truncate:
|
truncate:
|
||||||
length: 50
|
length: 50
|
||||||
mode: end
|
mode: end
|
||||||
type: clipboard
|
type: clipboard
|
||||||
- bar:
|
- bar:
|
||||||
- label:
|
- label:
|
||||||
name: power-btn
|
name: power-btn
|
||||||
on_click: popup:toggle
|
on_click: popup:toggle
|
||||||
type: button
|
|
||||||
class: power-menu
|
|
||||||
popup:
|
|
||||||
- orientation: vertical
|
|
||||||
type: box
|
|
||||||
widgets:
|
|
||||||
- label: Power menu
|
|
||||||
name: header
|
|
||||||
type: label
|
|
||||||
- type: box
|
|
||||||
widgets:
|
|
||||||
- class: power-btn
|
|
||||||
label: <span font-size='40pt'></span>
|
|
||||||
on_click: '!shutdown now'
|
|
||||||
type: button
|
type: button
|
||||||
- class: power-btn
|
class: power-menu
|
||||||
label: <span font-size='40pt'></span>
|
popup:
|
||||||
on_click: '!reboot'
|
- orientation: vertical
|
||||||
type: button
|
type: box
|
||||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
widgets:
|
||||||
name: uptime
|
- label: Power menu
|
||||||
type: label
|
name: header
|
||||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
type: label
|
||||||
type: custom
|
- type: box
|
||||||
- type: clock
|
widgets:
|
||||||
|
- class: power-btn
|
||||||
|
label: <span font-size='40pt'></span>
|
||||||
|
on_click: '!shutdown now'
|
||||||
|
type: button
|
||||||
|
- class: power-btn
|
||||||
|
label: <span font-size='40pt'></span>
|
||||||
|
on_click: '!reboot'
|
||||||
|
type: button
|
||||||
|
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
|
name: uptime
|
||||||
|
type: label
|
||||||
|
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
|
type: custom
|
||||||
|
- type: clock
|
||||||
icon_theme: Paper
|
icon_theme: Paper
|
||||||
position: bottom
|
position: bottom
|
||||||
start:
|
start:
|
||||||
- all_monitors: false
|
- all_monitors: false
|
||||||
name_map:
|
name_map:
|
||||||
'1':
|
'1': ﭮ
|
||||||
'2': icon:firefox
|
'2': icon:firefox
|
||||||
'3':
|
'3':
|
||||||
Code:
|
Code:
|
||||||
Games: icon:steam
|
Games: icon:steam
|
||||||
type: workspaces
|
type: workspaces
|
||||||
- favorites:
|
- favorites:
|
||||||
- firefox
|
- firefox
|
||||||
- discord
|
- discord
|
||||||
- steam
|
- Steam
|
||||||
show_icons: true
|
show_icons: true
|
||||||
show_names: false
|
show_names: false
|
||||||
type: launcher
|
type: launcher
|
||||||
- label: 'random num: {{500:echo FIXME}}'
|
- label: 'random num: {{500:echo $RANDOM}}'
|
||||||
type: label
|
type: label
|
||||||
|
|
||||||
|
|||||||
@@ -1,201 +1,240 @@
|
|||||||
@define-color color_bg #2d2d2d;
|
|
||||||
@define-color color_bg_dark #1c1c1c;
|
|
||||||
@define-color color_border #424242;
|
|
||||||
@define-color color_border_active #6699cc;
|
|
||||||
@define-color color_text #ffffff;
|
|
||||||
@define-color color_urgent #8f0a0a;
|
|
||||||
|
|
||||||
/* -- base styles -- */
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
/* `otf-font-awesome` is required to be installed for icons */
|
||||||
font-family: Noto Sans Nerd Font, sans-serif;
|
font-family: Noto Sans Nerd Font, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
box, menubar, button {
|
|
||||||
background-color: @color_bg;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, label {
|
|
||||||
color: @color_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: @color_bg_dark;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#bar {
|
#bar {
|
||||||
border-top: 1px solid @color_border;
|
border-top: 1px solid #424242;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup {
|
.background, .container {
|
||||||
border: 1px solid @color_border;
|
background-color: #2d2d2d;
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#workspaces .item {
|
||||||
/* -- clipboard -- */
|
color: white;
|
||||||
|
background-color: #2d2d2d;
|
||||||
.clipboard {
|
border-radius: 0;
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-clipboard .item {
|
#workspaces .item.focused {
|
||||||
padding-bottom: 0.3em;
|
box-shadow: inset 0 -3px;
|
||||||
border-bottom: 1px solid @color_border;
|
background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#workspaces *:not(.focused):hover {
|
||||||
|
box-shadow: inset 0 -3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- clock -- */
|
#launcher .item {
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.clock {
|
#launcher .item:not(.focused):hover {
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher .open {
|
||||||
|
border-bottom: 2px solid #6699cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher .focused {
|
||||||
|
color: white;
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
border-bottom: 4px solid #6699cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher .urgent {
|
||||||
|
color: white;
|
||||||
|
background-color: #8f0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-launcher .popup-item {
|
||||||
|
color: white;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-launcher .popup-item:hover {
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-launcher .popup-item:not(:first-child) {
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#clock {
|
||||||
|
color: white;
|
||||||
|
background-color: #2d2d2d;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-clock .calendar-clock {
|
#clock:hover {
|
||||||
color: @color_text;
|
background-color: #1c1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#script {
|
||||||
|
padding-left: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sysinfo {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sysinfo #item {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tray {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tray .item {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border: 1px solid #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clock {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calendar-clock {
|
||||||
|
color: white;
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
padding-bottom: 0.1em;
|
padding-bottom: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-clock .calendar {
|
#calendar {
|
||||||
background-color: @color_bg;
|
background-color: #2d2d2d;
|
||||||
color: @color_text;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-clock .calendar .header {
|
#calendar .header {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
border-top: 1px solid @color_border;
|
border-top: 1px solid #424242;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-clock .calendar:selected {
|
#calendar:selected {
|
||||||
background-color: @color_border_active;
|
background-color: #6699cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#music:hover {
|
||||||
/* -- launcher -- */
|
background-color: #1c1c1c;
|
||||||
|
|
||||||
.launcher .item {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher .item:not(.focused):hover {
|
#popup-music {
|
||||||
background-color: @color_bg_dark;
|
color: white;
|
||||||
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher .open {
|
#popup-music #album-art {
|
||||||
border-bottom: 1px solid @color_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher .focused {
|
|
||||||
border-bottom: 2px solid @color_border_active;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher .urgent {
|
|
||||||
border-bottom-color: @color_urgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-launcher {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-launcher .popup-item:not(:first-child) {
|
|
||||||
border-top: 1px solid @color_border;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -- music -- */
|
|
||||||
|
|
||||||
.music:hover * {
|
|
||||||
background-color: @color_bg_dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-music .album-art {
|
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-music .icon-box {
|
#popup-music #title .icon *, #popup-music #title .label {
|
||||||
margin-right: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-music .title .icon, .popup-music .title .label {
|
|
||||||
font-size: 1.7em;
|
font-size: 1.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-music .controls *:disabled {
|
#popup-music #controls * {
|
||||||
color: @color_border;
|
border-radius: 0;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-music .volume .slider slider {
|
#popup-music #controls *:disabled {
|
||||||
border-radius: 100%;
|
color: #424242;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-music .volume .icon {
|
#popup-music #volume > box:last-child label {
|
||||||
margin-left: 4px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-music .progress .slider slider {
|
#focused {
|
||||||
border-radius: 100%;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- script -- */
|
.power-menu {
|
||||||
|
|
||||||
.script {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -- sys_info -- */
|
|
||||||
|
|
||||||
.sysinfo {
|
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sysinfo .item {
|
.power-menu #power-btn {
|
||||||
margin-left: 5px;
|
color: white;
|
||||||
|
background-color: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.power-menu #power-btn:hover {
|
||||||
/* -- tray -- */
|
background-color: #1c1c1c;
|
||||||
|
|
||||||
.tray {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-power-menu {
|
||||||
/* -- workspaces -- */
|
padding: 1em;
|
||||||
|
|
||||||
.workspaces .item.focused {
|
|
||||||
box-shadow: inset 0 -3px;
|
|
||||||
background-color: @color_bg_dark;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspaces .item:hover {
|
|
||||||
box-shadow: inset 0 -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -- custom: power menu -- */
|
|
||||||
|
|
||||||
.popup-power-menu #header {
|
.popup-power-menu #header {
|
||||||
|
color: white;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
|
border-bottom: 1px solid white;
|
||||||
padding-bottom: 0.4em;
|
padding-bottom: 0.4em;
|
||||||
margin-bottom: 0.6em;
|
margin-bottom: 0.8em;
|
||||||
border-bottom: 1px solid @color_border;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-power-menu .power-btn {
|
.popup-power-menu .power-btn {
|
||||||
border: 1px solid @color_border;
|
color: white;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border: 1px solid white;
|
||||||
padding: 0.6em 1em;
|
padding: 0.6em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-power-menu #buttons > *:nth-child(1) .power-btn {
|
.popup-power-menu .power-btn + .power-btn {
|
||||||
margin-right: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-power-menu .power-btn:hover {
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#clipboard * {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clipboard {
|
||||||
|
padding: 1em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clipboard .item {
|
||||||
|
border-bottom: 1px solid #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clipboard .btn > *:nth-child(2) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clipboard .btn-remove {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border-left: 1px solid #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-clipboard .btn-remove:hover {
|
||||||
|
color: #fcc;
|
||||||
|
}
|
||||||
92
flake.lock
generated
92
flake.lock
generated
@@ -1,35 +1,12 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1703439018,
|
|
||||||
"narHash": "sha256-VT+06ft/x3eMZ1MJxWzQP3zXFGcrxGo5VR2rB7t88hs=",
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"rev": "afdcd41180e3dfe4dac46b5ee396e3b12ccc967a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681202837,
|
"lastModified": 1659877975,
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -38,45 +15,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"naersk": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1698420672,
|
|
||||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "naersk",
|
|
||||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "naersk",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1704008649,
|
"lastModified": 1680213900,
|
||||||
"narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=",
|
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1703637592,
|
|
||||||
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
|
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
|
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -88,9 +33,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crane": "crane",
|
"nixpkgs": "nixpkgs",
|
||||||
"naersk": "naersk",
|
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"rust-overlay": "rust-overlay"
|
"rust-overlay": "rust-overlay"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -102,11 +45,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1703902408,
|
"lastModified": 1680229280,
|
||||||
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
|
"narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
|
"rev": "aa480d799023141e1b9e5d6108700de63d9ad002",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -114,21 +57,6 @@
|
|||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
57
flake.nix
57
flake.nix
@@ -6,18 +6,11 @@
|
|||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
crane = {
|
|
||||||
url = "github:ipetkov/crane";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
naersk.url = "github:nix-community/naersk";
|
|
||||||
};
|
};
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
rust-overlay,
|
rust-overlay,
|
||||||
crane,
|
|
||||||
naersk,
|
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
inherit (nixpkgs) lib;
|
inherit (nixpkgs) lib;
|
||||||
@@ -34,18 +27,10 @@
|
|||||||
rust-overlay.overlays.default
|
rust-overlay.overlays.default
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
mkRustToolchain = pkgs:
|
mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
|
||||||
pkgs.rust-bin.stable.latest.default.override {
|
|
||||||
extensions = ["rust-src"];
|
|
||||||
};
|
|
||||||
in {
|
in {
|
||||||
overlays.default = final: prev: let
|
overlays.default = final: prev: let
|
||||||
rust = mkRustToolchain final;
|
rust = mkRustToolchain final;
|
||||||
craneLib = (crane.mkLib final).overrideToolchain rust;
|
|
||||||
naersk' = prev.callPackage naersk {
|
|
||||||
cargo = rust;
|
|
||||||
rustc = rust;
|
|
||||||
};
|
|
||||||
|
|
||||||
rustPlatform = prev.makeRustPlatform {
|
rustPlatform = prev.makeRustPlatform {
|
||||||
cargo = rust;
|
cargo = rust;
|
||||||
@@ -57,33 +42,11 @@
|
|||||||
(builtins.substring 4 2 longDate)
|
(builtins.substring 4 2 longDate)
|
||||||
(builtins.substring 6 2 longDate)
|
(builtins.substring 6 2 longDate)
|
||||||
]);
|
]);
|
||||||
builder = "naersk";
|
|
||||||
in {
|
in {
|
||||||
ironbar = let
|
ironbar = prev.callPackage ./nix/default.nix {
|
||||||
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
||||||
in
|
inherit rustPlatform;
|
||||||
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 (
|
packages = genSystems (
|
||||||
system: let
|
system: let
|
||||||
@@ -119,14 +82,6 @@
|
|||||||
gtk-layer-shell
|
gtk-layer-shell
|
||||||
pkg-config
|
pkg-config
|
||||||
openssl
|
openssl
|
||||||
gdk-pixbuf
|
|
||||||
glib
|
|
||||||
glib-networking
|
|
||||||
shared-mime-info
|
|
||||||
gnome.adwaita-icon-theme
|
|
||||||
hicolor-icon-theme
|
|
||||||
gsettings-desktop-schemas
|
|
||||||
libxkbcommon
|
|
||||||
];
|
];
|
||||||
|
|
||||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||||
@@ -193,8 +148,8 @@
|
|||||||
ExecStart = "${pkg}/bin/ironbar";
|
ExecStart = "${pkg}/bin/ironbar";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [
|
Install.WantedBy = [
|
||||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||||
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
|
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
113
nix/default.nix
113
nix/default.nix
@@ -19,77 +19,46 @@
|
|||||||
lib,
|
lib,
|
||||||
version ? "git",
|
version ? "git",
|
||||||
features ? [],
|
features ? [],
|
||||||
builderName ? "nix",
|
}:
|
||||||
builder ? {},
|
rustPlatform.buildRustPackage rec {
|
||||||
}: let
|
inherit version;
|
||||||
basePkg = rec {
|
pname = "ironbar";
|
||||||
inherit version;
|
src = builtins.path {
|
||||||
pname = "ironbar";
|
name = "ironbar";
|
||||||
src = builtins.path {
|
path = lib.cleanSource ../.;
|
||||||
name = "ironbar";
|
};
|
||||||
path = lib.cleanSource ../.;
|
buildNoDefaultFeatures =
|
||||||
};
|
if features == []
|
||||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
then false
|
||||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
else true;
|
||||||
propagatedBuildInputs = [
|
buildFeatures = features;
|
||||||
gtk3
|
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
||||||
];
|
cargoLock.lockFile = ../Cargo.lock;
|
||||||
preFixup = ''
|
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||||
gappsWrapperArgs+=(
|
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||||
# Thumbnailers
|
propagatedBuildInputs = [
|
||||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
gtk3
|
||||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
];
|
||||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
preFixup = ''
|
||||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
gappsWrapperArgs+=(
|
||||||
)
|
# Thumbnailers
|
||||||
'';
|
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||||
passthru = {
|
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||||
updateScript = gnome.updateScript {
|
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||||
packageName = pname;
|
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||||
attrPath = "gnome.${pname}";
|
)
|
||||||
};
|
'';
|
||||||
};
|
passthru = {
|
||||||
meta = with lib; {
|
updateScript = gnome.updateScript {
|
||||||
homepage = "https://github.com/JakeStanger/ironbar";
|
packageName = pname;
|
||||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
attrPath = "gnome.${pname}";
|
||||||
license = licenses.mit;
|
|
||||||
platforms = platforms.linux;
|
|
||||||
mainProgram = "ironbar";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
flags = let
|
meta = with lib; {
|
||||||
noDefault =
|
homepage = "https://github.com/JakeStanger/ironbar";
|
||||||
if features == []
|
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||||
then ""
|
license = licenses.mit;
|
||||||
else "--no-default-features";
|
platforms = platforms.linux;
|
||||||
featuresStr =
|
mainProgram = "ironbar";
|
||||||
if features == []
|
};
|
||||||
then ""
|
}
|
||||||
else ''-F "${builtins.concatStringsSep "," features}"'';
|
|
||||||
in [noDefault featuresStr];
|
|
||||||
in
|
|
||||||
if builderName == "naersk"
|
|
||||||
then
|
|
||||||
builder.buildPackage (basePkg
|
|
||||||
// {
|
|
||||||
cargoOptions = old: old ++ flags;
|
|
||||||
})
|
|
||||||
else if builderName == "crane"
|
|
||||||
then
|
|
||||||
builder.buildPackage (basePkg
|
|
||||||
// {
|
|
||||||
cargoExtraArgs = builtins.concatStringsSep " " flags;
|
|
||||||
doCheck = false;
|
|
||||||
})
|
|
||||||
else
|
|
||||||
rustPlatform.buildRustPackage (basePkg
|
|
||||||
// {
|
|
||||||
buildNoDefaultFeatures =
|
|
||||||
if features == []
|
|
||||||
then false
|
|
||||||
else true;
|
|
||||||
buildFeatures = features;
|
|
||||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
|
||||||
cargoLock.lockFile = ../Cargo.lock;
|
|
||||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Migrates CSS selectors from widget names to CSS classes.
|
|
||||||
# These changed as part of the 0.12 release.
|
|
||||||
|
|
||||||
# ⚠ This script will **NOT** check for custom styles and may mangle them!
|
|
||||||
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
|
|
||||||
|
|
||||||
style_path="$HOME/.config/ironbar/style.css"
|
|
||||||
|
|
||||||
# general
|
|
||||||
sed -i 's/#icon/.icon/g' "$style_path"
|
|
||||||
sed -i 's/#label/.label/g' "$style_path"
|
|
||||||
sed -i 's/#image/.image/g' "$style_path"
|
|
||||||
|
|
||||||
# clipboard
|
|
||||||
sed -i 's/#clipboard/.clipboard/g' "$style_path"
|
|
||||||
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
|
|
||||||
|
|
||||||
# clock
|
|
||||||
sed -i 's/#clock/.clock/g' "$style_path"
|
|
||||||
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
|
|
||||||
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
|
|
||||||
sed -i 's/#calendar/.calendar/g' "$style_path"
|
|
||||||
|
|
||||||
# custom
|
|
||||||
sed -i 's/#custom/.custom/g' "$style_path"
|
|
||||||
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
|
|
||||||
|
|
||||||
# focused
|
|
||||||
sed -i 's/#focused/.focused/g' "$style_path"
|
|
||||||
|
|
||||||
# launcher
|
|
||||||
sed -i 's/#launcher/.launcher/g' "$style_path"
|
|
||||||
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
|
|
||||||
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
|
|
||||||
|
|
||||||
# music
|
|
||||||
sed -i 's/#music/.music/g' "$style_path"
|
|
||||||
sed -i 's/#contents/.contents/g' "$style_path"
|
|
||||||
sed -i 's/#popup-music/.popup-music/g' "$style_path"
|
|
||||||
sed -i 's/#album-art/.album-art/g' "$style_path"
|
|
||||||
sed -i 's/#title/.title/g' "$style_path"
|
|
||||||
sed -i 's/#album/.album/g' "$style_path"
|
|
||||||
sed -i 's/#artist/.artist/g' "$style_path"
|
|
||||||
sed -i 's/#controls/.controls/g' "$style_path"
|
|
||||||
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
|
|
||||||
sed -i 's/#btn-play/.btn-play/g' "$style_path"
|
|
||||||
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
|
|
||||||
sed -i 's/#btn-next/.btn-next/g' "$style_path"
|
|
||||||
sed -i 's/#volume/.volume/g' "$style_path"
|
|
||||||
sed -i 's/#slider/.slider/g' "$style_path"
|
|
||||||
|
|
||||||
# script
|
|
||||||
sed -i 's/#script/.script/g' "$style_path"
|
|
||||||
|
|
||||||
# sys_info
|
|
||||||
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
|
|
||||||
sed -i 's/#item/.item/g' "$style_path"
|
|
||||||
|
|
||||||
# tray
|
|
||||||
sed -i 's/#tray/.tray/g' "$style_path"
|
|
||||||
|
|
||||||
# upower
|
|
||||||
sed -i 's/#upower/.upower/g' "$style_path"
|
|
||||||
sed -i 's/#button/.button/g' "$style_path"
|
|
||||||
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
|
|
||||||
sed -i 's/#upower-details/.upower-details/g' "$style_path"
|
|
||||||
|
|
||||||
# workspaces
|
|
||||||
sed -i 's/#workspaces/.workspaces/g' "$style_path"
|
|
||||||
sed -i 's/#item/.item/g' "$style_path"
|
|
||||||
17
shell.nix
17
shell.nix
@@ -1,17 +0,0 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
|
|
||||||
pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
cargo
|
|
||||||
clippy
|
|
||||||
rustfmt
|
|
||||||
gtk3
|
|
||||||
gtk-layer-shell
|
|
||||||
gcc
|
|
||||||
openssl
|
|
||||||
];
|
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
pkg-config
|
|
||||||
];
|
|
||||||
}
|
|
||||||
487
src/bar.rs
487
src/bar.rs
@@ -1,313 +1,125 @@
|
|||||||
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
||||||
use crate::modules::{
|
use crate::modules::{create_module, wrap_widget, ModuleInfo, ModuleLocation};
|
||||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
|
||||||
};
|
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
use crate::{Config, Ironbar};
|
use crate::Config;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::Propagation;
|
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||||
use gtk_layer_shell::LayerShell;
|
use std::sync::{Arc, RwLock};
|
||||||
use std::cell::RefCell;
|
use glib::signal::Inhibit;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
/// Creates a new window for a bar,
|
||||||
enum Inner {
|
/// sets it up and adds its widgets.
|
||||||
New { config: Option<Config> },
|
pub fn create_bar(
|
||||||
Loaded { popup: Rc<RefCell<Popup>> },
|
app: &Application,
|
||||||
|
monitor: &Monitor,
|
||||||
|
monitor_name: &str,
|
||||||
|
config: Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
let win = ApplicationWindow::builder().application(app).build();
|
||||||
|
|
||||||
|
setup_layer_shell(
|
||||||
|
&win,
|
||||||
|
monitor,
|
||||||
|
config.position,
|
||||||
|
config.anchor_to_edges,
|
||||||
|
config.margin,
|
||||||
|
);
|
||||||
|
|
||||||
|
let orientation = config.position.get_orientation();
|
||||||
|
|
||||||
|
let content = gtk::Box::builder()
|
||||||
|
.orientation(orientation)
|
||||||
|
.spacing(0)
|
||||||
|
.hexpand(false)
|
||||||
|
.name("bar");
|
||||||
|
|
||||||
|
let content = if orientation == Orientation::Horizontal {
|
||||||
|
content.height_request(config.height)
|
||||||
|
} else {
|
||||||
|
content.width_request(config.height)
|
||||||
|
}
|
||||||
|
.build();
|
||||||
|
|
||||||
|
content.style_context().add_class("container");
|
||||||
|
|
||||||
|
let start = create_container("start", orientation);
|
||||||
|
let center = create_container("center", orientation);
|
||||||
|
let end = create_container("end", orientation);
|
||||||
|
|
||||||
|
content.append(&start);
|
||||||
|
content.set_center_widget(Some(¢er));
|
||||||
|
content.pack_end(&end, false, false, 0);
|
||||||
|
|
||||||
|
load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||||
|
win.append(&content);
|
||||||
|
|
||||||
|
win.connect_destroy_event(|_, _| {
|
||||||
|
info!("Shutting down");
|
||||||
|
// gtk::main_quit();
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!("Showing bar");
|
||||||
|
|
||||||
|
// show each box but do not use `show_all`.
|
||||||
|
// this ensures `show_if` option works as intended.
|
||||||
|
start.show();
|
||||||
|
center.show();
|
||||||
|
end.show();
|
||||||
|
content.show();
|
||||||
|
win.show();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
/// Sets up GTK layer shell for a provided application window.
|
||||||
pub struct Bar {
|
fn setup_layer_shell(
|
||||||
name: String,
|
win: &ApplicationWindow,
|
||||||
monitor_name: String,
|
monitor: &Monitor,
|
||||||
position: BarPosition,
|
position: BarPosition,
|
||||||
|
anchor_to_edges: bool,
|
||||||
|
margin: MarginConfig,
|
||||||
|
) {
|
||||||
|
gtk_layer_shell::init_for_window(win);
|
||||||
|
gtk_layer_shell::set_monitor(win, monitor);
|
||||||
|
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||||
|
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
||||||
|
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
|
||||||
|
|
||||||
window: ApplicationWindow,
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
|
||||||
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
|
||||||
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
|
||||||
|
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
|
||||||
|
|
||||||
content: gtk::Box,
|
let bar_orientation = position.get_orientation();
|
||||||
|
|
||||||
start: gtk::Box,
|
gtk_layer_shell::set_anchor(
|
||||||
center: gtk::Box,
|
win,
|
||||||
end: gtk::Box,
|
gtk_layer_shell::Edge::Top,
|
||||||
|
position == BarPosition::Top
|
||||||
inner: Inner,
|
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||||
}
|
);
|
||||||
|
gtk_layer_shell::set_anchor(
|
||||||
impl Bar {
|
win,
|
||||||
pub fn new(app: &Application, monitor_name: String, config: Config) -> Self {
|
gtk_layer_shell::Edge::Bottom,
|
||||||
let window = ApplicationWindow::builder()
|
position == BarPosition::Bottom
|
||||||
.application(app)
|
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||||
.type_(WindowType::Toplevel)
|
);
|
||||||
.build();
|
gtk_layer_shell::set_anchor(
|
||||||
|
win,
|
||||||
let name = config
|
gtk_layer_shell::Edge::Left,
|
||||||
.name
|
position == BarPosition::Left
|
||||||
.clone()
|
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||||
.unwrap_or_else(|| format!("bar-{}", Ironbar::unique_id()));
|
);
|
||||||
|
gtk_layer_shell::set_anchor(
|
||||||
window.set_widget_name(&name);
|
win,
|
||||||
|
gtk_layer_shell::Edge::Right,
|
||||||
let position = config.position;
|
position == BarPosition::Right
|
||||||
let orientation = position.get_orientation();
|
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||||
|
);
|
||||||
let content = gtk::Box::builder()
|
|
||||||
.orientation(orientation)
|
|
||||||
.spacing(0)
|
|
||||||
.hexpand(false)
|
|
||||||
.name("bar");
|
|
||||||
|
|
||||||
let content = if orientation == Orientation::Horizontal {
|
|
||||||
content.height_request(config.height)
|
|
||||||
} else {
|
|
||||||
content.width_request(config.height)
|
|
||||||
}
|
|
||||||
.build();
|
|
||||||
|
|
||||||
content.style_context().add_class("container");
|
|
||||||
|
|
||||||
let start = create_container("start", orientation);
|
|
||||||
let center = create_container("center", orientation);
|
|
||||||
let end = create_container("end", orientation);
|
|
||||||
|
|
||||||
content.add(&start);
|
|
||||||
content.set_center_widget(Some(¢er));
|
|
||||||
content.pack_end(&end, false, false, 0);
|
|
||||||
|
|
||||||
window.add(&content);
|
|
||||||
|
|
||||||
window.connect_destroy_event(|_, _| {
|
|
||||||
info!("Shutting down");
|
|
||||||
gtk::main_quit();
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
|
|
||||||
Bar {
|
|
||||||
name,
|
|
||||||
monitor_name,
|
|
||||||
position,
|
|
||||||
window,
|
|
||||||
content,
|
|
||||||
start,
|
|
||||||
center,
|
|
||||||
end,
|
|
||||||
inner: Inner::New {
|
|
||||||
config: Some(config),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(mut self, monitor: &Monitor) -> Result<Self> {
|
|
||||||
let Inner::New { ref mut config } = self.inner else {
|
|
||||||
return Ok(self);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(config) = config.take() else {
|
|
||||||
return Ok(self);
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Initializing bar '{}' on '{}'",
|
|
||||||
self.name, self.monitor_name
|
|
||||||
);
|
|
||||||
|
|
||||||
self.setup_layer_shell(
|
|
||||||
&self.window,
|
|
||||||
true,
|
|
||||||
config.anchor_to_edges,
|
|
||||||
config.margin,
|
|
||||||
monitor,
|
|
||||||
);
|
|
||||||
|
|
||||||
let start_hidden = config.start_hidden.unwrap_or(config.autohide.is_some());
|
|
||||||
|
|
||||||
if let Some(autohide) = config.autohide {
|
|
||||||
let hotspot_window = Window::new(WindowType::Toplevel);
|
|
||||||
|
|
||||||
Self::setup_autohide(&self.window, &hotspot_window, autohide);
|
|
||||||
self.setup_layer_shell(
|
|
||||||
&hotspot_window,
|
|
||||||
false,
|
|
||||||
config.anchor_to_edges,
|
|
||||||
config.margin,
|
|
||||||
monitor,
|
|
||||||
);
|
|
||||||
|
|
||||||
if start_hidden {
|
|
||||||
hotspot_window.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let load_result = self.load_modules(config, monitor)?;
|
|
||||||
|
|
||||||
self.show(!start_hidden);
|
|
||||||
|
|
||||||
self.inner = Inner::Loaded {
|
|
||||||
popup: load_result.popup,
|
|
||||||
};
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets up GTK layer shell for a provided application window.
|
|
||||||
fn setup_layer_shell(
|
|
||||||
&self,
|
|
||||||
win: &impl IsA<Window>,
|
|
||||||
exclusive_zone: bool,
|
|
||||||
anchor_to_edges: bool,
|
|
||||||
margin: MarginConfig,
|
|
||||||
monitor: &Monitor,
|
|
||||||
) {
|
|
||||||
let position = self.position;
|
|
||||||
|
|
||||||
win.init_layer_shell();
|
|
||||||
win.set_monitor(monitor);
|
|
||||||
win.set_layer(gtk_layer_shell::Layer::Top);
|
|
||||||
win.set_namespace(env!("CARGO_PKG_NAME"));
|
|
||||||
|
|
||||||
if exclusive_zone {
|
|
||||||
win.auto_exclusive_zone_enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
win.set_layer_shell_margin(gtk_layer_shell::Edge::Top, margin.top);
|
|
||||||
win.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, margin.bottom);
|
|
||||||
win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left);
|
|
||||||
win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right);
|
|
||||||
|
|
||||||
let bar_orientation = position.get_orientation();
|
|
||||||
|
|
||||||
win.set_anchor(
|
|
||||||
gtk_layer_shell::Edge::Top,
|
|
||||||
position == BarPosition::Top
|
|
||||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
|
||||||
);
|
|
||||||
win.set_anchor(
|
|
||||||
gtk_layer_shell::Edge::Bottom,
|
|
||||||
position == BarPosition::Bottom
|
|
||||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
|
||||||
);
|
|
||||||
win.set_anchor(
|
|
||||||
gtk_layer_shell::Edge::Left,
|
|
||||||
position == BarPosition::Left
|
|
||||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
|
||||||
);
|
|
||||||
win.set_anchor(
|
|
||||||
gtk_layer_shell::Edge::Right,
|
|
||||||
position == BarPosition::Right
|
|
||||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_autohide(window: &ApplicationWindow, hotspot_window: &Window, timeout: u64) {
|
|
||||||
hotspot_window.hide();
|
|
||||||
|
|
||||||
hotspot_window.set_opacity(0.0);
|
|
||||||
hotspot_window.set_decorated(false);
|
|
||||||
hotspot_window.set_size_request(0, 1);
|
|
||||||
|
|
||||||
{
|
|
||||||
let hotspot_window = hotspot_window.clone();
|
|
||||||
|
|
||||||
window.connect_leave_notify_event(move |win, _| {
|
|
||||||
let win = win.clone();
|
|
||||||
let hotspot_window = hotspot_window.clone();
|
|
||||||
|
|
||||||
glib::timeout_add_local_once(Duration::from_millis(timeout), move || {
|
|
||||||
win.hide();
|
|
||||||
hotspot_window.show();
|
|
||||||
});
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let win = window.clone();
|
|
||||||
|
|
||||||
hotspot_window.connect_enter_notify_event(move |hotspot_win, _| {
|
|
||||||
hotspot_win.hide();
|
|
||||||
win.show();
|
|
||||||
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the configured modules onto a bar.
|
|
||||||
fn load_modules(&self, config: Config, monitor: &Monitor) -> Result<BarLoadResult> {
|
|
||||||
let icon_theme = IconTheme::new();
|
|
||||||
if let Some(ref theme) = config.icon_theme {
|
|
||||||
icon_theme.set_custom_theme(Some(theme));
|
|
||||||
}
|
|
||||||
|
|
||||||
let app = &self.window.application().expect("to exist");
|
|
||||||
|
|
||||||
macro_rules! info {
|
|
||||||
($location:expr) => {
|
|
||||||
ModuleInfo {
|
|
||||||
app,
|
|
||||||
bar_position: config.position,
|
|
||||||
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 = Rc::new(RefCell::new(popup));
|
|
||||||
|
|
||||||
if let Some(modules) = config.start {
|
|
||||||
let info = info!(ModuleLocation::Left);
|
|
||||||
add_modules(&self.start, modules, &info, &popup)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(modules) = config.center {
|
|
||||||
let info = info!(ModuleLocation::Center);
|
|
||||||
add_modules(&self.center, modules, &info, &popup)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(modules) = config.end {
|
|
||||||
let info = info!(ModuleLocation::Right);
|
|
||||||
add_modules(&self.end, modules, &info, &popup)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = BarLoadResult { popup };
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show(&self, include_window: bool) {
|
|
||||||
debug!("Showing bar: {}", self.name);
|
|
||||||
|
|
||||||
// show each box but do not use `show_all`.
|
|
||||||
// this ensures `show_if` option works as intended.
|
|
||||||
self.start.show();
|
|
||||||
self.center.show();
|
|
||||||
self.end.show();
|
|
||||||
self.content.show();
|
|
||||||
|
|
||||||
if include_window {
|
|
||||||
self.window.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn popup(&self) -> Rc<RefCell<Popup>> {
|
|
||||||
match &self.inner {
|
|
||||||
Inner::New { .. } => {
|
|
||||||
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
|
|
||||||
}
|
|
||||||
Inner::Loaded { popup } => popup.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `gtk::Box` container to place widgets inside.
|
/// Creates a `gtk::Box` container to place widgets inside.
|
||||||
@@ -322,9 +134,50 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
|
|||||||
container
|
container
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Loads the configured modules onto a bar.
|
||||||
struct BarLoadResult {
|
fn load_modules(
|
||||||
popup: Rc<RefCell<Popup>>,
|
left: >k::Box,
|
||||||
|
center: >k::Box,
|
||||||
|
right: >k::Box,
|
||||||
|
app: &Application,
|
||||||
|
config: Config,
|
||||||
|
monitor: &Monitor,
|
||||||
|
output_name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let icon_theme = IconTheme::new();
|
||||||
|
if let Some(ref theme) = config.icon_theme {
|
||||||
|
icon_theme.set_custom_theme(Some(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! info {
|
||||||
|
($location:expr) => {
|
||||||
|
ModuleInfo {
|
||||||
|
app,
|
||||||
|
bar_position: config.position,
|
||||||
|
monitor,
|
||||||
|
output_name,
|
||||||
|
location: $location,
|
||||||
|
icon_theme: &icon_theme,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(modules) = config.start {
|
||||||
|
let info = info!(ModuleLocation::Left);
|
||||||
|
add_modules(left, modules, &info, config.popup_gap)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(modules) = config.center {
|
||||||
|
let info = info!(ModuleLocation::Center);
|
||||||
|
add_modules(center, modules, &info, config.popup_gap)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(modules) = config.end {
|
||||||
|
let info = info!(ModuleLocation::Right);
|
||||||
|
add_modules(right, modules, &info, config.popup_gap)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds modules into a provided GTK box,
|
/// Adds modules into a provided GTK box,
|
||||||
@@ -333,29 +186,23 @@ fn add_modules(
|
|||||||
content: >k::Box,
|
content: >k::Box,
|
||||||
modules: Vec<ModuleConfig>,
|
modules: Vec<ModuleConfig>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
popup: &Rc<RefCell<Popup>>,
|
popup_gap: i32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let popup = Popup::new(info, popup_gap);
|
||||||
|
let popup = Arc::new(RwLock::new(popup));
|
||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
macro_rules! add_module {
|
macro_rules! add_module {
|
||||||
($module:expr, $id:expr) => {{
|
($module:expr, $id:expr) => {{
|
||||||
let common = $module.common.take().expect("common config to exist");
|
let common = $module.common.take().expect("Common config did not exist");
|
||||||
let widget_parts = create_module(
|
let widget = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
|
||||||
*$module,
|
let container = wrap_widget(&widget, common, orientation);
|
||||||
$id,
|
content.append(&container);
|
||||||
common.name.clone(),
|
|
||||||
&info,
|
|
||||||
&Rc::clone(&popup),
|
|
||||||
)?;
|
|
||||||
set_widget_identifiers(&widget_parts, &common);
|
|
||||||
|
|
||||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
|
||||||
content.add(&container);
|
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
for config in modules {
|
for (id, config) in modules.into_iter().enumerate() {
|
||||||
let id = Ironbar::unique_id();
|
|
||||||
match config {
|
match config {
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||||
@@ -381,13 +228,3 @@ fn add_modules(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_bar(
|
|
||||||
app: &Application,
|
|
||||||
monitor: &Monitor,
|
|
||||||
monitor_name: String,
|
|
||||||
config: Config,
|
|
||||||
) -> Result<Bar> {
|
|
||||||
let bar = Bar::new(app, monitor_name, config);
|
|
||||||
bar.init(monitor)
|
|
||||||
}
|
|
||||||
|
|||||||
44
src/bridge_channel.rs
Normal file
44
src/bridge_channel.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use crate::send;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
/// MPSC async -> sync channel.
|
||||||
|
/// The sender uses `tokio::sync::mpsc`
|
||||||
|
/// while the receiver uses `glib::MainContext::channel`.
|
||||||
|
///
|
||||||
|
/// This makes it possible to send events asynchronously
|
||||||
|
/// and receive them on the main thread,
|
||||||
|
/// allowing UI updates to be handled on the receiving end.
|
||||||
|
pub struct BridgeChannel<T> {
|
||||||
|
async_tx: mpsc::Sender<T>,
|
||||||
|
sync_rx: glib::Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> BridgeChannel<T> {
|
||||||
|
/// Creates a new channel
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (async_tx, mut async_rx) = mpsc::channel(32);
|
||||||
|
let (sync_tx, sync_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(val) = async_rx.recv().await {
|
||||||
|
send!(sync_tx, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { async_tx, sync_rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a clone of the sender.
|
||||||
|
pub fn create_sender(&self) -> mpsc::Sender<T> {
|
||||||
|
self.async_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a callback to the receiver.
|
||||||
|
pub fn recv<F>(self, f: F) -> glib::SourceId
|
||||||
|
where
|
||||||
|
F: FnMut(T) -> glib::Continue + 'static,
|
||||||
|
{
|
||||||
|
self.sync_rx.attach(None, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/cli.rs
19
src/cli.rs
@@ -1,19 +0,0 @@
|
|||||||
use crate::ipc::commands::Command;
|
|
||||||
use crate::ipc::responses::Response;
|
|
||||||
use clap::Parser;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Serialize, Deserialize)]
|
|
||||||
#[command(version)]
|
|
||||||
pub struct Args {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: Option<Command>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_response(response: Response) {
|
|
||||||
match response {
|
|
||||||
Response::Ok => println!("ok"),
|
|
||||||
Response::OkValue { value } => println!("ok\n{value}"),
|
|
||||||
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
use super::wayland::{self, ClipboardItem};
|
use super::wayland::{self, ClipboardItem};
|
||||||
use crate::{arc_mut, lock, spawn, try_send};
|
use crate::{lock, try_send};
|
||||||
use indexmap::map::Iter;
|
use indexmap::map::Iter;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, trace};
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ClipboardEvent {
|
pub enum ClipboardEvent {
|
||||||
@@ -25,33 +26,20 @@ pub struct ClipboardClient {
|
|||||||
|
|
||||||
impl ClipboardClient {
|
impl ClipboardClient {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
trace!("Initializing clipboard client");
|
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
|
||||||
|
|
||||||
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
|
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
|
||||||
|
|
||||||
let cache = arc_mut!(ClipboardCache::new());
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let senders = senders.clone();
|
let senders = senders.clone();
|
||||||
let cache = cache.clone();
|
let cache = cache.clone();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let (mut rx, item) = {
|
let mut rx = {
|
||||||
let wl = wayland::get_client();
|
let wl = wayland::get_client().await;
|
||||||
let wl = lock!(wl);
|
|
||||||
wl.subscribe_clipboard()
|
wl.subscribe_clipboard()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = item {
|
|
||||||
let senders = lock!(senders);
|
|
||||||
let iter = senders.iter();
|
|
||||||
for (tx, _) in iter {
|
|
||||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
lock!(cache).insert(item, senders.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Ok(item) = rx.recv().await {
|
while let Ok(item) = rx.recv().await {
|
||||||
debug!("Received clipboard item (ID: {})", item.id);
|
debug!("Received clipboard item (ID: {})", item.id);
|
||||||
|
|
||||||
@@ -71,6 +59,7 @@ impl ClipboardClient {
|
|||||||
let iter = senders.iter();
|
let iter = senders.iter();
|
||||||
for (tx, sender_cache_size) in iter {
|
for (tx, sender_cache_size) in iter {
|
||||||
if cache_size == *sender_cache_size {
|
if cache_size == *sender_cache_size {
|
||||||
|
// let mut cache = lock!(cache);
|
||||||
let removed_id = lock!(cache)
|
let removed_id = lock!(cache)
|
||||||
.remove_ref_first()
|
.remove_ref_first()
|
||||||
.expect("Clipboard cache unexpectedly empty");
|
.expect("Clipboard cache unexpectedly empty");
|
||||||
@@ -94,11 +83,18 @@ impl ClipboardClient {
|
|||||||
Self { senders, cache }
|
Self { senders, cache }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
||||||
let (tx, rx) = mpsc::channel(16);
|
let (tx, rx) = mpsc::channel(16);
|
||||||
|
|
||||||
|
let wl = wayland::get_client().await;
|
||||||
|
wl.roundtrip();
|
||||||
|
|
||||||
{
|
{
|
||||||
let cache = lock!(self.cache);
|
let mut cache = lock!(self.cache);
|
||||||
|
|
||||||
|
if let Some(item) = wl.get_clipboard() {
|
||||||
|
cache.insert_or_inc_ref(item);
|
||||||
|
}
|
||||||
|
|
||||||
let iter = cache.iter();
|
let iter = cache.iter();
|
||||||
for (_, (item, _)) in iter {
|
for (_, (item, _)) in iter {
|
||||||
@@ -106,12 +102,15 @@ impl ClipboardClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock!(self.senders).push((tx, cache_size));
|
{
|
||||||
|
let mut senders = lock!(self.senders);
|
||||||
|
senders.push((tx, cache_size));
|
||||||
|
}
|
||||||
|
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy(&self, id: usize) {
|
pub async fn copy(&self, id: usize) {
|
||||||
debug!("Copying item with id {id}");
|
debug!("Copying item with id {id}");
|
||||||
|
|
||||||
let item = {
|
let item = {
|
||||||
@@ -120,8 +119,7 @@ impl ClipboardClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
let wl = wayland::get_client();
|
let wl = wayland::get_client().await;
|
||||||
let wl = lock!(wl);
|
|
||||||
wl.copy_to_clipboard(item);
|
wl.copy_to_clipboard(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +171,13 @@ impl ClipboardCache {
|
|||||||
.map(|(item, _)| item)
|
.map(|(item, _)| item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts an entry with `ref_count` initial references,
|
||||||
|
/// or increments the `ref_count` by 1 if it already exists.
|
||||||
|
fn insert_or_inc_ref(&mut self, item: Arc<ClipboardItem>) {
|
||||||
|
let mut item = self.cache.entry(item.id).or_insert((item, 0));
|
||||||
|
item.1 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes the entry with key `id`.
|
/// Removes the entry with key `id`.
|
||||||
/// This ignores references.
|
/// This ignores references.
|
||||||
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
|
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
use crate::{lock, send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||||
use hyprland::event_listener::EventListener;
|
use hyprland::event_listener::EventListenerMutable as EventListener;
|
||||||
use hyprland::prelude::*;
|
use hyprland::prelude::*;
|
||||||
use hyprland::shared::{HyprDataVec, WorkspaceType};
|
use hyprland::shared::WorkspaceType;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
pub struct EventClient {
|
pub struct EventClient {
|
||||||
@@ -34,25 +36,28 @@ impl EventClient {
|
|||||||
let mut event_listener = EventListener::new();
|
let mut event_listener = EventListener::new();
|
||||||
|
|
||||||
// we need a lock to ensure events don't run at the same time
|
// we need a lock to ensure events don't run at the same time
|
||||||
let lock = arc_mut!(());
|
let lock = Arc::new(Mutex::new(()));
|
||||||
|
|
||||||
// cache the active workspace since Hyprland doesn't give us the prev active
|
// 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 = Self::get_active_workspace().expect("Failed to get active workspace");
|
||||||
let active = arc_mut!(Some(active));
|
let active = Arc::new(Mutex::new(Some(active)));
|
||||||
|
|
||||||
{
|
{
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let lock = lock.clone();
|
let lock = lock.clone();
|
||||||
let active = active.clone();
|
let active = active.clone();
|
||||||
|
|
||||||
event_listener.add_workspace_added_handler(move |workspace_type| {
|
event_listener.add_workspace_added_handler(move |workspace_type, _state| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
debug!("Added workspace: {workspace_type:?}");
|
debug!("Added workspace: {workspace_type:?}");
|
||||||
|
|
||||||
let workspace_name = get_workspace_name(workspace_type);
|
let workspace_name = get_workspace_name(workspace_type);
|
||||||
let prev_workspace = lock!(active);
|
let prev_workspace = lock!(active);
|
||||||
|
let focused = prev_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |w| w.name == workspace_name);
|
||||||
|
|
||||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||||
|
|
||||||
if let Some(workspace) = workspace {
|
if let Some(workspace) = workspace {
|
||||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||||
@@ -65,7 +70,7 @@ impl EventClient {
|
|||||||
let lock = lock.clone();
|
let lock = lock.clone();
|
||||||
let active = active.clone();
|
let active = active.clone();
|
||||||
|
|
||||||
event_listener.add_workspace_change_handler(move |workspace_type| {
|
event_listener.add_workspace_change_handler(move |workspace_type, _state| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
|
|
||||||
let mut prev_workspace = lock!(active);
|
let mut prev_workspace = lock!(active);
|
||||||
@@ -76,7 +81,10 @@ impl EventClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let workspace_name = get_workspace_name(workspace_type);
|
let workspace_name = get_workspace_name(workspace_type);
|
||||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
let focused = prev_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |w| w.name == workspace_name);
|
||||||
|
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||||
|
|
||||||
workspace.map_or_else(
|
workspace.map_or_else(
|
||||||
|| {
|
|| {
|
||||||
@@ -84,7 +92,8 @@ impl EventClient {
|
|||||||
},
|
},
|
||||||
|workspace| {
|
|workspace| {
|
||||||
// there may be another type of update so dispatch that regardless of focus change
|
// there may be another type of update so dispatch that regardless of focus change
|
||||||
if !workspace.visibility.is_focused() {
|
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
|
||||||
|
if !focused {
|
||||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,9 +106,9 @@ impl EventClient {
|
|||||||
let lock = lock.clone();
|
let lock = lock.clone();
|
||||||
let active = active.clone();
|
let active = active.clone();
|
||||||
|
|
||||||
event_listener.add_active_monitor_change_handler(move |event_data| {
|
event_listener.add_active_monitor_change_handler(move |event_data, _state| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
let workspace_type = event_data.workspace;
|
let workspace_type = event_data.1;
|
||||||
|
|
||||||
let mut prev_workspace = lock!(active);
|
let mut prev_workspace = lock!(active);
|
||||||
|
|
||||||
@@ -109,11 +118,12 @@ impl EventClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let workspace_name = get_workspace_name(workspace_type);
|
let workspace_name = get_workspace_name(workspace_type);
|
||||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
let focused = prev_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |w| w.name == workspace_name);
|
||||||
|
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||||
|
|
||||||
if let Some((false, workspace)) =
|
if let (Some(workspace), false) = (workspace, focused) {
|
||||||
workspace.map(|w| (w.visibility.is_focused(), w))
|
|
||||||
{
|
|
||||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to locate workspace");
|
error!("Unable to locate workspace");
|
||||||
@@ -125,20 +135,23 @@ impl EventClient {
|
|||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let lock = lock.clone();
|
let lock = lock.clone();
|
||||||
|
|
||||||
event_listener.add_workspace_moved_handler(move |event_data| {
|
event_listener.add_workspace_moved_handler(move |event_data, _state| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
let workspace_type = event_data.workspace;
|
let workspace_type = event_data.1;
|
||||||
debug!("Received workspace move: {workspace_type:?}");
|
debug!("Received workspace move: {workspace_type:?}");
|
||||||
|
|
||||||
let mut prev_workspace = lock!(active);
|
let mut prev_workspace = lock!(active);
|
||||||
|
|
||||||
let workspace_name = get_workspace_name(workspace_type);
|
let workspace_name = get_workspace_name(workspace_type);
|
||||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
let focused = prev_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |w| w.name == workspace_name);
|
||||||
|
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||||
|
|
||||||
if let Some(workspace) = workspace {
|
if let Some(workspace) = workspace {
|
||||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||||
|
|
||||||
if !workspace.visibility.is_focused() {
|
if !focused {
|
||||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +159,7 @@ impl EventClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
event_listener.add_workspace_destroy_handler(move |workspace_type| {
|
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
debug!("Received workspace destroy: {workspace_type:?}");
|
debug!("Received workspace destroy: {workspace_type:?}");
|
||||||
|
|
||||||
@@ -168,28 +181,32 @@ impl EventClient {
|
|||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
tx: &Sender<WorkspaceUpdate>,
|
tx: &Sender<WorkspaceUpdate>,
|
||||||
) {
|
) {
|
||||||
|
let old = prev_workspace
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.name.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
send!(
|
send!(
|
||||||
tx,
|
tx,
|
||||||
WorkspaceUpdate::Focus {
|
WorkspaceUpdate::Focus {
|
||||||
old: prev_workspace.take(),
|
old,
|
||||||
new: workspace.clone(),
|
new: workspace.name.clone(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
prev_workspace.replace(workspace);
|
prev_workspace.replace(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a workspace by name from the server, given the active workspace if known.
|
/// Gets a workspace by name from the server.
|
||||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
|
///
|
||||||
|
/// Use `focused` to manually mark the workspace as focused,
|
||||||
|
/// as this is not automatically checked.
|
||||||
|
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
|
||||||
Workspaces::get()
|
Workspaces::get()
|
||||||
.expect("Failed to get workspaces")
|
.expect("Failed to get workspaces")
|
||||||
.find_map(|w| {
|
.find_map(|w| {
|
||||||
if w.name == name {
|
if w.name == name {
|
||||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
Some(Workspace::from((focused, w)))
|
||||||
create_is_visible()(w)
|
|
||||||
}));
|
|
||||||
|
|
||||||
Some(Workspace::from((vis, w)))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -198,19 +215,16 @@ impl EventClient {
|
|||||||
|
|
||||||
/// Gets the active workspace from the server.
|
/// Gets the active workspace from the server.
|
||||||
fn get_active_workspace() -> Result<Workspace> {
|
fn get_active_workspace() -> Result<Workspace> {
|
||||||
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?;
|
let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
|
||||||
Ok(w)
|
Ok(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceClient for EventClient {
|
impl WorkspaceClient for EventClient {
|
||||||
fn focus(&self, id: String) -> Result<()> {
|
fn focus(&self, id: String) -> Result<()> {
|
||||||
let identifier = match id.parse::<i32>() {
|
Dispatch::call(DispatchType::Workspace(
|
||||||
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
|
WorkspaceIdentifierWithSpecial::Name(&id),
|
||||||
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
|
))?;
|
||||||
};
|
|
||||||
|
|
||||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,16 +234,13 @@ impl WorkspaceClient for EventClient {
|
|||||||
{
|
{
|
||||||
let tx = self.workspace_tx.clone();
|
let tx = self.workspace_tx.clone();
|
||||||
|
|
||||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
let active_name = HWorkspace::get_active()
|
||||||
let is_visible = create_is_visible();
|
.map(|active| active.name)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let workspaces = Workspaces::get()
|
let workspaces = Workspaces::get()
|
||||||
.expect("Failed to get workspaces")
|
.expect("Failed to get workspaces")
|
||||||
.map(|w| {
|
.map(|w| Workspace::from((w.name == active_name, w)))
|
||||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
|
||||||
|
|
||||||
Workspace::from((vis, w))
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
send!(tx, WorkspaceUpdate::Init(workspaces));
|
send!(tx, WorkspaceUpdate::Init(workspaces));
|
||||||
@@ -258,39 +269,13 @@ fn get_workspace_name(name: WorkspaceType) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a function which determines if a workspace is visible.
|
impl From<(bool, hyprland::data::Workspace)> for Workspace {
|
||||||
///
|
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
|
||||||
/// This function makes a Hyprland call that allocates so it should be cached when possible,
|
|
||||||
/// but it is only valid so long as workspaces do not change so it should not be stored long term
|
|
||||||
fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
|
|
||||||
let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), HyprDataVec::to_vec);
|
|
||||||
|
|
||||||
move |w| monitors.iter().any(|m| m.active_workspace.id == w.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(Visibility, HWorkspace)> for Workspace {
|
|
||||||
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: workspace.id.to_string(),
|
id: workspace.id.to_string(),
|
||||||
name: workspace.name,
|
name: workspace.name,
|
||||||
monitor: workspace.monitor,
|
monitor: workspace.monitor,
|
||||||
visibility,
|
focused,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility
|
|
||||||
where
|
|
||||||
F: FnOnce(&'f HWorkspace) -> bool,
|
|
||||||
'a: 'f,
|
|
||||||
{
|
|
||||||
fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self {
|
|
||||||
if Some(workspace.name.as_str()) == active_name {
|
|
||||||
Self::focused()
|
|
||||||
} else if is_visible(workspace) {
|
|
||||||
Self::visible()
|
|
||||||
} else {
|
|
||||||
Self::Hidden
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,38 +75,8 @@ pub struct Workspace {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
/// Name of the monitor (output) the workspace is located on
|
/// Name of the monitor (output) the workspace is located on
|
||||||
pub monitor: String,
|
pub monitor: String,
|
||||||
/// How visible the workspace is
|
/// Whether the workspace is in focus
|
||||||
pub visibility: Visibility,
|
pub focused: bool,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum Visibility {
|
|
||||||
Visible(bool),
|
|
||||||
Hidden,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Visibility {
|
|
||||||
pub fn visible() -> Self {
|
|
||||||
Self::Visible(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focused() -> Self {
|
|
||||||
Self::Visible(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_visible(self) -> bool {
|
|
||||||
matches!(self, Self::Visible(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_focused(self) -> bool {
|
|
||||||
if let Self::Visible(focused) = self {
|
|
||||||
focused
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -116,17 +86,13 @@ pub enum WorkspaceUpdate {
|
|||||||
Init(Vec<Workspace>),
|
Init(Vec<Workspace>),
|
||||||
Add(Workspace),
|
Add(Workspace),
|
||||||
Remove(String),
|
Remove(String),
|
||||||
|
Update(Workspace),
|
||||||
Move(Workspace),
|
Move(Workspace),
|
||||||
/// Declares focus moved from the old workspace to the new.
|
/// Declares focus moved from the old workspace to the new.
|
||||||
Focus {
|
Focus {
|
||||||
old: Option<Workspace>,
|
old: String,
|
||||||
new: Workspace,
|
new: String,
|
||||||
},
|
},
|
||||||
/// An update was triggered by the compositor but this was not mapped by Ironbar.
|
|
||||||
///
|
|
||||||
/// This is purely used for ergonomics within the compositor clients
|
|
||||||
/// and should be ignored by consumers.
|
|
||||||
Unknown,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WorkspaceClient {
|
pub trait WorkspaceClient {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||||
use crate::{await_sync, send, spawn};
|
use crate::{await_sync, send};
|
||||||
use async_once::AsyncOnce;
|
use async_once::AsyncOnce;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
|
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
|
||||||
|
use tokio::spawn;
|
||||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
@@ -31,11 +32,8 @@ impl SwayEventClient {
|
|||||||
|
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
trace!("event: {:?}", event);
|
trace!("event: {:?}", event);
|
||||||
if let Event::Workspace(event) = event? {
|
if let Event::Workspace(ev) = event? {
|
||||||
let event = WorkspaceUpdate::from(*event);
|
workspace_tx.send(WorkspaceUpdate::from(*ev))?;
|
||||||
if !matches!(event, WorkspaceUpdate::Unknown) {
|
|
||||||
workspace_tx.send(event)?;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,50 +105,22 @@ pub fn get_sub_client() -> &'static SwayEventClient {
|
|||||||
|
|
||||||
impl From<Node> for Workspace {
|
impl From<Node> for Workspace {
|
||||||
fn from(node: Node) -> Self {
|
fn from(node: Node) -> Self {
|
||||||
let visibility = Visibility::from(&node);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: node.id.to_string(),
|
id: node.id.to_string(),
|
||||||
name: node.name.unwrap_or_default(),
|
name: node.name.unwrap_or_default(),
|
||||||
monitor: node.output.unwrap_or_default(),
|
monitor: node.output.unwrap_or_default(),
|
||||||
visibility,
|
focused: node.focused,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<swayipc_async::Workspace> for Workspace {
|
impl From<swayipc_async::Workspace> for Workspace {
|
||||||
fn from(workspace: swayipc_async::Workspace) -> Self {
|
fn from(workspace: swayipc_async::Workspace) -> Self {
|
||||||
let visibility = Visibility::from(&workspace);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: workspace.id.to_string(),
|
id: workspace.id.to_string(),
|
||||||
name: workspace.name,
|
name: workspace.name,
|
||||||
monitor: workspace.output,
|
monitor: workspace.output,
|
||||||
visibility,
|
focused: workspace.focused,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Node> for Visibility {
|
|
||||||
fn from(node: &Node) -> Self {
|
|
||||||
if node.focused {
|
|
||||||
Self::focused()
|
|
||||||
} else if node.visible.unwrap_or(false) {
|
|
||||||
Self::visible()
|
|
||||||
} else {
|
|
||||||
Self::Hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&swayipc_async::Workspace> for Visibility {
|
|
||||||
fn from(workspace: &swayipc_async::Workspace) -> Self {
|
|
||||||
if workspace.focused {
|
|
||||||
Self::focused()
|
|
||||||
} else if workspace.visible {
|
|
||||||
Self::visible()
|
|
||||||
} else {
|
|
||||||
Self::Hidden
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,13 +139,21 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
WorkspaceChange::Focus => Self::Focus {
|
WorkspaceChange::Focus => Self::Focus {
|
||||||
old: event.old.map(Workspace::from),
|
old: event
|
||||||
new: Workspace::from(event.current.expect("Missing current workspace")),
|
.old
|
||||||
|
.expect("Missing old workspace")
|
||||||
|
.name
|
||||||
|
.unwrap_or_default(),
|
||||||
|
new: event
|
||||||
|
.current
|
||||||
|
.expect("Missing current workspace")
|
||||||
|
.name
|
||||||
|
.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
WorkspaceChange::Move => {
|
WorkspaceChange::Move => {
|
||||||
Self::Move(event.current.expect("Missing current workspace").into())
|
Self::Move(event.current.expect("Missing current workspace").into())
|
||||||
}
|
}
|
||||||
_ => Self::Unknown,
|
_ => Self::Update(event.current.expect("Missing current workspace").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,9 @@ pub mod mpd;
|
|||||||
#[cfg(feature = "music+mpris")]
|
#[cfg(feature = "music+mpris")]
|
||||||
pub mod mpris;
|
pub mod mpris;
|
||||||
|
|
||||||
pub const TICK_INTERVAL_MS: u64 = 200;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PlayerUpdate {
|
pub enum PlayerUpdate {
|
||||||
/// Triggered when the track or player state notably changes,
|
|
||||||
/// such as a new track playing, the player being paused, or a volume change.
|
|
||||||
Update(Box<Option<Track>>, Status),
|
Update(Box<Option<Track>>, Status),
|
||||||
/// Triggered at regular intervals while a track is playing.
|
|
||||||
/// Used to keep track of the progress through the current track.
|
|
||||||
ProgressTick(ProgressTick),
|
|
||||||
/// Triggered when the client disconnects from the player.
|
|
||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,25 +27,21 @@ pub struct Track {
|
|||||||
pub cover_path: Option<String>,
|
pub cover_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PlayerState {
|
pub enum PlayerState {
|
||||||
Playing,
|
Playing,
|
||||||
Paused,
|
Paused,
|
||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub state: PlayerState,
|
pub state: PlayerState,
|
||||||
pub volume_percent: Option<u8>,
|
pub volume_percent: u8,
|
||||||
pub playlist_position: u32,
|
|
||||||
pub playlist_length: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct ProgressTick {
|
|
||||||
pub duration: Option<Duration>,
|
pub duration: Option<Duration>,
|
||||||
pub elapsed: Option<Duration>,
|
pub elapsed: Option<Duration>,
|
||||||
|
pub playlist_position: u32,
|
||||||
|
pub playlist_length: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MusicClient {
|
pub trait MusicClient {
|
||||||
@@ -63,7 +51,6 @@ pub trait MusicClient {
|
|||||||
fn prev(&self) -> Result<()>;
|
fn prev(&self) -> Result<()>;
|
||||||
|
|
||||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||||
fn seek(&self, duration: Duration) -> Result<()>;
|
|
||||||
|
|
||||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use super::{
|
use super::{MusicClient, Status, Track};
|
||||||
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
use crate::await_sync;
|
||||||
};
|
use crate::clients::music::{PlayerState, PlayerUpdate};
|
||||||
use crate::{await_sync, send, spawn};
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||||
use mpd_client::commands::SeekMode;
|
|
||||||
use mpd_client::protocol::MpdProtocolError;
|
use mpd_client::protocol::MpdProtocolError;
|
||||||
use mpd_client::responses::{PlayState, Song};
|
use mpd_client::responses::{PlayState, Song};
|
||||||
use mpd_client::tag::Tag;
|
use mpd_client::tag::Tag;
|
||||||
@@ -17,7 +15,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::{TcpStream, UnixStream};
|
use tokio::net::{TcpStream, UnixStream};
|
||||||
use tokio::sync::broadcast;
|
use tokio::spawn;
|
||||||
|
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
@@ -30,8 +29,8 @@ lazy_static! {
|
|||||||
pub struct MpdClient {
|
pub struct MpdClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
music_dir: PathBuf,
|
music_dir: PathBuf,
|
||||||
tx: broadcast::Sender<PlayerUpdate>,
|
tx: Sender<PlayerUpdate>,
|
||||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
_rx: Receiver<PlayerUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -58,7 +57,7 @@ impl MpdClient {
|
|||||||
let (client, mut state_changes) =
|
let (client, mut state_changes) =
|
||||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||||
|
|
||||||
let (tx, rx) = broadcast::channel(16);
|
let (tx, rx) = channel(16);
|
||||||
|
|
||||||
{
|
{
|
||||||
let music_dir = music_dir.clone();
|
let music_dir = music_dir.clone();
|
||||||
@@ -79,19 +78,7 @@ impl MpdClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
|
Ok::<(), SendError<(Option<Track>, Status)>>(())
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let client = client.clone();
|
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
loop {
|
|
||||||
Self::send_tick_update(&client, &tx).await;
|
|
||||||
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,9 +92,9 @@ impl MpdClient {
|
|||||||
|
|
||||||
async fn send_update(
|
async fn send_update(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
tx: &broadcast::Sender<PlayerUpdate>,
|
tx: &Sender<PlayerUpdate>,
|
||||||
music_dir: &Path,
|
music_dir: &Path,
|
||||||
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
) -> Result<(), SendError<PlayerUpdate>> {
|
||||||
let current_song = client.command(commands::CurrentSong).await;
|
let current_song = client.command(commands::CurrentSong).await;
|
||||||
let status = client.command(commands::Status).await;
|
let status = client.command(commands::Status).await;
|
||||||
|
|
||||||
@@ -115,33 +102,17 @@ impl MpdClient {
|
|||||||
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
||||||
let status = Status::from(status);
|
let status = Status::from(status);
|
||||||
|
|
||||||
let update = PlayerUpdate::Update(Box::new(track), status);
|
tx.send(PlayerUpdate::Update(Box::new(track), status))?;
|
||||||
send!(tx, update);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
|
|
||||||
let status = client.command(commands::Status).await;
|
|
||||||
|
|
||||||
if let Ok(status) = status {
|
|
||||||
if status.state == PlayState::Playing {
|
|
||||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
|
||||||
duration: status.duration,
|
|
||||||
elapsed: status.elapsed,
|
|
||||||
});
|
|
||||||
|
|
||||||
send!(tx, update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_connected(&self) -> bool {
|
fn is_connected(&self) -> bool {
|
||||||
!self.client.is_connection_closed()
|
!self.client.is_connection_closed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
fn send_disconnect_update(&self) -> Result<(), SendError<PlayerUpdate>> {
|
||||||
info!("Connection to MPD server lost");
|
info!("Connection to MPD server lost");
|
||||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -211,12 +182,7 @@ impl MusicClient for MpdClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seek(&self, duration: Duration) -> Result<()> {
|
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||||
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
|
||||||
let rx = self.tx.subscribe();
|
let rx = self.tx.subscribe();
|
||||||
await_sync(async {
|
await_sync(async {
|
||||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||||
@@ -325,7 +291,9 @@ impl From<mpd_client::responses::Status> for Status {
|
|||||||
fn from(status: mpd_client::responses::Status) -> Self {
|
fn from(status: mpd_client::responses::Status) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: PlayerState::from(status.state),
|
state: PlayerState::from(status.state),
|
||||||
volume_percent: Some(status.volume),
|
volume_percent: status.volume,
|
||||||
|
duration: status.duration,
|
||||||
|
elapsed: status.elapsed,
|
||||||
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
||||||
playlist_length: status.playlist_length as u32,
|
playlist_length: status.playlist_length as u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
|
use super::{MusicClient, PlayerUpdate, Status, Track};
|
||||||
use crate::clients::music::ProgressTick;
|
use crate::clients::music::PlayerState;
|
||||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
use crate::{lock, send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||||
@@ -8,8 +8,8 @@ use std::collections::HashSet;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{cmp, string};
|
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||||
use tokio::sync::broadcast;
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -18,18 +18,18 @@ lazy_static! {
|
|||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
current_player: Arc<Mutex<Option<String>>>,
|
current_player: Arc<Mutex<Option<String>>>,
|
||||||
tx: broadcast::Sender<PlayerUpdate>,
|
tx: Sender<PlayerUpdate>,
|
||||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
_rx: Receiver<PlayerUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let (tx, rx) = broadcast::channel(32);
|
let (tx, rx) = channel(32);
|
||||||
|
|
||||||
let current_player = arc_mut!(None);
|
let current_player = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
{
|
{
|
||||||
let players_list = arc_mut!(HashSet::new());
|
let players_list = Arc::new(Mutex::new(HashSet::new()));
|
||||||
let current_player = current_player.clone();
|
let current_player = current_player.clone();
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
|
|
||||||
@@ -83,20 +83,6 @@ impl Client {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let current_player = current_player.clone();
|
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
spawn_blocking(move || {
|
|
||||||
let player_finder = PlayerFinder::new().expect("to get new player finder");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
Self::send_tick_update(&player_finder, ¤t_player, &tx);
|
|
||||||
sleep(Duration::from_millis(TICK_INTERVAL_MS));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_player,
|
current_player,
|
||||||
tx,
|
tx,
|
||||||
@@ -108,7 +94,7 @@ impl Client {
|
|||||||
player_id: String,
|
player_id: String,
|
||||||
players: Arc<Mutex<HashSet<String>>>,
|
players: Arc<Mutex<HashSet<String>>>,
|
||||||
current_player: Arc<Mutex<Option<String>>>,
|
current_player: Arc<Mutex<Option<String>>>,
|
||||||
tx: broadcast::Sender<PlayerUpdate>,
|
tx: Sender<PlayerUpdate>,
|
||||||
) {
|
) {
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
let player_finder = PlayerFinder::new()?;
|
let player_finder = PlayerFinder::new()?;
|
||||||
@@ -151,7 +137,7 @@ impl Client {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
|
fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
|
||||||
debug!("Sending update using '{}'", player.identity());
|
debug!("Sending update using '{}'", player.identity());
|
||||||
|
|
||||||
let metadata = player.get_metadata()?;
|
let metadata = player.get_metadata()?;
|
||||||
@@ -161,7 +147,10 @@ impl Client {
|
|||||||
|
|
||||||
let track_list = player.get_track_list();
|
let track_list = player.get_track_list();
|
||||||
|
|
||||||
let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok();
|
let volume_percent = player
|
||||||
|
.get_volume()
|
||||||
|
.map(|vol| (vol * 100.0) as u8)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let status = Status {
|
let status = Status {
|
||||||
// MRPIS doesn't seem to provide playlist info reliably,
|
// MRPIS doesn't seem to provide playlist info reliably,
|
||||||
@@ -169,6 +158,8 @@ impl Client {
|
|||||||
playlist_position: 1,
|
playlist_position: 1,
|
||||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
||||||
state: PlayerState::from(playback_status),
|
state: PlayerState::from(playback_status),
|
||||||
|
elapsed: player.get_position().ok(),
|
||||||
|
duration: metadata.length(),
|
||||||
volume_percent,
|
volume_percent,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,26 +180,6 @@ impl Client {
|
|||||||
player_finder.find_by_name(player_name).ok()
|
player_finder.find_by_name(player_name).ok()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_tick_update(
|
|
||||||
player_finder: &PlayerFinder,
|
|
||||||
current_player: &Mutex<Option<String>>,
|
|
||||||
tx: &broadcast::Sender<PlayerUpdate>,
|
|
||||||
) {
|
|
||||||
if let Some(player) = lock!(current_player)
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|name| player_finder.find_by_name(name).ok())
|
|
||||||
{
|
|
||||||
if let Ok(metadata) = player.get_metadata() {
|
|
||||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
|
||||||
elapsed: player.get_position().ok(),
|
|
||||||
duration: metadata.length(),
|
|
||||||
});
|
|
||||||
|
|
||||||
send!(tx, update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! command {
|
macro_rules! command {
|
||||||
@@ -244,30 +215,14 @@ impl MusicClient for Client {
|
|||||||
|
|
||||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||||
if let Some(player) = Self::get_player(self) {
|
if let Some(player) = Self::get_player(self) {
|
||||||
player.set_volume(f64::from(vol) / 100.0)?;
|
player.set_volume(vol as f64 / 100.0)?;
|
||||||
} else {
|
} else {
|
||||||
error!("Could not find player");
|
error!("Could not find player");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seek(&self, duration: Duration) -> Result<()> {
|
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||||
if let Some(player) = Self::get_player(self) {
|
|
||||||
let pos = player.get_position().unwrap_or_default();
|
|
||||||
|
|
||||||
let duration = duration.as_micros() as i64;
|
|
||||||
let position = pos.as_micros() as i64;
|
|
||||||
|
|
||||||
let seek = cmp::max(duration, 0) - position;
|
|
||||||
|
|
||||||
player.seek(seek)?;
|
|
||||||
} else {
|
|
||||||
error!("Could not find player");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
|
||||||
debug!("Creating new subscription");
|
debug!("Creating new subscription");
|
||||||
let rx = self.tx.subscribe();
|
let rx = self.tx.subscribe();
|
||||||
|
|
||||||
@@ -275,14 +230,6 @@ impl MusicClient for Client {
|
|||||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let status = Status {
|
|
||||||
playlist_position: 0,
|
|
||||||
playlist_length: 0,
|
|
||||||
state: PlayerState::Stopped,
|
|
||||||
volume_percent: None,
|
|
||||||
};
|
|
||||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rx
|
rx
|
||||||
@@ -299,18 +246,9 @@ impl From<Metadata> for Track {
|
|||||||
const KEY_GENRE: &str = "xesam:genre";
|
const KEY_GENRE: &str = "xesam:genre";
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
title: value
|
title: value.title().map(std::string::ToString::to_string),
|
||||||
.title()
|
album: value.album_name().map(std::string::ToString::to_string),
|
||||||
.map(std::string::ToString::to_string)
|
artist: value.artists().map(|artists| artists.join(", ")),
|
||||||
.and_then(replace_empty_none),
|
|
||||||
album: value
|
|
||||||
.album_name()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.and_then(replace_empty_none),
|
|
||||||
artist: value
|
|
||||||
.artists()
|
|
||||||
.map(|artists| artists.join(", "))
|
|
||||||
.and_then(replace_empty_none),
|
|
||||||
date: value
|
date: value
|
||||||
.get(KEY_DATE)
|
.get(KEY_DATE)
|
||||||
.and_then(mpris::MetadataValue::as_string)
|
.and_then(mpris::MetadataValue::as_string)
|
||||||
@@ -321,7 +259,7 @@ impl From<Metadata> for Track {
|
|||||||
.and_then(mpris::MetadataValue::as_str_array)
|
.and_then(mpris::MetadataValue::as_str_array)
|
||||||
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
||||||
track: value.track_number().map(|track| track as u64),
|
track: value.track_number().map(|track| track as u64),
|
||||||
cover_path: value.art_url().map(string::ToString::to_string),
|
cover_path: value.art_url().map(|s| s.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,11 +273,3 @@ impl From<PlaybackStatus> for PlayerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_empty_none(string: String) -> Option<String> {
|
|
||||||
if string.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use crate::{arc_mut, lock, send, spawn, Ironbar};
|
use crate::{lock, send};
|
||||||
use async_once::AsyncOnce;
|
use async_once::AsyncOnce;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use system_tray::message::menu::TrayMenu;
|
use stray::message::menu::TrayMenu;
|
||||||
use system_tray::message::tray::StatusNotifierItem;
|
use stray::message::tray::StatusNotifierItem;
|
||||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||||
use system_tray::StatusNotifierWatcher;
|
use stray::StatusNotifierWatcher;
|
||||||
|
use tokio::spawn;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::sync::{broadcast, mpsc};
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
@@ -22,16 +23,14 @@ pub struct TrayEventReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TrayEventReceiver {
|
impl TrayEventReceiver {
|
||||||
async fn new() -> system_tray::error::Result<Self> {
|
async fn new() -> stray::error::Result<Self> {
|
||||||
let id = format!("ironbar-{}", Ironbar::unique_id());
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(16);
|
let (tx, rx) = mpsc::channel(16);
|
||||||
let (b_tx, b_rx) = broadcast::channel(16);
|
let (b_tx, b_rx) = broadcast::channel(16);
|
||||||
|
|
||||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
let mut host = tray.create_notifier_host("ironbar").await?;
|
||||||
|
|
||||||
let tray = arc_mut!(BTreeMap::new());
|
let tray = Arc::new(Mutex::new(BTreeMap::new()));
|
||||||
|
|
||||||
{
|
{
|
||||||
let b_tx = b_tx.clone();
|
let b_tx = b_tx.clone();
|
||||||
@@ -104,7 +103,7 @@ lazy_static! {
|
|||||||
let value = loop {
|
let value = loop {
|
||||||
retries += 1;
|
retries += 1;
|
||||||
|
|
||||||
let tray = Box::pin(TrayEventReceiver::new()).await;
|
let tray = TrayEventReceiver::new().await;
|
||||||
|
|
||||||
match tray {
|
match tray {
|
||||||
Ok(tray) => break Some(tray),
|
Ok(tray) => break Some(tray),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use zbus::fdo::PropertiesProxy;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||||
let dbus = Box::pin(zbus::Connection::system())
|
let dbus = zbus::Connection::system()
|
||||||
.await
|
.await
|
||||||
.expect("failed to create connection to system bus");
|
.expect("failed to create connection to system bus");
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +1,79 @@
|
|||||||
use super::wlr_foreign_toplevel::handle::ToplevelHandle;
|
use super::wlr_foreign_toplevel::{
|
||||||
use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
handle::{ToplevelEvent, ToplevelInfo},
|
||||||
use super::wlr_foreign_toplevel::ToplevelEvent;
|
manager::listen_for_toplevels,
|
||||||
use super::Environment;
|
};
|
||||||
use crate::error::ERR_CHANNEL_RECV;
|
use super::{DData, Env, ToplevelHandler};
|
||||||
use crate::{send, spawn_blocking};
|
use crate::{error as err, send};
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
use indexmap::IndexMap;
|
||||||
|
use smithay_client_toolkit::environment::Environment;
|
||||||
|
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||||
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||||
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
|
use smithay_client_toolkit::WaylandSource;
|
||||||
use smithay_client_toolkit::registry::RegistryState;
|
|
||||||
use smithay_client_toolkit::seat::SeatState;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::mpsc;
|
use std::sync::{Arc, RwLock};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::{broadcast, oneshot};
|
||||||
use tracing::{debug, error, trace};
|
use tokio::task::spawn_blocking;
|
||||||
use wayland_client::globals::registry_queue_init;
|
use tracing::{debug, error};
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
use wayland_client::Connection;
|
use wayland_client::{ConnectError, Display, EventQueue};
|
||||||
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||||
|
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||||
|
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
||||||
|
};
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(feature = "clipboard")] {
|
if #[cfg(feature = "clipboard")] {
|
||||||
use super::ClipboardItem;
|
use super::{ClipboardItem};
|
||||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler};
|
||||||
use crate::lock;
|
use crate::{read_lock, write_lock};
|
||||||
use std::sync::Arc;
|
use tokio::spawn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
/// Sends a request for all the outputs.
|
|
||||||
/// These are then sent on the `output` channel.
|
|
||||||
Outputs,
|
|
||||||
/// Sends a request for all the seats.
|
|
||||||
/// These are then sent ont the `seat` channel.
|
|
||||||
Seats,
|
|
||||||
/// Sends a request for all the toplevels.
|
|
||||||
/// These are then sent on the `toplevel_init` channel.
|
|
||||||
Toplevels,
|
|
||||||
/// Sends a request for the current clipboard item.
|
|
||||||
/// This is then sent on the `clipboard_init` channel.
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
Clipboard,
|
|
||||||
/// Copies the value to the clipboard
|
/// Copies the value to the clipboard
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
CopyToClipboard(Arc<ClipboardItem>),
|
CopyToClipboard(Arc<ClipboardItem>),
|
||||||
/// Forces a dispatch, flushing any currently queued events
|
/// Forces a dispatch, flushing any currently queued events
|
||||||
Roundtrip,
|
Refresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WaylandClient {
|
pub struct WaylandClient {
|
||||||
// External channels
|
pub outputs: Vec<OutputInfo>,
|
||||||
|
pub seats: Vec<WlSeat>,
|
||||||
|
|
||||||
|
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
|
clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>,
|
||||||
|
|
||||||
// Internal channels
|
|
||||||
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
|
|
||||||
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
|
|
||||||
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
|
|
||||||
|
|
||||||
request_tx: Sender<Request>,
|
request_tx: Sender<Request>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WaylandClient {
|
impl WaylandClient {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) async fn new() -> Self {
|
||||||
|
let (output_tx, output_rx) = oneshot::channel();
|
||||||
|
let (seat_tx, seat_rx) = oneshot::channel();
|
||||||
|
|
||||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||||
|
|
||||||
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
||||||
#[cfg(feature = "clipboard")]
|
let toplevels2 = toplevels.clone();
|
||||||
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
|
|
||||||
let (output_tx, output_rx) = mpsc::channel();
|
|
||||||
let (seat_tx, seat_rx) = mpsc::channel();
|
|
||||||
|
|
||||||
let toplevel_tx2 = toplevel_tx.clone();
|
let toplevel_tx2 = toplevel_tx.clone();
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(feature = "clipboard")] {
|
if #[cfg(feature = "clipboard")] {
|
||||||
let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
|
let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32);
|
||||||
|
let clipboard = Arc::new(RwLock::new(None));
|
||||||
let clipboard_tx2 = clipboard_tx.clone();
|
let clipboard_tx2 = clipboard_tx.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,99 +82,85 @@ impl WaylandClient {
|
|||||||
|
|
||||||
// `queue` is not `Send` so we need to handle everything inside the task
|
// `queue` is not `Send` so we need to handle everything inside the task
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
|
let toplevels = toplevels2;
|
||||||
let toplevel_tx = toplevel_tx2;
|
let toplevel_tx = toplevel_tx2;
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
let clipboard_tx = clipboard_tx2;
|
|
||||||
|
|
||||||
let conn =
|
let (env, _display, queue) =
|
||||||
Connection::connect_to_env().expect("Failed to connect to Wayland compositor");
|
Self::new_environment().expect("Failed to connect to Wayland compositor");
|
||||||
let (globals, queue) =
|
|
||||||
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
|
|
||||||
|
|
||||||
let qh = queue.handle();
|
|
||||||
let mut event_loop =
|
let mut event_loop =
|
||||||
EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
|
EventLoop::<DData>::try_new().expect("Failed to create new event loop");
|
||||||
|
WaylandSource::new(queue)
|
||||||
WaylandSource::new(conn, queue)
|
.quick_insert(event_loop.handle())
|
||||||
.insert(event_loop.handle())
|
|
||||||
.expect("Failed to insert Wayland event queue into event loop");
|
.expect("Failed to insert Wayland event queue into event loop");
|
||||||
|
|
||||||
let loop_handle = event_loop.handle();
|
let outputs = Self::get_outputs(&env);
|
||||||
|
send!(output_tx, outputs);
|
||||||
|
|
||||||
// Initialize the registry handling
|
let seats = env.get_all_seats();
|
||||||
// so other parts of Smithay's client toolkit may bind globals.
|
|
||||||
let registry_state = RegistryState::new(&globals);
|
|
||||||
|
|
||||||
let output_delegate = OutputState::new(&globals, &qh);
|
|
||||||
let seat_delegate = SeatState::new(&globals, &qh);
|
|
||||||
|
|
||||||
|
// TODO: Actually handle seats properly
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
let data_control_device_manager_delegate =
|
let default_seat = seats[0].detach();
|
||||||
DataControlDeviceManagerState::bind(&globals, &qh)
|
|
||||||
.expect("data device manager is not available");
|
|
||||||
|
|
||||||
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
|
send!(
|
||||||
.expect("foreign toplevel manager is not available");
|
seat_tx,
|
||||||
|
seats
|
||||||
|
.into_iter()
|
||||||
|
.map(|seat| seat.detach())
|
||||||
|
.collect::<Vec<WlSeat>>()
|
||||||
|
);
|
||||||
|
|
||||||
let mut env = Environment {
|
let handle = event_loop.handle();
|
||||||
registry_state,
|
handle
|
||||||
output_state: output_delegate,
|
.insert_source(ev_rx, move |event, _metadata, ddata| {
|
||||||
seat_state: seat_delegate,
|
// let env = &ddata.env;
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
data_control_device_manager_state: data_control_device_manager_delegate,
|
|
||||||
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
|
|
||||||
seats: vec![],
|
|
||||||
handles: HashMap::new(),
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard: crate::arc_mut!(None),
|
|
||||||
toplevel_tx,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard_tx,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
data_control_devices: vec![],
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
selection_offers: vec![],
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
copy_paste_sources: vec![],
|
|
||||||
loop_handle: event_loop.handle(),
|
|
||||||
};
|
|
||||||
|
|
||||||
loop_handle
|
|
||||||
.insert_source(ev_rx, move |event, _metadata, env| {
|
|
||||||
trace!("{event:?}");
|
|
||||||
match event {
|
match event {
|
||||||
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
|
Event::Msg(Request::Refresh) => debug!("Received refresh event"),
|
||||||
Event::Msg(Request::Outputs) => {
|
|
||||||
trace!("Received get outputs request");
|
|
||||||
|
|
||||||
send!(output_tx, env.output_info());
|
|
||||||
}
|
|
||||||
Event::Msg(Request::Seats) => {
|
|
||||||
trace!("Receive get seats request");
|
|
||||||
send!(seat_tx, env.seats.clone());
|
|
||||||
}
|
|
||||||
Event::Msg(Request::Toplevels) => {
|
|
||||||
trace!("Receive get toplevels request");
|
|
||||||
send!(toplevel_init_tx, env.handles.clone());
|
|
||||||
}
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
Event::Msg(Request::Clipboard) => {
|
|
||||||
trace!("Receive get clipboard requests");
|
|
||||||
let clipboard = lock!(env.clipboard).clone();
|
|
||||||
send!(clipboard_init_tx, clipboard);
|
|
||||||
}
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
Event::Msg(Request::CopyToClipboard(value)) => {
|
Event::Msg(Request::CopyToClipboard(value)) => {
|
||||||
env.copy_to_clipboard(value, &qh);
|
super::wlr_data_control::copy_to_clipboard(
|
||||||
|
&ddata.env,
|
||||||
|
&default_seat,
|
||||||
|
&value,
|
||||||
|
)
|
||||||
|
.expect("Failed to copy to clipboard");
|
||||||
}
|
}
|
||||||
Event::Closed => panic!("Channel unexpectedly closed"),
|
Event::Closed => panic!("Channel unexpectedly closed"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("Failed to insert channel into event queue");
|
.expect("Failed to insert channel into event queue");
|
||||||
|
|
||||||
|
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
|
||||||
|
|
||||||
|
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
|
||||||
|
super::wlr_foreign_toplevel::update_toplevels(
|
||||||
|
&toplevels,
|
||||||
|
handle,
|
||||||
|
event,
|
||||||
|
&toplevel_tx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "clipboard")] {
|
||||||
|
let clipboard_tx = clipboard_tx2;
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
|
let _offer_listener = listen_to_devices(&env, move |_seat, event, ddata| {
|
||||||
|
debug!("Received clipboard event");
|
||||||
|
super::wlr_data_control::receive_offer(event, &handle, clipboard_tx.clone(), ddata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = DData {
|
||||||
|
env,
|
||||||
|
offer_tokens: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
trace!("Dispatching event loop");
|
if let Err(err) = event_loop.dispatch(None, &mut data) {
|
||||||
if let Err(err) = event_loop.dispatch(None, &mut env) {
|
|
||||||
error!(
|
error!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||||
@@ -194,76 +169,119 @@ impl WaylandClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// keep track of current clipboard item
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
{
|
||||||
|
let clipboard = clipboard.clone();
|
||||||
|
spawn(async move {
|
||||||
|
while let Ok(item) = clipboard_rx.recv().await {
|
||||||
|
let mut clipboard = write_lock!(clipboard);
|
||||||
|
clipboard.replace(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||||
|
|
||||||
|
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
outputs,
|
||||||
|
seats,
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
clipboard,
|
||||||
|
toplevels,
|
||||||
toplevel_tx,
|
toplevel_tx,
|
||||||
_toplevel_rx: toplevel_rx,
|
_toplevel_rx: toplevel_rx,
|
||||||
toplevel_init_rx,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard_init_rx,
|
|
||||||
output_rx,
|
|
||||||
seat_rx,
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
clipboard_tx,
|
clipboard_tx,
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
_clipboard_rx: clipboard_rx,
|
|
||||||
request_tx: ev_tx,
|
request_tx: ev_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe_toplevels(
|
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
|
||||||
&self,
|
self.toplevel_tx.subscribe()
|
||||||
) -> (
|
|
||||||
broadcast::Receiver<ToplevelEvent>,
|
|
||||||
HashMap<usize, ToplevelHandle>,
|
|
||||||
) {
|
|
||||||
let rx = self.toplevel_tx.subscribe();
|
|
||||||
|
|
||||||
let receiver = &self.toplevel_init_rx;
|
|
||||||
send!(self.request_tx, Request::Toplevels);
|
|
||||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
|
||||||
|
|
||||||
(rx, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
pub fn subscribe_clipboard(
|
pub fn subscribe_clipboard(&self) -> broadcast::Receiver<Arc<ClipboardItem>> {
|
||||||
&self,
|
self.clipboard_tx.subscribe()
|
||||||
) -> (
|
|
||||||
broadcast::Receiver<Arc<ClipboardItem>>,
|
|
||||||
Option<Arc<ClipboardItem>>,
|
|
||||||
) {
|
|
||||||
let rx = self.clipboard_tx.subscribe();
|
|
||||||
|
|
||||||
let receiver = &self.clipboard_init_rx;
|
|
||||||
send!(self.request_tx, Request::Clipboard);
|
|
||||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
|
||||||
|
|
||||||
(rx, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force a roundtrip on the wayland connection,
|
|
||||||
/// flushing any queued events and immediately receiving any new ones.
|
|
||||||
pub fn roundtrip(&self) {
|
pub fn roundtrip(&self) {
|
||||||
trace!("Sending roundtrip request");
|
send!(self.request_tx, Request::Refresh);
|
||||||
send!(self.request_tx, Request::Roundtrip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_outputs(&self) -> Vec<OutputInfo> {
|
#[cfg(feature = "clipboard")]
|
||||||
trace!("Sending get outputs request");
|
pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> {
|
||||||
|
let clipboard = read_lock!(self.clipboard);
|
||||||
send!(self.request_tx, Request::Outputs);
|
clipboard.as_ref().cloned()
|
||||||
self.output_rx.recv().expect(ERR_CHANNEL_RECV)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_seats(&self) -> Vec<WlSeat> {
|
|
||||||
trace!("Sending get seats request");
|
|
||||||
|
|
||||||
send!(self.request_tx, Request::Seats);
|
|
||||||
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
||||||
send!(self.request_tx, Request::CopyToClipboard(item));
|
send!(self.request_tx, Request::CopyToClipboard(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
|
||||||
|
let outputs = env.get_all_outputs();
|
||||||
|
|
||||||
|
outputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|output| with_output_info(output, Clone::clone))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_environment() -> Result<(Environment<Env>, Display, EventQueue), ConnectError> {
|
||||||
|
Display::connect_to_env().and_then(|display| {
|
||||||
|
let mut queue = display.create_event_queue();
|
||||||
|
let ret = {
|
||||||
|
let mut sctk_seats = smithay_client_toolkit::seat::SeatHandler::new();
|
||||||
|
let sctk_data_device_manager =
|
||||||
|
smithay_client_toolkit::data_device::DataDeviceHandler::init(&mut sctk_seats);
|
||||||
|
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
let data_control_device = DataControlDeviceHandler::init(&mut sctk_seats);
|
||||||
|
|
||||||
|
let sctk_primary_selection_manager =
|
||||||
|
smithay_client_toolkit::primary_selection::PrimarySelectionHandler::init(
|
||||||
|
&mut sctk_seats,
|
||||||
|
);
|
||||||
|
|
||||||
|
let display = ::smithay_client_toolkit::reexports::client::Proxy::clone(&display);
|
||||||
|
let env = Environment::new(
|
||||||
|
&display.attach(queue.token()),
|
||||||
|
&mut queue,
|
||||||
|
Env {
|
||||||
|
sctk_compositor: smithay_client_toolkit::environment::SimpleGlobal::new(),
|
||||||
|
sctk_subcompositor: smithay_client_toolkit::environment::SimpleGlobal::new(
|
||||||
|
),
|
||||||
|
sctk_shm: smithay_client_toolkit::shm::ShmHandler::new(),
|
||||||
|
sctk_outputs: smithay_client_toolkit::output::OutputHandler::new(),
|
||||||
|
sctk_seats,
|
||||||
|
sctk_data_device_manager,
|
||||||
|
sctk_primary_selection_manager,
|
||||||
|
toplevel: ToplevelHandler::init(),
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
data_control_device,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(env) = env.as_ref() {
|
||||||
|
let _psm = env.get_primary_selection_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
env
|
||||||
|
};
|
||||||
|
match ret {
|
||||||
|
Ok(env) => Ok((env, display, queue)),
|
||||||
|
Err(_e) => display.protocol_error().map_or_else(
|
||||||
|
|| Err(ConnectError::NoCompositorListening),
|
||||||
|
|perr| {
|
||||||
|
panic!("[SCTK] A protocol error occured during initial setup: {perr}");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
/// It is necessary to store macros in a separate file due to a compilation error.
|
|
||||||
/// I believe this stems from the feature flags.
|
|
||||||
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
|
|
||||||
|
|
||||||
// --- Data Control Device --- \\
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_data_control_device_manager {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_data_control_device {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_data_control_offer {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_data_control_source {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
|
|
||||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Foreign Toplevel --- \\
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_foreign_toplevel_manager {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
|
|
||||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! delegate_foreign_toplevel_handle {
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
|
|
||||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
|
||||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
|
||||||
[
|
|
||||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
|
|
||||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,113 +1,75 @@
|
|||||||
mod client;
|
mod client;
|
||||||
|
|
||||||
mod macros;
|
|
||||||
mod wl_output;
|
|
||||||
mod wl_seat;
|
|
||||||
mod wlr_foreign_toplevel;
|
mod wlr_foreign_toplevel;
|
||||||
|
|
||||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
|
||||||
use crate::{arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use smithay_client_toolkit::output::OutputState;
|
|
||||||
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
|
||||||
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
|
|
||||||
use smithay_client_toolkit::seat::SeatState;
|
|
||||||
use smithay_client_toolkit::{
|
|
||||||
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use async_once::AsyncOnce;
|
||||||
use tokio::sync::broadcast;
|
use lazy_static::lazy_static;
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use std::fmt::Debug;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use smithay_client_toolkit::default_environment;
|
||||||
|
use smithay_client_toolkit::environment::Environment;
|
||||||
|
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||||
|
use wayland_client::{Attached, Interface};
|
||||||
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||||
|
pub use wlr_foreign_toplevel::handle::{ToplevelChange, ToplevelEvent, ToplevelInfo};
|
||||||
|
use wlr_foreign_toplevel::manager::{ToplevelHandler};
|
||||||
|
|
||||||
pub use self::client::WaylandClient;
|
pub use client::WaylandClient;
|
||||||
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
|
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(feature = "clipboard")] {
|
if #[cfg(feature = "clipboard")] {
|
||||||
mod wlr_data_control;
|
mod wlr_data_control;
|
||||||
|
|
||||||
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
|
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
|
||||||
use self::wlr_data_control::device::DataControlDevice;
|
use wlr_data_control::manager::DataControlDeviceHandler;
|
||||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
|
||||||
use self::wlr_data_control::source::CopyPasteSource;
|
|
||||||
use self::wlr_data_control::SelectionOfferItem;
|
|
||||||
|
|
||||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||||
|
|
||||||
pub struct DataControlDeviceEntry {
|
|
||||||
seat: WlSeat,
|
|
||||||
device: DataControlDevice,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Environment {
|
/// A utility for lazy-loading globals.
|
||||||
pub registry_state: RegistryState,
|
/// Taken from `smithay_client_toolkit` where it's not exposed
|
||||||
pub output_state: OutputState,
|
#[derive(Debug)]
|
||||||
pub seat_state: SeatState,
|
enum LazyGlobal<I: Interface> {
|
||||||
pub foreign_toplevel_manager_state: ToplevelManagerState,
|
Unknown,
|
||||||
#[cfg(feature = "clipboard")]
|
Seen { id: u32, version: u32 },
|
||||||
pub data_control_device_manager_state: DataControlDeviceManagerState,
|
Bound(Attached<I>),
|
||||||
pub loop_handle: LoopHandle<'static, Self>,
|
|
||||||
|
|
||||||
pub seats: Vec<WlSeat>,
|
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
pub data_control_devices: Vec<DataControlDeviceEntry>,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
pub selection_offers: Vec<SelectionOfferItem>,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
pub copy_paste_sources: Vec<CopyPasteSource>,
|
|
||||||
|
|
||||||
pub handles: HashMap<usize, ToplevelHandle>,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
|
|
||||||
|
|
||||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we need to say we are delegating the responsibility of output related events for our application data
|
pub struct DData {
|
||||||
// type to the requisite delegate.
|
env: Environment<Env>,
|
||||||
delegate_output!(Environment);
|
offer_tokens: HashMap<u128, RegistrationToken>,
|
||||||
delegate_seat!(Environment);
|
}
|
||||||
|
|
||||||
delegate_foreign_toplevel_manager!(Environment);
|
|
||||||
delegate_foreign_toplevel_handle!(Environment);
|
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(feature = "clipboard")] {
|
if #[cfg(feature = "clipboard")] {
|
||||||
delegate_data_control_device_manager!(Environment);
|
default_environment!(Env,
|
||||||
delegate_data_control_device!(Environment);
|
fields = [
|
||||||
delegate_data_control_source!(Environment);
|
toplevel: ToplevelHandler,
|
||||||
delegate_data_control_offer!(Environment);
|
data_control_device: DataControlDeviceHandler
|
||||||
|
],
|
||||||
|
singles = [
|
||||||
|
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||||
|
ZwlrDataControlManagerV1 => data_control_device
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
default_environment!(Env,
|
||||||
|
fields = [
|
||||||
|
toplevel: ToplevelHandler,
|
||||||
|
],
|
||||||
|
singles = [
|
||||||
|
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In order for our delegate to know of the existence of globals, we need to implement registry
|
|
||||||
// handling for the program. This trait will forward events to the RegistryHandler trait
|
|
||||||
// implementations.
|
|
||||||
delegate_registry!(Environment);
|
|
||||||
|
|
||||||
// In order for delegate_registry to work, our application data type needs to provide a way for the
|
|
||||||
// implementation to access the registry state.
|
|
||||||
//
|
|
||||||
// We also need to indicate which delegates will get told about globals being created. We specify
|
|
||||||
// the types of the delegates inside the array.
|
|
||||||
impl ProvidesRegistryState for Environment {
|
|
||||||
fn registry(&mut self) -> &mut RegistryState {
|
|
||||||
&mut self.registry_state
|
|
||||||
}
|
|
||||||
registry_handlers![OutputState, SeatState];
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
|
static ref CLIENT: AsyncOnce<WaylandClient> =
|
||||||
|
AsyncOnce::new(async { WaylandClient::new().await });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
pub async fn get_client() -> &'static WaylandClient {
|
||||||
CLIENT.clone()
|
CLIENT.get().await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
use super::Environment;
|
|
||||||
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
|
|
||||||
use tracing::debug;
|
|
||||||
use wayland_client::protocol::wl_output;
|
|
||||||
use wayland_client::{Connection, QueueHandle};
|
|
||||||
|
|
||||||
impl Environment {
|
|
||||||
pub fn output_info(&mut self) -> Vec<OutputInfo> {
|
|
||||||
self.output_state
|
|
||||||
.outputs()
|
|
||||||
.filter_map(|output| self.output_state.info(&output))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
|
|
||||||
// output and to provide an instance of the output state to the delegate when dispatching events.
|
|
||||||
impl OutputHandler for Environment {
|
|
||||||
// First we need to provide a way to access the delegate.
|
|
||||||
//
|
|
||||||
// This is needed because delegate implementations for handling events use the application data type in
|
|
||||||
// their function signatures. This allows the implementation to access an instance of the type.
|
|
||||||
fn output_state(&mut self) -> &mut OutputState {
|
|
||||||
&mut self.output_state
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then there exist these functions that indicate the lifecycle of an output.
|
|
||||||
// These will be called as appropriate by the delegate implementation.
|
|
||||||
|
|
||||||
fn new_output(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
_output: wl_output::WlOutput,
|
|
||||||
) {
|
|
||||||
debug!("Handler received new output");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_output(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
_output: wl_output::WlOutput,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output_destroyed(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
_output: wl_output::WlOutput,
|
|
||||||
) {
|
|
||||||
debug!("Handle received output destruction");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use super::Environment;
|
|
||||||
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
|
|
||||||
use tracing::debug;
|
|
||||||
use wayland_client::protocol::wl_seat;
|
|
||||||
use wayland_client::{Connection, QueueHandle};
|
|
||||||
|
|
||||||
impl SeatHandler for Environment {
|
|
||||||
fn seat_state(&mut self) -> &mut SeatState {
|
|
||||||
&mut self.seat_state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
|
||||||
debug!("Handler received new seat");
|
|
||||||
self.seats.push(seat);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_capability(
|
|
||||||
&mut self,
|
|
||||||
_: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
seat: wl_seat::WlSeat,
|
|
||||||
_: Capability,
|
|
||||||
) {
|
|
||||||
debug!("Handler received new capability");
|
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
|
||||||
if !self
|
|
||||||
.data_control_devices
|
|
||||||
.iter_mut()
|
|
||||||
.any(|entry| entry.seat == seat)
|
|
||||||
{
|
|
||||||
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 data_control_device = data_control_device_manager.get_data_device(qh, &seat);
|
|
||||||
self.data_control_devices
|
|
||||||
.push(super::DataControlDeviceEntry {
|
|
||||||
seat: seat.clone(),
|
|
||||||
device: data_control_device,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.seats.iter().any(|s| s == &seat) {
|
|
||||||
self.seats.push(seat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_capability(
|
|
||||||
&mut self,
|
|
||||||
_: &Connection,
|
|
||||||
_: &QueueHandle<Self>,
|
|
||||||
_: wl_seat::WlSeat,
|
|
||||||
_: Capability,
|
|
||||||
) {
|
|
||||||
debug!("Handler received capability removal");
|
|
||||||
// Not applicable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
|
||||||
debug!("Handler received seat removal");
|
|
||||||
self.seats.retain(|s| s != &seat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +1,88 @@
|
|||||||
use super::manager::DataControlDeviceManagerState;
|
use super::offer::DataControlOffer;
|
||||||
use super::offer::{
|
use super::source::DataControlSource;
|
||||||
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
|
|
||||||
};
|
|
||||||
use crate::error::ERR_WAYLAND_DATA;
|
|
||||||
use crate::lock;
|
use crate::lock;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::warn;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
|
use wayland_client::{Attached, DispatchData, Main};
|
||||||
use wayland_protocols_wlr::data_control::v1::client::{
|
use wayland_protocols::wlr::unstable::data_control::v1::client::{
|
||||||
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
||||||
|
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||||
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DataControlDevice {
|
#[derive(Debug)]
|
||||||
pub device: ZwlrDataControlDeviceV1,
|
struct Inner {
|
||||||
|
offer: Option<Arc<DataControlOffer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
impl Inner {
|
||||||
pub struct DataControlDeviceInner {
|
fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) {
|
||||||
/// the active selection offer and its data
|
self.offer.replace(Arc::new(DataControlOffer::new(offer)));
|
||||||
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
|
|
||||||
/// the active undetermined offers and their data
|
|
||||||
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct DataControlDeviceData {
|
|
||||||
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DataControlDeviceDataExt: Send + Sync {
|
|
||||||
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
|
|
||||||
|
|
||||||
fn data_control_device_data(&self) -> &DataControlDeviceData;
|
|
||||||
|
|
||||||
fn selection_mime_types(&self) -> Vec<String> {
|
|
||||||
let inner = self.data_control_device_data();
|
|
||||||
lock!(lock!(inner.inner).selection_offer)
|
|
||||||
.as_ref()
|
|
||||||
.map(|offer| {
|
|
||||||
let data = offer
|
|
||||||
.data::<Self::DataControlOfferInner>()
|
|
||||||
.expect(ERR_WAYLAND_DATA);
|
|
||||||
data.mime_types()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataControlDeviceDataExt for DataControlDevice {
|
#[derive(Debug, Clone)]
|
||||||
type DataControlOfferInner = DataControlOfferData;
|
pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>);
|
||||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
|
||||||
self.device.data().expect(ERR_WAYLAND_DATA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataControlDeviceDataExt for DataControlDeviceData {
|
fn data_control_device_implem<F>(
|
||||||
type DataControlOfferInner = DataControlOfferData;
|
event: Event,
|
||||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
inner: &mut Inner,
|
||||||
self
|
implem: &mut F,
|
||||||
}
|
ddata: DispatchData,
|
||||||
}
|
) where
|
||||||
|
F: FnMut(DataControlDeviceEvent, DispatchData),
|
||||||
/// Handler trait for `DataDevice` events.
|
|
||||||
///
|
|
||||||
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
|
|
||||||
pub trait DataControlDeviceHandler: Sized {
|
|
||||||
/// Advertises a new selection.
|
|
||||||
fn selection(
|
|
||||||
&mut self,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
data_device: DataControlDevice,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrDataControlDeviceV1, U>
|
|
||||||
+ Dispatch<ZwlrDataControlOfferV1, V>
|
|
||||||
+ DataControlDeviceHandler
|
|
||||||
+ DataControlOfferHandler
|
|
||||||
+ 'static,
|
|
||||||
U: DataControlDeviceDataExt,
|
|
||||||
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
|
|
||||||
{
|
{
|
||||||
event_created_child!(D, ZwlrDataControlDeviceV1, [
|
match event {
|
||||||
0 => (ZwlrDataControlOfferV1, V::default())
|
Event::DataOffer { id } => {
|
||||||
]);
|
inner.new_offer(&id);
|
||||||
|
}
|
||||||
fn event(
|
Event::Selection { id: Some(offer) } => {
|
||||||
state: &mut D,
|
let inner_offer = inner
|
||||||
data_device: &ZwlrDataControlDeviceV1,
|
.offer
|
||||||
event: Event,
|
.clone()
|
||||||
data: &U,
|
.expect("Offer should exist at this stage");
|
||||||
conn: &Connection,
|
if offer == inner_offer.offer {
|
||||||
qh: &QueueHandle<D>,
|
implem(DataControlDeviceEvent(inner_offer), ddata);
|
||||||
) {
|
|
||||||
let data = data.data_control_device_data();
|
|
||||||
let inner = lock!(data.inner);
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::DataOffer { id } => {
|
|
||||||
// XXX Drop done here to prevent Mutex deadlocks.S
|
|
||||||
|
|
||||||
lock!(inner.undetermined_offers).push(id.clone());
|
|
||||||
let data = id
|
|
||||||
.data::<V>()
|
|
||||||
.expect(ERR_WAYLAND_DATA)
|
|
||||||
.data_control_offer_data();
|
|
||||||
data.init_undetermined_offer(&id);
|
|
||||||
|
|
||||||
// Append the data offer to our list of offers.
|
|
||||||
drop(inner);
|
|
||||||
}
|
}
|
||||||
Event::Selection { id } => {
|
}
|
||||||
let mut selection_offer = lock!(inner.selection_offer);
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(offer) = id {
|
pub struct DataControlDevice {
|
||||||
let mut undetermined = lock!(inner.undetermined_offers);
|
device: ZwlrDataControlDeviceV1,
|
||||||
if let Some(i) = undetermined.iter().position(|o| o == &offer) {
|
_inner: Arc<Mutex<Inner>>,
|
||||||
undetermined.remove(i);
|
}
|
||||||
}
|
|
||||||
drop(undetermined);
|
|
||||||
|
|
||||||
let data = offer
|
impl DataControlDevice {
|
||||||
.data::<V>()
|
pub fn init_for_seat<F>(
|
||||||
.expect(ERR_WAYLAND_DATA)
|
manager: &Attached<ZwlrDataControlManagerV1>,
|
||||||
.data_control_offer_data();
|
seat: &WlSeat,
|
||||||
data.to_selection_offer();
|
mut callback: F,
|
||||||
// XXX Drop done here to prevent Mutex deadlocks.
|
) -> Self
|
||||||
*selection_offer = Some(offer.clone());
|
where
|
||||||
drop(selection_offer);
|
F: FnMut(DataControlDeviceEvent, DispatchData) + 'static,
|
||||||
drop(inner);
|
{
|
||||||
state.selection(
|
let inner = Arc::new(Mutex::new(Inner { offer: None }));
|
||||||
conn,
|
|
||||||
qh,
|
let device = manager.get_data_device(seat);
|
||||||
DataControlDevice {
|
|
||||||
device: data_device.clone(),
|
{
|
||||||
},
|
let inner = inner.clone();
|
||||||
);
|
device.quick_assign(move |_handle, event, ddata| {
|
||||||
} else {
|
let mut inner = lock!(inner);
|
||||||
*selection_offer = None;
|
data_control_device_implem(event, &mut inner, &mut callback, ddata);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
Event::Finished => {
|
|
||||||
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
|
Self {
|
||||||
}
|
device: device.detach(),
|
||||||
_ => {}
|
_inner: inner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_selection(&self, source: &Option<DataControlSource>) {
|
||||||
|
self.device
|
||||||
|
.set_selection(source.as_ref().map(|s| &s.source));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,253 @@
|
|||||||
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
|
use super::device::{DataControlDevice, DataControlDeviceEvent};
|
||||||
use super::offer::DataControlOfferData;
|
use super::source::DataControlSource;
|
||||||
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
|
use smithay_client_toolkit::data_device::WritePipe;
|
||||||
use smithay_client_toolkit::error::GlobalError;
|
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
use smithay_client_toolkit::seat::{SeatHandling, SeatListener};
|
||||||
use std::marker::PhantomData;
|
use smithay_client_toolkit::MissingGlobal;
|
||||||
use tracing::debug;
|
use std::cell::RefCell;
|
||||||
use wayland_client::globals::{BindError, GlobalList};
|
use std::rc::{self, Rc};
|
||||||
|
use tracing::warn;
|
||||||
|
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
use wayland_client::{Attached, DispatchData};
|
||||||
use wayland_protocols_wlr::data_control::v1::client::{
|
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
|
||||||
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
|
|
||||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
|
||||||
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
|
enum DataControlDeviceHandlerInner {
|
||||||
manager: ZwlrDataControlManagerV1,
|
Ready {
|
||||||
_phantom: PhantomData<V>,
|
manager: Attached<ZwlrDataControlManagerV1>,
|
||||||
|
devices: Vec<(WlSeat, DataControlDevice)>,
|
||||||
|
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||||
|
},
|
||||||
|
Pending {
|
||||||
|
seats: Vec<WlSeat>,
|
||||||
|
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataControlDeviceManagerState {
|
impl DataControlDeviceHandlerInner {
|
||||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
|
||||||
where
|
let (seats, status_listeners) = if let Self::Pending {
|
||||||
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
|
seats,
|
||||||
{
|
status_listeners,
|
||||||
let manager = globals.bind(qh, 1..=2, GlobalData)?;
|
} = self
|
||||||
debug!("Bound to ZwlDataControlManagerV1 global");
|
{
|
||||||
Ok(Self {
|
(std::mem::take(seats), status_listeners.clone())
|
||||||
|
} else {
|
||||||
|
warn!("Ignoring second zwlr_data_control_manager_v1");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut devices = Vec::new();
|
||||||
|
|
||||||
|
for seat in seats {
|
||||||
|
let my_seat = seat.clone();
|
||||||
|
let status_listeners = status_listeners.clone();
|
||||||
|
let device =
|
||||||
|
DataControlDevice::init_for_seat(&manager, &seat, move |event, dispatch_data| {
|
||||||
|
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
|
||||||
|
});
|
||||||
|
devices.push((seat.clone(), device));
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = Self::Ready {
|
||||||
manager,
|
manager,
|
||||||
_phantom: PhantomData,
|
devices,
|
||||||
})
|
status_listeners,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// creates a data source for copy paste
|
fn get_manager(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
|
||||||
pub fn create_copy_paste_source<'s, D, I>(
|
match self {
|
||||||
&self,
|
Self::Ready { manager, .. } => Some(manager.clone()),
|
||||||
qh: &QueueHandle<D>,
|
Self::Pending { .. } => None,
|
||||||
mime_types: I,
|
|
||||||
) -> CopyPasteSource
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
|
||||||
I: IntoIterator<Item = &'s str>,
|
|
||||||
{
|
|
||||||
CopyPasteSource {
|
|
||||||
inner: self.create_data_control_source(qh, mime_types),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// creates a data source
|
fn new_seat(&mut self, seat: &WlSeat) {
|
||||||
fn create_data_control_source<'s, D, I>(
|
match self {
|
||||||
&self,
|
Self::Ready {
|
||||||
qh: &QueueHandle<D>,
|
manager,
|
||||||
mime_types: I,
|
devices,
|
||||||
) -> ZwlrDataControlSourceV1
|
status_listeners,
|
||||||
where
|
} => {
|
||||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
if devices.iter().any(|(s, _)| s == seat) {
|
||||||
I: IntoIterator<Item = &'s str>,
|
// the seat already exists, nothing to do
|
||||||
{
|
return;
|
||||||
let source =
|
}
|
||||||
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
|
let my_seat = seat.clone();
|
||||||
|
let status_listeners = status_listeners.clone();
|
||||||
for mime in mime_types {
|
let device =
|
||||||
source.offer(mime.to_string());
|
DataControlDevice::init_for_seat(manager, seat, move |event, dispatch_data| {
|
||||||
}
|
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
|
||||||
|
});
|
||||||
source
|
devices.push((seat.clone(), device));
|
||||||
}
|
}
|
||||||
|
Self::Pending { seats, .. } => {
|
||||||
/// create a new data source for a given seat with some user data
|
seats.push(seat.clone());
|
||||||
pub fn create_data_control_source_with_data<D, U>(
|
}
|
||||||
&self,
|
|
||||||
qh: &QueueHandle<D>,
|
|
||||||
data: U,
|
|
||||||
) -> ZwlrDataControlSourceV1
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
|
|
||||||
U: DataControlSourceDataExt + 'static,
|
|
||||||
{
|
|
||||||
self.manager.create_data_source(qh, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// create a new data device for a given seat
|
|
||||||
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
|
|
||||||
{
|
|
||||||
DataControlDevice {
|
|
||||||
device: self.get_data_control_device_with_data(
|
|
||||||
qh,
|
|
||||||
seat,
|
|
||||||
DataControlDeviceData::default(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// create a new data device for a given seat with some user data
|
fn remove_seat(&mut self, seat: &WlSeat) {
|
||||||
pub fn get_data_control_device_with_data<D, U>(
|
match self {
|
||||||
&self,
|
Self::Ready { devices, .. } => devices.retain(|(s, _)| s != seat),
|
||||||
qh: &QueueHandle<D>,
|
Self::Pending { seats, .. } => seats.retain(|s| s != seat),
|
||||||
seat: &WlSeat,
|
}
|
||||||
data: U,
|
}
|
||||||
) -> ZwlrDataControlDeviceV1
|
|
||||||
|
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||||
where
|
where
|
||||||
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
|
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||||
U: DataControlDeviceDataExt + 'static,
|
|
||||||
{
|
{
|
||||||
self.manager.get_data_device(seat, qh, data)
|
match self {
|
||||||
|
Self::Ready { manager, .. } => {
|
||||||
|
let source = DataControlSource::new(manager, mime_types, callback);
|
||||||
|
Some(source)
|
||||||
|
}
|
||||||
|
Self::Pending { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||||
|
where
|
||||||
|
F: FnOnce(&DataControlDevice),
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Ready { devices, .. } => {
|
||||||
|
let device = devices
|
||||||
|
.iter()
|
||||||
|
.find_map(|(s, device)| if s == seat { Some(device) } else { None });
|
||||||
|
|
||||||
|
device.map_or(Err(MissingGlobal), |device| {
|
||||||
|
f(device);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Self::Pending { .. } => Err(MissingGlobal),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState {
|
pub struct DataControlDeviceHandler {
|
||||||
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> {
|
inner: Rc<RefCell<DataControlDeviceHandlerInner>>,
|
||||||
Ok(self.manager.clone())
|
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||||
|
_seat_listener: SeatListener,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataControlDeviceHandler {
|
||||||
|
pub fn init<S>(seat_handler: &mut S) -> Self
|
||||||
|
where
|
||||||
|
S: SeatHandling,
|
||||||
|
{
|
||||||
|
let status_listeners = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
|
let inner = Rc::new(RefCell::new(DataControlDeviceHandlerInner::Pending {
|
||||||
|
seats: Vec::new(),
|
||||||
|
status_listeners: status_listeners.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let seat_inner = inner.clone();
|
||||||
|
let seat_listener = seat_handler.listen(move |seat, seat_data, _| {
|
||||||
|
if seat_data.defunct {
|
||||||
|
seat_inner.borrow_mut().remove_seat(&seat);
|
||||||
|
} else {
|
||||||
|
seat_inner.borrow_mut().new_seat(&seat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
_seat_listener: seat_listener,
|
||||||
|
status_listeners,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState
|
impl GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
|
||||||
where
|
fn created(
|
||||||
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>,
|
&mut self,
|
||||||
{
|
registry: Attached<WlRegistry>,
|
||||||
fn event(
|
id: u32,
|
||||||
_state: &mut D,
|
version: u32,
|
||||||
_proxy: &ZwlrDataControlManagerV1,
|
_ddata: DispatchData,
|
||||||
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
|
|
||||||
_data: &GlobalData,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qhandle: &QueueHandle<D>,
|
|
||||||
) {
|
) {
|
||||||
unreachable!()
|
// data control manager is supported until version 2
|
||||||
|
let version = std::cmp::min(version, 2);
|
||||||
|
|
||||||
|
let manager = registry.bind::<ZwlrDataControlManagerV1>(version, id);
|
||||||
|
self.inner.borrow_mut().init_manager((*manager).clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
|
||||||
|
RefCell::borrow(&self.inner).get_manager()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataControlDeviceStatusCallback =
|
||||||
|
dyn FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
|
||||||
|
|
||||||
|
/// Notifies the callbacks of an event on the data device
|
||||||
|
fn notify_status_listeners(
|
||||||
|
seat: &WlSeat,
|
||||||
|
event: &DataControlDeviceEvent,
|
||||||
|
mut ddata: DispatchData,
|
||||||
|
listeners: &RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>,
|
||||||
|
) {
|
||||||
|
listeners.borrow_mut().retain(|lst| {
|
||||||
|
rc::Weak::upgrade(lst).map_or(false, |cb| {
|
||||||
|
(cb.borrow_mut())(seat.clone(), event.clone(), ddata.reborrow());
|
||||||
|
true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DataControlDeviceStatusListener {
|
||||||
|
_cb: Rc<RefCell<DataControlDeviceStatusCallback>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DataControlDeviceHandling {
|
||||||
|
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||||
|
where
|
||||||
|
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
|
||||||
|
|
||||||
|
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||||
|
where
|
||||||
|
F: FnOnce(&DataControlDevice);
|
||||||
|
|
||||||
|
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||||
|
where
|
||||||
|
F: FnMut(String, WritePipe, DispatchData) + 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataControlDeviceHandling for DataControlDeviceHandler {
|
||||||
|
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||||
|
where
|
||||||
|
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
|
||||||
|
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
|
||||||
|
DataControlDeviceStatusListener { _cb: rc }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||||
|
where
|
||||||
|
F: FnOnce(&DataControlDevice),
|
||||||
|
{
|
||||||
|
RefCell::borrow(&self.inner).with_device(seat, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||||
|
where
|
||||||
|
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
RefCell::borrow(&self.inner).create_source(mime_types, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen_to_devices<E, F>(env: &Environment<E>, f: F) -> DataControlDeviceStatusListener
|
||||||
|
where
|
||||||
|
E: DataControlDeviceHandling,
|
||||||
|
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,33 +3,35 @@ pub mod manager;
|
|||||||
pub mod offer;
|
pub mod offer;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
|
|
||||||
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
use super::Env;
|
||||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
use crate::clients::wayland::DData;
|
||||||
use self::source::DataControlSourceHandler;
|
use crate::send;
|
||||||
use crate::clients::wayland::Environment;
|
use color_eyre::Report;
|
||||||
use crate::{lock, send, Ironbar};
|
use device::{DataControlDevice, DataControlDeviceEvent};
|
||||||
use device::DataControlDevice;
|
|
||||||
use glib::Bytes;
|
use glib::Bytes;
|
||||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener};
|
||||||
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
|
use smithay_client_toolkit::data_device::WritePipe;
|
||||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
use smithay_client_toolkit::environment::Environment;
|
||||||
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken};
|
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
||||||
use std::cmp::min;
|
use smithay_client_toolkit::MissingGlobal;
|
||||||
use std::fmt::{Debug, Formatter};
|
use source::DataControlSource;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{ErrorKind, Read, Write};
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
use std::io::{Read, Write};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fs, io};
|
use std::time::UNIX_EPOCH;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
use wayland_client::{Connection, QueueHandle};
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
use wayland_client::DispatchData;
|
||||||
|
|
||||||
|
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||||
|
|
||||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||||
|
|
||||||
pub struct SelectionOfferItem {
|
fn get_id() -> usize {
|
||||||
offer: SelectionOffer,
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
token: Option<RegistrationToken>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq)]
|
#[derive(Debug, Clone, Eq)]
|
||||||
@@ -45,27 +47,77 @@ impl PartialEq<Self> for ClipboardItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ClipboardValue {
|
pub enum ClipboardValue {
|
||||||
Text(String),
|
Text(String),
|
||||||
Image(Bytes),
|
Image(Bytes),
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for ClipboardValue {
|
impl DataControlDeviceHandling for Env {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||||
write!(
|
where
|
||||||
f,
|
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||||
"{}",
|
{
|
||||||
match self {
|
self.data_control_device.listen(f)
|
||||||
Self::Text(text) => text.clone(),
|
|
||||||
Self::Image(bytes) => {
|
|
||||||
format!("[{} Bytes]", bytes.len())
|
|
||||||
}
|
|
||||||
Self::Other => "[Unknown]".to_string(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||||
|
where
|
||||||
|
F: FnOnce(&DataControlDevice),
|
||||||
|
{
|
||||||
|
self.data_control_device.with_data_control_device(seat, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||||
|
where
|
||||||
|
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
self.data_control_device.create_source(mime_types, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_clipboard<E>(
|
||||||
|
env: &Environment<E>,
|
||||||
|
seat: &WlSeat,
|
||||||
|
item: &ClipboardItem,
|
||||||
|
) -> Result<(), MissingGlobal>
|
||||||
|
where
|
||||||
|
E: DataControlDeviceHandling,
|
||||||
|
{
|
||||||
|
debug!("Copying item with id {} [{}]", item.id, item.mime_type);
|
||||||
|
trace!("Copying: {item:?}");
|
||||||
|
|
||||||
|
let item = item.clone();
|
||||||
|
|
||||||
|
env.with_inner(|env| {
|
||||||
|
let mime_types = vec![INTERNAL_MIME_TYPE.to_string(), item.mime_type];
|
||||||
|
let source = env.create_source(mime_types, move |mime_type, mut pipe, _ddata| {
|
||||||
|
debug!(
|
||||||
|
"Triggering source callback for item with id {} [{}]",
|
||||||
|
item.id, mime_type
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIXME: Not working for large (buffered) values in xwayland
|
||||||
|
let bytes = match &item.value {
|
||||||
|
ClipboardValue::Text(text) => text.as_bytes(),
|
||||||
|
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||||
|
ClipboardValue::Other => panic!(
|
||||||
|
"{:?}",
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Attempted to copy unsupported mime type",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = pipe.write_all(bytes) {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
env.with_data_control_device(seat, |device| device.set_selection(&source))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -81,296 +133,126 @@ enum MimeTypeCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MimeType {
|
impl MimeType {
|
||||||
fn parse(mime_type: &str) -> Option<Self> {
|
fn parse(mime_types: &[String]) -> Option<Self> {
|
||||||
match mime_type.to_lowercase().as_str() {
|
mime_types
|
||||||
"text"
|
.iter()
|
||||||
| "string"
|
.map(|s| s.to_lowercase())
|
||||||
| "utf8_string"
|
.find_map(|mime_type| match mime_type.as_str() {
|
||||||
| "text/plain"
|
"text"
|
||||||
| "text/plain;charset=utf-8"
|
| "string"
|
||||||
| "text/plain;charset=iso-8859-1"
|
| "utf8_string"
|
||||||
| "text/plain;charset=us-ascii"
|
| "text/plain"
|
||||||
| "text/plain;charset=unicode" => Some(Self {
|
| "text/plain;charset=utf-8"
|
||||||
value: mime_type.to_string(),
|
| "text/plain;charset=iso-8859-1"
|
||||||
category: MimeTypeCategory::Text,
|
| "text/plain;charset=us-ascii"
|
||||||
}),
|
| "text/plain;charset=unicode" => Some(Self {
|
||||||
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
value: mime_type,
|
||||||
| "image/x-bmp" | "image/icon" => Some(Self {
|
category: MimeTypeCategory::Text,
|
||||||
value: mime_type.to_string(),
|
}),
|
||||||
category: MimeTypeCategory::Image,
|
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
||||||
}),
|
| "image/x-bmp" | "image/icon" => Some(Self {
|
||||||
_ => None,
|
value: mime_type,
|
||||||
}
|
category: MimeTypeCategory::Image,
|
||||||
}
|
}),
|
||||||
|
_ => None,
|
||||||
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
|
})
|
||||||
mime_types.iter().find_map(|mime| Self::parse(mime))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Environment {
|
pub fn receive_offer(
|
||||||
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
|
event: DataControlDeviceEvent,
|
||||||
debug!("Copying item to clipboard: {item:?}");
|
handle: &LoopHandle<DData>,
|
||||||
|
tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||||
|
mut ddata: DispatchData,
|
||||||
|
) {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Could not get epoch, system time is probably very wrong")
|
||||||
|
.as_nanos();
|
||||||
|
|
||||||
// TODO: Proper device tracking
|
let offer = event.0;
|
||||||
let device = self.data_control_devices.first();
|
|
||||||
if let Some(device) = device {
|
|
||||||
let source = self
|
|
||||||
.data_control_device_manager_state
|
|
||||||
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
|
|
||||||
|
|
||||||
source.set_selection(&device.device);
|
let ddata = ddata
|
||||||
self.copy_paste_sources.push(source);
|
.get::<DData>()
|
||||||
|
.expect("Expected dispatch data to exist");
|
||||||
|
|
||||||
lock!(self.clipboard).replace(item);
|
let handle2 = handle.clone();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
let res = offer.with_mime_types(|mime_types| {
|
||||||
let value = match mime_type.category {
|
debug!("Offer mime types: {mime_types:?}");
|
||||||
MimeTypeCategory::Text => {
|
|
||||||
let mut txt = String::new();
|
|
||||||
file.read_to_string(&mut txt)?;
|
|
||||||
|
|
||||||
ClipboardValue::Text(txt)
|
|
||||||
}
|
|
||||||
MimeTypeCategory::Image => {
|
|
||||||
let mut bytes = vec![];
|
|
||||||
file.read_to_end(&mut bytes)?;
|
|
||||||
let bytes = Bytes::from(&bytes);
|
|
||||||
|
|
||||||
ClipboardValue::Image(bytes)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ClipboardItem {
|
|
||||||
id: Ironbar::unique_id(),
|
|
||||||
value,
|
|
||||||
mime_type: mime_type.value.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataControlDeviceHandler for Environment {
|
|
||||||
fn selection(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
data_device: DataControlDevice,
|
|
||||||
) {
|
|
||||||
debug!("Handler received selection event");
|
|
||||||
|
|
||||||
let mime_types = data_device.selection_mime_types();
|
|
||||||
|
|
||||||
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
||||||
return;
|
debug!("Skipping value provided by bar");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(offer) = data_device.selection_offer() {
|
let mime_type = MimeType::parse(mime_types);
|
||||||
self.selection_offers
|
debug!("Detected mime type: {mime_type:?}");
|
||||||
.push(SelectionOfferItem { offer, token: None });
|
|
||||||
|
|
||||||
let cur_offer = self
|
match mime_type {
|
||||||
.selection_offers
|
Some(mime_type) => {
|
||||||
.last_mut()
|
debug!("[{timestamp}] Sending clipboard read request ({mime_type:?})");
|
||||||
.expect("Failed to get current offer");
|
let read_pipe = offer.receive(mime_type.value.clone())?;
|
||||||
|
let source = handle.insert_source(read_pipe, move |(), file, ddata| {
|
||||||
|
debug!(
|
||||||
|
"[{timestamp}] Reading clipboard contents ({:?})",
|
||||||
|
&mime_type.category
|
||||||
|
);
|
||||||
|
match read_file(&mime_type, file) {
|
||||||
|
Ok(item) => {
|
||||||
|
send!(tx, Arc::new(item));
|
||||||
|
}
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
if let Some(src) = ddata.offer_tokens.remove(×tamp) {
|
||||||
lock!(self.clipboard).take();
|
handle2.remove(src);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ddata.offer_tokens.insert(timestamp, source);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
// send an event so the clipboard module is aware it's changed
|
// send an event so the clipboard module is aware it's changed
|
||||||
send!(
|
send!(
|
||||||
self.clipboard_tx,
|
tx,
|
||||||
Arc::new(ClipboardItem {
|
Arc::new(ClipboardItem {
|
||||||
id: usize::MAX,
|
id: usize::MAX,
|
||||||
mime_type: String::new(),
|
mime_type: String::new(),
|
||||||
value: ClipboardValue::Other
|
value: ClipboardValue::Other
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
|
||||||
let offer_clone = cur_offer.offer.clone();
|
|
||||||
|
|
||||||
let tx = self.clipboard_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) => {
|
|
||||||
let item = Arc::new(item);
|
|
||||||
lock!(clipboard).replace(item.clone());
|
|
||||||
send!(tx, 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);
|
|
||||||
}
|
|
||||||
Err(err) => error!("{err:?}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), Report>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataControlOfferHandler for Environment {
|
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||||
fn offer(
|
let value = match mime_type.category {
|
||||||
&mut self,
|
MimeTypeCategory::Text => {
|
||||||
_conn: &Connection,
|
let mut txt = String::new();
|
||||||
_qh: &QueueHandle<Self>,
|
file.read_to_string(&mut txt)?;
|
||||||
_offer: &mut DataControlDeviceOffer,
|
|
||||||
_mime_type: String,
|
|
||||||
) {
|
|
||||||
trace!("Handler received offer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataControlSourceHandler for Environment {
|
ClipboardValue::Text(txt)
|
||||||
fn accept_mime(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
_source: &ZwlrDataControlSourceV1,
|
|
||||||
mime: Option<String>,
|
|
||||||
) {
|
|
||||||
debug!("Accepted mime type: {mime:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_request(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
source: &ZwlrDataControlSourceV1,
|
|
||||||
mime: String,
|
|
||||||
write_pipe: WritePipe,
|
|
||||||
) {
|
|
||||||
debug!("Handler received source send request event ({mime})");
|
|
||||||
|
|
||||||
if let Some(item) = lock!(self.clipboard).clone() {
|
|
||||||
let fd = OwnedFd::from(write_pipe);
|
|
||||||
if self
|
|
||||||
.copy_paste_sources
|
|
||||||
.iter_mut()
|
|
||||||
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
|
|
||||||
{
|
|
||||||
trace!("Source found, writing to file");
|
|
||||||
|
|
||||||
let mut bytes = match &item.value {
|
|
||||||
ClipboardValue::Text(text) => text.as_bytes(),
|
|
||||||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
|
||||||
ClipboardValue::Other => panic!(
|
|
||||||
"{:?}",
|
|
||||||
io::Error::new(ErrorKind::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 mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
|
|
||||||
|
|
||||||
trace!("Num bytes: {}", bytes.len());
|
|
||||||
|
|
||||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
|
||||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
while !bytes.is_empty() {
|
|
||||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
|
||||||
|
|
||||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
|
||||||
|
|
||||||
epoll_fd
|
|
||||||
.wait(&mut events, 100)
|
|
||||||
.expect("Failed to wait to epoll");
|
|
||||||
|
|
||||||
match file.write(chunk) {
|
|
||||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err:?}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("Failed to find source");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
MimeTypeCategory::Image => {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
file.read_to_end(&mut bytes)?;
|
||||||
|
let bytes = Bytes::from(&bytes);
|
||||||
|
|
||||||
fn cancelled(
|
ClipboardValue::Image(bytes)
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
source: &ZwlrDataControlSourceV1,
|
|
||||||
) {
|
|
||||||
debug!("Handler received source cancelled event");
|
|
||||||
|
|
||||||
self.copy_paste_sources
|
|
||||||
.iter()
|
|
||||||
.position(|s| s.inner() == source)
|
|
||||||
.map(|pos| self.copy_paste_sources.remove(pos));
|
|
||||||
source.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to increase the fd pipe size to the requested number of bytes.
|
|
||||||
/// The kernel will automatically round this up to the nearest page size.
|
|
||||||
/// If the requested size is larger than the kernel max (normally 1MB),
|
|
||||||
/// it will be clamped at this.
|
|
||||||
///
|
|
||||||
/// Returns the new size if succeeded
|
|
||||||
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
|
||||||
// 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")
|
|
||||||
.trim()
|
|
||||||
.parse::<usize>()
|
|
||||||
.expect("Failed to parse pipe-max-size contents");
|
|
||||||
|
|
||||||
let size = min(size, max_pipe_size);
|
|
||||||
|
|
||||||
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
|
|
||||||
|
|
||||||
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))?;
|
|
||||||
trace!("New pipe size: {res}");
|
|
||||||
|
|
||||||
if res < size as i32 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
size as i32
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(new_size)
|
Ok(ClipboardItem {
|
||||||
|
id: get_id(),
|
||||||
|
value,
|
||||||
|
mime_type: mime_type.value.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,186 +1,74 @@
|
|||||||
use super::manager::DataControlDeviceManagerState;
|
|
||||||
use crate::lock;
|
use crate::lock;
|
||||||
use nix::fcntl::OFlag;
|
use nix::fcntl::OFlag;
|
||||||
use nix::unistd::{close, pipe2};
|
use nix::unistd::{close, pipe2};
|
||||||
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
|
use smithay_client_toolkit::data_device::ReadPipe;
|
||||||
use smithay_client_toolkit::data_device_manager::ReadPipe;
|
use std::io;
|
||||||
use std::ops::DerefMut;
|
use std::os::fd::FromRawFd;
|
||||||
use std::os::fd::{BorrowedFd, FromRawFd};
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::{trace, warn};
|
use tracing::warn;
|
||||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
use wayland_client::Main;
|
||||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||||
Event, ZwlrDataControlOfferV1,
|
Event, ZwlrDataControlOfferV1,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UndeterminedOffer {
|
struct Inner {
|
||||||
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>,
|
mime_types: Vec<String>,
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for UndeterminedOffer {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.data_offer == other.data_offer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SelectionOffer {
|
pub struct DataControlOffer {
|
||||||
pub data_offer: ZwlrDataControlOfferV1,
|
inner: Arc<Mutex<Inner>>,
|
||||||
|
pub(crate) offer: ZwlrDataControlOfferV1,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for SelectionOffer {
|
impl DataControlOffer {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> Self {
|
||||||
self.data_offer == other.data_offer
|
let inner = Arc::new(Mutex::new(Inner {
|
||||||
}
|
mime_types: Vec::new(),
|
||||||
}
|
}));
|
||||||
|
|
||||||
impl SelectionOffer {
|
{
|
||||||
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
|
let inner = inner.clone();
|
||||||
unsafe { receive(&self.data_offer, mime_type) }.map_err(DataOfferError::Io)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
offer.quick_assign(move |_, event, _| {
|
||||||
pub enum DataControlDeviceOffer {
|
let mut inner = lock!(inner);
|
||||||
Selection(SelectionOffer),
|
if let Event::Offer { mime_type } = event {
|
||||||
Undetermined(UndeterminedOffer),
|
inner.mime_types.push(mime_type);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for DataControlDeviceOffer {
|
Self {
|
||||||
fn default() -> Self {
|
offer: offer.detach(),
|
||||||
Self::Undetermined(UndeterminedOffer { data_offer: None })
|
inner,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct DataControlOfferData {
|
|
||||||
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct DataControlDeviceOfferInner {
|
|
||||||
pub(crate) offer: DataControlDeviceOffer,
|
|
||||||
pub(crate) mime_types: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataControlOfferData {
|
|
||||||
pub(crate) fn push_mime_type(&self, mime_type: String) {
|
|
||||||
lock!(self.inner).mime_types.push(mime_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn to_selection_offer(&self) {
|
|
||||||
let mut inner = lock!(self.inner);
|
|
||||||
match &mut inner.deref_mut().offer {
|
|
||||||
DataControlDeviceOffer::Selection(_) => {}
|
|
||||||
DataControlDeviceOffer::Undetermined(o) => {
|
|
||||||
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
|
|
||||||
data_offer: o.data_offer.clone().expect("Missing current data offer"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
|
pub fn with_mime_types<F, T>(&self, f: F) -> T
|
||||||
let mut inner = lock!(self.inner);
|
where
|
||||||
match &mut inner.deref_mut().offer {
|
F: FnOnce(&[String]) -> T,
|
||||||
DataControlDeviceOffer::Selection(_) => {
|
{
|
||||||
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
|
let inner = lock!(self.inner);
|
||||||
data_offer: Some(offer.clone()),
|
f(&inner.mime_types)
|
||||||
});
|
}
|
||||||
}
|
|
||||||
DataControlDeviceOffer::Undetermined(o) => {
|
pub fn receive(&self, mime_type: String) -> io::Result<ReadPipe> {
|
||||||
o.data_offer = Some(offer.clone());
|
// create a pipe
|
||||||
}
|
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||||
|
|
||||||
|
self.offer.receive(mime_type, writefd);
|
||||||
|
|
||||||
|
if let Err(err) = close(writefd) {
|
||||||
|
warn!("Failed to close write pipe: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait DataControlOfferDataExt {
|
impl Drop for DataControlOffer {
|
||||||
fn data_control_offer_data(&self) -> &DataControlOfferData;
|
fn drop(&mut self) {
|
||||||
fn mime_types(&self) -> Vec<String>;
|
self.offer.destroy();
|
||||||
fn as_selection_offer(&self) -> Option<SelectionOffer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataControlOfferDataExt for DataControlOfferData {
|
|
||||||
fn data_control_offer_data(&self) -> &DataControlOfferData {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mime_types(&self) -> Vec<String> {
|
|
||||||
lock!(self.inner).mime_types.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_selection_offer(&self) -> Option<SelectionOffer> {
|
|
||||||
match &lock!(self.inner).offer {
|
|
||||||
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
|
|
||||||
DataControlDeviceOffer::Undetermined(_) => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler trait for `DataOffer` events.
|
|
||||||
///
|
|
||||||
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
|
|
||||||
pub trait DataControlOfferHandler: Sized {
|
|
||||||
// Called for each mime type the data offer advertises.
|
|
||||||
fn offer(
|
|
||||||
&mut self,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
offer: &mut DataControlDeviceOffer,
|
|
||||||
mime_type: String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
|
|
||||||
U: DataControlOfferDataExt,
|
|
||||||
{
|
|
||||||
fn event(
|
|
||||||
state: &mut D,
|
|
||||||
_offer: &ZwlrDataControlOfferV1,
|
|
||||||
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
|
|
||||||
data: &U,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<D>,
|
|
||||||
) {
|
|
||||||
let data = data.data_control_offer_data();
|
|
||||||
|
|
||||||
if let Event::Offer { mime_type } = event {
|
|
||||||
trace!("Adding new offer with type '{mime_type}'");
|
|
||||||
data.push_mime_type(mime_type.clone());
|
|
||||||
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request to receive the data of a given mime type.
|
|
||||||
///
|
|
||||||
/// You can do this several times, as a reaction to motion of
|
|
||||||
/// the dnd cursor, or to inspect the data in order to choose your
|
|
||||||
/// response.
|
|
||||||
///
|
|
||||||
/// Note that you should *not* read the contents right away in a
|
|
||||||
/// blocking way, as you may deadlock your application doing so.
|
|
||||||
/// At least make sure you flush your events to the server before
|
|
||||||
/// doing so.
|
|
||||||
///
|
|
||||||
/// 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> {
|
|
||||||
// create a pipe
|
|
||||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
|
||||||
|
|
||||||
offer.receive(mime_type, BorrowedFd::borrow_raw(writefd));
|
|
||||||
|
|
||||||
if let Err(err) = close(writefd) {
|
|
||||||
warn!("Failed to close write pipe: {}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FromRawFd::from_raw_fd(readfd))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,101 +1,54 @@
|
|||||||
use super::device::DataControlDevice;
|
use smithay_client_toolkit::data_device::WritePipe;
|
||||||
use super::manager::DataControlDeviceManagerState;
|
use std::os::fd::FromRawFd;
|
||||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
use wayland_client::{Attached, DispatchData};
|
||||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
use wayland_protocols::wlr::unstable::data_control::v1::client::{
|
||||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
|
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||||
Event, ZwlrDataControlSourceV1,
|
zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
fn data_control_source_impl<F>(
|
||||||
pub struct DataControlSourceData {}
|
source: &ZwlrDataControlSourceV1,
|
||||||
|
event: Event,
|
||||||
pub trait DataControlSourceDataExt: Send + Sync {
|
implem: &mut F,
|
||||||
fn data_source_data(&self) -> &DataControlSourceData;
|
ddata: DispatchData,
|
||||||
}
|
) where
|
||||||
|
F: FnMut(String, WritePipe, DispatchData),
|
||||||
impl DataControlSourceDataExt for DataControlSourceData {
|
{
|
||||||
fn data_source_data(&self) -> &DataControlSourceData {
|
match event {
|
||||||
self
|
Event::Send { mime_type, fd } => {
|
||||||
|
let pipe = unsafe { FromRawFd::from_raw_fd(fd) };
|
||||||
|
implem(mime_type, pipe, ddata);
|
||||||
|
}
|
||||||
|
Event::Cancelled => source.destroy(),
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler trait for `DataSource` events.
|
pub struct DataControlSource {
|
||||||
///
|
pub(crate) source: ZwlrDataControlSourceV1,
|
||||||
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
|
|
||||||
pub trait DataControlSourceHandler: Sized {
|
|
||||||
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
|
||||||
fn accept_mime(
|
|
||||||
&mut self,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
source: &ZwlrDataControlSourceV1,
|
|
||||||
mime: Option<String>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The client has requested the data for this source to be sent.
|
|
||||||
/// Send the data, then close the fd.
|
|
||||||
fn send_request(
|
|
||||||
&mut self,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
source: &ZwlrDataControlSourceV1,
|
|
||||||
mime: String,
|
|
||||||
fd: WritePipe,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The data source is no longer valid
|
|
||||||
/// Cleanup & destroy this resource
|
|
||||||
fn cancelled(
|
|
||||||
&mut self,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
source: &ZwlrDataControlSourceV1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
|
impl DataControlSource {
|
||||||
where
|
pub fn new<F>(
|
||||||
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
|
manager: &Attached<ZwlrDataControlManagerV1>,
|
||||||
U: DataControlSourceDataExt,
|
mime_types: Vec<String>,
|
||||||
{
|
mut callback: F,
|
||||||
fn event(
|
) -> Self
|
||||||
state: &mut D,
|
where
|
||||||
source: &ZwlrDataControlSourceV1,
|
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||||
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
|
{
|
||||||
_data: &U,
|
let source = manager.create_data_source();
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<D>,
|
source.quick_assign(move |source, evt, ddata| {
|
||||||
) {
|
data_control_source_impl(&source, evt, &mut callback, ddata);
|
||||||
match event {
|
});
|
||||||
Event::Send { mime_type, fd } => {
|
|
||||||
state.send_request(conn, qh, source, mime_type, fd.into());
|
for mime_type in mime_types {
|
||||||
}
|
source.offer(mime_type);
|
||||||
Event::Cancelled => {
|
}
|
||||||
state.cancelled(conn, qh, source);
|
|
||||||
}
|
Self {
|
||||||
_ => {}
|
source: source.detach(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct CopyPasteSource {
|
|
||||||
pub(crate) inner: ZwlrDataControlSourceV1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CopyPasteSource {
|
|
||||||
/// Set the selection of the provided data device as a response to the event with with provided serial.
|
|
||||||
pub fn set_selection(&self, device: &DataControlDevice) {
|
|
||||||
device.device.set_selection(Some(&self.inner));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for CopyPasteSource {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.inner.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,177 +1,152 @@
|
|||||||
use super::manager::ToplevelManagerState;
|
|
||||||
use crate::{lock, Ironbar};
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use wayland_client::protocol::wl_output::WlOutput;
|
use wayland_client::{DispatchData, Main};
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1};
|
||||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
use crate::write_lock;
|
||||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
|
|
||||||
Event, ZwlrForeignToplevelHandleV1,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
const STATE_ACTIVE: u32 = 2;
|
||||||
pub struct ToplevelHandle {
|
const STATE_FULLSCREEN: u32 = 3;
|
||||||
pub handle: ZwlrForeignToplevelHandleV1,
|
|
||||||
|
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||||
|
|
||||||
|
fn get_id() -> usize {
|
||||||
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for ToplevelHandle {
|
#[derive(Debug, Clone, Default)]
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.handle == other.handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToplevelHandle {
|
|
||||||
pub fn info(&self) -> Option<ToplevelInfo> {
|
|
||||||
trace!("Retrieving handle info");
|
|
||||||
|
|
||||||
let data = self.handle.data::<ToplevelHandleData>()?;
|
|
||||||
data.info()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus(&self, seat: &WlSeat) {
|
|
||||||
trace!("Activating handle");
|
|
||||||
self.handle.activate(seat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ToplevelHandleData {
|
|
||||||
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToplevelHandleData {
|
|
||||||
fn info(&self) -> Option<ToplevelInfo> {
|
|
||||||
lock!(self.inner).current_info.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ToplevelHandleDataInner {
|
|
||||||
initial_done: bool,
|
|
||||||
output: Option<WlOutput>,
|
|
||||||
|
|
||||||
current_info: Option<ToplevelInfo>,
|
|
||||||
pending_info: ToplevelInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ToplevelInfo {
|
pub struct ToplevelInfo {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub active: bool,
|
||||||
pub fullscreen: bool,
|
pub fullscreen: bool,
|
||||||
pub focused: bool,
|
|
||||||
|
ready: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ToplevelInfo {
|
impl ToplevelInfo {
|
||||||
fn default() -> Self {
|
fn new() -> Self {
|
||||||
|
let id = get_id();
|
||||||
Self {
|
Self {
|
||||||
id: Ironbar::unique_id(),
|
id,
|
||||||
app_id: String::new(),
|
..Default::default()
|
||||||
title: String::new(),
|
|
||||||
fullscreen: false,
|
|
||||||
focused: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ToplevelHandleDataExt {
|
pub struct Toplevel;
|
||||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ToplevelEvent {
|
||||||
|
pub toplevel: ToplevelInfo,
|
||||||
|
pub change: ToplevelChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToplevelHandleDataExt for ToplevelHandleData {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData {
|
pub enum ToplevelChange {
|
||||||
self
|
New,
|
||||||
}
|
Close,
|
||||||
|
Title(String),
|
||||||
|
Focus(bool),
|
||||||
|
Fullscreen(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ToplevelHandleHandler: Sized {
|
fn toplevel_implem<F>(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData)
|
||||||
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
|
||||||
|
|
||||||
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
|
||||||
|
|
||||||
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
|
|
||||||
where
|
where
|
||||||
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
|
F: FnMut(ToplevelEvent, DispatchData),
|
||||||
U: ToplevelHandleDataExt,
|
|
||||||
{
|
{
|
||||||
fn event(
|
trace!("event: {event:?} (info: {info:?})");
|
||||||
state: &mut D,
|
|
||||||
handle: &ZwlrForeignToplevelHandleV1,
|
|
||||||
event: Event,
|
|
||||||
data: &U,
|
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<D>,
|
|
||||||
) {
|
|
||||||
const STATE_ACTIVE: u32 = 2;
|
|
||||||
const STATE_FULLSCREEN: u32 = 3;
|
|
||||||
|
|
||||||
let data = data.toplevel_handle_data();
|
let change = match event {
|
||||||
|
Event::AppId { app_id } => {
|
||||||
|
info.app_id = app_id;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Event::Title { title } => {
|
||||||
|
info.title = title.clone();
|
||||||
|
|
||||||
trace!("Processing handle event: {event:?}");
|
if info.ready {
|
||||||
|
Some(ToplevelChange::Title(title))
|
||||||
match event {
|
} else {
|
||||||
Event::Title { title } => {
|
None
|
||||||
lock!(data.inner).pending_info.title = title;
|
|
||||||
}
|
}
|
||||||
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
|
}
|
||||||
Event::State { state } => {
|
Event::State { state } => {
|
||||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||||
// the u32 then represents a value in the `State` enum.
|
// the u32 then represents a value in the `State` enum.
|
||||||
assert_eq!(state.len() % 4, 0);
|
assert_eq!(state.len() % 4, 0);
|
||||||
let state = (0..state.len() / 4)
|
|
||||||
.map(|i| {
|
|
||||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
|
||||||
.try_into()
|
|
||||||
.expect("Received invalid state length");
|
|
||||||
u32::from_le_bytes(slice)
|
|
||||||
})
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
|
let state = (0..state.len() / 4)
|
||||||
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN);
|
.map(|i| {
|
||||||
}
|
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||||
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
|
.try_into()
|
||||||
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
|
.expect("Received invalid state length");
|
||||||
Event::Closed => state.remove_handle(
|
u32::from_le_bytes(slice)
|
||||||
conn,
|
})
|
||||||
qh,
|
.collect::<HashSet<_>>();
|
||||||
ToplevelHandle {
|
|
||||||
handle: handle.clone(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Event::Done => {
|
|
||||||
{
|
|
||||||
let pending_info = lock!(data.inner).pending_info.clone();
|
|
||||||
lock!(data.inner).current_info = Some(pending_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
if lock!(data.inner).initial_done {
|
let new_active = state.contains(&STATE_ACTIVE);
|
||||||
state.update_handle(
|
let new_fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||||
conn,
|
|
||||||
qh,
|
let change = if info.ready && new_active != info.active {
|
||||||
ToplevelHandle {
|
Some(ToplevelChange::Focus(new_active))
|
||||||
handle: handle.clone(),
|
} else if info.ready && new_fullscreen != info.fullscreen {
|
||||||
},
|
Some(ToplevelChange::Fullscreen(new_fullscreen))
|
||||||
);
|
} else {
|
||||||
} else {
|
None
|
||||||
lock!(data.inner).initial_done = true;
|
};
|
||||||
state.new_handle(
|
|
||||||
conn,
|
info.active = new_active;
|
||||||
qh,
|
info.fullscreen = new_fullscreen;
|
||||||
ToplevelHandle {
|
|
||||||
handle: handle.clone(),
|
change
|
||||||
},
|
}
|
||||||
);
|
Event::Closed => {
|
||||||
}
|
if info.ready {
|
||||||
|
Some(ToplevelChange::Close)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::OutputEnter { output: _ }
|
||||||
|
| Event::OutputLeave { output: _ }
|
||||||
|
| Event::Parent { parent: _ } => None,
|
||||||
|
Event::Done => {
|
||||||
|
if info.ready || info.app_id.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
info.ready = true;
|
||||||
|
Some(ToplevelChange::New)
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Event processed");
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(change) = change {
|
||||||
|
let event = ToplevelEvent {
|
||||||
|
change,
|
||||||
|
toplevel: info.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
implem(event, ddata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toplevel {
|
||||||
|
pub fn init<F>(handle: &Main<ZwlrForeignToplevelHandleV1>, mut callback: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut(ToplevelEvent, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
let inner = Arc::new(RwLock::new(ToplevelInfo::new()));
|
||||||
|
|
||||||
|
handle.quick_assign(move |_handle, event, ddata| {
|
||||||
|
let mut inner = write_lock!(inner);
|
||||||
|
toplevel_implem(event, &mut inner, &mut callback, ddata);
|
||||||
|
});
|
||||||
|
|
||||||
|
Self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,163 @@
|
|||||||
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
|
use super::handle::{Toplevel, ToplevelEvent};
|
||||||
use smithay_client_toolkit::error::GlobalError;
|
use crate::wayland::LazyGlobal;
|
||||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||||
use std::marker::PhantomData;
|
use std::cell::RefCell;
|
||||||
use tracing::{debug, warn};
|
use std::rc::{self, Rc};
|
||||||
use wayland_client::globals::{BindError, GlobalList};
|
use tracing::warn;
|
||||||
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
|
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
|
use wayland_client::{Attached, DispatchData};
|
||||||
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||||
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
|
zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ToplevelManagerState<V = ToplevelHandleData> {
|
struct ToplevelHandlerInner {
|
||||||
manager: ZwlrForeignToplevelManagerV1,
|
manager: LazyGlobal<ZwlrForeignToplevelManagerV1>,
|
||||||
_phantom: PhantomData<V>,
|
registry: Option<Attached<WlRegistry>>,
|
||||||
|
toplevels: Vec<Toplevel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToplevelManagerState {
|
impl ToplevelHandlerInner {
|
||||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
const fn new() -> Self {
|
||||||
where
|
let toplevels = vec![];
|
||||||
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
|
|
||||||
{
|
|
||||||
let manager = globals.bind(qh, 1..=3, GlobalData)?;
|
|
||||||
debug!("Bound to ZwlForeignToplevelManagerV1 global");
|
|
||||||
Ok(Self {
|
|
||||||
manager,
|
|
||||||
_phantom: PhantomData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ToplevelManagerHandler: Sized {
|
Self {
|
||||||
/// Advertises a new toplevel.
|
registry: None,
|
||||||
fn toplevel(
|
manager: LazyGlobal::Unknown,
|
||||||
&mut self,
|
toplevels,
|
||||||
conn: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
manager: ToplevelManagerState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
|
|
||||||
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
|
|
||||||
Ok(self.manager.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
|
|
||||||
where
|
|
||||||
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
|
|
||||||
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
|
|
||||||
+ ToplevelManagerHandler
|
|
||||||
+ ToplevelHandleHandler
|
|
||||||
+ 'static,
|
|
||||||
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
|
|
||||||
{
|
|
||||||
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
|
|
||||||
0 => (ZwlrForeignToplevelHandleV1, V::default())
|
|
||||||
]);
|
|
||||||
|
|
||||||
fn event(
|
|
||||||
state: &mut D,
|
|
||||||
toplevel_manager: &ZwlrForeignToplevelManagerV1,
|
|
||||||
event: Event,
|
|
||||||
_data: &GlobalData,
|
|
||||||
conn: &Connection,
|
|
||||||
qhandle: &QueueHandle<D>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
Event::Toplevel { toplevel: _ } => {
|
|
||||||
state.toplevel(
|
|
||||||
conn,
|
|
||||||
qhandle,
|
|
||||||
ToplevelManagerState {
|
|
||||||
manager: toplevel_manager.clone(),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Event::Finished => {
|
|
||||||
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ToplevelHandler {
|
||||||
|
inner: Rc<RefCell<ToplevelHandlerInner>>,
|
||||||
|
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToplevelHandler {
|
||||||
|
pub fn init() -> Self {
|
||||||
|
let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new()));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
status_listeners: Rc::new(RefCell::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalHandler<ZwlrForeignToplevelManagerV1> for ToplevelHandler {
|
||||||
|
fn created(
|
||||||
|
&mut self,
|
||||||
|
registry: Attached<WlRegistry>,
|
||||||
|
id: u32,
|
||||||
|
version: u32,
|
||||||
|
_ddata: DispatchData,
|
||||||
|
) {
|
||||||
|
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||||
|
if inner.registry.is_none() {
|
||||||
|
inner.registry = Some(registry);
|
||||||
|
}
|
||||||
|
if matches!(inner.manager, LazyGlobal::Unknown) {
|
||||||
|
inner.manager = LazyGlobal::Seen { id, version }
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self) -> Option<Attached<ZwlrForeignToplevelManagerV1>> {
|
||||||
|
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||||
|
|
||||||
|
match inner.manager {
|
||||||
|
LazyGlobal::Bound(ref mgr) => Some(mgr.clone()),
|
||||||
|
LazyGlobal::Unknown => None,
|
||||||
|
LazyGlobal::Seen { id, version } => {
|
||||||
|
let registry = inner.registry.as_ref().expect("Failed to get registry");
|
||||||
|
// current max protocol version = 3
|
||||||
|
let version = std::cmp::min(version, 3);
|
||||||
|
let manager = registry.bind::<ZwlrForeignToplevelManagerV1>(version, id);
|
||||||
|
|
||||||
|
{
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
let status_listeners = self.status_listeners.clone();
|
||||||
|
|
||||||
|
manager.quick_assign(move |_, event, _ddata| {
|
||||||
|
let mut inner = RefCell::borrow_mut(&inner);
|
||||||
|
let status_listeners = status_listeners.clone();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
zwlr_foreign_toplevel_manager_v1::Event::Toplevel {
|
||||||
|
toplevel: handle,
|
||||||
|
} => {
|
||||||
|
let toplevel =
|
||||||
|
Toplevel::init(&handle.clone(), move |event, ddata| {
|
||||||
|
notify_status_listeners(
|
||||||
|
&handle,
|
||||||
|
&event,
|
||||||
|
ddata,
|
||||||
|
&status_listeners,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
inner.toplevels.push(toplevel);
|
||||||
|
}
|
||||||
|
zwlr_foreign_toplevel_manager_v1::Event::Finished => {}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.manager = LazyGlobal::Bound((*manager).clone());
|
||||||
|
Some((*manager).clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToplevelStatusCallback =
|
||||||
|
dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||||
|
|
||||||
|
/// Notifies the callbacks of an event on the toplevel
|
||||||
|
fn notify_status_listeners(
|
||||||
|
toplevel: &ZwlrForeignToplevelHandleV1,
|
||||||
|
event: &ToplevelEvent,
|
||||||
|
mut ddata: DispatchData,
|
||||||
|
listeners: &RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>,
|
||||||
|
) {
|
||||||
|
listeners.borrow_mut().retain(|lst| {
|
||||||
|
rc::Weak::upgrade(lst).map_or(false, |cb| {
|
||||||
|
(cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow());
|
||||||
|
true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToplevelStatusListener {
|
||||||
|
_cb: Rc<RefCell<ToplevelStatusCallback>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToplevelHandling {
|
||||||
|
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||||
|
where
|
||||||
|
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToplevelHandling for ToplevelHandler {
|
||||||
|
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||||
|
where
|
||||||
|
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
|
||||||
|
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
|
||||||
|
ToplevelStatusListener { _cb: rc }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener
|
||||||
|
where
|
||||||
|
E: ToplevelHandling,
|
||||||
|
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||||
|
{
|
||||||
|
env.with_inner(move |inner| ToplevelHandling::listen(inner, f))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,84 +1,39 @@
|
|||||||
|
use std::sync::RwLock;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use tokio::sync::broadcast::Sender;
|
||||||
|
use tracing::trace;
|
||||||
|
use super::Env;
|
||||||
|
use handle::{ToplevelEvent, ToplevelChange, ToplevelInfo};
|
||||||
|
use manager::{ToplevelHandling, ToplevelStatusListener};
|
||||||
|
use wayland_client::DispatchData;
|
||||||
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||||
|
use crate::{send, write_lock};
|
||||||
|
|
||||||
pub mod handle;
|
pub mod handle;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
|
||||||
use self::handle::ToplevelHandleHandler;
|
impl ToplevelHandling for Env {
|
||||||
use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
|
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||||
use crate::clients::wayland::Environment;
|
where
|
||||||
use tracing::{debug, error, trace};
|
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||||
use wayland_client::{Connection, QueueHandle};
|
{
|
||||||
|
self.toplevel.listen(f)
|
||||||
use crate::send;
|
|
||||||
pub use handle::{ToplevelHandle, ToplevelInfo};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ToplevelEvent {
|
|
||||||
New(ToplevelHandle),
|
|
||||||
Update(ToplevelHandle),
|
|
||||||
Remove(ToplevelHandle),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToplevelManagerHandler for Environment {
|
|
||||||
fn toplevel(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
_manager: ToplevelManagerState,
|
|
||||||
) {
|
|
||||||
debug!("Manager received new handle");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToplevelHandleHandler for Environment {
|
pub fn update_toplevels(
|
||||||
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
|
toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>,
|
||||||
trace!("Handler received new handle");
|
handle: ZwlrForeignToplevelHandleV1,
|
||||||
|
event: ToplevelEvent,
|
||||||
|
tx: &Sender<ToplevelEvent>,
|
||||||
|
) {
|
||||||
|
trace!("Received toplevel event: {:?}", event);
|
||||||
|
|
||||||
match handle.info() {
|
if event.change == ToplevelChange::Close {
|
||||||
Some(info) => {
|
write_lock!(toplevels).remove(&event.toplevel.id);
|
||||||
trace!("Adding new handle: {info:?}");
|
} else {
|
||||||
self.handles.insert(info.id, handle.clone());
|
write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
|
||||||
send!(self.toplevel_tx, ToplevelEvent::New(handle));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Handle is missing information!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_handle(
|
send!(tx, event);
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
handle: ToplevelHandle,
|
|
||||||
) {
|
|
||||||
trace!("Handler received handle update");
|
|
||||||
|
|
||||||
match handle.info() {
|
|
||||||
Some(info) => {
|
|
||||||
trace!("Updating handle: {info:?}");
|
|
||||||
self.handles.insert(info.id, handle.clone());
|
|
||||||
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Handle is missing information!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_handle(
|
|
||||||
&mut self,
|
|
||||||
_conn: &Connection,
|
|
||||||
_qh: &QueueHandle<Self>,
|
|
||||||
handle: ToplevelHandle,
|
|
||||||
) {
|
|
||||||
debug!("Handler received handle close");
|
|
||||||
match handle.info() {
|
|
||||||
Some(info) => {
|
|
||||||
self.handles.remove(&info.id);
|
|
||||||
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Handle is missing information!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
use glib::signal::Inhibit;
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
use crate::script::{Script, ScriptInput};
|
use crate::script::{Script, ScriptInput};
|
||||||
use glib::Propagation;
|
use crate::send;
|
||||||
use gtk::gdk::ScrollDirection;
|
use gtk::gdk::ScrollDirection;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
use gtk::{GestureClick, Orientation, Revealer, RevealerTransitionType, Widget};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
/// Common configuration options
|
/// Common configuration options
|
||||||
/// which can be set on every module.
|
/// which can be set on every module.
|
||||||
#[derive(Debug, Default, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct CommonConfig {
|
pub struct CommonConfig {
|
||||||
pub class: Option<String>,
|
pub show_if: Option<ScriptInput>,
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
pub show_if: Option<DynamicBool>,
|
|
||||||
pub transition_type: Option<TransitionType>,
|
pub transition_type: Option<TransitionType>,
|
||||||
pub transition_duration: Option<u32>,
|
pub transition_duration: Option<u32>,
|
||||||
|
|
||||||
@@ -39,16 +38,17 @@ pub enum TransitionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TransitionType {
|
impl TransitionType {
|
||||||
pub const fn to_revealer_transition_type(
|
pub fn to_revealer_transition_type(&self, orientation: Orientation) -> RevealerTransitionType {
|
||||||
&self,
|
|
||||||
orientation: Orientation,
|
|
||||||
) -> RevealerTransitionType {
|
|
||||||
match (self, orientation) {
|
match (self, orientation) {
|
||||||
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft,
|
(TransitionType::SlideStart, Orientation::Horizontal) => {
|
||||||
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
RevealerTransitionType::SlideLeft
|
||||||
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight,
|
}
|
||||||
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
(TransitionType::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
||||||
(Self::Crossfade, _) => RevealerTransitionType::Crossfade,
|
(TransitionType::SlideEnd, Orientation::Horizontal) => {
|
||||||
|
RevealerTransitionType::SlideRight
|
||||||
|
}
|
||||||
|
(TransitionType::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
||||||
|
(TransitionType::Crossfade, _) => RevealerTransitionType::Crossfade,
|
||||||
_ => RevealerTransitionType::None,
|
_ => RevealerTransitionType::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,14 +56,16 @@ impl TransitionType {
|
|||||||
|
|
||||||
impl CommonConfig {
|
impl CommonConfig {
|
||||||
/// Configures the module's container according to the common config options.
|
/// Configures the module's container according to the common config options.
|
||||||
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) {
|
pub fn install<W: IsA<Widget>>(mut self, widget: &W, revealer: &Revealer) {
|
||||||
self.install_show_if(container, revealer);
|
self.install_show_if(widget, revealer);
|
||||||
|
|
||||||
let left_click_script = self.on_click_left.map(Script::new_polling);
|
let left_click_script = self.on_click_left.map(Script::new_polling);
|
||||||
let middle_click_script = self.on_click_middle.map(Script::new_polling);
|
let middle_click_script = self.on_click_middle.map(Script::new_polling);
|
||||||
let right_click_script = self.on_click_right.map(Script::new_polling);
|
let right_click_script = self.on_click_right.map(Script::new_polling);
|
||||||
|
|
||||||
container.connect_button_press_event(move |_, event| {
|
let gesture = GestureClick::new();
|
||||||
|
|
||||||
|
gesture.connect_pressed(move |_, event| {
|
||||||
let script = match event.button() {
|
let script = match event.button() {
|
||||||
1 => left_click_script.as_ref(),
|
1 => left_click_script.as_ref(),
|
||||||
2 => middle_click_script.as_ref(),
|
2 => middle_click_script.as_ref(),
|
||||||
@@ -75,14 +77,12 @@ impl CommonConfig {
|
|||||||
trace!("Running on-click script: {}", event.button());
|
trace!("Running on-click script: {}", event.button());
|
||||||
script.run_as_oneshot(None);
|
script.run_as_oneshot(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||||
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
||||||
|
|
||||||
container.connect_scroll_event(move |_, event| {
|
widget.connect_scroll_event(move |_, event| {
|
||||||
let script = match event.direction() {
|
let script = match event.direction() {
|
||||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||||
@@ -94,15 +94,15 @@ impl CommonConfig {
|
|||||||
script.run_as_oneshot(None);
|
script.run_as_oneshot(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
macro_rules! install_oneshot {
|
macro_rules! install_oneshot {
|
||||||
($option:expr, $method:ident) => {
|
($option:expr, $method:ident) => {
|
||||||
$option.map(Script::new_polling).map(|script| {
|
$option.map(Script::new_polling).map(|script| {
|
||||||
container.$method(move |_, _| {
|
widget.$method(move |_, _| {
|
||||||
script.run_as_oneshot(None);
|
script.run_as_oneshot(None);
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -112,36 +112,48 @@ impl CommonConfig {
|
|||||||
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
||||||
|
|
||||||
if let Some(tooltip) = self.tooltip {
|
if let Some(tooltip) = self.tooltip {
|
||||||
let container = container.clone();
|
let container = widget.clone();
|
||||||
dynamic_string(&tooltip, move |string| {
|
DynamicString::new(&tooltip, move |string| {
|
||||||
container.set_tooltip_text(Some(&string));
|
container.set_tooltip_text(Some(&string));
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) {
|
fn install_show_if<W: IsA<Widget>>(&mut self, widget: &W, revealer: &Revealer) {
|
||||||
self.show_if.take().map_or_else(
|
self.show_if.take().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
container.show_all();
|
widget.set_visible(true)
|
||||||
},
|
},
|
||||||
|show_if| {
|
|show_if| {
|
||||||
let container = container.clone();
|
let script = Script::new_polling(show_if);
|
||||||
|
let widget = widget.clone();
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(None, |_, success| {
|
||||||
|
send!(tx, success);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let revealer = revealer.clone();
|
let revealer = revealer.clone();
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
|
|
||||||
show_if.subscribe(move |success| {
|
rx.attach(None, move |success| {
|
||||||
if success {
|
if success {
|
||||||
container.show_all();
|
container.show_all();
|
||||||
}
|
}
|
||||||
revealer.set_reveal_child(success);
|
revealer.set_reveal_child(success);
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
revealer.connect_child_revealed_notify(move |revealer| {
|
revealer.connect_child_revealed_notify(move |revealer| {
|
||||||
if !revealer.reveals_child() {
|
if !revealer.reveals_child() {
|
||||||
container.hide();
|
container.hide()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,17 +21,16 @@ use crate::modules::tray::TrayModule;
|
|||||||
use crate::modules::upower::UpowerModule;
|
use crate::modules::upower::UpowerModule;
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
use crate::modules::workspaces::WorkspacesModule;
|
use crate::modules::workspaces::WorkspacesModule;
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub use self::common::{CommonConfig, TransitionType};
|
pub use self::common::{CommonConfig, TransitionType};
|
||||||
pub use self::truncate::TruncateMode;
|
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum ModuleConfig {
|
pub enum ModuleConfig {
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clock")]
|
||||||
Clipboard(Box<ClipboardModule>),
|
Clipboard(Box<ClipboardModule>),
|
||||||
#[cfg(feature = "clock")]
|
#[cfg(feature = "clock")]
|
||||||
Clock(Box<ClockModule>),
|
Clock(Box<ClockModule>),
|
||||||
@@ -97,18 +96,10 @@ pub struct Config {
|
|||||||
pub margin: MarginConfig,
|
pub margin: MarginConfig,
|
||||||
#[serde(default = "default_popup_gap")]
|
#[serde(default = "default_popup_gap")]
|
||||||
pub popup_gap: i32,
|
pub popup_gap: i32,
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub start_hidden: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub autohide: Option<u64>,
|
|
||||||
|
|
||||||
/// GTK icon theme to use.
|
/// GTK icon theme to use.
|
||||||
pub icon_theme: Option<String>,
|
pub icon_theme: Option<String>,
|
||||||
|
|
||||||
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
|
||||||
|
|
||||||
pub start: Option<Vec<ModuleConfig>>,
|
pub start: Option<Vec<ModuleConfig>>,
|
||||||
pub center: Option<Vec<ModuleConfig>>,
|
pub center: Option<Vec<ModuleConfig>>,
|
||||||
pub end: Option<Vec<ModuleConfig>>,
|
pub end: Option<Vec<ModuleConfig>>,
|
||||||
@@ -116,38 +107,6 @@ pub struct Config {
|
|||||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
cfg_if! {
|
|
||||||
if #[cfg(feature = "clock")] {
|
|
||||||
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let end = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
position: BarPosition::default(),
|
|
||||||
height: default_bar_height(),
|
|
||||||
margin: MarginConfig::default(),
|
|
||||||
name: None,
|
|
||||||
start_hidden: None,
|
|
||||||
autohide: None,
|
|
||||||
popup_gap: default_popup_gap(),
|
|
||||||
icon_theme: None,
|
|
||||||
ironvar_defaults: None,
|
|
||||||
start: Some(vec![ModuleConfig::Label(
|
|
||||||
LabelModule::new("ℹ️ Using default config".to_string()).into(),
|
|
||||||
)]),
|
|
||||||
center: Some(vec![ModuleConfig::Focused(Box::default())]),
|
|
||||||
end,
|
|
||||||
anchor_to_edges: default_true(),
|
|
||||||
monitors: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_bar_height() -> i32 {
|
const fn default_bar_height() -> i32 {
|
||||||
42
|
42
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,16 @@
|
|||||||
use lazy_static::lazy_static;
|
use std::collections::HashMap;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::fs::File;
|
||||||
use std::env;
|
use std::io;
|
||||||
use std::fs;
|
use std::io::BufRead;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use walkdir::WalkDir;
|
||||||
use tracing::warn;
|
|
||||||
use walkdir::{DirEntry, WalkDir};
|
|
||||||
|
|
||||||
use crate::lock;
|
/// Gets directories that should contain `.desktop` files
|
||||||
|
|
||||||
type DesktopFile = HashMap<String, Vec<String>>;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> =
|
|
||||||
Mutex::new(HashMap::new());
|
|
||||||
|
|
||||||
/// These are the keys that in the cache
|
|
||||||
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> =
|
|
||||||
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds directories that should contain `.desktop` files
|
|
||||||
/// and exist on the filesystem.
|
/// and exist on the filesystem.
|
||||||
fn find_application_dirs() -> Vec<PathBuf> {
|
fn find_application_dirs() -> Vec<PathBuf> {
|
||||||
let mut dirs = vec![
|
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
||||||
PathBuf::from("/usr/share/applications"), // system installed apps
|
let user_dir = dirs::data_local_dir();
|
||||||
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) {
|
|
||||||
xdg_dir.push("applications");
|
|
||||||
dirs.push(xdg_dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_dir = dirs::data_local_dir(); // user installed apps
|
|
||||||
if let Some(mut user_dir) = user_dir {
|
if let Some(mut user_dir) = user_dir {
|
||||||
user_dir.push("applications");
|
user_dir.push("applications");
|
||||||
dirs.push(user_dir);
|
dirs.push(user_dir);
|
||||||
@@ -45,164 +19,56 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
|||||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
dirs.into_iter().filter(|dir| dir.exists()).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")
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to locate a `.desktop` file for an app id
|
/// Attempts to locate a `.desktop` file for an app id
|
||||||
|
/// (or app class).
|
||||||
|
///
|
||||||
|
/// A simple case-insensitive check is performed on filename == `app_id`.
|
||||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||||
// this is necessary to invalidate the cache
|
let dirs = find_application_dirs();
|
||||||
let files = find_desktop_files();
|
|
||||||
|
|
||||||
find_desktop_file_by_filename(app_id, &files)
|
for dir in dirs {
|
||||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the correct desktop file using a simple condition check
|
let entry = walker.find(|entry| match entry {
|
||||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
Ok(entry) => {
|
||||||
let with_names = files
|
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
||||||
.iter()
|
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
||||||
.map(|f| {
|
file_name == test_name
|
||||||
(
|
|
||||||
f,
|
|
||||||
f.file_stem()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_lowercase(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
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 Some(parsed_desktop_file) = parse_desktop_file(file) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
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")
|
|
||||||
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
// second pass - check name key for substring
|
|
||||||
.or_else(|| {
|
|
||||||
files.iter().find(|(_, desktop_file)| {
|
|
||||||
desktop_file
|
|
||||||
.get("Name")
|
|
||||||
.map(|names| {
|
|
||||||
names
|
|
||||||
.iter()
|
|
||||||
.any(|name| name.to_lowercase().contains(app_id))
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// 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 Some((key, value)) = line.split_once('=') else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = key.trim();
|
|
||||||
let value = value.trim();
|
|
||||||
|
|
||||||
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
|
|
||||||
Some((key, value))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
_ => false,
|
||||||
.for_each(|(key, value)| {
|
|
||||||
desktop_file
|
|
||||||
.entry(key.to_string())
|
|
||||||
.or_default()
|
|
||||||
.push(value.to_string());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(desktop_file)
|
if let Some(Ok(entry)) = entry {
|
||||||
|
let path = entry.path().to_owned();
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a desktop file into a flat hashmap of keys/values.
|
||||||
|
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let lines = io::BufReader::new(file).lines();
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
for line in lines.flatten() {
|
||||||
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
|
map.insert(key.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get the icon name from the app's `.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> {
|
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||||
let Some(path) = find_desktop_file(app_id) else {
|
find_desktop_file(app_id).and_then(|file| {
|
||||||
return None;
|
let map = parse_desktop_file(file);
|
||||||
};
|
map.map_or(None, |map| {
|
||||||
|
map.get("Icon").map(std::string::ToString::to_string)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/dynamic_string.rs
Normal file
160
src/dynamic_string.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use crate::script::{OutputStream, Script};
|
||||||
|
use crate::{lock, send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
/// A segment of a dynamic string,
|
||||||
|
/// containing either a static string
|
||||||
|
/// or a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DynamicStringSegment {
|
||||||
|
Static(String),
|
||||||
|
Dynamic(Script),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A string with embedded scripts for dynamic content.
|
||||||
|
pub struct DynamicString;
|
||||||
|
|
||||||
|
impl DynamicString {
|
||||||
|
/// Creates a new dynamic string, based off the input template.
|
||||||
|
/// Runs `f` with the compiled string each time one of the scripts updates.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rs
|
||||||
|
/// DynamicString::new(&text, move |string| {
|
||||||
|
/// label.set_markup(&string);
|
||||||
|
/// Continue(true)
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn new<F>(input: &str, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut(String) -> Continue + 'static,
|
||||||
|
{
|
||||||
|
let segments = Self::parse_input(input);
|
||||||
|
|
||||||
|
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
for (i, segment) in segments.into_iter().enumerate() {
|
||||||
|
match segment {
|
||||||
|
DynamicStringSegment::Static(str) => {
|
||||||
|
lock!(label_parts).push(str);
|
||||||
|
}
|
||||||
|
DynamicStringSegment::Dynamic(script) => {
|
||||||
|
let tx = tx.clone();
|
||||||
|
let label_parts = label_parts.clone();
|
||||||
|
|
||||||
|
// insert blank value to preserve segment order
|
||||||
|
lock!(label_parts).push(String::new());
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(None, |out, _| {
|
||||||
|
if let OutputStream::Stdout(out) = out {
|
||||||
|
let mut label_parts = lock!(label_parts);
|
||||||
|
|
||||||
|
let _ = std::mem::replace(&mut label_parts[i], out);
|
||||||
|
|
||||||
|
let string = label_parts.join("");
|
||||||
|
send!(tx, string);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
{
|
||||||
|
let label_parts = lock!(label_parts).join("");
|
||||||
|
send!(tx, label_parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
rx.attach(None, f);
|
||||||
|
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the input string into static and dynamic segments
|
||||||
|
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||||
|
if !input.contains("{{") {
|
||||||
|
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut segments = vec![];
|
||||||
|
|
||||||
|
let mut chars = input.chars().collect::<Vec<_>>();
|
||||||
|
while !chars.is_empty() {
|
||||||
|
let char_pair = if chars.len() > 1 {
|
||||||
|
Some(&chars[..=1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (token, skip) = if let Some(['{', '{']) = char_pair {
|
||||||
|
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||||
|
|
||||||
|
let str = chars
|
||||||
|
.windows(2)
|
||||||
|
.skip(2)
|
||||||
|
.take_while(|win| win != &['}', '}'])
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(
|
||||||
|
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
||||||
|
len + SKIP_BRACKETS,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let mut str = chars
|
||||||
|
.windows(2)
|
||||||
|
.take_while(|win| win != &['{', '{'])
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||||
|
if chars.len() == str.len() + 1 {
|
||||||
|
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||||
|
str.push(remaining_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(DynamicStringSegment::Static(str), len)
|
||||||
|
};
|
||||||
|
|
||||||
|
// quick runtime check to make sure the parser is working as expected
|
||||||
|
assert_ne!(skip, 0);
|
||||||
|
|
||||||
|
segments.push(token);
|
||||||
|
chars.drain(..skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
segments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test() {
|
||||||
|
// TODO: see if we can run gtk tests in ci
|
||||||
|
if gtk::init().is_ok() {
|
||||||
|
let label = gtk::Label::new(None);
|
||||||
|
DynamicString::new(
|
||||||
|
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
|
||||||
|
move |string| {
|
||||||
|
label.set_label(&string);
|
||||||
|
Continue(true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
use crate::script::Script;
|
|
||||||
use crate::{glib_recv_mpsc, spawn, try_send};
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
use crate::{send_async, Ironbar};
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum DynamicBool {
|
|
||||||
/// Either a script or variable, to be determined.
|
|
||||||
Unknown(String),
|
|
||||||
Script(Script),
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
Variable(Box<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DynamicBool {
|
|
||||||
pub fn subscribe<F>(self, mut f: F)
|
|
||||||
where
|
|
||||||
F: FnMut(bool) + 'static,
|
|
||||||
{
|
|
||||||
let value = match self {
|
|
||||||
Self::Unknown(input) => {
|
|
||||||
if input.starts_with('#') {
|
|
||||||
cfg_if! {
|
|
||||||
if #[cfg(feature = "ipc")] {
|
|
||||||
Self::Variable(input.into())
|
|
||||||
} else {
|
|
||||||
Self::Unknown(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let script = Script::from(input.as_str());
|
|
||||||
Self::Script(script)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => self,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
glib_recv_mpsc!(rx, val => f(val));
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
match value {
|
|
||||||
DynamicBool::Script(script) => {
|
|
||||||
script
|
|
||||||
.run(None, |_, success| {
|
|
||||||
try_send!(tx, success);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
DynamicBool::Variable(variable) => {
|
|
||||||
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);
|
|
||||||
|
|
||||||
while let Ok(value) = rx.recv().await {
|
|
||||||
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
|
||||||
send_async!(tx, has_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DynamicBool::Unknown(_) => unreachable!(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a string ironvar is 'truthy',
|
|
||||||
/// i.e should be evaluated to true.
|
|
||||||
///
|
|
||||||
/// This loosely follows the common JavaScript cases.
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
fn is_truthy(string: &str) -> bool {
|
|
||||||
!(string.is_empty() || string == "0" || string == "false")
|
|
||||||
}
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
use crate::script::{OutputStream, Script};
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
use crate::Ironbar;
|
|
||||||
use crate::{arc_mut, glib_recv_mpsc, lock, spawn, try_send};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
/// A segment of a dynamic string,
|
|
||||||
/// containing either a static string
|
|
||||||
/// or a script.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DynamicStringSegment {
|
|
||||||
Static(String),
|
|
||||||
Script(Script),
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
Variable(Box<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new dynamic string, based off the input template.
|
|
||||||
/// Runs `f` with the compiled string each time one of the scripts or variables updates.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// dynamic_string(&text, move |string| {
|
|
||||||
/// label.set_markup(&string);
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
pub fn dynamic_string<F>(input: &str, mut f: F)
|
|
||||||
where
|
|
||||||
F: FnMut(String) + 'static,
|
|
||||||
{
|
|
||||||
let tokens = parse_input(input);
|
|
||||||
|
|
||||||
let label_parts = arc_mut!(vec![]);
|
|
||||||
let (tx, rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
for (i, segment) in tokens.into_iter().enumerate() {
|
|
||||||
match segment {
|
|
||||||
DynamicStringSegment::Static(str) => {
|
|
||||||
lock!(label_parts).push(str);
|
|
||||||
}
|
|
||||||
DynamicStringSegment::Script(script) => {
|
|
||||||
let tx = tx.clone();
|
|
||||||
let label_parts = label_parts.clone();
|
|
||||||
|
|
||||||
// insert blank value to preserve segment order
|
|
||||||
lock!(label_parts).push(String::new());
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
script
|
|
||||||
.run(None, |out, _| {
|
|
||||||
if let OutputStream::Stdout(out) = out {
|
|
||||||
let mut label_parts = lock!(label_parts);
|
|
||||||
|
|
||||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
|
||||||
|
|
||||||
let string = label_parts.join("");
|
|
||||||
try_send!(tx, string);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
DynamicStringSegment::Variable(name) => {
|
|
||||||
let tx = tx.clone();
|
|
||||||
let label_parts = label_parts.clone();
|
|
||||||
|
|
||||||
// insert blank value to preserve segment order
|
|
||||||
lock!(label_parts).push(String::new());
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
let variable_manager = Ironbar::variable_manager();
|
|
||||||
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
|
|
||||||
|
|
||||||
while let Ok(value) = rx.recv().await {
|
|
||||||
if let Some(value) = value {
|
|
||||||
let mut label_parts = lock!(label_parts);
|
|
||||||
|
|
||||||
let _: String = std::mem::replace(&mut label_parts[i], value);
|
|
||||||
|
|
||||||
let string = label_parts.join("");
|
|
||||||
try_send!(tx, string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib_recv_mpsc!(rx , val => f(val));
|
|
||||||
|
|
||||||
// initialize
|
|
||||||
{
|
|
||||||
let label_parts = lock!(label_parts).join("");
|
|
||||||
try_send!(tx, label_parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the input string into static and dynamic segments
|
|
||||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
|
||||||
// short-circuit parser if it's all static
|
|
||||||
if !input.contains("{{") && !input.contains('#') {
|
|
||||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut tokens = vec![];
|
|
||||||
|
|
||||||
let mut chars = input.chars().collect::<Vec<_>>();
|
|
||||||
while !chars.is_empty() {
|
|
||||||
let char_pair = if chars.len() > 1 {
|
|
||||||
Some(&chars[..=1])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (token, skip) = match char_pair {
|
|
||||||
Some(['{', '{']) => parse_script(&chars),
|
|
||||||
Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2),
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
Some(['#', _]) => parse_variable(&chars),
|
|
||||||
_ => parse_static(&chars),
|
|
||||||
};
|
|
||||||
|
|
||||||
// quick runtime check to make sure the parser is working as expected
|
|
||||||
assert_ne!(skip, 0);
|
|
||||||
|
|
||||||
tokens.push(token);
|
|
||||||
chars.drain(..skip);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
|
|
||||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
|
||||||
|
|
||||||
let str = chars
|
|
||||||
.windows(2)
|
|
||||||
.skip(2)
|
|
||||||
.take_while(|win| win != &['}', '}'])
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let len = str.chars().count() + SKIP_BRACKETS;
|
|
||||||
let script = Script::from(str.as_str());
|
|
||||||
|
|
||||||
(DynamicStringSegment::Script(script), len)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
|
|
||||||
const SKIP_HASH: usize = 1;
|
|
||||||
|
|
||||||
let str = chars
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.take_while(|&c| !c.is_whitespace())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let len = str.chars().count() + SKIP_HASH;
|
|
||||||
let value = str.into();
|
|
||||||
|
|
||||||
(DynamicStringSegment::Variable(value), len)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
|
|
||||||
let mut str = chars
|
|
||||||
.windows(2)
|
|
||||||
.take_while(|&win| win != ['{', '{'] && win[0] != '#')
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let mut char_count = str.chars().count();
|
|
||||||
|
|
||||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
|
||||||
if chars.len() == char_count + 1 {
|
|
||||||
let remaining_char = *chars.get(char_count).expect("Failed to find last char");
|
|
||||||
str.push(remaining_char);
|
|
||||||
char_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
(DynamicStringSegment::Static(str), char_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static() {
|
|
||||||
const INPUT: &str = "hello world";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 1);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_odd_char_count() {
|
|
||||||
const INPUT: &str = "hello";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 1);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_script() {
|
|
||||||
const INPUT: &str = "{{echo hello}}";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 1);
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_variable() {
|
|
||||||
const INPUT: &str = "#variable";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 1);
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_script() {
|
|
||||||
const INPUT: &str = "hello {{echo world}}";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 2);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_variable() {
|
|
||||||
const INPUT: &str = "hello #subject";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 2);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_script_static() {
|
|
||||||
const INPUT: &str = "hello {{echo world}} foo";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 3);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
|
||||||
);
|
|
||||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_variable_static() {
|
|
||||||
const INPUT: &str = "hello #subject foo";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 3);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
|
||||||
);
|
|
||||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_static_script_variable() {
|
|
||||||
const INPUT: &str = "hello {{echo world}} #foo";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 4);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
|
||||||
);
|
|
||||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_escape_hash() {
|
|
||||||
const INPUT: &str = "number ###num";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 3);
|
|
||||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
|
|
||||||
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_script_with_hash() {
|
|
||||||
const INPUT: &str = "{{echo #hello}}";
|
|
||||||
let tokens = parse_input(INPUT);
|
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 1);
|
|
||||||
assert!(
|
|
||||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#![doc = include_str!("../../docs/Dynamic values.md")]
|
|
||||||
|
|
||||||
mod dynamic_bool;
|
|
||||||
mod dynamic_string;
|
|
||||||
|
|
||||||
pub use dynamic_bool::DynamicBool;
|
|
||||||
pub use dynamic_string::dynamic_string;
|
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
pub enum ExitCode {
|
pub enum ExitCode {
|
||||||
GtkDisplay = 1,
|
GtkDisplay = 1,
|
||||||
CreateBars = 2,
|
CreateBars = 2,
|
||||||
|
Config = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
|
pub const ERR_OUTPUTS: &str = "GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||||
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
||||||
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
|
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
|
||||||
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
|
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
|
||||||
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
|
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
|
||||||
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
|
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
|
||||||
|
|
||||||
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
use glib::IsA;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Orientation, Widget};
|
|
||||||
|
|
||||||
/// Represents a widget's size
|
|
||||||
/// and location relative to the bar's start edge.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct WidgetGeometry {
|
|
||||||
/// Position of the start edge of the widget
|
|
||||||
/// from the start edge of the bar.
|
|
||||||
pub position: i32,
|
|
||||||
/// The length of the widget.
|
|
||||||
pub size: i32,
|
|
||||||
/// The length of the bar.
|
|
||||||
pub bar_size: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IronbarGtkExt {
|
|
||||||
/// Adds a new CSS class to the widget.
|
|
||||||
fn add_class(&self, class: &str);
|
|
||||||
/// Gets the geometry for the widget
|
|
||||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
|
||||||
|
|
||||||
/// Gets a data tag on a widget, if it exists.
|
|
||||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
|
|
||||||
/// Sets a data tag on a widget.
|
|
||||||
fn set_tag<V: 'static>(&self, key: &str, value: V);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: IsA<Widget>> IronbarGtkExt for W {
|
|
||||||
fn add_class(&self, class: &str) {
|
|
||||||
self.style_context().add_class(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
|
|
||||||
let allocation = self.allocation();
|
|
||||||
|
|
||||||
let widget_size = if orientation == Orientation::Horizontal {
|
|
||||||
allocation.width()
|
|
||||||
} else {
|
|
||||||
allocation.height()
|
|
||||||
};
|
|
||||||
|
|
||||||
let top_level = self.toplevel().expect("Failed to get top-level widget");
|
|
||||||
let top_level_allocation = top_level.allocation();
|
|
||||||
|
|
||||||
let bar_size = if orientation == Orientation::Horizontal {
|
|
||||||
top_level_allocation.width()
|
|
||||||
} else {
|
|
||||||
top_level_allocation.height()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (widget_x, widget_y) = self
|
|
||||||
.translate_coordinates(&top_level, 0, 0)
|
|
||||||
.unwrap_or((0, 0));
|
|
||||||
|
|
||||||
let widget_pos = if orientation == Orientation::Horizontal {
|
|
||||||
widget_x
|
|
||||||
} else {
|
|
||||||
widget_y
|
|
||||||
};
|
|
||||||
|
|
||||||
WidgetGeometry {
|
|
||||||
position: widget_pos,
|
|
||||||
size: widget_size,
|
|
||||||
bar_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
|
|
||||||
unsafe { self.data(key).map(|val| val.as_ref()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_tag<V: 'static>(&self, key: &str, value: V) {
|
|
||||||
unsafe { self.set_data(key, value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::ImageProvider;
|
use super::ImageProvider;
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||||
@@ -9,17 +9,17 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
|||||||
|
|
||||||
if ImageProvider::is_definitely_image_input(input) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
image.add_class("image");
|
image.set_widget_name("image");
|
||||||
image.add_class("icon");
|
|
||||||
|
|
||||||
match ImageProvider::parse(input, icon_theme, false, size)
|
match ImageProvider::parse(input, icon_theme, size)
|
||||||
.map(|provider| provider.load_into_image(image.clone()))
|
.and_then(|provider| provider.load_into_image(image.clone()))
|
||||||
{
|
{
|
||||||
Some(_) => {
|
Ok(_) => {
|
||||||
button.set_image(Some(&image));
|
button.set_image(Some(&image));
|
||||||
button.set_always_show_image(true);
|
button.set_always_show_image(true);
|
||||||
}
|
}
|
||||||
None => {
|
Err(err) => {
|
||||||
|
error!("{err:?}");
|
||||||
button.set_label(input);
|
button.set_label(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,19 +36,20 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
|
|||||||
|
|
||||||
if ImageProvider::is_definitely_image_input(input) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
image.add_class("icon");
|
image.set_widget_name("image");
|
||||||
image.add_class("image");
|
|
||||||
|
|
||||||
container.add(&image);
|
container.append(&image);
|
||||||
|
|
||||||
ImageProvider::parse(input, icon_theme, false, size)
|
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
||||||
.map(|provider| provider.load_into_image(image));
|
.and_then(|provider| provider.load_into_image(image))
|
||||||
|
{
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let label = Label::new(Some(input));
|
let label = Label::new(Some(input));
|
||||||
label.add_class("icon");
|
label.set_widget_name("label");
|
||||||
label.add_class("text-icon");
|
|
||||||
|
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
container
|
container
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
use crate::desktop_file::get_desktop_icon_name;
|
use crate::desktop_file::get_desktop_icon_name;
|
||||||
#[cfg(feature = "http")]
|
|
||||||
use crate::{glib_recv_mpsc, send_async, spawn};
|
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use color_eyre::{Help, Report, Result};
|
use color_eyre::{Help, Report, Result};
|
||||||
use gtk::cairo::Surface;
|
|
||||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
|
||||||
use gtk::gdk_pixbuf::Pixbuf;
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IconLookupFlags, IconTheme};
|
use gtk::{IconLookupFlags, IconTheme};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
#[cfg(feature = "http")]
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
cfg_if!(
|
cfg_if!(
|
||||||
if #[cfg(feature = "http")] {
|
if #[cfg(feature = "http")] {
|
||||||
|
use crate::send;
|
||||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||||
|
use tokio::spawn;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -43,98 +38,60 @@ impl<'a> ImageProvider<'a> {
|
|||||||
///
|
///
|
||||||
/// Note this checks that icons exist in theme, or files exist on disk
|
/// Note this checks that icons exist in theme, or files exist on disk
|
||||||
/// but no other check is performed.
|
/// but no other check is performed.
|
||||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
|
||||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
let location = Self::get_location(input, theme, size)?;
|
||||||
|
Ok(Self { location, size })
|
||||||
Some(Self { location, size })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the input starts with a prefix
|
/// Returns true if the input starts with a prefix
|
||||||
/// that is supported by the parser
|
/// that is supported by the parser
|
||||||
/// (ie the parser would not fallback to checking the input).
|
/// (ie the parser would not fallback to checking the input).
|
||||||
|
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||||
input.starts_with("icon:")
|
input.starts_with("icon:")
|
||||||
|| input.starts_with("file://")
|
|| input.starts_with("file://")
|
||||||
|| input.starts_with("http://")
|
|| input.starts_with("http://")
|
||||||
|| input.starts_with("https://")
|
|| input.starts_with("https://")
|
||||||
|| input.starts_with('/')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_location(
|
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
|
||||||
input: &str,
|
|
||||||
theme: &'a IconTheme,
|
|
||||||
size: i32,
|
|
||||||
use_fallback: bool,
|
|
||||||
recurse_depth: usize,
|
|
||||||
) -> Option<ImageLocation<'a>> {
|
|
||||||
macro_rules! fallback {
|
|
||||||
() => {
|
|
||||||
if use_fallback {
|
|
||||||
Some(Self::get_fallback_icon(theme))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_RECURSE_DEPTH: usize = 2;
|
|
||||||
|
|
||||||
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
|
|
||||||
|
|
||||||
let (input_type, input_name) = input
|
let (input_type, input_name) = input
|
||||||
.split_once(':')
|
.split_once(':')
|
||||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||||
|
|
||||||
match input_type {
|
match input_type {
|
||||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
|
||||||
name: input_name.to_string(),
|
name: input_name.to_string(),
|
||||||
theme,
|
theme,
|
||||||
}),
|
}),
|
||||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
Some(input_type) if input_type == "file" => Ok(ImageLocation::Local(PathBuf::from(
|
||||||
input_name[2..].to_string(),
|
input_name[2..].to_string(),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||||
input.parse().ok().map(ImageLocation::Remote)
|
Ok(ImageLocation::Remote(input.parse()?))
|
||||||
}
|
}
|
||||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
None if input.starts_with("steam_app_") => Ok(ImageLocation::Steam(
|
||||||
input_name.chars().skip("steam_app_".len()).collect(),
|
input_name.chars().skip("steam_app_".len()).collect(),
|
||||||
)),
|
)),
|
||||||
None if theme
|
None if theme
|
||||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||||
.is_some() =>
|
.is_some() =>
|
||||||
{
|
{
|
||||||
Some(ImageLocation::Icon {
|
Ok(ImageLocation::Icon {
|
||||||
name: input_name.to_string(),
|
name: input_name.to_string(),
|
||||||
theme,
|
theme,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(input_type) => {
|
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))
|
||||||
warn!(
|
.note("You may need to recompile with support if available")),
|
||||||
"{:?}",
|
|
||||||
Report::msg(format!("Unsupported image type: {input_type}"))
|
|
||||||
.note("You may need to recompile with support if available")
|
|
||||||
);
|
|
||||||
fallback!()
|
|
||||||
}
|
|
||||||
None if PathBuf::from(input_name).is_file() => {
|
None if PathBuf::from(input_name).is_file() => {
|
||||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
||||||
}
|
|
||||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
|
||||||
None if should_parse_desktop_file => {
|
|
||||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
|
||||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
|
||||||
}) {
|
|
||||||
location
|
|
||||||
} else {
|
|
||||||
warn!("Failed to find image: {input}");
|
|
||||||
fallback!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
warn!("Failed to find image: {input}");
|
|
||||||
fallback!()
|
|
||||||
}
|
}
|
||||||
|
None => get_desktop_icon_name(input_name).map_or_else(
|
||||||
|
|| Err(Report::msg(format!("Unknown image type: '{input}'"))),
|
||||||
|
|input| Self::get_location(&input, theme, size),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,38 +102,33 @@ impl<'a> ImageProvider<'a> {
|
|||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
if let ImageLocation::Remote(url) = &self.location {
|
if let ImageLocation::Remote(url) = &self.location {
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
let (tx, rx) = mpsc::channel(64);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let bytes = Self::get_bytes_from_http(url).await;
|
let bytes = Self::get_bytes_from_http(url).await;
|
||||||
if let Ok(bytes) = bytes {
|
if let Ok(bytes) = bytes {
|
||||||
send_async!(tx, bytes);
|
send!(tx, bytes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let size = self.size;
|
let size = self.size;
|
||||||
glib_recv_mpsc!(rx, bytes => {
|
rx.attach(None, move |bytes| {
|
||||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||||
|
|
||||||
let scale = image.scale_factor();
|
|
||||||
let scaled_size = size * scale;
|
|
||||||
|
|
||||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||||
&stream,
|
&stream,
|
||||||
scaled_size,
|
size,
|
||||||
scaled_size,
|
size,
|
||||||
true,
|
true,
|
||||||
Some(&Cancellable::new()),
|
Some(&Cancellable::new()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Different error types makes this a bit awkward
|
match pixbuf {
|
||||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
|
||||||
{
|
|
||||||
Ok(Err(err)) => error!("{err:?}"),
|
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Continue(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -189,35 +141,18 @@ impl<'a> ImageProvider<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to synchronously fetch an image from location
|
|
||||||
/// and load into into the image.
|
|
||||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||||
let scale = image.scale_factor();
|
|
||||||
|
|
||||||
let pixbuf = match &self.location {
|
let pixbuf = match &self.location {
|
||||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
ImageLocation::Icon { name, theme } => {
|
||||||
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
self.get_from_icon(name, theme, image.scale_factor())
|
||||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
}
|
||||||
|
ImageLocation::Local(path) => self.get_from_file(path),
|
||||||
|
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
_ => unreachable!(), // handled above
|
_ => unreachable!(), // handled above
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Self::create_and_load_surface(&pixbuf, image, scale)
|
image.set_pixbuf(Some(&pixbuf));
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
|
||||||
/// using the provided scaling factor.
|
|
||||||
/// The surface is then loaded into the provided image.
|
|
||||||
///
|
|
||||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
|
||||||
fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image, scale: i32) -> Result<()> {
|
|
||||||
let surface = unsafe {
|
|
||||||
let ptr =
|
|
||||||
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
|
|
||||||
Surface::from_raw_full(ptr)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
image.set_from_surface(Some(&surface));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -226,7 +161,7 @@ impl<'a> ImageProvider<'a> {
|
|||||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||||
let pixbuf =
|
let pixbuf =
|
||||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
@@ -237,15 +172,14 @@ impl<'a> ImageProvider<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from a local file.
|
/// Attempts to get a `Pixbuf` from a local file.
|
||||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
|
||||||
let scaled_size = self.size * scale;
|
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
|
||||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
|
||||||
Ok(pixbuf)
|
Ok(pixbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from a local file,
|
/// Attempts to get a `Pixbuf` from a local file,
|
||||||
/// using the Steam game ID to look it up.
|
/// using the Steam game ID to look it up.
|
||||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
|
||||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||||
let path = dirs::data_dir().map_or_else(
|
let path = dirs::data_dir().map_or_else(
|
||||||
|| Err(Report::msg("Missing XDG data dir")),
|
|| Err(Report::msg("Missing XDG data dir")),
|
||||||
@@ -256,7 +190,7 @@ impl<'a> ImageProvider<'a> {
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.get_from_file(&path, scale)
|
self.get_from_file(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||||
@@ -274,11 +208,4 @@ impl<'a> ImageProvider<'a> {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
|
|
||||||
ImageLocation::Icon {
|
|
||||||
name: "dialog-question-symbolic".to_string(),
|
|
||||||
theme,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
use super::Ipc;
|
|
||||||
use crate::ipc::{Command, Response};
|
|
||||||
use color_eyre::Result;
|
|
||||||
use color_eyre::{Help, Report};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
|
|
||||||
impl Ipc {
|
|
||||||
/// Sends a command to the IPC server.
|
|
||||||
/// The server response is returned.
|
|
||||||
pub async fn send(&self, command: Command) -> Result<Response> {
|
|
||||||
let mut stream = match UnixStream::connect(&self.path).await {
|
|
||||||
Ok(stream) => Ok(stream),
|
|
||||||
Err(err) => Err(Report::new(err)
|
|
||||||
.wrap_err("Failed to connect to Ironbar IPC server")
|
|
||||||
.suggestion("Is Ironbar running?")),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let write_buffer = serde_json::to_vec(&command)?;
|
|
||||||
stream.write_all(&write_buffer).await?;
|
|
||||||
|
|
||||||
let mut read_buffer = vec![0; 1024];
|
|
||||||
let bytes = stream.read(&mut read_buffer).await?;
|
|
||||||
|
|
||||||
let response = serde_json::from_slice(&read_buffer[..bytes])?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use clap::Subcommand;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
pub enum Command {
|
|
||||||
/// Return "ok"
|
|
||||||
Ping,
|
|
||||||
|
|
||||||
/// Open the GTK inspector
|
|
||||||
Inspect,
|
|
||||||
|
|
||||||
/// Reload the config
|
|
||||||
Reload,
|
|
||||||
|
|
||||||
/// Set an `ironvar` value.
|
|
||||||
/// This creates it if it does not already exist, and updates it if it does.
|
|
||||||
/// Any references to this variable are automatically and immediately updated.
|
|
||||||
/// Keys and values can be any valid UTF-8 string.
|
|
||||||
Set {
|
|
||||||
/// Variable key. Can be any alphanumeric ASCII string.
|
|
||||||
key: Box<str>,
|
|
||||||
/// Variable value. Can be any valid UTF-8 string.
|
|
||||||
value: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Get the current value of an `ironvar`.
|
|
||||||
Get {
|
|
||||||
/// Variable key.
|
|
||||||
key: Box<str>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Load an additional CSS stylesheet.
|
|
||||||
/// The sheet is automatically hot-reloaded.
|
|
||||||
LoadCss {
|
|
||||||
/// The path to the sheet.
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Set the visibility of the bar with the given name.
|
|
||||||
SetVisible {
|
|
||||||
///Bar name to target.
|
|
||||||
bar_name: String,
|
|
||||||
/// The visibility status.
|
|
||||||
#[arg(short, long)]
|
|
||||||
visible: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Get the visibility of the bar with the given name.
|
|
||||||
GetVisible {
|
|
||||||
/// Bar name to target.
|
|
||||||
bar_name: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Toggle a popup open/closed.
|
|
||||||
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
|
|
||||||
TogglePopup {
|
|
||||||
/// The name of the monitor the bar is located on.
|
|
||||||
bar_name: String,
|
|
||||||
/// The name of the widget.
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Open a popup, regardless of current state.
|
|
||||||
OpenPopup {
|
|
||||||
/// The name of the monitor the bar is located on.
|
|
||||||
bar_name: String,
|
|
||||||
/// The name of the widget.
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Close a popup, regardless of current state.
|
|
||||||
ClosePopup {
|
|
||||||
/// The name of the monitor the bar is located on.
|
|
||||||
bar_name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
mod client;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod responses;
|
|
||||||
mod server;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
pub use commands::Command;
|
|
||||||
pub use responses::Response;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Ipc {
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ipc {
|
|
||||||
/// Creates a new IPC instance.
|
|
||||||
/// This can be used as both a server and client.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
|
|
||||||
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
|
|
||||||
.join("ironbar-ipc.sock");
|
|
||||||
|
|
||||||
if format!("{}", ipc_socket_file.display()).len() > 100 {
|
|
||||||
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: ipc_socket_file,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path(&self) -> &Path {
|
|
||||||
self.path.as_path()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
pub enum Response {
|
|
||||||
Ok,
|
|
||||||
OkValue { value: String },
|
|
||||||
Err { message: Option<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
/// Creates a new `Response::Error`.
|
|
||||||
pub fn error(message: &str) -> Self {
|
|
||||||
Self::Err {
|
|
||||||
message: Some(message.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use color_eyre::{Report, Result};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::Application;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
|
||||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
|
||||||
use tracing::{debug, error, info, warn};
|
|
||||||
|
|
||||||
use crate::ipc::{Command, Response};
|
|
||||||
use crate::modules::PopupButton;
|
|
||||||
use crate::style::load_css;
|
|
||||||
use crate::{glib_recv_mpsc, read_lock, send_async, spawn, try_send, write_lock, Ironbar};
|
|
||||||
|
|
||||||
use super::Ipc;
|
|
||||||
|
|
||||||
impl Ipc {
|
|
||||||
/// Starts the IPC server on its socket.
|
|
||||||
///
|
|
||||||
/// Once started, the server will begin accepting connections.
|
|
||||||
pub fn start(&self, application: &Application, ironbar: Rc<Ironbar>) {
|
|
||||||
let (cmd_tx, cmd_rx) = mpsc::channel(32);
|
|
||||||
let (res_tx, mut res_rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
let path = self.path.clone();
|
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
warn!("Socket already exists. Did Ironbar exit abruptly?");
|
|
||||||
warn!("Attempting IPC shutdown to allow binding to address");
|
|
||||||
Self::shutdown(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
info!("Starting IPC on {}", path.display());
|
|
||||||
|
|
||||||
let listener = match UnixListener::bind(&path) {
|
|
||||||
Ok(listener) => listener,
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"{:?}",
|
|
||||||
Report::new(err).wrap_err("Unable to start IPC server")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match listener.accept().await {
|
|
||||||
Ok((stream, _addr)) => {
|
|
||||||
if let Err(err) =
|
|
||||||
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
|
|
||||||
{
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let application = application.clone();
|
|
||||||
glib_recv_mpsc!(cmd_rx, command => {
|
|
||||||
let res = Self::handle_command(command, &application, &ironbar);
|
|
||||||
try_send!(res_tx, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes an incoming connections,
|
|
||||||
/// reads the command message, and sends the response.
|
|
||||||
///
|
|
||||||
/// The connection is closed once the response has been written.
|
|
||||||
async fn handle_connection(
|
|
||||||
mut stream: UnixStream,
|
|
||||||
cmd_tx: &Sender<Command>,
|
|
||||||
res_rx: &mut Receiver<Response>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (mut stream_read, mut stream_write) = stream.split();
|
|
||||||
|
|
||||||
let mut read_buffer = vec![0; 1024];
|
|
||||||
let bytes = stream_read.read(&mut read_buffer).await?;
|
|
||||||
|
|
||||||
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
|
|
||||||
|
|
||||||
debug!("Received command: {command:?}");
|
|
||||||
|
|
||||||
send_async!(cmd_tx, command);
|
|
||||||
let res = res_rx
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.unwrap_or(Response::Err { message: None });
|
|
||||||
let res = serde_json::to_vec(&res)?;
|
|
||||||
|
|
||||||
stream_write.write_all(&res).await?;
|
|
||||||
stream_write.shutdown().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes an input command, runs it and returns with the appropriate response.
|
|
||||||
///
|
|
||||||
/// This runs on the main thread, allowing commands to interact with GTK.
|
|
||||||
fn handle_command(command: Command, application: &Application, ironbar: &Ironbar) -> Response {
|
|
||||||
match command {
|
|
||||||
Command::Inspect => {
|
|
||||||
gtk::Window::set_interactive_debugging(true);
|
|
||||||
Response::Ok
|
|
||||||
}
|
|
||||||
Command::Reload => {
|
|
||||||
info!("Closing existing bars");
|
|
||||||
let windows = application.windows();
|
|
||||||
for window in windows {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
*ironbar.bars.borrow_mut() = crate::load_interface(application);
|
|
||||||
|
|
||||||
Response::Ok
|
|
||||||
}
|
|
||||||
Command::Set { key, value } => {
|
|
||||||
let variable_manager = Ironbar::variable_manager();
|
|
||||||
let mut variable_manager = write_lock!(variable_manager);
|
|
||||||
match variable_manager.set(key, value) {
|
|
||||||
Ok(()) => Response::Ok,
|
|
||||||
Err(err) => Response::error(&format!("{err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::Get { key } => {
|
|
||||||
let variable_manager = Ironbar::variable_manager();
|
|
||||||
let value = read_lock!(variable_manager).get(&key);
|
|
||||||
match value {
|
|
||||||
Some(value) => Response::OkValue { value },
|
|
||||||
None => Response::error("Variable not found"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::LoadCss { path } => {
|
|
||||||
if path.exists() {
|
|
||||||
load_css(path);
|
|
||||||
Response::Ok
|
|
||||||
} else {
|
|
||||||
Response::error("File not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::TogglePopup { bar_name, name } => {
|
|
||||||
let bar = ironbar.bar_by_name(&bar_name);
|
|
||||||
|
|
||||||
match bar {
|
|
||||||
Some(bar) => {
|
|
||||||
let popup = bar.popup();
|
|
||||||
let current_widget = popup.borrow().current_widget();
|
|
||||||
|
|
||||||
popup.borrow_mut().hide();
|
|
||||||
|
|
||||||
let data = popup
|
|
||||||
.borrow()
|
|
||||||
.cache
|
|
||||||
.iter()
|
|
||||||
.find(|(_, value)| value.name == name)
|
|
||||||
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
|
||||||
|
|
||||||
match data {
|
|
||||||
Some((id, Some(button))) if current_widget != Some(id) => {
|
|
||||||
let button_id = button.popup_id();
|
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
|
|
||||||
if popup.is_visible() {
|
|
||||||
popup.hide();
|
|
||||||
} else {
|
|
||||||
popup.show(id, button_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Response::Ok
|
|
||||||
}
|
|
||||||
Some((_, None)) => Response::error("Module has no popup functionality"),
|
|
||||||
Some(_) => Response::Ok,
|
|
||||||
None => Response::error("Invalid module name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Response::error("Invalid bar name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::OpenPopup { bar_name, name } => {
|
|
||||||
let bar = ironbar.bar_by_name(&bar_name);
|
|
||||||
|
|
||||||
match bar {
|
|
||||||
Some(bar) => {
|
|
||||||
let popup = bar.popup();
|
|
||||||
|
|
||||||
// only one popup per bar, so hide if open for another widget
|
|
||||||
popup.borrow_mut().hide();
|
|
||||||
|
|
||||||
let data = popup
|
|
||||||
.borrow()
|
|
||||||
.cache
|
|
||||||
.iter()
|
|
||||||
.find(|(_, value)| value.name == name)
|
|
||||||
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
|
|
||||||
|
|
||||||
match data {
|
|
||||||
Some((id, Some(button))) => {
|
|
||||||
let button_id = button.popup_id();
|
|
||||||
popup.borrow_mut().show(id, button_id);
|
|
||||||
|
|
||||||
Response::Ok
|
|
||||||
}
|
|
||||||
Some((_, None)) => Response::error("Module has no popup functionality"),
|
|
||||||
None => Response::error("Invalid module name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Response::error("Invalid bar name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::ClosePopup { bar_name } => {
|
|
||||||
let bar = ironbar.bar_by_name(&bar_name);
|
|
||||||
|
|
||||||
match bar {
|
|
||||||
Some(bar) => {
|
|
||||||
let popup = bar.popup();
|
|
||||||
popup.borrow_mut().hide();
|
|
||||||
|
|
||||||
Response::Ok
|
|
||||||
}
|
|
||||||
None => Response::error("Invalid bar name"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::Ping => Response::Ok,
|
|
||||||
Command::SetVisible { bar_name, visible } => {
|
|
||||||
let windows = application.windows();
|
|
||||||
let found = windows
|
|
||||||
.iter()
|
|
||||||
.find(|window| window.widget_name() == bar_name);
|
|
||||||
|
|
||||||
if let Some(window) = found {
|
|
||||||
window.set_visible(visible);
|
|
||||||
Response::Ok
|
|
||||||
} else {
|
|
||||||
Response::error("Bar not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::GetVisible { bar_name } => {
|
|
||||||
let windows = application.windows();
|
|
||||||
let found = windows
|
|
||||||
.iter()
|
|
||||||
.find(|window| window.widget_name() == bar_name);
|
|
||||||
|
|
||||||
if let Some(window) = found {
|
|
||||||
Response::OkValue {
|
|
||||||
value: window.is_visible().to_string(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Response::error("Bar not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shuts down the IPC server,
|
|
||||||
/// removing the socket file in the process.
|
|
||||||
///
|
|
||||||
/// Note this is static as the `Ipc` struct is not `Send`.
|
|
||||||
pub fn shutdown<P: AsRef<Path>>(path: P) {
|
|
||||||
fs::remove_file(&path).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
103
src/ironvar.rs
103
src/ironvar.rs
@@ -1,103 +0,0 @@
|
|||||||
#![doc = include_str!("../docs/Ironvars.md")]
|
|
||||||
|
|
||||||
use crate::send;
|
|
||||||
use color_eyre::{Report, Result};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tokio::sync::broadcast;
|
|
||||||
|
|
||||||
/// Global singleton manager for `IronVar` variables.
|
|
||||||
pub struct VariableManager {
|
|
||||||
variables: HashMap<Box<str>, IronVar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VariableManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VariableManager {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
variables: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value for a variable,
|
|
||||||
/// creating it if it does not exist.
|
|
||||||
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
|
|
||||||
if Self::key_is_valid(&key) {
|
|
||||||
if let Some(var) = self.variables.get_mut(&key) {
|
|
||||||
var.set(Some(value));
|
|
||||||
} else {
|
|
||||||
let var = IronVar::new(Some(value));
|
|
||||||
self.variables.insert(key, var);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Report::msg("Invalid key"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current value of an `ironvar`.
|
|
||||||
/// Prefer to use `subscribe` where possible.
|
|
||||||
pub fn get(&self, key: &str) -> Option<String> {
|
|
||||||
self.variables.get(key).and_then(IronVar::get)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribes to an `ironvar`, creating it if it does not exist.
|
|
||||||
/// Any time the var is set, its value is sent on the channel.
|
|
||||||
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
|
||||||
self.variables
|
|
||||||
.entry(key)
|
|
||||||
.or_insert_with(|| IronVar::new(None))
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_is_valid(key: &str) -> bool {
|
|
||||||
!key.is_empty()
|
|
||||||
&& key
|
|
||||||
.chars()
|
|
||||||
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ironbar dynamic variable representation.
|
|
||||||
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct IronVar {
|
|
||||||
value: Option<String>,
|
|
||||||
tx: broadcast::Sender<Option<String>>,
|
|
||||||
_rx: broadcast::Receiver<Option<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IronVar {
|
|
||||||
/// Creates a new variable.
|
|
||||||
fn new(value: Option<String>) -> Self {
|
|
||||||
let (tx, rx) = broadcast::channel(32);
|
|
||||||
|
|
||||||
Self { value, tx, _rx: rx }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current variable value.
|
|
||||||
/// Prefer to subscribe to changes where possible.
|
|
||||||
fn get(&self) -> Option<String> {
|
|
||||||
self.value.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the current variable value.
|
|
||||||
/// The change is broadcast to all receivers.
|
|
||||||
fn set(&mut self, value: Option<String>) {
|
|
||||||
self.value = value.clone();
|
|
||||||
send!(self.tx, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribes to the variable.
|
|
||||||
/// The latest value is immediately sent to all receivers.
|
|
||||||
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
|
|
||||||
let rx = self.tx.subscribe();
|
|
||||||
send!(self.tx, self.value.clone());
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,7 +58,7 @@ fn install_tracing() -> Result<WorkerGuard> {
|
|||||||
const DEFAULT_LOG: &str = "info";
|
const DEFAULT_LOG: &str = "info";
|
||||||
const DEFAULT_FILE_LOG: &str = "warn";
|
const DEFAULT_FILE_LOG: &str = "warn";
|
||||||
|
|
||||||
let fmt_layer = fmt::layer().with_target(true).with_line_number(true);
|
let fmt_layer = fmt::layer().with_target(true);
|
||||||
let filter_layer =
|
let filter_layer =
|
||||||
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
||||||
|
|
||||||
|
|||||||
100
src/macros.rs
100
src/macros.rs
@@ -1,7 +1,7 @@
|
|||||||
/// Sends a message on an asynchronous `Sender` using `send()`
|
/// Sends a message on an asynchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send_async!(tx, "my message");
|
/// send_async!(tx, "my message");
|
||||||
@@ -16,7 +16,7 @@ macro_rules! send_async {
|
|||||||
/// Sends a message on an synchronous `Sender` using `send()`
|
/// Sends a message on an synchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send!(tx, "my message");
|
/// send!(tx, "my message");
|
||||||
@@ -31,7 +31,7 @@ macro_rules! send {
|
|||||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// try_send!(tx, "my message");
|
/// try_send!(tx, "my message");
|
||||||
@@ -43,79 +43,25 @@ macro_rules! try_send {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
|
||||||
/// in a loop.
|
|
||||||
///
|
|
||||||
/// This allows use of `GObjects` and futures in the same context.
|
|
||||||
///
|
|
||||||
/// For use with receivers which return a `Result`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// let (tx, mut rx) = broadcast::channel(32);
|
|
||||||
/// glib_recv(rx, msg => println!("{msg}"));
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! glib_recv {
|
|
||||||
($rx:expr, $val:ident => $expr:expr) => {{
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
// re-delcare in case ie `context.subscribe()` is passed directly
|
|
||||||
let mut rx = $rx;
|
|
||||||
while let Ok($val) = rx.recv().await {
|
|
||||||
$expr
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
|
||||||
/// in a loop.
|
|
||||||
///
|
|
||||||
/// This allows use of `GObjects` and futures in the same context.
|
|
||||||
///
|
|
||||||
/// For use with receivers which return an `Option`,
|
|
||||||
/// such as Tokio's `mpsc` channel.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// let (tx, mut rx) = broadcast::channel(32);
|
|
||||||
/// glib_recv_mpsc(rx, msg => println!("{msg}"));
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! glib_recv_mpsc {
|
|
||||||
($rx:expr, $val:ident => $expr:expr) => {{
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
// re-delcare in case ie `context.subscribe()` is passed directly
|
|
||||||
let mut rx = $rx;
|
|
||||||
while let Some($val) = rx.recv().await {
|
|
||||||
$expr
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Locks a `Mutex`.
|
/// Locks a `Mutex`.
|
||||||
/// Panics if the `Mutex` cannot be locked.
|
/// Panics if the `Mutex` cannot be locked.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = lock!(my_mutex);
|
/// let mut val = lock!(my_mutex);
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! lock {
|
macro_rules! lock {
|
||||||
($mutex:expr) => {{
|
($mutex:expr) => {
|
||||||
tracing::trace!("Locking {}", std::stringify!($mutex));
|
|
||||||
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
|
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
|
||||||
}};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a read lock on a `RwLock`.
|
/// Gets a read lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let val = read_lock!(my_rwlock);
|
/// let val = read_lock!(my_rwlock);
|
||||||
@@ -130,7 +76,7 @@ macro_rules! read_lock {
|
|||||||
/// Gets a write lock on a `RwLock`.
|
/// Gets a write lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// # Usage:
|
/// Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = write_lock!(my_rwlock);
|
/// let mut val = write_lock!(my_rwlock);
|
||||||
@@ -141,33 +87,3 @@ macro_rules! write_lock {
|
|||||||
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps `val` in a new `Arc<Mutex<T>>`.
|
|
||||||
///
|
|
||||||
/// # Usage:
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// let val = arc_mut!(MyService::new());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! arc_mut {
|
|
||||||
($val:expr) => {
|
|
||||||
std::sync::Arc::new(std::sync::Mutex::new($val))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps `val` in a new `Arc<RwLock<T>>`.
|
|
||||||
///
|
|
||||||
/// # Usage:
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// let val = arc_rw!(MyService::new());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! arc_rw {
|
|
||||||
($val:expr) => {
|
|
||||||
std::sync::Arc::new(std::sync::RwLock::new($val))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
420
src/main.rs
420
src/main.rs
@@ -1,54 +1,13 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::env;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::sync::{mpsc, Arc};
|
|
||||||
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
use clap::Parser;
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use color_eyre::Report;
|
|
||||||
use dirs::config_dir;
|
|
||||||
use glib::PropertySet;
|
|
||||||
use gtk::gdk::Display;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::Application;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use tokio::task::{block_in_place, JoinHandle};
|
|
||||||
use tracing::{debug, error, info, warn};
|
|
||||||
use universal_config::ConfigLoader;
|
|
||||||
|
|
||||||
use clients::wayland;
|
|
||||||
|
|
||||||
use crate::bar::{create_bar, Bar};
|
|
||||||
use crate::config::{Config, MonitorConfig};
|
|
||||||
use crate::error::ExitCode;
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
use crate::ironvar::VariableManager;
|
|
||||||
use crate::style::load_css;
|
|
||||||
|
|
||||||
mod bar;
|
mod bar;
|
||||||
#[cfg(feature = "cli")]
|
mod bridge_channel;
|
||||||
mod cli;
|
|
||||||
mod clients;
|
mod clients;
|
||||||
mod config;
|
mod config;
|
||||||
mod desktop_file;
|
mod desktop_file;
|
||||||
mod dynamic_value;
|
mod dynamic_string;
|
||||||
mod error;
|
mod error;
|
||||||
mod gtk_helpers;
|
|
||||||
mod image;
|
mod image;
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
mod ipc;
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
mod ironvar;
|
|
||||||
mod logging;
|
mod logging;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod modules;
|
mod modules;
|
||||||
@@ -56,240 +15,116 @@ mod popup;
|
|||||||
mod script;
|
mod script;
|
||||||
mod style;
|
mod style;
|
||||||
|
|
||||||
|
use crate::bar::create_bar;
|
||||||
|
use crate::config::{Config, MonitorConfig};
|
||||||
|
use crate::style::load_css;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use color_eyre::Report;
|
||||||
|
use dirs::config_dir;
|
||||||
|
use gtk::gdk::{Display, Monitor};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::Application;
|
||||||
|
use std::env;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::exit;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tokio::task::block_in_place;
|
||||||
|
|
||||||
|
use crate::error::ExitCode;
|
||||||
|
use clients::wayland::{self, WaylandClient};
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
use universal_config::ConfigLoader;
|
||||||
|
|
||||||
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
let _guard = logging::install_logging();
|
let _guard = logging::install_logging();
|
||||||
|
|
||||||
cfg_if! {
|
info!("Ironbar version {}", VERSION);
|
||||||
if #[cfg(feature = "cli")] {
|
info!("Starting application");
|
||||||
run_with_args();
|
|
||||||
} else {
|
|
||||||
start_ironbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
let wayland_client = wayland::get_client().await;
|
||||||
fn run_with_args() {
|
|
||||||
let args = cli::Args::parse();
|
|
||||||
|
|
||||||
match args.command {
|
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||||
Some(command) => {
|
|
||||||
let rt = create_runtime();
|
|
||||||
rt.block_on(async move {
|
|
||||||
let ipc = ipc::Ipc::new();
|
|
||||||
match ipc.send(command).await {
|
|
||||||
Ok(res) => cli::handle_response(res),
|
|
||||||
Err(err) => error!("{err:?}"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => start_ironbar(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
app.connect_activate(move |app| {
|
||||||
|
let display = Display::default().map_or_else(
|
||||||
|
|| {
|
||||||
|
let report = Report::msg("Failed to get default GTK display");
|
||||||
|
error!("{:?}", report);
|
||||||
|
exit(ExitCode::GtkDisplay as i32)
|
||||||
|
},
|
||||||
|
|display| display,
|
||||||
|
);
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|
||||||
static ref RUNTIME: Arc<Runtime> = Arc::new(create_runtime());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Ironbar {
|
|
||||||
bars: Rc<RefCell<Vec<Bar>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ironbar {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
bars: Rc::new(RefCell::new(vec![])),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start(self) {
|
|
||||||
info!("Ironbar version {}", VERSION);
|
|
||||||
info!("Starting application");
|
|
||||||
|
|
||||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
|
||||||
|
|
||||||
let running = AtomicBool::new(false);
|
|
||||||
|
|
||||||
let instance = Rc::new(self);
|
|
||||||
|
|
||||||
// force start wayland client ahead of ui
|
|
||||||
let wl = wayland::get_client();
|
|
||||||
lock!(wl).roundtrip();
|
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
|
||||||
if running.load(Ordering::Relaxed) {
|
|
||||||
info!("Ironbar already running, returning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
running.set(true);
|
|
||||||
|
|
||||||
cfg_if! {
|
|
||||||
if #[cfg(feature = "ipc")] {
|
|
||||||
let ipc = ipc::Ipc::new();
|
|
||||||
ipc.start(app, instance.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*instance.bars.borrow_mut() = load_interface(app);
|
|
||||||
|
|
||||||
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
|
||||||
|| {
|
|
||||||
config_dir().map_or_else(
|
|
||||||
|| {
|
|
||||||
let report = Report::msg("Failed to locate user config dir");
|
|
||||||
error!("{:?}", report);
|
|
||||||
exit(ExitCode::CreateBars as i32);
|
|
||||||
},
|
|
||||||
|dir| dir.join("ironbar").join("style.css"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
PathBuf::from,
|
|
||||||
);
|
|
||||||
|
|
||||||
if style_path.exists() {
|
|
||||||
load_css(style_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
let ipc_path = ipc.path().to_path_buf();
|
|
||||||
spawn_blocking(move || {
|
|
||||||
rx.recv().expect("to receive from channel");
|
|
||||||
|
|
||||||
info!("Shutting down");
|
|
||||||
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
ipc::Ipc::shutdown(ipc_path);
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
|
|
||||||
.expect("Error setting Ctrl-C handler");
|
|
||||||
|
|
||||||
// TODO: Start wayland client - listen for outputs
|
|
||||||
// All bar loading should happen as an event response to this
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ignore CLI args
|
|
||||||
// Some are provided by swaybar_config but not currently supported
|
|
||||||
app.run_with_args(&Vec::<&str>::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current Tokio runtime.
|
|
||||||
#[must_use]
|
|
||||||
pub fn runtime() -> Arc<Runtime> {
|
|
||||||
RUNTIME.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
|
|
||||||
/// This is just a static `AtomicUsize` that increments every time this function is called.
|
|
||||||
pub fn unique_id() -> usize {
|
|
||||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the `Ironvar` manager singleton.
|
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
#[must_use]
|
|
||||||
pub fn variable_manager() -> Arc<RwLock<VariableManager>> {
|
|
||||||
VARIABLE_MANAGER.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a clone of a bar by its unique name.
|
|
||||||
///
|
|
||||||
/// Since the bar contains mostly GTK objects,
|
|
||||||
/// the clone is cheap enough to not worry about.
|
|
||||||
#[must_use]
|
|
||||||
pub fn bar_by_name(&self, name: &str) -> Option<Bar> {
|
|
||||||
self.bars
|
|
||||||
.borrow()
|
|
||||||
.iter()
|
|
||||||
.find(|&bar| bar.name() == name)
|
|
||||||
.cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_ironbar() {
|
|
||||||
let ironbar = Ironbar::new();
|
|
||||||
ironbar.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the Ironbar config and interface.
|
|
||||||
pub fn load_interface(app: &Application) -> Vec<Bar> {
|
|
||||||
let display = Display::default().map_or_else(
|
|
||||||
|| {
|
|
||||||
let report = Report::msg("Failed to get default GTK display");
|
|
||||||
error!("{:?}", report);
|
|
||||||
exit(ExitCode::GtkDisplay as i32)
|
|
||||||
},
|
|
||||||
|display| display,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut config = env::var("IRONBAR_CONFIG")
|
|
||||||
.map_or_else(
|
|
||||||
|_| ConfigLoader::new("ironbar").find_and_load(),
|
|_| ConfigLoader::new("ironbar").find_and_load(),
|
||||||
ConfigLoader::load,
|
ConfigLoader::load,
|
||||||
)
|
);
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
error!("Failed to load config: {}", err);
|
|
||||||
warn!("Falling back to the default config");
|
|
||||||
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
|
|
||||||
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
|
|
||||||
|
|
||||||
Config::default()
|
let config = match config_res {
|
||||||
});
|
Ok(config) => config,
|
||||||
|
Err(err) => {
|
||||||
debug!("Loaded config file");
|
error!("{:?}", err);
|
||||||
|
exit(ExitCode::Config as i32)
|
||||||
#[cfg(feature = "ipc")]
|
|
||||||
if let Some(ironvars) = config.ironvar_defaults.take() {
|
|
||||||
let variable_manager = Ironbar::variable_manager();
|
|
||||||
for (k, v) in ironvars {
|
|
||||||
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
|
|
||||||
warn!("Ignoring invalid ironvar: '{k}'");
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
match create_bars(app, &display, &config) {
|
debug!("Loaded config file");
|
||||||
Ok(bars) => {
|
|
||||||
debug!("Created {} bars", bars.len());
|
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||||
bars
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(ExitCode::CreateBars as i32);
|
exit(ExitCode::CreateBars as i32);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
debug!("Created bars");
|
||||||
|
|
||||||
|
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
||||||
|
|| {
|
||||||
|
config_dir().map_or_else(
|
||||||
|
|| {
|
||||||
|
let report = Report::msg("Failed to locate user config dir");
|
||||||
|
error!("{:?}", report);
|
||||||
|
exit(ExitCode::CreateBars as i32);
|
||||||
|
},
|
||||||
|
|dir| dir.join("ironbar").join("style.css"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
PathBuf::from,
|
||||||
|
);
|
||||||
|
|
||||||
|
if style_path.exists() {
|
||||||
|
load_css(style_path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ignore CLI args
|
||||||
|
// Some are provided by swaybar_config but not currently supported
|
||||||
|
app.run_with_args(&Vec::<&str>::new());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates each of the bars across each of the (configured) outputs.
|
/// Creates each of the bars across each of the (configured) outputs.
|
||||||
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<Vec<Bar>> {
|
fn create_bars(
|
||||||
let wl = wayland::get_client();
|
app: &Application,
|
||||||
let outputs = lock!(wl).get_outputs();
|
display: &Display,
|
||||||
|
wl: &WaylandClient,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
let outputs = wl.outputs.as_slice();
|
||||||
|
|
||||||
debug!("Received {} outputs from Wayland", outputs.len());
|
debug!("Received {} outputs from Wayland", outputs.len());
|
||||||
debug!("Outputs: {:?}", outputs);
|
debug!("Outputs: {:?}", outputs);
|
||||||
|
|
||||||
let num_monitors = display.n_monitors();
|
for monitor in display.monitors().iter::<Monitor>() {
|
||||||
|
let monitor = monitor.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let show_default_bar =
|
|
||||||
config.start.is_some() || config.center.is_some() || config.end.is_some();
|
|
||||||
|
|
||||||
let mut all_bars = vec![];
|
|
||||||
for i in 0..num_monitors {
|
for i in 0..num_monitors {
|
||||||
let monitor = display
|
let monitor = display
|
||||||
.monitor(i)
|
.monitor(i)
|
||||||
@@ -297,66 +132,35 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
|
|||||||
let output = outputs
|
let output = outputs
|
||||||
.get(i as usize)
|
.get(i as usize)
|
||||||
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
||||||
|
let monitor_name = &output.name;
|
||||||
|
|
||||||
let Some(monitor_name) = &output.name else {
|
config.monitors.as_ref().map_or_else(
|
||||||
continue;
|
|| {
|
||||||
};
|
info!("Creating bar on '{}'", monitor_name);
|
||||||
|
create_bar(app, &monitor, monitor_name, config.clone())
|
||||||
|
},
|
||||||
|
|config| {
|
||||||
|
let config = config.get(monitor_name);
|
||||||
|
match &config {
|
||||||
|
Some(MonitorConfig::Single(config)) => {
|
||||||
|
info!("Creating bar on '{}'", monitor_name);
|
||||||
|
create_bar(app, &monitor, monitor_name, config.clone())
|
||||||
|
}
|
||||||
|
Some(MonitorConfig::Multiple(configs)) => {
|
||||||
|
for config in configs {
|
||||||
|
info!("Creating bar on '{}'", monitor_name);
|
||||||
|
create_bar(app, &monitor, monitor_name, config.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut bars = match config
|
Ok(())
|
||||||
.monitors
|
}
|
||||||
.as_ref()
|
_ => Ok(()),
|
||||||
.and_then(|config| config.get(monitor_name))
|
}
|
||||||
{
|
},
|
||||||
Some(MonitorConfig::Single(config)) => {
|
)?;
|
||||||
vec![create_bar(
|
|
||||||
app,
|
|
||||||
&monitor,
|
|
||||||
monitor_name.to_string(),
|
|
||||||
config.clone(),
|
|
||||||
)?]
|
|
||||||
}
|
|
||||||
Some(MonitorConfig::Multiple(configs)) => configs
|
|
||||||
.iter()
|
|
||||||
.map(|config| create_bar(app, &monitor, monitor_name.to_string(), config.clone()))
|
|
||||||
.collect::<Result<_>>()?,
|
|
||||||
None if show_default_bar => vec![create_bar(
|
|
||||||
app,
|
|
||||||
&monitor,
|
|
||||||
monitor_name.to_string(),
|
|
||||||
config.clone(),
|
|
||||||
)?],
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
all_bars.append(&mut bars);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(all_bars)
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
fn create_runtime() -> Runtime {
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("tokio to create a valid runtime")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls `spawn` on the Tokio runtime.
|
|
||||||
pub fn spawn<F>(f: F) -> JoinHandle<F::Output>
|
|
||||||
where
|
|
||||||
F: Future + Send + 'static,
|
|
||||||
F::Output: Send + 'static,
|
|
||||||
{
|
|
||||||
Ironbar::runtime().spawn(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls `spawn_blocking` on the Tokio runtime.
|
|
||||||
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
|
|
||||||
where
|
|
||||||
F: FnOnce() -> R + Send + 'static,
|
|
||||||
R: Send + 'static,
|
|
||||||
{
|
|
||||||
Ironbar::runtime().spawn_blocking(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blocks on a `Future` until it resolves.
|
/// Blocks on a `Future` until it resolves.
|
||||||
@@ -370,5 +174,5 @@ where
|
|||||||
///
|
///
|
||||||
/// TODO: remove all instances of this once async trait funcs are stable
|
/// TODO: remove all instances of this once async trait funcs are stable
|
||||||
pub fn await_sync<F: Future>(f: F) -> F::Output {
|
pub fn await_sync<F: Future>(f: F) -> F::Output {
|
||||||
block_in_place(|| Ironbar::runtime().block_on(f))
|
block_in_place(|| Handle::current().block_on(f))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ use crate::clients::clipboard::{self, ClipboardEvent};
|
|||||||
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
||||||
use crate::config::{CommonConfig, TruncateMode};
|
use crate::config::{CommonConfig, TruncateMode};
|
||||||
use crate::image::new_icon_button;
|
use crate::image::new_icon_button;
|
||||||
use crate::modules::{
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
use crate::popup::Popup;
|
||||||
};
|
use crate::try_send;
|
||||||
use crate::{glib_recv, spawn, try_send};
|
|
||||||
use glib::Propagation;
|
|
||||||
use gtk::gdk_pixbuf::Pixbuf;
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
|
use gtk::{Button, Image, Label, Orientation, RadioButton, Widget};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use glib::signal::Inhibit;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
@@ -72,8 +72,8 @@ impl Module<Button> for ClipboardModule {
|
|||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> color_eyre::Result<()> {
|
) -> color_eyre::Result<()> {
|
||||||
let max_items = self.max_items;
|
let max_items = self.max_items;
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut rx = {
|
let mut rx = {
|
||||||
let client = clipboard::get_client();
|
let client = clipboard::get_client();
|
||||||
client.subscribe(max_items)
|
client.subscribe(max_items).await
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
@@ -112,7 +112,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
let client = clipboard::get_client();
|
let client = clipboard::get_client();
|
||||||
match event {
|
match event {
|
||||||
UIEvent::Copy(id) => client.copy(id),
|
UIEvent::Copy(id) => client.copy(id).await,
|
||||||
UIEvent::Remove(id) => client.remove(id),
|
UIEvent::Remove(id) => client.remove(id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,45 +125,53 @@ impl Module<Button> for ClipboardModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> color_eyre::Result<ModuleParts<Button>> {
|
) -> color_eyre::Result<ModuleWidget<Button>> {
|
||||||
|
let position = info.bar_position;
|
||||||
|
|
||||||
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
||||||
button.style_context().add_class("btn");
|
button.style_context().add_class("btn");
|
||||||
|
|
||||||
let tx = context.tx.clone();
|
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
let pos = Popup::widget_geometry(button, position.get_orientation());
|
||||||
|
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
||||||
});
|
});
|
||||||
|
|
||||||
let rx = context.subscribe();
|
// we need to bind to the receiver as the channel does not open
|
||||||
let popup = self
|
// until the popup is first opened.
|
||||||
.into_popup(context.controller_tx, rx, info)
|
context.widget_rx.attach(None, |_| Continue(true));
|
||||||
.into_popup_parts(vec![&button]);
|
|
||||||
|
|
||||||
Ok(ModuleParts::new(button, popup))
|
Ok(ModuleWidget {
|
||||||
|
widget: button,
|
||||||
|
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
tx: Sender<Self::ReceiveMessage>,
|
||||||
rx: broadcast::Receiver<Self::SendMessage>,
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box>
|
) -> Option<gtk::Box>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
let container = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(10)
|
||||||
|
.name("popup-clipboard")
|
||||||
|
.build();
|
||||||
|
|
||||||
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
||||||
container.add(&entries);
|
container.append(&entries);
|
||||||
|
|
||||||
let hidden_option = RadioButton::new();
|
let hidden_option = RadioButton::new();
|
||||||
entries.add(&hidden_option);
|
entries.append(&hidden_option);
|
||||||
|
|
||||||
let mut items = HashMap::new();
|
let mut items = HashMap::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
let hidden_option = hidden_option.clone();
|
let hidden_option = hidden_option.clone();
|
||||||
glib_recv!(rx, event => {
|
rx.attach(None, move |event| {
|
||||||
match event {
|
match event {
|
||||||
ControllerEvent::Add(id, item) => {
|
ControllerEvent::Add(id, item) => {
|
||||||
debug!("Adding new value with ID {}", id);
|
debug!("Adding new value with ID {}", id);
|
||||||
@@ -176,7 +184,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
let button = RadioButton::from_widget(&hidden_option);
|
let button = RadioButton::from_widget(&hidden_option);
|
||||||
|
|
||||||
let label = Label::new(Some(value));
|
let label = Label::new(Some(value));
|
||||||
button.add(&label);
|
button.append(&label);
|
||||||
|
|
||||||
if let Some(truncate) = self.truncate {
|
if let Some(truncate) = self.truncate {
|
||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
@@ -211,7 +219,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
button.set_active(true); // if just added, should be on clipboard
|
button.set_active(true); // if just added, should be on clipboard
|
||||||
|
|
||||||
let button_wrapper = EventBox::new();
|
let button_wrapper = EventBox::new();
|
||||||
button_wrapper.add(&button);
|
button_wrapper.append(&button);
|
||||||
|
|
||||||
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
||||||
button_wrapper.set_above_child(true);
|
button_wrapper.set_above_child(true);
|
||||||
@@ -229,7 +237,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
try_send!(tx, UIEvent::Copy(id));
|
try_send!(tx, UIEvent::Copy(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Propagation::Stop
|
Inhibit(true)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,12 +262,11 @@ impl Module<Button> for ClipboardModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
row.add(&button_wrapper);
|
row.append(&button_wrapper);
|
||||||
row.pack_end(&remove_button, false, false, 0);
|
row.pack_end(&remove_button, false, false, 0);
|
||||||
|
|
||||||
entries.add(&row);
|
entries.append(&row);
|
||||||
entries.reorder_child(&row, 0);
|
entries.reorder_child(&row, 0);
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
items.insert(id, (row, button));
|
items.insert(id, (row, button));
|
||||||
}
|
}
|
||||||
@@ -288,10 +295,11 @@ impl Module<Button> for ClipboardModule {
|
|||||||
hidden_option.set_active(true);
|
hidden_option.set_active(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
hidden_option.hide();
|
hidden_option.hide();
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
use std::env;
|
use crate::config::CommonConfig;
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use chrono::{DateTime, Local, Locale};
|
use crate::popup::Popup;
|
||||||
|
use crate::{send_async, try_send};
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Align, Button, Calendar, Label, Orientation};
|
use gtk::{Align, Button, Calendar, Label, Orientation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::config::CommonConfig;
|
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
|
||||||
use crate::modules::{
|
|
||||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
|
||||||
};
|
|
||||||
use crate::{glib_recv, send_async, spawn, try_send};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ClockModule {
|
pub struct ClockModule {
|
||||||
/// Date/time format string.
|
/// Date/time format string.
|
||||||
@@ -25,48 +22,14 @@ pub struct ClockModule {
|
|||||||
#[serde(default = "default_format")]
|
#[serde(default = "default_format")]
|
||||||
format: String,
|
format: String,
|
||||||
|
|
||||||
#[serde(default = "default_popup_format")]
|
|
||||||
format_popup: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_locale")]
|
|
||||||
locale: String,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ClockModule {
|
|
||||||
fn default() -> Self {
|
|
||||||
ClockModule {
|
|
||||||
format: default_format(),
|
|
||||||
format_popup: default_popup_format(),
|
|
||||||
locale: default_locale(),
|
|
||||||
common: Some(CommonConfig::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_format() -> String {
|
fn default_format() -> String {
|
||||||
String::from("%d/%m/%Y %H:%M")
|
String::from("%d/%m/%Y %H:%M")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_popup_format() -> String {
|
|
||||||
String::from("%H:%M:%S")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_locale() -> String {
|
|
||||||
env::var("LC_TIME")
|
|
||||||
.or_else(|_| env::var("LANG"))
|
|
||||||
.map_or_else(|_| "POSIX".to_string(), strip_tail)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_tail(string: String) -> String {
|
|
||||||
string
|
|
||||||
.split_once('.')
|
|
||||||
.map(|(head, _)| head.to_string())
|
|
||||||
.unwrap_or(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module<Button> for ClockModule {
|
impl Module<Button> for ClockModule {
|
||||||
type SendMessage = DateTime<Local>;
|
type SendMessage = DateTime<Local>;
|
||||||
type ReceiveMessage = ();
|
type ReceiveMessage = ();
|
||||||
@@ -96,59 +59,66 @@ impl Module<Button> for ClockModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<Button>> {
|
) -> Result<ModuleWidget<Button>> {
|
||||||
let button = Button::new();
|
let button = Button::new();
|
||||||
let label = Label::new(None);
|
let label = Label::new(None);
|
||||||
label.set_angle(info.bar_position.get_angle());
|
label.set_angle(info.bar_position.get_angle());
|
||||||
button.add(&label);
|
button.append(&label);
|
||||||
|
|
||||||
let tx = context.tx.clone();
|
let orientation = info.bar_position.get_orientation();
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
try_send!(
|
||||||
|
context.tx,
|
||||||
|
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let format = self.format.clone();
|
let format = self.format.clone();
|
||||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
{
|
||||||
|
context.widget_rx.attach(None, move |date| {
|
||||||
|
let date_string = format!("{}", date.format(&format));
|
||||||
|
label.set_label(&date_string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let rx = context.subscribe();
|
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||||
glib_recv!(rx, date => {
|
|
||||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
|
||||||
label.set_label(&date_string);
|
|
||||||
});
|
|
||||||
|
|
||||||
let popup = self
|
Ok(ModuleWidget {
|
||||||
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
|
widget: button,
|
||||||
.into_popup_parts(vec![&button]);
|
popup,
|
||||||
|
})
|
||||||
Ok(ModuleParts::new(button, popup))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||||
rx: broadcast::Receiver<Self::SendMessage>,
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box> {
|
) -> Option<gtk::Box> {
|
||||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
let container = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.name("popup-clock")
|
||||||
|
.build();
|
||||||
|
|
||||||
let clock = Label::builder().halign(Align::Center).build();
|
let clock = Label::builder()
|
||||||
clock.add_class("calendar-clock");
|
.name("calendar-clock")
|
||||||
|
.halign(Align::Center)
|
||||||
|
.build();
|
||||||
|
let format = "%H:%M:%S";
|
||||||
|
|
||||||
container.add(&clock);
|
container.append(&clock);
|
||||||
|
|
||||||
let calendar = Calendar::new();
|
let calendar = Calendar::builder().name("calendar").build();
|
||||||
calendar.add_class("calendar");
|
container.append(&calendar);
|
||||||
container.add(&calendar);
|
|
||||||
|
|
||||||
let format = self.format_popup;
|
{
|
||||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
rx.attach(None, move |date| {
|
||||||
|
let date_string = format!("{}", date.format(format));
|
||||||
glib_recv!(rx, date => {
|
clock.set_label(&date_string);
|
||||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
Continue(true)
|
||||||
clock.set_label(&date_string);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
|
|||||||
|
|
||||||
if let Some(widgets) = self.widgets {
|
if let Some(widgets) = self.widgets {
|
||||||
for widget in widgets {
|
for widget in widgets {
|
||||||
widget.widget.add_to(&container, &context, widget.common);
|
widget.widget.add_to(&container, context, widget.common);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::popup::Popup;
|
||||||
|
use crate::{build, try_send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, Label};
|
use gtk::{Button, Label};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::dynamic_value::dynamic_string;
|
|
||||||
use crate::modules::PopupButton;
|
|
||||||
use crate::{build, try_send};
|
|
||||||
|
|
||||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ButtonWidget {
|
pub struct ButtonWidget {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@@ -21,19 +19,20 @@ impl CustomWidget for ButtonWidget {
|
|||||||
|
|
||||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
let button = build!(self, Self::Widget);
|
let button = build!(self, Self::Widget);
|
||||||
context.popup_buttons.borrow_mut().push(button.clone());
|
|
||||||
|
|
||||||
if let Some(text) = self.label {
|
if let Some(text) = self.label {
|
||||||
let label = Label::new(None);
|
let label = Label::new(None);
|
||||||
label.set_use_markup(true);
|
label.set_use_markup(true);
|
||||||
button.add(&label);
|
button.append(&label);
|
||||||
|
|
||||||
dynamic_string(&text, move |string| {
|
DynamicString::new(&text, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(exec) = self.on_click {
|
if let Some(exec) = self.on_click {
|
||||||
|
let bar_orientation = context.bar_orientation;
|
||||||
let tx = context.tx.clone();
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
@@ -42,7 +41,7 @@ impl CustomWidget for ButtonWidget {
|
|||||||
ExecEvent {
|
ExecEvent {
|
||||||
cmd: exec.clone(),
|
cmd: exec.clone(),
|
||||||
args: None,
|
args: None,
|
||||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
geometry: Popup::widget_geometry(button, bar_orientation),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::build;
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::image::ImageProvider;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Image;
|
use gtk::Image;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tracing::error;
|
||||||
use crate::build;
|
|
||||||
use crate::dynamic_value::dynamic_string;
|
|
||||||
use crate::image::ImageProvider;
|
|
||||||
|
|
||||||
use super::{CustomWidget, CustomWidgetContext};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ImageWidget {
|
pub struct ImageWidget {
|
||||||
@@ -31,9 +30,15 @@ impl CustomWidget for ImageWidget {
|
|||||||
let gtk_image = gtk_image.clone();
|
let gtk_image = gtk_image.clone();
|
||||||
let icon_theme = context.icon_theme.clone();
|
let icon_theme = context.icon_theme.clone();
|
||||||
|
|
||||||
dynamic_string(&self.src, move |src| {
|
DynamicString::new(&self.src, move |src| {
|
||||||
ImageProvider::parse(&src, &icon_theme, false, self.size)
|
let res = ImageProvider::parse(&src, &icon_theme, self.size)
|
||||||
.map(|image| image.load_into_image(gtk_image.clone()));
|
.and_then(|image| image.load_into_image(gtk_image.clone()));
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::build;
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::build;
|
|
||||||
use crate::dynamic_value::dynamic_string;
|
|
||||||
|
|
||||||
use super::{CustomWidget, CustomWidgetContext};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct LabelWidget {
|
pub struct LabelWidget {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@@ -24,8 +22,9 @@ impl CustomWidget for LabelWidget {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
dynamic_string(&self.label, move |string| {
|
DynamicString::new(&self.label, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,21 +13,23 @@ use crate::config::CommonConfig;
|
|||||||
use crate::modules::custom::button::ButtonWidget;
|
use crate::modules::custom::button::ButtonWidget;
|
||||||
use crate::modules::custom::progress::ProgressWidget;
|
use crate::modules::custom::progress::ProgressWidget;
|
||||||
use crate::modules::{
|
use crate::modules::{
|
||||||
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
|
||||||
};
|
};
|
||||||
|
use crate::popup::WidgetGeometry;
|
||||||
use crate::script::Script;
|
use crate::script::Script;
|
||||||
use crate::{send_async, spawn};
|
use crate::send_async;
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Orientation};
|
use gtk::{IconTheme, Orientation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cell::RefCell;
|
use tokio::spawn;
|
||||||
use std::rc::Rc;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::sync::{broadcast, mpsc};
|
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct CustomModule {
|
pub struct CustomModule {
|
||||||
|
/// Container class name
|
||||||
|
class: Option<String>,
|
||||||
/// Widgets to add to the bar container
|
/// Widgets to add to the bar container
|
||||||
bar: Vec<WidgetConfig>,
|
bar: Vec<WidgetConfig>,
|
||||||
/// Widgets to add to the popup container
|
/// Widgets to add to the popup container
|
||||||
@@ -56,12 +58,11 @@ pub enum Widget {
|
|||||||
Progress(ProgressWidget),
|
Progress(ProgressWidget),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy)]
|
||||||
struct CustomWidgetContext<'a> {
|
struct CustomWidgetContext<'a> {
|
||||||
tx: &'a mpsc::Sender<ExecEvent>,
|
tx: &'a Sender<ExecEvent>,
|
||||||
bar_orientation: Orientation,
|
bar_orientation: Orientation,
|
||||||
icon_theme: &'a IconTheme,
|
icon_theme: &'a IconTheme,
|
||||||
popup_buttons: Rc<RefCell<Vec<Button>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait CustomWidget {
|
trait CustomWidget {
|
||||||
@@ -116,11 +117,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
|||||||
|
|
||||||
impl Widget {
|
impl Widget {
|
||||||
/// Creates this widget and adds it to the parent container
|
/// Creates this widget and adds it to the parent container
|
||||||
fn add_to(self, parent: >k::Box, context: &CustomWidgetContext, common: CommonConfig) {
|
fn add_to(self, parent: >k::Box, context: CustomWidgetContext, common: CommonConfig) {
|
||||||
macro_rules! create {
|
macro_rules! create {
|
||||||
($widget:expr) => {
|
($widget:expr) => {
|
||||||
wrap_widget(
|
wrap_widget(
|
||||||
&$widget.into_widget(context.clone()),
|
&$widget.into_widget(context),
|
||||||
common,
|
common,
|
||||||
context.bar_orientation,
|
context.bar_orientation,
|
||||||
)
|
)
|
||||||
@@ -144,7 +145,7 @@ impl Widget {
|
|||||||
pub struct ExecEvent {
|
pub struct ExecEvent {
|
||||||
cmd: String,
|
cmd: String,
|
||||||
args: Option<Vec<String>>,
|
args: Option<Vec<String>>,
|
||||||
id: usize,
|
geometry: WidgetGeometry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for CustomModule {
|
impl Module<gtk::Box> for CustomModule {
|
||||||
@@ -158,8 +159,8 @@ impl Module<gtk::Box> for CustomModule {
|
|||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
@@ -174,9 +175,9 @@ impl Module<gtk::Box> for CustomModule {
|
|||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
} else if event.cmd == "popup:toggle" {
|
} else if event.cmd == "popup:toggle" {
|
||||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
|
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
|
||||||
} else if event.cmd == "popup:open" {
|
} else if event.cmd == "popup:open" {
|
||||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
|
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
||||||
} else if event.cmd == "popup:close" {
|
} else if event.cmd == "popup:close" {
|
||||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
} else {
|
} else {
|
||||||
@@ -192,30 +193,29 @@ impl Module<gtk::Box> for CustomModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
let container = gtk::Box::builder().orientation(orientation).build();
|
let container = gtk::Box::builder().orientation(orientation).build();
|
||||||
|
|
||||||
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
|
if let Some(ref class) = self.class {
|
||||||
|
container.style_context().add_class(class);
|
||||||
|
}
|
||||||
|
|
||||||
let custom_context = CustomWidgetContext {
|
let custom_context = CustomWidgetContext {
|
||||||
tx: &context.controller_tx,
|
tx: &context.controller_tx,
|
||||||
bar_orientation: orientation,
|
bar_orientation: orientation,
|
||||||
icon_theme: info.icon_theme,
|
icon_theme: info.icon_theme,
|
||||||
popup_buttons: popup_buttons.clone(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.bar.clone().into_iter().for_each(|widget| {
|
self.bar.clone().into_iter().for_each(|widget| {
|
||||||
widget
|
widget
|
||||||
.widget
|
.widget
|
||||||
.add_to(&container, &custom_context, widget.common);
|
.add_to(&container, custom_context, widget.common);
|
||||||
});
|
});
|
||||||
|
|
||||||
let popup = self
|
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||||
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
|
|
||||||
.into_popup_parts_owned(popup_buttons.take());
|
|
||||||
|
|
||||||
Ok(ModuleParts {
|
Ok(ModuleWidget {
|
||||||
widget: container,
|
widget: container,
|
||||||
popup,
|
popup,
|
||||||
})
|
})
|
||||||
@@ -223,32 +223,35 @@ impl Module<gtk::Box> for CustomModule {
|
|||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
tx: Sender<Self::ReceiveMessage>,
|
||||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
_rx: glib::Receiver<Self::SendMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box>
|
) -> Option<gtk::Box>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
let container = gtk::Box::builder().name("popup-custom").build();
|
||||||
|
|
||||||
|
if let Some(class) = self.class {
|
||||||
|
container
|
||||||
|
.style_context()
|
||||||
|
.add_class(format!("popup-{class}").as_str());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(popup) = self.popup {
|
if let Some(popup) = self.popup {
|
||||||
let custom_context = CustomWidgetContext {
|
let custom_context = CustomWidgetContext {
|
||||||
tx: &tx,
|
tx: &tx,
|
||||||
bar_orientation: info.bar_position.get_orientation(),
|
bar_orientation: info.bar_position.get_orientation(),
|
||||||
icon_theme: info.icon_theme,
|
icon_theme: info.icon_theme,
|
||||||
popup_buttons: Rc::new(RefCell::new(vec![])),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for widget in popup {
|
for widget in popup {
|
||||||
widget
|
widget
|
||||||
.widget
|
.widget
|
||||||
.add_to(&container, &custom_context, widget.common);
|
.add_to(&container, custom_context, widget.common);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::modules::custom::set_length;
|
||||||
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
|
use crate::{build, send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::ProgressBar;
|
use gtk::ProgressBar;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
use tokio::spawn;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::dynamic_value::dynamic_string;
|
|
||||||
use crate::modules::custom::set_length;
|
|
||||||
use crate::script::{OutputStream, Script, ScriptInput};
|
|
||||||
use crate::{build, glib_recv_mpsc, spawn, try_send};
|
|
||||||
|
|
||||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ProgressWidget {
|
pub struct ProgressWidget {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@@ -47,13 +45,13 @@ impl CustomWidget for ProgressWidget {
|
|||||||
let script = Script::from(value);
|
let script = Script::from(value);
|
||||||
let progress = progress.clone();
|
let progress = progress.clone();
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(128);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script
|
script
|
||||||
.run(None, move |stream, _success| match stream {
|
.run(None, move |stream, _success| match stream {
|
||||||
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
||||||
Ok(value) => try_send!(tx, value),
|
Ok(value) => send!(tx, value),
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
},
|
},
|
||||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||||
@@ -61,15 +59,19 @@ impl CustomWidget for ProgressWidget {
|
|||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
|
||||||
glib_recv_mpsc!(rx, value => progress.set_fraction(value / self.max));
|
rx.attach(None, move |value| {
|
||||||
|
progress.set_fraction(value / self.max);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(text) = self.label {
|
if let Some(text) = self.label {
|
||||||
let progress = progress.clone();
|
let progress = progress.clone();
|
||||||
progress.set_show_text(true);
|
progress.set_show_text(true);
|
||||||
|
|
||||||
dynamic_string(&text, move |string| {
|
DynamicString::new(&text, move |string| {
|
||||||
progress.set_text(Some(&string));
|
progress.set_text(Some(&string));
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
use glib::Propagation;
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
use std::cell::Cell;
|
use crate::modules::custom::set_length;
|
||||||
use std::ops::Neg;
|
use crate::popup::Popup;
|
||||||
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
|
use crate::{build, send, try_send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Scale;
|
use gtk::Scale;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
use std::cell::Cell;
|
||||||
|
use std::ops::Neg;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
|
use tokio::spawn;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::modules::custom::set_length;
|
|
||||||
use crate::script::{OutputStream, Script, ScriptInput};
|
|
||||||
use crate::{build, glib_recv_mpsc, spawn, try_send};
|
|
||||||
|
|
||||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct SliderWidget {
|
pub struct SliderWidget {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@@ -78,10 +76,10 @@ impl CustomWidget for SliderWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
scale.set_value(value + delta);
|
scale.set_value(value + delta);
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
scale.connect_change_value(move |_, _, val| {
|
scale.connect_change_value(move |scale, _, val| {
|
||||||
// GTK will send values outside min/max range
|
// GTK will send values outside min/max range
|
||||||
let val = val.clamp(min, max);
|
let val = val.clamp(min, max);
|
||||||
|
|
||||||
@@ -91,14 +89,14 @@ impl CustomWidget for SliderWidget {
|
|||||||
ExecEvent {
|
ExecEvent {
|
||||||
cmd: on_change.clone(),
|
cmd: on_change.clone(),
|
||||||
args: Some(vec![val.to_string()]),
|
args: Some(vec![val.to_string()]),
|
||||||
id: usize::MAX // ignored
|
geometry: Popup::widget_geometry(scale, context.bar_orientation),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
prev_value.set(val);
|
prev_value.set(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +104,13 @@ impl CustomWidget for SliderWidget {
|
|||||||
let script = Script::from(value);
|
let script = Script::from(value);
|
||||||
let scale = scale.clone();
|
let scale = scale.clone();
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(128);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script
|
script
|
||||||
.run(None, move |stream, _success| match stream {
|
.run(None, move |stream, _success| match stream {
|
||||||
OutputStream::Stdout(out) => match out.parse() {
|
OutputStream::Stdout(out) => match out.parse() {
|
||||||
Ok(value) => try_send!(tx, value),
|
Ok(value) => send!(tx, value),
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
},
|
},
|
||||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||||
@@ -120,7 +118,10 @@ impl CustomWidget for SliderWidget {
|
|||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
|
||||||
glib_recv_mpsc!(rx, value => scale.set_value(value));
|
rx.attach(None, move |value| {
|
||||||
|
scale.set_value(value);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
scale
|
scale
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::clients::wayland::{self, ToplevelEvent};
|
use crate::clients::wayland::{self, ToplevelChange};
|
||||||
use crate::config::{CommonConfig, TruncateMode};
|
use crate::config::{CommonConfig, TruncateMode};
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
|
||||||
use crate::image::ImageProvider;
|
use crate::image::ImageProvider;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::{glib_recv, lock, send_async, spawn, try_send};
|
use crate::{await_sync, read_lock, send_async};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::debug;
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct FocusedModule {
|
pub struct FocusedModule {
|
||||||
@@ -30,24 +31,12 @@ pub struct FocusedModule {
|
|||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FocusedModule {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
show_icon: crate::config::default_true(),
|
|
||||||
show_title: crate::config::default_true(),
|
|
||||||
icon_size: default_icon_size(),
|
|
||||||
truncate: None,
|
|
||||||
common: Some(CommonConfig::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_icon_size() -> i32 {
|
const fn default_icon_size() -> i32 {
|
||||||
32
|
32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for FocusedModule {
|
impl Module<gtk::Box> for FocusedModule {
|
||||||
type SendMessage = Option<(String, String)>;
|
type SendMessage = (String, String);
|
||||||
type ReceiveMessage = ();
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
@@ -60,52 +49,38 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
_rx: Receiver<Self::ReceiveMessage>,
|
_rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let focused = await_sync(async {
|
||||||
|
let wl = wayland::get_client().await;
|
||||||
|
let toplevels = read_lock!(wl.toplevels);
|
||||||
|
|
||||||
|
toplevels
|
||||||
|
.iter()
|
||||||
|
.find(|(_, (top, _))| top.active)
|
||||||
|
.map(|(_, (top, _))| top.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(top) = focused {
|
||||||
|
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
|
||||||
|
}
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let (mut wlrx, handles) = {
|
let mut wlrx = {
|
||||||
let wl = wayland::get_client();
|
let wl = wayland::get_client().await;
|
||||||
let wl = lock!(wl);
|
|
||||||
wl.subscribe_toplevels()
|
wl.subscribe_toplevels()
|
||||||
};
|
};
|
||||||
|
|
||||||
let focused = handles.values().find_map(|handle| {
|
|
||||||
handle
|
|
||||||
.info()
|
|
||||||
.and_then(|info| if info.focused { Some(info) } else { None })
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(focused) = focused {
|
|
||||||
try_send!(
|
|
||||||
tx,
|
|
||||||
ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id)))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Ok(event) = wlrx.recv().await {
|
while let Ok(event) = wlrx.recv().await {
|
||||||
match event {
|
let update = match event.change {
|
||||||
ToplevelEvent::Update(handle) => {
|
ToplevelChange::Focus(focus) => focus,
|
||||||
let info = handle.info().unwrap_or_default();
|
ToplevelChange::Title(_) => event.toplevel.active,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
if info.focused {
|
if update {
|
||||||
debug!("Changing focus");
|
send_async!(
|
||||||
send_async!(
|
tx,
|
||||||
tx,
|
ModuleUpdateEvent::Update((event.toplevel.title, event.toplevel.app_id))
|
||||||
ModuleUpdateEvent::Update(Some((
|
);
|
||||||
info.title.clone(),
|
|
||||||
info.app_id.clone()
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
send_async!(tx, ModuleUpdateEvent::Update(None));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToplevelEvent::Remove(handle) => {
|
|
||||||
let info = handle.info().unwrap_or_default();
|
|
||||||
if info.focused {
|
|
||||||
debug!("Clearing focus");
|
|
||||||
send_async!(tx, ModuleUpdateEvent::Update(None));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToplevelEvent::New(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,51 +92,41 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
let icon_theme = info.icon_theme;
|
let icon_theme = info.icon_theme;
|
||||||
|
|
||||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
||||||
|
|
||||||
let icon = gtk::Image::new();
|
let icon = gtk::Image::builder().name("icon").build();
|
||||||
if self.show_icon {
|
let label = Label::builder().name("label").build();
|
||||||
icon.add_class("icon");
|
|
||||||
container.add(&icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = Label::new(None);
|
|
||||||
label.add_class("label");
|
|
||||||
|
|
||||||
if let Some(truncate) = self.truncate {
|
if let Some(truncate) = self.truncate {
|
||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.add(&label);
|
container.append(&icon);
|
||||||
|
container.append(&label);
|
||||||
|
|
||||||
{
|
{
|
||||||
let icon_theme = icon_theme.clone();
|
let icon_theme = icon_theme.clone();
|
||||||
glib_recv!(context.subscribe(), data => {
|
context.widget_rx.attach(None, move |(name, id)| {
|
||||||
if let Some((name, id)) = data {
|
if self.show_icon {
|
||||||
if self.show_icon {
|
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
.and_then(|image| image.load_into_image(icon.clone()))
|
||||||
.map(|image| image.load_into_image(icon.clone()))
|
{
|
||||||
{
|
error!("{err:?}");
|
||||||
Some(Ok(())) => icon.show(),
|
|
||||||
_ => icon.hide(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.show_title {
|
|
||||||
label.show();
|
|
||||||
label.set_label(&name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
icon.hide();
|
|
||||||
label.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.show_title {
|
||||||
|
label.set_label(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ModuleParts {
|
Ok(ModuleWidget {
|
||||||
widget: container,
|
widget: container,
|
||||||
popup: None,
|
popup: None,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::dynamic_value::dynamic_string;
|
use crate::dynamic_string::DynamicString;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::{glib_recv, try_send};
|
use crate::try_send;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -16,15 +17,6 @@ pub struct LabelModule {
|
|||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LabelModule {
|
|
||||||
pub(crate) fn new(label: String) -> Self {
|
|
||||||
Self {
|
|
||||||
label,
|
|
||||||
common: Some(CommonConfig::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module<Label> for LabelModule {
|
impl Module<Label> for LabelModule {
|
||||||
type SendMessage = String;
|
type SendMessage = String;
|
||||||
type ReceiveMessage = ();
|
type ReceiveMessage = ();
|
||||||
@@ -39,8 +31,9 @@ impl Module<Label> for LabelModule {
|
|||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
dynamic_string(&self.label, move |string| {
|
DynamicString::new(&self.label, move |string| {
|
||||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -50,16 +43,18 @@ impl Module<Label> for LabelModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<Label>> {
|
) -> Result<ModuleWidget<Label>> {
|
||||||
let label = Label::new(None);
|
let label = Label::new(None);
|
||||||
label.set_use_markup(true);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
glib_recv!(context.subscribe(), string => label.set_markup(&string));
|
context.widget_rx.attach(None, move |string| {
|
||||||
|
label.set_label(&string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ModuleParts {
|
Ok(ModuleWidget {
|
||||||
widget: label,
|
widget: label,
|
||||||
popup: None,
|
popup: None,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
use super::open_state::OpenState;
|
use super::open_state::OpenState;
|
||||||
use crate::clients::wayland::ToplevelHandle;
|
use crate::clients::wayland::ToplevelInfo;
|
||||||
use crate::config::BarPosition;
|
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
|
||||||
use crate::image::ImageProvider;
|
use crate::image::ImageProvider;
|
||||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||||
use crate::modules::ModuleUpdateEvent;
|
use crate::modules::ModuleUpdateEvent;
|
||||||
|
use crate::popup::Popup;
|
||||||
use crate::{read_lock, try_send};
|
use crate::{read_lock, try_send};
|
||||||
use color_eyre::{Report, Result};
|
|
||||||
use glib::Propagation;
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme};
|
use gtk::{Button, IconTheme, Orientation};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
@@ -38,30 +35,24 @@ impl Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Merges the provided node into this launcher item
|
/// Merges the provided node into this launcher item
|
||||||
pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> {
|
pub fn merge_toplevel(&mut self, node: ToplevelInfo) -> Window {
|
||||||
let info = handle
|
let id = node.id;
|
||||||
.info()
|
|
||||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
||||||
|
|
||||||
let id = info.id;
|
|
||||||
|
|
||||||
if self.windows.is_empty() {
|
if self.windows.is_empty() {
|
||||||
self.name = info.title;
|
self.name = node.title.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = Window::try_from(handle)?;
|
let window: Window = node.into();
|
||||||
self.windows.insert(id, window.clone());
|
self.windows.insert(id, window.clone());
|
||||||
|
|
||||||
self.recalculate_open_state();
|
self.recalculate_open_state();
|
||||||
|
|
||||||
Ok(window)
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) {
|
pub fn unmerge_toplevel(&mut self, node: &ToplevelInfo) {
|
||||||
if let Some(info) = handle.info() {
|
self.windows.remove(&node.id);
|
||||||
self.windows.remove(&info.id);
|
self.recalculate_open_state();
|
||||||
self.recalculate_open_state();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_window_name(&mut self, window_id: usize, name: String) {
|
pub fn set_window_name(&mut self, window_id: usize, name: String) {
|
||||||
@@ -97,29 +88,22 @@ impl Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ToplevelHandle> for Item {
|
impl From<ToplevelInfo> for Item {
|
||||||
type Error = Report;
|
fn from(toplevel: ToplevelInfo) -> Self {
|
||||||
|
let open_state = OpenState::from_toplevel(&toplevel);
|
||||||
fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> {
|
let name = toplevel.title.clone();
|
||||||
let info = handle
|
let app_id = toplevel.app_id.clone();
|
||||||
.info()
|
|
||||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
||||||
|
|
||||||
let name = info.title.clone();
|
|
||||||
let app_id = info.app_id.clone();
|
|
||||||
let open_state = OpenState::from(&info);
|
|
||||||
|
|
||||||
let mut windows = IndexMap::new();
|
let mut windows = IndexMap::new();
|
||||||
let window = Window::try_from(handle)?;
|
windows.insert(toplevel.id, toplevel.into());
|
||||||
windows.insert(info.id, window);
|
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
app_id,
|
app_id,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
open_state,
|
open_state,
|
||||||
windows,
|
windows,
|
||||||
name,
|
name,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,30 +112,17 @@ pub struct Window {
|
|||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub open_state: OpenState,
|
pub open_state: OpenState,
|
||||||
handle: ToplevelHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ToplevelHandle> for Window {
|
impl From<ToplevelInfo> for Window {
|
||||||
type Error = Report;
|
fn from(node: ToplevelInfo) -> Self {
|
||||||
|
let open_state = OpenState::from_toplevel(&node);
|
||||||
|
|
||||||
fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> {
|
Self {
|
||||||
let info = handle
|
id: node.id,
|
||||||
.info()
|
name: node.title,
|
||||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
|
||||||
let open_state = OpenState::from(&info);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
id: info.id,
|
|
||||||
name: info.title,
|
|
||||||
open_state,
|
open_state,
|
||||||
handle,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Window {
|
|
||||||
pub fn focus(&self, seat: &WlSeat) {
|
|
||||||
self.handle.focus(seat);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +149,7 @@ impl ItemButton {
|
|||||||
item: &Item,
|
item: &Item,
|
||||||
appearance: AppearanceOptions,
|
appearance: AppearanceOptions,
|
||||||
icon_theme: &IconTheme,
|
icon_theme: &IconTheme,
|
||||||
bar_position: BarPosition,
|
orientation: Orientation,
|
||||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||||
controller_tx: &Sender<ItemEvent>,
|
controller_tx: &Sender<ItemEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -193,14 +164,17 @@ impl ItemButton {
|
|||||||
if appearance.show_icons {
|
if appearance.show_icons {
|
||||||
let gtk_image = gtk::Image::new();
|
let gtk_image = gtk::Image::new();
|
||||||
let image =
|
let image =
|
||||||
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
|
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
|
||||||
if let Some(image) = image {
|
match image {
|
||||||
button.set_image(Some(>k_image));
|
Ok(image) => {
|
||||||
button.set_always_show_image(true);
|
button.set_image(Some(>k_image));
|
||||||
|
button.set_always_show_image(true);
|
||||||
|
|
||||||
if let Err(err) = image.load_into_image(gtk_image) {
|
if let Err(err) = image.load_into_image(gtk_image) {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,45 +225,16 @@ impl ItemButton {
|
|||||||
|
|
||||||
try_send!(
|
try_send!(
|
||||||
tx,
|
tx,
|
||||||
ModuleUpdateEvent::OpenPopupAt(
|
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
|
||||||
button.geometry(bar_position.get_orientation())
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
button.connect_leave_notify_event(move |button, ev| {
|
|
||||||
const THRESHOLD: f64 = 5.0;
|
|
||||||
|
|
||||||
let alloc = button.allocation();
|
|
||||||
|
|
||||||
let (x, y) = ev.position();
|
|
||||||
|
|
||||||
let close = match bar_position {
|
|
||||||
BarPosition::Top => y + THRESHOLD < f64::from(alloc.height()),
|
|
||||||
BarPosition::Bottom => y > THRESHOLD,
|
|
||||||
BarPosition::Left => x + THRESHOLD < f64::from(alloc.width()),
|
|
||||||
BarPosition::Right => x > THRESHOLD,
|
|
||||||
};
|
|
||||||
|
|
||||||
if close {
|
|
||||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
|
||||||
}
|
|
||||||
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
button.show_all();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
button,
|
button,
|
||||||
persistent: item.favorite,
|
persistent: item.favorite,
|
||||||
|
|||||||
@@ -3,22 +3,23 @@ mod open_state;
|
|||||||
|
|
||||||
use self::item::{Item, ItemButton, Window};
|
use self::item::{Item, ItemButton, Window};
|
||||||
use self::open_state::OpenState;
|
use self::open_state::OpenState;
|
||||||
use crate::clients::wayland::{self, ToplevelEvent};
|
use crate::clients::wayland::{self, ToplevelChange};
|
||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::desktop_file::find_desktop_file;
|
use crate::desktop_file::find_desktop_file;
|
||||||
use crate::modules::launcher::item::AppearanceOptions;
|
use crate::modules::launcher::item::AppearanceOptions;
|
||||||
use crate::modules::{
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
use crate::{lock, read_lock, try_send, write_lock};
|
||||||
};
|
|
||||||
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
|
|
||||||
use color_eyre::{Help, Report};
|
use color_eyre::{Help, Report};
|
||||||
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, Orientation};
|
use gtk::{Button, Orientation};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
@@ -90,8 +91,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let items = self
|
let items = self
|
||||||
.favorites
|
.favorites
|
||||||
@@ -108,68 +109,72 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
.collect::<IndexMap<_, _>>()
|
.collect::<IndexMap<_, _>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
let items = arc_mut!(items);
|
let items = Arc::new(Mutex::new(items));
|
||||||
|
|
||||||
let items2 = Arc::clone(&items);
|
{
|
||||||
let tx2 = tx.clone();
|
let items = Arc::clone(&items);
|
||||||
spawn(async move {
|
let tx = tx.clone();
|
||||||
let items = items2;
|
spawn(async move {
|
||||||
let tx = tx2;
|
let wl = wayland::get_client().await;
|
||||||
|
let open_windows = read_lock!(wl.toplevels);
|
||||||
|
|
||||||
let (mut wlrx, handles) = {
|
let open_windows = open_windows.clone();
|
||||||
let wl = wayland::get_client();
|
for (_, (window, _)) in open_windows {
|
||||||
let wl = lock!(wl);
|
let mut items = lock!(items);
|
||||||
wl.subscribe_toplevels()
|
let item = items.get_mut(&window.app_id);
|
||||||
};
|
match item {
|
||||||
|
Some(item) => {
|
||||||
for handle in handles.values() {
|
item.merge_toplevel(window);
|
||||||
let Some(info) = handle.info() else { continue };
|
}
|
||||||
|
None => {
|
||||||
let mut items = lock!(items);
|
items.insert(window.app_id.clone(), window.into());
|
||||||
let item = items.get_mut(&info.app_id);
|
}
|
||||||
match item {
|
|
||||||
Some(item) => {
|
|
||||||
item.merge_toplevel(handle.clone())?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
items.insert(info.app_id.clone(), Item::try_from(handle.clone())?);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let items = lock!(items);
|
let items = lock!(items);
|
||||||
let items = items.iter();
|
let items = items.iter();
|
||||||
for (_, item) in items {
|
for (_, item) in items {
|
||||||
try_send!(
|
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||||
tx,
|
item.clone(),
|
||||||
ModuleUpdateEvent::Update(LauncherUpdate::AddItem(item.clone()))
|
)))?;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Ok::<(), Report>(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let items2 = Arc::clone(&items);
|
||||||
|
spawn(async move {
|
||||||
|
let items = items2;
|
||||||
|
|
||||||
|
let mut wlrx = {
|
||||||
|
let wl = wayland::get_client().await;
|
||||||
|
wl.subscribe_toplevels()
|
||||||
|
};
|
||||||
|
|
||||||
let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
|
let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
|
||||||
|
|
||||||
while let Ok(event) = wlrx.recv().await {
|
while let Ok(event) = wlrx.recv().await {
|
||||||
trace!("event: {:?}", event);
|
trace!("event: {:?}", event);
|
||||||
|
|
||||||
match event {
|
let window = event.toplevel;
|
||||||
ToplevelEvent::New(handle) => {
|
let app_id = window.app_id.clone();
|
||||||
let Some(info) = handle.info() else { continue };
|
|
||||||
|
|
||||||
|
match event.change {
|
||||||
|
ToplevelChange::New => {
|
||||||
let new_item = {
|
let new_item = {
|
||||||
let mut items = lock!(items);
|
let mut items = lock!(items);
|
||||||
let item = items.get_mut(&info.app_id);
|
let item = items.get_mut(&app_id);
|
||||||
match item {
|
match item {
|
||||||
None => {
|
None => {
|
||||||
let item: Item = handle.try_into()?;
|
let item: Item = window.into();
|
||||||
|
items.insert(app_id.clone(), item.clone());
|
||||||
items.insert(info.app_id.clone(), item.clone());
|
|
||||||
|
|
||||||
ItemOrWindow::Item(item)
|
ItemOrWindow::Item(item)
|
||||||
}
|
}
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
let window = item.merge_toplevel(handle)?;
|
let window = item.merge_toplevel(window);
|
||||||
ItemOrWindow::Window(window)
|
ItemOrWindow::Window(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,40 +185,20 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
send_update(LauncherUpdate::AddItem(item)).await
|
send_update(LauncherUpdate::AddItem(item)).await
|
||||||
}
|
}
|
||||||
ItemOrWindow::Window(window) => {
|
ItemOrWindow::Window(window) => {
|
||||||
send_update(LauncherUpdate::AddWindow(info.app_id.clone(), window))
|
send_update(LauncherUpdate::AddWindow(app_id, window)).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}?;
|
}?;
|
||||||
}
|
}
|
||||||
ToplevelEvent::Update(handle) => {
|
ToplevelChange::Close => {
|
||||||
let Some(info) = handle.info() else { continue };
|
|
||||||
|
|
||||||
if let Some(item) = lock!(items).get_mut(&info.app_id) {
|
|
||||||
item.set_window_focused(info.id, info.focused);
|
|
||||||
item.set_window_name(info.id, info.title.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused))
|
|
||||||
.await?;
|
|
||||||
send_update(LauncherUpdate::Title(
|
|
||||||
info.app_id.clone(),
|
|
||||||
info.id,
|
|
||||||
info.title.clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
ToplevelEvent::Remove(handle) => {
|
|
||||||
let Some(info) = handle.info() else { continue };
|
|
||||||
|
|
||||||
let remove_item = {
|
let remove_item = {
|
||||||
let mut items = lock!(items);
|
let mut items = lock!(items);
|
||||||
let item = items.get_mut(&info.app_id);
|
let item = items.get_mut(&app_id);
|
||||||
match item {
|
match item {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
item.unmerge_toplevel(&handle);
|
item.unmerge_toplevel(&window);
|
||||||
|
|
||||||
if item.windows.is_empty() {
|
if item.windows.is_empty() {
|
||||||
items.remove(&info.app_id);
|
items.remove(&app_id);
|
||||||
Some(ItemOrWindowId::Item)
|
Some(ItemOrWindowId::Item)
|
||||||
} else {
|
} else {
|
||||||
Some(ItemOrWindowId::Window)
|
Some(ItemOrWindowId::Window)
|
||||||
@@ -225,28 +210,56 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
match remove_item {
|
match remove_item {
|
||||||
Some(ItemOrWindowId::Item) => {
|
Some(ItemOrWindowId::Item) => {
|
||||||
send_update(LauncherUpdate::RemoveItem(info.app_id.clone()))
|
send_update(LauncherUpdate::RemoveItem(app_id)).await?;
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
Some(ItemOrWindowId::Window) => {
|
Some(ItemOrWindowId::Window) => {
|
||||||
send_update(LauncherUpdate::RemoveWindow(
|
send_update(LauncherUpdate::RemoveWindow(app_id, window.id))
|
||||||
info.app_id.clone(),
|
.await?;
|
||||||
info.id,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
ToplevelChange::Focus(focused) => {
|
||||||
|
let mut update_title = false;
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
if let Some(item) = lock!(items).get_mut(&app_id) {
|
||||||
|
item.set_window_focused(window.id, true);
|
||||||
|
|
||||||
|
// might be switching focus between windows of same app
|
||||||
|
if item.windows.len() > 1 {
|
||||||
|
item.set_window_name(window.id, window.title.clone());
|
||||||
|
update_title = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Focus(app_id.clone(), focused)).await?;
|
||||||
|
|
||||||
|
if update_title {
|
||||||
|
send_update(LauncherUpdate::Title(app_id, window.id, window.title))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToplevelChange::Title(title) => {
|
||||||
|
if let Some(item) = lock!(items).get_mut(&app_id) {
|
||||||
|
item.set_window_name(window.id, title.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
send_update(LauncherUpdate::Title(app_id, window.id, title)).await?;
|
||||||
|
}
|
||||||
|
ToplevelChange::Fullscreen(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<LauncherUpdate>>>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen to ui events
|
// listen to ui events
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
|
trace!("{:?}", event);
|
||||||
|
|
||||||
if let ItemEvent::OpenItem(app_id) = event {
|
if let ItemEvent::OpenItem(app_id) = event {
|
||||||
find_desktop_file(&app_id).map_or_else(
|
find_desktop_file(&app_id).map_or_else(
|
||||||
|| error!("Could not find desktop file for {}", app_id),
|
|| error!("Could not find desktop file for {}", app_id),
|
||||||
@@ -270,39 +283,27 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
let wl = wayland::get_client().await;
|
||||||
|
|
||||||
let wl = wayland::get_client();
|
|
||||||
let items = lock!(items);
|
let items = lock!(items);
|
||||||
|
|
||||||
let id = match event {
|
let id = match event {
|
||||||
ItemEvent::FocusItem(app_id) => items.get(&app_id).and_then(|item| {
|
ItemEvent::FocusItem(app_id) => items
|
||||||
item.windows
|
.get(&app_id)
|
||||||
.iter()
|
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
|
||||||
.find(|(_, win)| !win.open_state.is_focused())
|
ItemEvent::FocusWindow(id) => Some(id), // FIXME: Broken on wlroots-git
|
||||||
.or_else(|| item.windows.first())
|
|
||||||
.map(|(_, win)| win.id)
|
|
||||||
}),
|
|
||||||
ItemEvent::FocusWindow(id) => Some(id),
|
|
||||||
ItemEvent::OpenItem(_) => unreachable!(),
|
ItemEvent::OpenItem(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if let Some(window) =
|
let toplevels = read_lock!(wl.toplevels);
|
||||||
items.iter().find_map(|(_, item)| item.windows.get(&id))
|
let seat = wl.seats.first().expect("Failed to get Wayland seat");
|
||||||
{
|
if let Some((_top, handle)) = toplevels.get(&id) {
|
||||||
debug!("Focusing window {id}: {}", window.name);
|
handle.activate(seat);
|
||||||
|
};
|
||||||
let seat = lock!(wl)
|
|
||||||
.get_seats()
|
|
||||||
.pop()
|
|
||||||
.expect("Failed to get Wayland seat");
|
|
||||||
window.focus(&seat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// roundtrip to immediately send activate event
|
// roundtrip to immediately send activate event
|
||||||
lock!(wl).roundtrip();
|
wl.roundtrip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -314,7 +315,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
) -> crate::Result<ModuleWidget<gtk::Box>> {
|
||||||
let icon_theme = info.icon_theme;
|
let icon_theme = info.icon_theme;
|
||||||
|
|
||||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
||||||
@@ -332,13 +333,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let show_names = self.show_names;
|
let show_names = self.show_names;
|
||||||
let bar_position = info.bar_position;
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
let mut buttons = IndexMap::<String, ItemButton>::new();
|
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||||
|
|
||||||
let tx = context.tx.clone();
|
context.widget_rx.attach(None, move |event| {
|
||||||
let rx = context.subscribe();
|
|
||||||
glib_recv!(rx, event => {
|
|
||||||
match event {
|
match event {
|
||||||
LauncherUpdate::AddItem(item) => {
|
LauncherUpdate::AddItem(item) => {
|
||||||
debug!("Adding item with id {}", item.app_id);
|
debug!("Adding item with id {}", item.app_id);
|
||||||
@@ -350,19 +349,18 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
&item,
|
&item,
|
||||||
appearance_options,
|
appearance_options,
|
||||||
&icon_theme,
|
&icon_theme,
|
||||||
bar_position,
|
orientation,
|
||||||
&tx,
|
&context.tx,
|
||||||
&controller_tx,
|
&controller_tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
container.add(&button.button);
|
container.append(&button.button);
|
||||||
buttons.insert(item.app_id, button);
|
buttons.insert(item.app_id, button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LauncherUpdate::AddWindow(app_id, win) => {
|
LauncherUpdate::AddWindow(app_id, _) => {
|
||||||
if let Some(button) = buttons.get(&app_id) {
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
button.set_open(true);
|
button.set_open(true);
|
||||||
button.set_focused(win.open_state.is_focused());
|
|
||||||
|
|
||||||
let mut menu_state = write_lock!(button.menu_state);
|
let mut menu_state = write_lock!(button.menu_state);
|
||||||
menu_state.num_windows += 1;
|
menu_state.num_windows += 1;
|
||||||
@@ -383,12 +381,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
LauncherUpdate::RemoveWindow(app_id, _) => {
|
||||||
debug!("Removing window {win_id} with id {app_id}");
|
|
||||||
|
|
||||||
if let Some(button) = buttons.get(&app_id) {
|
if let Some(button) = buttons.get(&app_id) {
|
||||||
button.set_focused(false);
|
|
||||||
|
|
||||||
let mut menu_state = write_lock!(button.menu_state);
|
let mut menu_state = write_lock!(button.menu_state);
|
||||||
menu_state.num_windows -= 1;
|
menu_state.num_windows -= 1;
|
||||||
}
|
}
|
||||||
@@ -411,15 +405,13 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
}
|
}
|
||||||
LauncherUpdate::Hover(_) => {}
|
LauncherUpdate::Hover(_) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let rx = context.subscribe();
|
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||||
let popup = self
|
Ok(ModuleWidget {
|
||||||
.into_popup(context.controller_tx, rx, info)
|
|
||||||
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
|
|
||||||
|
|
||||||
Ok(ModuleParts {
|
|
||||||
widget: container,
|
widget: container,
|
||||||
popup,
|
popup,
|
||||||
})
|
})
|
||||||
@@ -427,24 +419,27 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
|
controller_tx: Sender<Self::ReceiveMessage>,
|
||||||
rx: broadcast::Receiver<Self::SendMessage>,
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box> {
|
) -> Option<gtk::Box> {
|
||||||
const MAX_WIDTH: i32 = 250;
|
const MAX_WIDTH: i32 = 250;
|
||||||
|
|
||||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
let container = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.name("popup-launcher")
|
||||||
|
.build();
|
||||||
|
|
||||||
// we need some content to force the container to have a size
|
// we need some content to force the container to have a size
|
||||||
let placeholder = Button::with_label("PLACEHOLDER");
|
let placeholder = Button::with_label("PLACEHOLDER");
|
||||||
placeholder.set_width_request(MAX_WIDTH);
|
placeholder.set_width_request(MAX_WIDTH);
|
||||||
container.add(&placeholder);
|
container.append(&placeholder);
|
||||||
|
|
||||||
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
glib_recv!(rx, event => {
|
rx.attach(None, move |event| {
|
||||||
match event {
|
match event {
|
||||||
LauncherUpdate::AddItem(item) => {
|
LauncherUpdate::AddItem(item) => {
|
||||||
let app_id = item.app_id.clone();
|
let app_id = item.app_id.clone();
|
||||||
@@ -461,8 +456,12 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let tx = controller_tx.clone();
|
let tx = controller_tx.clone();
|
||||||
button.connect_clicked(move |_| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||||
|
|
||||||
|
if let Some(win) = button.window() {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +485,12 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let tx = controller_tx.clone();
|
let tx = controller_tx.clone();
|
||||||
button.connect_clicked(move |_button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||||
|
|
||||||
|
if let Some(win) = button.window() {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,15 +525,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
if let Some(buttons) = buttons.get(&app_id) {
|
if let Some(buttons) = buttons.get(&app_id) {
|
||||||
for (_, button) in buttons {
|
for (_, button) in buttons {
|
||||||
button.style_context().add_class("popup-item");
|
button.style_context().add_class("popup-item");
|
||||||
container.add(button);
|
container.append(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
container.set_width_request(MAX_WIDTH);
|
container.set_width_request(MAX_WIDTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,9 +547,6 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
/// This is a hacky number derived from
|
/// This is a hacky number derived from
|
||||||
/// "what fits inside the 250px popup"
|
/// "what fits inside the 250px popup"
|
||||||
/// and probably won't hold up with wide fonts.
|
/// and probably won't hold up with wide fonts.
|
||||||
///
|
|
||||||
/// TODO: Migrate this to truncate system
|
|
||||||
///
|
|
||||||
fn clamp(str: &str) -> String {
|
fn clamp(str: &str) -> String {
|
||||||
const MAX_CHARS: usize = 24;
|
const MAX_CHARS: usize = 24;
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ pub enum OpenState {
|
|||||||
Open { focused: bool },
|
Open { focused: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ToplevelInfo> for OpenState {
|
impl OpenState {
|
||||||
fn from(info: &ToplevelInfo) -> Self {
|
/// Creates from `SwayNode`
|
||||||
|
pub const fn from_toplevel(toplevel: &ToplevelInfo) -> Self {
|
||||||
Self::Open {
|
Self::Open {
|
||||||
focused: info.focused,
|
focused: toplevel.active,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl OpenState {
|
|
||||||
/// Creates open with focused
|
/// Creates open with focused
|
||||||
pub const fn focused(focused: bool) -> Self {
|
pub const fn focused(focused: bool) -> Self {
|
||||||
Self::Open { focused }
|
Self::Open { focused }
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use color_eyre::Result;
|
|
||||||
use glib::IsA;
|
|
||||||
use gtk::gdk::{EventMask, Monitor};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
|
|
||||||
use tokio::sync::{broadcast, mpsc};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::config::{BarPosition, CommonConfig, TransitionType};
|
|
||||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
|
||||||
use crate::popup::Popup;
|
|
||||||
use crate::{glib_recv_mpsc, send};
|
|
||||||
|
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
/// Displays the current date and time.
|
/// Displays the current date and time.
|
||||||
@@ -41,6 +24,19 @@ pub mod upower;
|
|||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
|
use crate::bridge_channel::BridgeChannel;
|
||||||
|
use crate::config::{BarPosition, CommonConfig, TransitionType};
|
||||||
|
use crate::popup::{Popup, WidgetGeometry};
|
||||||
|
use crate::{read_lock, send, write_lock};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use glib::IsA;
|
||||||
|
use gtk::gdk::{EventMask, Monitor};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ModuleLocation {
|
pub enum ModuleLocation {
|
||||||
Left,
|
Left,
|
||||||
@@ -56,98 +52,30 @@ pub struct ModuleInfo<'a> {
|
|||||||
pub icon_theme: &'a IconTheme,
|
pub icon_theme: &'a IconTheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub enum ModuleUpdateEvent<T: Clone> {
|
pub enum ModuleUpdateEvent<T> {
|
||||||
/// Sends an update to the module UI.
|
/// Sends an update to the module UI
|
||||||
Update(T),
|
Update(T),
|
||||||
/// Toggles the open state of the popup.
|
/// Toggles the open state of the popup.
|
||||||
/// Takes the button ID.
|
TogglePopup(WidgetGeometry),
|
||||||
TogglePopup(usize),
|
|
||||||
/// Force sets the popup open.
|
/// Force sets the popup open.
|
||||||
/// Takes the button ID.
|
/// Takes the button X position and width.
|
||||||
OpenPopup(usize),
|
OpenPopup(WidgetGeometry),
|
||||||
OpenPopupAt(WidgetGeometry),
|
|
||||||
/// Force sets the popup closed.
|
/// Force sets the popup closed.
|
||||||
ClosePopup,
|
ClosePopup,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WidgetContext<TSend, TReceive>
|
pub struct WidgetContext<TSend, TReceive> {
|
||||||
where
|
|
||||||
TSend: Clone,
|
|
||||||
{
|
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
|
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
|
||||||
pub update_tx: broadcast::Sender<TSend>,
|
|
||||||
pub controller_tx: mpsc::Sender<TReceive>,
|
pub controller_tx: mpsc::Sender<TReceive>,
|
||||||
|
pub widget_rx: glib::Receiver<TSend>,
|
||||||
_update_rx: broadcast::Receiver<TSend>,
|
pub popup_rx: glib::Receiver<TSend>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<TSend, TReceive> WidgetContext<TSend, TReceive>
|
pub struct ModuleWidget<W: IsA<Widget>> {
|
||||||
where
|
|
||||||
TSend: Clone,
|
|
||||||
{
|
|
||||||
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
|
|
||||||
self.update_tx.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ModuleParts<W: IsA<Widget>> {
|
|
||||||
pub widget: W,
|
pub widget: W,
|
||||||
pub popup: Option<ModulePopupParts>,
|
pub popup: Option<gtk::Box>,
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: IsA<Widget>> ModuleParts<W> {
|
|
||||||
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
|
|
||||||
Self { widget, popup }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ModulePopupParts {
|
|
||||||
/// The popup container, with all its contents
|
|
||||||
pub container: gtk::Box,
|
|
||||||
/// An array of buttons which can be used for opening the popup.
|
|
||||||
/// For most modules, this will only be a single button.
|
|
||||||
/// For some advanced modules, such as `Launcher`, this is all item buttons.
|
|
||||||
pub buttons: Vec<Button>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ModulePopup {
|
|
||||||
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
|
|
||||||
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModulePopup for Option<gtk::Box> {
|
|
||||||
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
|
|
||||||
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
|
|
||||||
self.map(|container| ModulePopupParts { container, buttons })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PopupButton {
|
|
||||||
fn try_popup_id(&self) -> Option<usize>;
|
|
||||||
fn popup_id(&self) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PopupButton for Button {
|
|
||||||
/// Gets the popup ID associated with this button, if there is one.
|
|
||||||
/// Will return `None` if this is not a popup button.
|
|
||||||
fn try_popup_id(&self) -> Option<usize> {
|
|
||||||
self.get_tag("popup-id").copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the popup ID associated with this button.
|
|
||||||
/// This should only be called on buttons which are known to be associated with popups.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// Will panic if an ID has not been set.
|
|
||||||
fn popup_id(&self) -> usize {
|
|
||||||
self.try_popup_id().expect("id to exist")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Module<W>
|
pub trait Module<W>
|
||||||
@@ -164,22 +92,18 @@ where
|
|||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
rx: mpsc::Receiver<Self::ReceiveMessage>,
|
rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()>
|
) -> Result<()>;
|
||||||
where
|
|
||||||
<Self as Module<W>>::SendMessage: Clone;
|
|
||||||
|
|
||||||
fn into_widget(
|
fn into_widget(
|
||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<W>>
|
) -> Result<ModuleWidget<W>>;
|
||||||
where
|
|
||||||
<Self as Module<W>>::SendMessage: Clone;
|
|
||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
_rx: glib::Receiver<Self::SendMessage>,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box>
|
) -> Option<gtk::Box>
|
||||||
where
|
where
|
||||||
@@ -194,59 +118,49 @@ where
|
|||||||
pub fn create_module<TModule, TWidget, TSend, TRec>(
|
pub fn create_module<TModule, TWidget, TSend, TRec>(
|
||||||
module: TModule,
|
module: TModule,
|
||||||
id: usize,
|
id: usize,
|
||||||
name: Option<String>,
|
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
popup: &Rc<RefCell<Popup>>,
|
popup: &Arc<RwLock<Popup>>,
|
||||||
) -> Result<ModuleParts<TWidget>>
|
) -> Result<TWidget>
|
||||||
where
|
where
|
||||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||||
TWidget: IsA<Widget>,
|
TWidget: IsA<Widget>,
|
||||||
TSend: Debug + Clone + Send + 'static,
|
TSend: Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
|
let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||||
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
|
let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
let (tx, rx) = broadcast::channel(64);
|
let channel = BridgeChannel::<ModuleUpdateEvent<TSend>>::new();
|
||||||
|
let (ui_tx, ui_rx) = mpsc::channel::<TRec>(16);
|
||||||
|
|
||||||
module.spawn_controller(info, ui_tx.clone(), controller_rx)?;
|
module.spawn_controller(info, channel.create_sender(), ui_rx)?;
|
||||||
|
|
||||||
let context = WidgetContext {
|
let context = WidgetContext {
|
||||||
id,
|
id,
|
||||||
tx: ui_tx,
|
widget_rx: w_rx,
|
||||||
update_tx: tx.clone(),
|
popup_rx: p_rx,
|
||||||
controller_tx,
|
tx: channel.create_sender(),
|
||||||
_update_rx: rx,
|
controller_tx: ui_tx,
|
||||||
};
|
};
|
||||||
|
|
||||||
let module_name = TModule::name();
|
let name = TModule::name();
|
||||||
let instance_name = name.unwrap_or_else(|| module_name.to_string());
|
|
||||||
|
|
||||||
let module_parts = module.into_widget(context, info)?;
|
let module_parts = module.into_widget(context, info)?;
|
||||||
module_parts.widget.add_class("widget");
|
module_parts.widget.set_widget_name(name);
|
||||||
module_parts.widget.add_class(module_name);
|
|
||||||
|
|
||||||
if let Some(popup_content) = module_parts.popup.clone() {
|
let mut has_popup = false;
|
||||||
popup_content
|
if let Some(popup_content) = module_parts.popup {
|
||||||
.container
|
register_popup_content(popup, id, popup_content);
|
||||||
.style_context()
|
has_popup = true;
|
||||||
.add_class(&format!("popup-{module_name}"));
|
|
||||||
|
|
||||||
register_popup_content(popup, id, instance_name, popup_content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
|
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
|
||||||
|
|
||||||
Ok(module_parts)
|
Ok(module_parts.widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers the popup content with the popup.
|
/// Registers the popup content with the popup.
|
||||||
fn register_popup_content(
|
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
|
||||||
popup: &Rc<RefCell<Popup>>,
|
write_lock!(popup).register_content(id, popup_content);
|
||||||
id: usize,
|
|
||||||
name: String,
|
|
||||||
popup_content: ModulePopupParts,
|
|
||||||
) {
|
|
||||||
popup.borrow_mut().register_content(id, name, popup_content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up the bridge channel receiver
|
/// Sets up the bridge channel receiver
|
||||||
@@ -255,103 +169,71 @@ fn register_popup_content(
|
|||||||
/// Handles opening/closing popups
|
/// Handles opening/closing popups
|
||||||
/// and communicating update messages between controllers and widgets/popups.
|
/// and communicating update messages between controllers and widgets/popups.
|
||||||
fn setup_receiver<TSend>(
|
fn setup_receiver<TSend>(
|
||||||
tx: broadcast::Sender<TSend>,
|
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
|
||||||
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
w_tx: glib::Sender<TSend>,
|
||||||
popup: Rc<RefCell<Popup>>,
|
p_tx: glib::Sender<TSend>,
|
||||||
|
popup: Arc<RwLock<Popup>>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
id: usize,
|
id: usize,
|
||||||
|
has_popup: bool,
|
||||||
) where
|
) where
|
||||||
TSend: Debug + Clone + Send + 'static,
|
TSend: Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
// some rare cases can cause the popup to incorrectly calculate its size on first open.
|
// some rare cases can cause the popup to incorrectly calculate its size on first open.
|
||||||
// we can fix that by just force re-rendering it on its first open.
|
// we can fix that by just force re-rendering it on its first open.
|
||||||
let mut has_popup_opened = false;
|
let mut has_popup_opened = false;
|
||||||
|
|
||||||
glib_recv_mpsc!(rx, ev => {
|
channel.recv(move |ev| {
|
||||||
match ev {
|
match ev {
|
||||||
ModuleUpdateEvent::Update(update) => {
|
ModuleUpdateEvent::Update(update) => {
|
||||||
send!(tx, update);
|
if has_popup {
|
||||||
|
send!(p_tx, update.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
send!(w_tx, update);
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::TogglePopup(button_id) => {
|
ModuleUpdateEvent::TogglePopup(geometry) => {
|
||||||
debug!("Toggling popup for {} [#{}]", name, id);
|
debug!("Toggling popup for {} [#{}]", name, id);
|
||||||
let mut popup = popup.borrow_mut();
|
let popup = read_lock!(popup);
|
||||||
if popup.is_visible() {
|
if popup.is_visible() {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
} else {
|
} else {
|
||||||
popup.show(id, button_id);
|
popup.show_content(id);
|
||||||
|
popup.show(geometry);
|
||||||
|
|
||||||
// force re-render on initial open to try and fix size issue
|
|
||||||
if !has_popup_opened {
|
if !has_popup_opened {
|
||||||
popup.show(id, button_id);
|
popup.show_content(id);
|
||||||
|
popup.show(geometry);
|
||||||
has_popup_opened = true;
|
has_popup_opened = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::OpenPopup(button_id) => {
|
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||||
debug!("Opening popup for {} [#{}]", name, id);
|
debug!("Opening popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
let popup = read_lock!(popup);
|
||||||
popup.hide();
|
popup.hide();
|
||||||
popup.show(id, button_id);
|
popup.show_content(id);
|
||||||
|
popup.show(geometry);
|
||||||
|
|
||||||
// force re-render on initial open to try and fix size issue
|
|
||||||
if !has_popup_opened {
|
if !has_popup_opened {
|
||||||
popup.show(id, button_id);
|
popup.show_content(id);
|
||||||
has_popup_opened = true;
|
popup.show(geometry);
|
||||||
}
|
|
||||||
}
|
|
||||||
ModuleUpdateEvent::OpenPopupAt(geometry) => {
|
|
||||||
debug!("Opening popup for {} [#{}]", name, id);
|
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
|
||||||
popup.hide();
|
|
||||||
popup.show_at(id, geometry);
|
|
||||||
|
|
||||||
// force re-render on initial open to try and fix size issue
|
|
||||||
if !has_popup_opened {
|
|
||||||
popup.show_at(id, geometry);
|
|
||||||
has_popup_opened = true;
|
has_popup_opened = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModuleUpdateEvent::ClosePopup => {
|
ModuleUpdateEvent::ClosePopup => {
|
||||||
debug!("Closing popup for {} [#{}]", name, id);
|
debug!("Closing popup for {} [#{}]", name, id);
|
||||||
|
|
||||||
let mut popup = popup.borrow_mut();
|
let popup = read_lock!(popup);
|
||||||
popup.hide();
|
popup.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
|
|
||||||
widget_parts: &ModuleParts<TWidget>,
|
|
||||||
common: &CommonConfig,
|
|
||||||
) {
|
|
||||||
if let Some(ref name) = common.name {
|
|
||||||
widget_parts.widget.set_widget_name(name);
|
|
||||||
|
|
||||||
if let Some(ref popup) = widget_parts.popup {
|
|
||||||
popup.container.set_widget_name(&format!("popup-{name}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref class) = common.class {
|
|
||||||
// gtk counts classes with spaces as the same class
|
|
||||||
for part in class.split(' ') {
|
|
||||||
widget_parts.widget.style_context().add_class(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref popup) = widget_parts.popup {
|
|
||||||
for part in class.split(' ') {
|
|
||||||
popup
|
|
||||||
.container
|
|
||||||
.style_context()
|
|
||||||
.add_class(&format!("popup-{part}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes a widget and adds it into a new `gtk::EventBox`.
|
/// Takes a widget and adds it into a new `gtk::EventBox`.
|
||||||
/// The event box container is returned.
|
/// The event box container is returned.
|
||||||
pub fn wrap_widget<W: IsA<Widget>>(
|
pub fn wrap_widget<W: IsA<Widget>>(
|
||||||
@@ -359,14 +241,14 @@ pub fn wrap_widget<W: IsA<Widget>>(
|
|||||||
common: CommonConfig,
|
common: CommonConfig,
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
) -> EventBox {
|
) -> EventBox {
|
||||||
let transition_type = common
|
|
||||||
.transition_type
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&TransitionType::SlideStart)
|
|
||||||
.to_revealer_transition_type(orientation);
|
|
||||||
|
|
||||||
let revealer = Revealer::builder()
|
let revealer = Revealer::builder()
|
||||||
.transition_type(transition_type)
|
.transition_type(
|
||||||
|
common
|
||||||
|
.transition_type
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&TransitionType::SlideStart)
|
||||||
|
.to_revealer_transition_type(orientation),
|
||||||
|
)
|
||||||
.transition_duration(common.transition_duration.unwrap_or(250))
|
.transition_duration(common.transition_duration.unwrap_or(250))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -374,12 +256,10 @@ pub fn wrap_widget<W: IsA<Widget>>(
|
|||||||
revealer.set_reveal_child(true);
|
revealer.set_reveal_child(true);
|
||||||
|
|
||||||
let container = EventBox::new();
|
let container = EventBox::new();
|
||||||
container.add_class("widget-container");
|
|
||||||
|
|
||||||
container.add_events(EventMask::SCROLL_MASK);
|
container.add_events(EventMask::SCROLL_MASK);
|
||||||
container.add(&revealer);
|
container.append(&revealer);
|
||||||
|
|
||||||
common.install_events(&container, &revealer);
|
common.install(&container, &revealer);
|
||||||
|
|
||||||
container
|
container
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,27 +121,27 @@ fn default_icon_pause() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_prev() -> String {
|
fn default_icon_prev() -> String {
|
||||||
String::from("")
|
String::from("\u{f9ad}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_next() -> String {
|
fn default_icon_next() -> String {
|
||||||
String::from("")
|
String::from("\u{f9ac}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_volume() -> String {
|
fn default_icon_volume() -> String {
|
||||||
String::from("")
|
String::from("墳")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_track() -> String {
|
fn default_icon_track() -> String {
|
||||||
String::from("")
|
String::from("\u{f886}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_album() -> String {
|
fn default_icon_album() -> String {
|
||||||
String::from("")
|
String::from("\u{f524}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_artist() -> String {
|
fn default_icon_artist() -> String {
|
||||||
String::from("")
|
String::from("\u{fd01}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_music_dir() -> PathBuf {
|
fn default_music_dir() -> PathBuf {
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
use std::path::PathBuf;
|
mod config;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
|
||||||
|
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
|
use crate::popup::Popup;
|
||||||
|
use crate::{send_async, try_send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::{Propagation, PropertySet};
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::clients::music::{
|
|
||||||
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
|
|
||||||
};
|
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
|
||||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
|
||||||
use crate::modules::PopupButton;
|
|
||||||
use crate::modules::{
|
|
||||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
|
||||||
};
|
|
||||||
use crate::{glib_recv, send_async, spawn, try_send};
|
|
||||||
|
|
||||||
pub use self::config::MusicModule;
|
pub use self::config::MusicModule;
|
||||||
use self::config::PlayerType;
|
use self::config::PlayerType;
|
||||||
|
|
||||||
mod config;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PlayerCommand {
|
pub enum PlayerCommand {
|
||||||
Previous,
|
Previous,
|
||||||
@@ -34,7 +28,6 @@ pub enum PlayerCommand {
|
|||||||
Pause,
|
Pause,
|
||||||
Next,
|
Next,
|
||||||
Volume(u8),
|
Volume(u8),
|
||||||
Seek(Duration),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a duration given in seconds
|
/// Formats a duration given in seconds
|
||||||
@@ -54,12 +47,6 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ControllerEvent {
|
|
||||||
Update(Option<SongUpdate>),
|
|
||||||
UpdateProgress(ProgressTick),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SongUpdate {
|
pub struct SongUpdate {
|
||||||
song: Track,
|
song: Track,
|
||||||
@@ -80,7 +67,7 @@ async fn get_client(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Module<Button> for MusicModule {
|
impl Module<Button> for MusicModule {
|
||||||
type SendMessage = ControllerEvent;
|
type SendMessage = Option<SongUpdate>;
|
||||||
type ReceiveMessage = PlayerCommand;
|
type ReceiveMessage = PlayerCommand;
|
||||||
|
|
||||||
fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
@@ -90,8 +77,8 @@ impl Module<Button> for MusicModule {
|
|||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let format = self.format.clone();
|
let format = self.format.clone();
|
||||||
|
|
||||||
@@ -116,7 +103,7 @@ impl Module<Button> for MusicModule {
|
|||||||
PlayerUpdate::Update(track, status) => match *track {
|
PlayerUpdate::Update(track, status) => match *track {
|
||||||
Some(track) => {
|
Some(track) => {
|
||||||
let display_string =
|
let display_string =
|
||||||
replace_tokens(format.as_str(), &tokens, &track);
|
replace_tokens(format.as_str(), &tokens, &track, &status);
|
||||||
|
|
||||||
let update = SongUpdate {
|
let update = SongUpdate {
|
||||||
song: track,
|
song: track,
|
||||||
@@ -124,24 +111,10 @@ impl Module<Button> for MusicModule {
|
|||||||
display_string,
|
display_string,
|
||||||
};
|
};
|
||||||
|
|
||||||
send_async!(
|
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
|
||||||
tx,
|
|
||||||
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
|
|
||||||
update
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
None => send_async!(
|
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
|
||||||
tx,
|
|
||||||
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
|
|
||||||
tx,
|
|
||||||
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
|
|
||||||
progress_tick
|
|
||||||
))
|
|
||||||
),
|
|
||||||
PlayerUpdate::Disconnect => break,
|
PlayerUpdate::Disconnect => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,8 +136,7 @@ impl Module<Button> for MusicModule {
|
|||||||
PlayerCommand::Play => client.play(),
|
PlayerCommand::Play => client.play(),
|
||||||
PlayerCommand::Pause => client.pause(),
|
PlayerCommand::Pause => client.pause(),
|
||||||
PlayerCommand::Next => client.next(),
|
PlayerCommand::Next => client.next(),
|
||||||
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
|
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), // .unwrap_or_else(|_| error!("Failed to update player volume")),
|
||||||
PlayerCommand::Seek(duration) => client.seek(duration),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
@@ -181,12 +153,10 @@ impl Module<Button> for MusicModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<Button>> {
|
) -> Result<ModuleWidget<Button>> {
|
||||||
let button = Button::new();
|
let button = Button::new();
|
||||||
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
|
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
|
||||||
button_contents.add_class("contents");
|
button.append(&button_contents);
|
||||||
|
|
||||||
button.add(&button_contents);
|
|
||||||
|
|
||||||
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
|
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
|
||||||
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
|
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
|
||||||
@@ -198,29 +168,28 @@ impl Module<Button> for MusicModule {
|
|||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
button_contents.add(&icon_pause);
|
button_contents.append(&icon_pause);
|
||||||
button_contents.add(&icon_play);
|
button_contents.append(&icon_play);
|
||||||
button_contents.add(&label);
|
button_contents.append(&label);
|
||||||
|
|
||||||
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
{
|
{
|
||||||
let tx = context.tx.clone();
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
try_send!(
|
||||||
|
tx,
|
||||||
|
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let button = button.clone();
|
let button = button.clone();
|
||||||
|
|
||||||
let tx = context.tx.clone();
|
let tx = context.tx.clone();
|
||||||
let rx = context.subscribe();
|
|
||||||
|
|
||||||
glib_recv!(rx, event => {
|
|
||||||
let ControllerEvent::Update(mut event) = event else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
context.widget_rx.attach(None, move |mut event| {
|
||||||
if let Some(event) = event.take() {
|
if let Some(event) = event.take() {
|
||||||
label.set_label(&event.display_string);
|
label.set_label(&event.display_string);
|
||||||
|
|
||||||
@@ -249,33 +218,38 @@ impl Module<Button> for MusicModule {
|
|||||||
button.hide();
|
button.hide();
|
||||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let rx = context.subscribe();
|
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||||
let popup = self
|
|
||||||
.into_popup(context.controller_tx, rx, info)
|
|
||||||
.into_popup_parts(vec![&button]);
|
|
||||||
|
|
||||||
Ok(ModuleParts::new(button, popup))
|
Ok(ModuleWidget {
|
||||||
|
widget: button,
|
||||||
|
popup,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_popup(
|
fn into_popup(
|
||||||
self,
|
self,
|
||||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
tx: Sender<Self::ReceiveMessage>,
|
||||||
rx: broadcast::Receiver<Self::SendMessage>,
|
rx: glib::Receiver<Self::SendMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Option<gtk::Box> {
|
) -> Option<gtk::Box> {
|
||||||
let icon_theme = info.icon_theme;
|
let icon_theme = info.icon_theme;
|
||||||
|
|
||||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
let container = gtk::Box::builder()
|
||||||
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
|
.orientation(Orientation::Horizontal)
|
||||||
|
.spacing(10)
|
||||||
|
.name("popup-music")
|
||||||
|
.build();
|
||||||
|
|
||||||
let album_image = gtk::Image::builder()
|
let album_image = gtk::Image::builder()
|
||||||
.width_request(128)
|
.width_request(128)
|
||||||
.height_request(128)
|
.height_request(128)
|
||||||
|
.name("album-art")
|
||||||
.build();
|
.build();
|
||||||
album_image.add_class("album-art");
|
|
||||||
|
|
||||||
let icons = self.icons;
|
let icons = self.icons;
|
||||||
|
|
||||||
@@ -284,53 +258,54 @@ impl Module<Button> for MusicModule {
|
|||||||
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
||||||
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
||||||
|
|
||||||
title_label.container.add_class("title");
|
title_label.container.set_widget_name("title");
|
||||||
album_label.container.add_class("album");
|
album_label.container.set_widget_name("album");
|
||||||
artist_label.container.add_class("artist");
|
artist_label.container.set_widget_name("artist");
|
||||||
|
|
||||||
info_box.add(&title_label.container);
|
info_box.append(&title_label.container);
|
||||||
info_box.add(&album_label.container);
|
info_box.append(&album_label.container);
|
||||||
info_box.add(&artist_label.container);
|
info_box.append(&artist_label.container);
|
||||||
|
|
||||||
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
|
let controls_box = gtk::Box::builder().name("controls").build();
|
||||||
controls_box.add_class("controls");
|
|
||||||
|
|
||||||
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
|
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
|
||||||
btn_prev.add_class("btn-prev");
|
btn_prev.set_widget_name("btn-prev");
|
||||||
|
|
||||||
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
|
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
|
||||||
btn_play.add_class("btn-play");
|
btn_play.set_widget_name("btn-play");
|
||||||
|
|
||||||
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
|
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
|
||||||
btn_pause.add_class("btn-pause");
|
btn_pause.set_widget_name("btn-pause");
|
||||||
|
|
||||||
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
|
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
|
||||||
btn_next.add_class("btn-next");
|
btn_next.set_widget_name("btn-next");
|
||||||
|
|
||||||
controls_box.add(&btn_prev);
|
controls_box.append(&btn_prev);
|
||||||
controls_box.add(&btn_play);
|
controls_box.append(&btn_play);
|
||||||
controls_box.add(&btn_pause);
|
controls_box.append(&btn_pause);
|
||||||
controls_box.add(&btn_next);
|
controls_box.append(&btn_next);
|
||||||
|
|
||||||
info_box.add(&controls_box);
|
info_box.append(&controls_box);
|
||||||
|
|
||||||
let volume_box = gtk::Box::new(Orientation::Vertical, 5);
|
let volume_box = gtk::Box::builder()
|
||||||
volume_box.add_class("volume");
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(5)
|
||||||
|
.name("volume")
|
||||||
|
.build();
|
||||||
|
|
||||||
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
|
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
|
||||||
volume_slider.set_inverted(true);
|
volume_slider.set_inverted(true);
|
||||||
volume_slider.add_class("slider");
|
volume_slider.set_widget_name("slider");
|
||||||
|
|
||||||
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
|
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
|
||||||
volume_icon.add_class("icon");
|
volume_icon.style_context().add_class("icon");
|
||||||
|
|
||||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||||
|
|
||||||
main_container.add(&album_image);
|
container.append(&album_image);
|
||||||
main_container.add(&info_box);
|
container.append(&info_box);
|
||||||
main_container.add(&volume_box);
|
container.append(&volume_box);
|
||||||
container.add(&main_container);
|
|
||||||
|
|
||||||
let tx_prev = tx.clone();
|
let tx_prev = tx.clone();
|
||||||
btn_prev.connect_clicked(move |_| {
|
btn_prev.connect_clicked(move |_| {
|
||||||
@@ -352,141 +327,85 @@ impl Module<Button> for MusicModule {
|
|||||||
try_send!(tx_next, PlayerCommand::Next);
|
try_send!(tx_next, PlayerCommand::Next);
|
||||||
});
|
});
|
||||||
|
|
||||||
let tx_vol = tx.clone();
|
let tx_vol = tx;
|
||||||
volume_slider.connect_change_value(move |_, _, val| {
|
volume_slider.connect_change_value(move |_, _, val| {
|
||||||
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
||||||
Propagation::Proceed
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
|
|
||||||
progress_box.add_class("progress");
|
|
||||||
|
|
||||||
let progress_label = Label::new(None);
|
|
||||||
progress_label.add_class("label");
|
|
||||||
|
|
||||||
let progress = Scale::builder()
|
|
||||||
.orientation(Orientation::Horizontal)
|
|
||||||
.draw_value(false)
|
|
||||||
.hexpand(true)
|
|
||||||
.build();
|
|
||||||
progress.add_class("slider");
|
|
||||||
|
|
||||||
progress_box.add(&progress);
|
|
||||||
progress_box.add(&progress_label);
|
|
||||||
container.add(&progress_box);
|
|
||||||
|
|
||||||
let drag_lock = Arc::new(AtomicBool::new(false));
|
|
||||||
{
|
|
||||||
let drag_lock = drag_lock.clone();
|
|
||||||
progress.connect_button_press_event(move |_, _| {
|
|
||||||
drag_lock.set(true);
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let drag_lock = drag_lock.clone();
|
|
||||||
progress.connect_button_release_event(move |scale, _| {
|
|
||||||
let value = scale.value();
|
|
||||||
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
|
|
||||||
|
|
||||||
drag_lock.set(false);
|
|
||||||
Propagation::Proceed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let icon_theme = icon_theme.clone();
|
let icon_theme = icon_theme.clone();
|
||||||
let image_size = self.cover_image_size;
|
let image_size = self.cover_image_size;
|
||||||
|
|
||||||
let mut prev_cover = None;
|
let mut prev_cover = None;
|
||||||
glib_recv!(rx, event => {
|
rx.attach(None, move |update| {
|
||||||
match event {
|
if let Some(update) = update {
|
||||||
ControllerEvent::Update(Some(update)) => {
|
// only update art when album changes
|
||||||
// only update art when album changes
|
let new_cover = update.song.cover_path;
|
||||||
let new_cover = update.song.cover_path;
|
if prev_cover != new_cover {
|
||||||
if prev_cover != new_cover {
|
prev_cover = new_cover.clone();
|
||||||
prev_cover = new_cover.clone();
|
let res = match new_cover.map(|cover_path| {
|
||||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
||||||
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
|
}) {
|
||||||
}) {
|
Some(Ok(image)) => image.load_into_image(album_image.clone()),
|
||||||
album_image.show();
|
Some(Err(err)) => {
|
||||||
image.load_into_image(album_image.clone())
|
album_image.set_from_pixbuf(None);
|
||||||
} else {
|
Err(err)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
album_image.set_from_pixbuf(None);
|
album_image.set_from_pixbuf(None);
|
||||||
album_image.hide();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
if let Err(err) = res {
|
||||||
update_popup_metadata_label(update.song.title, &title_label);
|
error!("{err:?}");
|
||||||
update_popup_metadata_label(update.song.album, &album_label);
|
|
||||||
update_popup_metadata_label(update.song.artist, &artist_label);
|
|
||||||
|
|
||||||
match update.status.state {
|
|
||||||
PlayerState::Stopped => {
|
|
||||||
btn_pause.hide();
|
|
||||||
btn_play.show();
|
|
||||||
btn_play.set_sensitive(false);
|
|
||||||
}
|
|
||||||
PlayerState::Playing => {
|
|
||||||
btn_play.set_sensitive(false);
|
|
||||||
btn_play.hide();
|
|
||||||
|
|
||||||
btn_pause.set_sensitive(true);
|
|
||||||
btn_pause.show();
|
|
||||||
}
|
|
||||||
PlayerState::Paused => {
|
|
||||||
btn_pause.set_sensitive(false);
|
|
||||||
btn_pause.hide();
|
|
||||||
|
|
||||||
btn_play.set_sensitive(true);
|
|
||||||
btn_play.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let enable_prev = update.status.playlist_position > 0;
|
|
||||||
|
|
||||||
let enable_next =
|
|
||||||
update.status.playlist_position < update.status.playlist_length;
|
|
||||||
|
|
||||||
btn_prev.set_sensitive(enable_prev);
|
|
||||||
btn_next.set_sensitive(enable_next);
|
|
||||||
|
|
||||||
if let Some(volume) = update.status.volume_percent {
|
|
||||||
volume_slider.set_value(f64::from(volume));
|
|
||||||
volume_box.show();
|
|
||||||
} else {
|
|
||||||
volume_box.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControllerEvent::UpdateProgress(progress_tick)
|
|
||||||
if !drag_lock.load(Ordering::Relaxed) =>
|
|
||||||
{
|
|
||||||
if let (Some(elapsed), Some(duration)) =
|
|
||||||
(progress_tick.elapsed, progress_tick.duration)
|
|
||||||
{
|
|
||||||
progress_label.set_label(&format!(
|
|
||||||
"{}/{}",
|
|
||||||
format_time(elapsed),
|
|
||||||
format_time(duration)
|
|
||||||
));
|
|
||||||
|
|
||||||
progress.set_value(elapsed.as_secs_f64());
|
title_label
|
||||||
progress.set_range(0.0, duration.as_secs_f64());
|
.label
|
||||||
progress_box.show_all();
|
.set_text(&update.song.title.unwrap_or_default());
|
||||||
} else {
|
album_label
|
||||||
progress_box.hide();
|
.label
|
||||||
|
.set_text(&update.song.album.unwrap_or_default());
|
||||||
|
artist_label
|
||||||
|
.label
|
||||||
|
.set_text(&update.song.artist.unwrap_or_default());
|
||||||
|
|
||||||
|
match update.status.state {
|
||||||
|
PlayerState::Stopped => {
|
||||||
|
btn_pause.hide();
|
||||||
|
btn_play.show();
|
||||||
|
btn_play.set_sensitive(false);
|
||||||
|
}
|
||||||
|
PlayerState::Playing => {
|
||||||
|
btn_play.set_sensitive(false);
|
||||||
|
btn_play.hide();
|
||||||
|
|
||||||
|
btn_pause.set_sensitive(true);
|
||||||
|
btn_pause.show();
|
||||||
|
}
|
||||||
|
PlayerState::Paused => {
|
||||||
|
btn_pause.set_sensitive(false);
|
||||||
|
btn_pause.hide();
|
||||||
|
|
||||||
|
btn_play.set_sensitive(true);
|
||||||
|
btn_play.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
let enable_prev = update.status.playlist_position > 0;
|
||||||
|
|
||||||
|
let enable_next =
|
||||||
|
update.status.playlist_position < update.status.playlist_length;
|
||||||
|
|
||||||
|
btn_prev.set_sensitive(enable_prev);
|
||||||
|
btn_next.set_sensitive(enable_next);
|
||||||
|
|
||||||
|
volume_slider.set_value(update.status.volume_percent as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,24 +413,17 @@ impl Module<Button> for MusicModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
|
|
||||||
match text {
|
|
||||||
Some(value) => {
|
|
||||||
label.label.set_text(&value);
|
|
||||||
label.container.show_all();
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
label.container.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces each of the formatting tokens in the formatting string
|
/// Replaces each of the formatting tokens in the formatting string
|
||||||
/// with actual data pulled from the music player
|
/// with actual data pulled from the music player
|
||||||
fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> String {
|
fn replace_tokens(
|
||||||
|
format_string: &str,
|
||||||
|
tokens: &Vec<String>,
|
||||||
|
song: &Track,
|
||||||
|
status: &Status,
|
||||||
|
) -> String {
|
||||||
let mut compiled_string = format_string.to_string();
|
let mut compiled_string = format_string.to_string();
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
let value = get_token_value(song, token);
|
let value = get_token_value(song, status, token);
|
||||||
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
|
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
|
||||||
}
|
}
|
||||||
compiled_string
|
compiled_string
|
||||||
@@ -519,7 +431,7 @@ fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> St
|
|||||||
|
|
||||||
/// Converts a string format token value
|
/// Converts a string format token value
|
||||||
/// into its respective value.
|
/// into its respective value.
|
||||||
fn get_token_value(song: &Track, token: &str) -> String {
|
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
|
||||||
match token {
|
match token {
|
||||||
"title" => song.title.clone(),
|
"title" => song.title.clone(),
|
||||||
"album" => song.album.clone(),
|
"album" => song.album.clone(),
|
||||||
@@ -528,6 +440,8 @@ fn get_token_value(song: &Track, token: &str) -> String {
|
|||||||
"disc" => song.disc.map(|x| x.to_string()),
|
"disc" => song.disc.map(|x| x.to_string()),
|
||||||
"genre" => song.genre.clone(),
|
"genre" => song.genre.clone(),
|
||||||
"track" => song.track.map(|x| x.to_string()),
|
"track" => song.track.map(|x| x.to_string()),
|
||||||
|
"duration" => status.duration.map(format_time),
|
||||||
|
"elapsed" => status.elapsed.map(format_time),
|
||||||
_ => Some(token.to_string()),
|
_ => Some(token.to_string()),
|
||||||
}
|
}
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -546,11 +460,11 @@ impl IconLabel {
|
|||||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||||
let label = Label::new(label);
|
let label = Label::new(label);
|
||||||
|
|
||||||
icon.add_class("icon-box");
|
icon.style_context().add_class("icon");
|
||||||
label.add_class("label");
|
label.style_context().add_class("label");
|
||||||
|
|
||||||
container.add(&icon);
|
container.append(&icon);
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
|
|
||||||
Self { label, container }
|
Self { label, container }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::script::{OutputStream, Script, ScriptMode};
|
use crate::script::{OutputStream, Script, ScriptMode};
|
||||||
use crate::{glib_recv, spawn, try_send};
|
use crate::try_send;
|
||||||
use color_eyre::{Help, Report, Result};
|
use color_eyre::{Help, Report, Result};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -82,16 +83,19 @@ impl Module<Label> for ScriptModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<Label>> {
|
) -> Result<ModuleWidget<Label>> {
|
||||||
let label = Label::builder().use_markup(true).build();
|
let label = Label::builder().use_markup(true).build();
|
||||||
label.set_angle(info.bar_position.get_angle());
|
label.set_angle(info.bar_position.get_angle());
|
||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
glib_recv!(context.subscribe(), s => label.set_markup(s.as_str()));
|
context.widget_rx.attach(None, move |s| {
|
||||||
|
label.set_markup(s.as_str());
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ModuleParts {
|
Ok(ModuleWidget {
|
||||||
widget: label,
|
widget: label,
|
||||||
popup: None,
|
popup: None,
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user