32 Commits

Author SHA1 Message Date
Jake Stanger
d40b3b7d80 chore(release): v0.10.0 2023-02-01 22:21:07 +00:00
Jake Stanger
181561fe2a Merge pull request #64 from JakeStanger/feat/build-flags
feat: add feature flags
2023-02-01 22:09:23 +00:00
Jake Stanger
7b23e61e7d docs(wiki): update screenshots and examples 2023-02-01 22:06:09 +00:00
Jake Stanger
6a39905b43 docs(compiling): add missing full stop 2023-02-01 21:08:03 +00:00
Jake Stanger
2780d98ee0 Merge branch 'master' into feat/build-flags
# Conflicts:
#	src/image/provider.rs
2023-02-01 21:07:36 +00:00
Jake Stanger
51d2c2279f fix(images): incorrectly resolving non-files 2023-02-01 21:05:58 +00:00
Jake Stanger
c347b6c944 feat: add feature flags
Flags allow you to disable certain functionality and compile with only select features to reduce build time.

Resolves #54.
2023-02-01 20:45:52 +00:00
Jake Stanger
e83618b1d6 ci: fix not updating system packages 2023-02-01 17:52:46 +00:00
Jake Stanger
90f57d61b9 docs(music): remove irrelevant icon format token
BREAKING CHANGE: (Missed from #96141d4) The `{icon}` token has been removed from the `music` module due to incompatibility with the new image/icon support. The icon now always displays as a separate widget before the label and should be removed from your formatting string.
2023-02-01 17:52:34 +00:00
Jake Stanger
0b9af6bb26 Merge pull request #63 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-02-01 16:02:09 +00:00
github-actions[bot]
11a65d4fbc flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/9b97ad7b4330aacda9b2343396eb3df8a853b4fc' (2023-01-25)
  → 'github:nixos/nixpkgs/2caf4ef5005ecc68141ecb4aac271079f7371c44' (2023-01-30)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/edd082ca16aa055d5504bea39da36b3ee68e4f1d' (2023-01-29)
  → 'github:oxalica/rust-overlay/48b1403150c3f5a9aeee8bc4c77c8926f29c6501' (2023-01-31)
2023-02-01 01:07:22 +00:00
Jake Stanger
054262365e Merge pull request #61 from JakeStanger/fix/hyprland-workspaces
fix(hyprland): issues with tracking workspaces
2023-01-30 22:38:05 +00:00
Jake Stanger
058c8f4228 fix(hyprland): issues with tracking workspaces 2023-01-30 22:24:00 +00:00
Jake Stanger
d78d851858 Merge pull request #60 from JakeStanger/fix/tray
fix(tray): some init issues
2023-01-30 18:49:45 +00:00
Jake Stanger
db72bc09b4 chore(hyprland): add debug logging 2023-01-30 18:49:30 +00:00
Jake Stanger
5fb412572f fix(tray): some init issues
It ain't perfect but it'll do.

Resolves #2.
2023-01-30 18:36:42 +00:00
Jake Stanger
400ac00d23 Merge pull request #59 from JakeStanger/feat/better-images
Better images
2023-01-30 12:13:10 +00:00
Jake Stanger
80a4b1d177 build(nix): update flake 2023-01-30 11:51:01 +00:00
Jake Stanger
96141d4990 feat(music): support for using images in name_map, additional icon options 2023-01-30 11:51:01 +00:00
Jake Stanger
b054c17d14 feat(workspaces): support for using images in name_map 2023-01-30 11:51:01 +00:00
Jake Stanger
3cf9be89fd feat: global icon theme setting
BREAKING CHANGE: This removes the `icon_theme` option from `launcher` and `focused`. You will need to set this at the top of your config instead.
2023-01-30 11:51:01 +00:00
Jake Stanger
393800aaa2 feat(custom): image widget 2023-01-30 11:51:01 +00:00
Jake Stanger
5772711192 fix(music): remote mpris album art not showing
Fixes #55.
2023-01-30 11:47:56 +00:00
Jake Stanger
15f0857859 refactor: replace icon loading with improved general image loading 2023-01-29 17:46:02 +00:00
Jake Stanger
8ba9826cd9 Merge pull request #58 from JakeStanger/feat/focus-trunc
feat(focused): ability to truncate label text
2023-01-28 23:14:08 +00:00
Jake Stanger
07dbf78010 feat(focused): ability to truncate label text 2023-01-28 23:01:44 +00:00
Jake Stanger
97502559b3 refactor(music): split config code into separate file 2023-01-28 22:43:22 +00:00
Jake Stanger
2b0eb6506a Merge pull request #57 from JakeStanger/feat/music-trunc
feat(music): ability to truncate button text
2023-01-28 22:23:55 +00:00
Jake Stanger
012762e102 refactor: swap out some code for existing macros 2023-01-28 22:07:05 +00:00
Jake Stanger
8691824db1 feat(music): ability to truncate button text
Adds new `truncate.mode` and `truncate.length` options, and `truncate` shorthand for mode.

Resolves #56.
2023-01-28 22:07:05 +00:00
Jake Stanger
ad97550583 build: update deps 2023-01-28 22:06:47 +00:00
JakeStanger
1ed3220733 docs: update CHANGELOG.md for v0.9.0 [skip ci] 2023-01-28 14:50:58 +00:00
57 changed files with 2374 additions and 1107 deletions

View File

@@ -23,12 +23,20 @@ jobs:
override: true
- name: Install build deps
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
- name: Check formatting
run: cargo fmt --check
- name: Clippy
- name: Clippy (base features)
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --no-default-features --features config+json
- name: Clippy (all features)
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.9.0] - 2023-01-28
### :boom: BREAKING CHANGES
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
`on_click` is now called `on_click_left` for consistency with new options.
- due to [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
The `mpd` module has been renamed to `music`. You will need to update the `type` value in your config and add `player_type` to continue using MPD. You will also need to update your styles.
### :sparkles: New Features
- [`1dd5863`](https://github.com/JakeStanger/ironbar/commit/1dd586343143bfd501a44c6556719fac9d582d6b) - better surface some config error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6e5d0c1`](https://github.com/JakeStanger/ironbar/commit/6e5d0c1e8c0b5d7e330608fc835e1e9733f156de) - **workspaces**: hyprland support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9ba28fe`](https://github.com/JakeStanger/ironbar/commit/9ba28fe7faf84e06febc2ffea089442f8f5b90a2) - **workspaces**: better ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`e1f523c`](https://github.com/JakeStanger/ironbar/commit/e1f523cf2a15b74a5c570dd7440db4c1b476d782) - **music**: popup artist label using wrong name *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`08cfbbc`](https://github.com/JakeStanger/ironbar/commit/08cfbbc2eaf6e74780dd7196efcc15ea6d2e7d12) - **music**: unable to go to prev with mpris *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0cefcbd`](https://github.com/JakeStanger/ironbar/commit/0cefcbd02b0af518352e35060644f281da249d3e) - **music**: wrong widget name on vol slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`90cd078`](https://github.com/JakeStanger/ironbar/commit/90cd078973b23b2291cf156e46729842f33c1806) - **mpd**: stops working if connection lost *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`2c1b292`](https://github.com/JakeStanger/ironbar/commit/2c1b2924d4a103183d3974ac066623a80277a79a) - move most of the horrible `add_module` macro content into proper functions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`fd2d7e5`](https://github.com/JakeStanger/ironbar/commit/fd2d7e5c7ab8de50c4621b19d07d8b012a451564) - move startup logging code to logging module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9d5049d`](https://github.com/JakeStanger/ironbar/commit/9d5049dde01cdb76f4772f8ce8f61a8b5bad3a50) - standardise error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5e21cbc`](https://github.com/JakeStanger/ironbar/commit/5e21cbcca6cc30d725acdea0f6561cfd6acdcc3c) - macros to reduce repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ea2c84d`](https://github.com/JakeStanger/ironbar/commit/ea2c84d1bd15798e32496397c4a6aa42fab39d95) - general code tidy-up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0d7ab54`](https://github.com/JakeStanger/ironbar/commit/0d7ab541604691455ed39c73e039ac0635307bc8) - remove redundant clone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`b97f018`](https://github.com/JakeStanger/ironbar/commit/b97f018e81aa55a871a12aa3e1e4b07b1f8eb50f) - update CHANGELOG.md for v0.8.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c223892`](https://github.com/JakeStanger/ironbar/commit/c223892a57b29ae56431fc585b8cec503f3206c7) - **workspaces**: update for hyprland/new ordering option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.8.0] - 2022-11-30
### :boom: BREAKING CHANGES
- due to [`df77020`](https://github.com/JakeStanger/ironbar/commit/df77020c5277ae9e379bb4fd67c221be5cb20426) - use snake_case for module tokens for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
@@ -156,4 +193,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0

536
Cargo.lock generated
View File

@@ -213,6 +213,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -366,6 +372,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@@ -603,6 +619,15 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encoding_rs"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-kinds"
version = "0.5.1"
@@ -688,6 +713,30 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "from_variants"
version = "1.0.0"
@@ -1054,6 +1103,25 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "h2"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1091,10 +1159,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hyprland"
version = "0.3.0-alpha.0"
name = "http"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a04a666b11a405dd7d74dbfb915e7d6f09bc0a52b0715204bf2e54d5572ab935"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyprland"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f76d4dad14a688266c346a9233e8cc5cea0c31a489d8811e3d93176338bafc0"
dependencies = [
"async-trait",
"doc-comment",
@@ -1106,6 +1245,8 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"strum",
"strum_macros",
"tokio",
]
@@ -1139,6 +1280,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indenter"
version = "0.3.3"
@@ -1184,11 +1335,18 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "ironbar"
version = "0.9.0"
version = "0.10.0"
dependencies = [
"async_once",
"cfg-if",
"chrono",
"color-eyre",
"dirs",
@@ -1204,6 +1362,7 @@ dependencies = [
"mpris",
"notify",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
@@ -1213,7 +1372,7 @@ dependencies = [
"swayipc-async",
"sysinfo",
"tokio",
"toml",
"toml 0.7.0",
"tracing",
"tracing-appender",
"tracing-error",
@@ -1280,7 +1439,7 @@ dependencies = [
"pest",
"pest_derive",
"serde",
"toml",
"toml 0.5.9",
]
[[package]]
@@ -1372,6 +1531,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -1437,6 +1602,24 @@ dependencies = [
"thiserror",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.23.2"
@@ -1485,6 +1668,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "notify"
version = "5.0.0"
@@ -1564,6 +1756,51 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl"
version = "0.10.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote 1.0.21",
"syn 1.0.105",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "ordered-stream"
version = "0.0.1"
@@ -1647,6 +1884,12 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.1"
@@ -1737,7 +1980,7 @@ checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
dependencies = [
"once_cell",
"thiserror",
"toml",
"toml 0.5.9",
]
[[package]]
@@ -1895,6 +2138,43 @@ dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
@@ -1910,6 +2190,12 @@ dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "ryu"
version = "1.0.11"
@@ -1925,6 +2211,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -1937,6 +2232,29 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "security-framework"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.11.0"
@@ -1997,6 +2315,27 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "serde_spanned"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.14"
@@ -2115,9 +2454,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stray"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e467969fcbf600ebb8d302aa6f2ee6cdf65d2a4c642844632bbdbcaf9c9e2b3e"
checksum = "358c1637c5ba4ccf1b6a0698de81454db644866cc426d1abc6d357b2efede511"
dependencies = [
"anyhow",
"byteorder",
@@ -2146,6 +2485,25 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.0",
"proc-macro2",
"quote 1.0.21",
"rustversion",
"syn 1.0.105",
]
[[package]]
name = "swayipc-async"
version = "2.0.1"
@@ -2226,7 +2584,7 @@ dependencies = [
"cfg-expr",
"heck 0.4.0",
"pkg-config",
"toml",
"toml 0.5.9",
"version-compare",
]
@@ -2320,6 +2678,21 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.23.0"
@@ -2351,6 +2724,16 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.11"
@@ -2362,6 +2745,20 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]]
name = "toml"
version = "0.5.9"
@@ -2371,6 +2768,46 @@ dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f560bc7fb3eb31f5eee1340c68a2160cad39605b7b9c9ec32045ddbdee13b85"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886f31a9b85b6182cabd4d8b07df3b451afcc216563748201490940d2a28ed36"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233d8716cdc5d20ec88a18a839edaf545edc71efa4a5ff700ef4a102c26cd8fa"
dependencies = [
"indexmap",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.37"
@@ -2454,6 +2891,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "typenum"
version = "1.16.0"
@@ -2476,12 +2919,27 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-bidi"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
@@ -2506,6 +2964,17 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68"
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.0"
@@ -2518,6 +2987,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
@@ -2574,6 +3049,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@@ -2611,6 +3096,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
@@ -2710,6 +3207,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
@@ -2807,6 +3314,15 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "xcursor"
version = "0.3.4"

View File

@@ -1,15 +1,51 @@
[package]
name = "ironbar"
version = "0.9.0"
version = "0.10.0"
edition = "2021"
license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar"
[features]
default = [
"http",
"config+all",
"clock",
"music+all",
"sys_info",
"tray",
"workspaces+all"
]
http = ["dep:reqwest"]
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
"config+json" = ["serde_json"]
"config+yaml" = ["serde_yaml"]
"config+toml" = ["toml"]
"config+corn" = ["libcorn"]
clock = ["chrono"]
music = ["regex"]
"music+all" = ["music", "music+mpris", "music+mpd"]
"music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd_client"]
sys_info = ["sysinfo", "regex"]
tray = ["stray"]
workspaces = ["futures-util"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
[dependencies]
# core
gtk = "0.16.0"
gtk-layer-shell = "0.5.0"
glib = "0.16.2"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process"] }
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-error = "0.2.0"
@@ -17,25 +53,43 @@ tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2"
serde = { version = "1.0.141", features = ["derive"] }
serde_json = "1.0.82"
serde_yaml = "0.9.4"
toml = "0.5.9"
libcorn = "0.6.1"
lazy_static = "1.4.0"
async_once = "0.2.6"
indexmap = "1.9.1"
futures-util = "0.3.21"
chrono = "0.4.19"
regex = { version = "1.6.0", default-features = false, features = ["std"] }
stray = { version = "0.1.2" }
dirs = "4.0.0"
walkdir = "2.3.2"
notify = { version = "5.0.0", default-features = false }
mpd_client = "1.0.0"
mpris = "2.0.0"
swayipc-async = { version = "2.0.1" }
hyprland = "0.3.0-alpha.0"
sysinfo = "0.27.0"
wayland-client = "0.29.5"
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
lazy_static = "1.4.0"
async_once = "0.2.6"
cfg-if = "1.0.0"
# http
reqwest = { version = "0.11.14", optional = true }
# config
serde_json = { version = "1.0.82", optional = true }
serde_yaml = { version = "0.9.4", optional = true }
toml = { version = "0.7.0", optional = true }
libcorn = { version = "0.6.1", optional = true }
# clock
chrono = { version = "0.4.19", optional = true }
# music
mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.0", optional = true }
# sys_info
sysinfo = { version = "0.27.0", optional = true }
# tray
stray = { version = "0.1.3", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.0", optional = true }
futures-util = { version = "0.3.21", optional = true }
# shared
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info

View File

@@ -6,8 +6,7 @@ It uses GTK3 and gtk-layer-shell.
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).
![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png)
![Screenshot of fully configured bar with MPD widget open](https://f.jstanger.dev/github/ironbar/bar.png)
## Installation
@@ -29,9 +28,11 @@ yay -S ironbar-git
### Nix Flake
A flake is included with the repo which can be used with home-manager.
#### Example
Here is an example nix flake that uses ironbar, this is just a
proof of concept, please adapt it to your config
Here is an example nix flake that uses Ironbar.
```nix
{
@@ -67,8 +68,9 @@ proof of concept, please adapt it to your config
```
#### Binary Caching
There is also a cachix cache at `https://app.cachix.org/cache/jakestanger`
incase you don't want to compile ironbar!
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`
in case you don't want to compile Ironbar.
### Source
@@ -80,6 +82,9 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar
```
By default, all features are enabled.
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling) for controlling which features are included.
[repo](https://github.com/jakestanger/ironbar)
## Running

51
docs/Compiling.md Normal file
View File

@@ -0,0 +1,51 @@
You can compile Ironbar from source using `cargo`.
Just clone the repo and build:
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
cargo build --release
# change path to wherever you want to install
install target/release/ironbar ~/.local/bin/ironbar
```
## Features
By default, all features are enabled for convenience. This can result in a significant compile time.
If you know you are not going to need all the features, you can compile with only the features you need.
As of `v0.10.0`, compiling with no features is about 33% faster.
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
This difference is expected to increase as the bar develops.
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
To build using only specific features, disable default features and pass a comma separated list to `cargo build`:
```shell
cargo build --release --no-default-features \
--features http,config+json,clock
```
> ⚠ Make sure you enable at least one `config` feature otherwise you will not be able to start the bar!
| Feature | Description |
|---------------------|-----------------------------------------------------------------------------------|
| **Core** | |
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
| config+all | Enables support for all configuration languages. |
| config+json | Enables configuration support for JSON. |
| config+yaml | Enables configuration support for YAML. |
| config+toml | Enables configuration support for TOML. |
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
| **Modules** | |
| clock | Enables the `clock` module. |
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |
| music+mpd | Enables the `music` module with MPD support. |
| sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. |
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |

View File

@@ -272,6 +272,7 @@ The following table lists each of the top-level bar config options:
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `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. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |

15
docs/Images.md Normal file
View File

@@ -0,0 +1,15 @@
Ironbar is capable of loading images from multiple sources.
In any situation where an option takes text or an icon,
you can use a string in any of the following formats, and it will automatically be detected as an image:
| Source | Example |
|-------------------------------|---------------------------------|
| GTK icon theme | `icon:firefox` |
| Local file | `file:///path/to/file.jpg` |
| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` |
Remote images are loaded asynchronously to avoid blocking the UI thread.
Be aware this can cause elements to change size upon load if the image is large enough.
Note that mixing text and images is not supported.
Your best option here is to use Nerd Font icons instead.

View File

@@ -2,12 +2,14 @@
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
- [Examples](https://github.com/JakeStanger/ironbar/tree/master/examples)
# Examples
- [Config](config)
- [Stylesheet](stylesheet)
- [Stylesheet](https://github.com/JakeStanger/ironbar/blob/master/examples/style.css)
## Custom

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,202 +1,10 @@
The below config shows a module of each type being used.
The configs linked below show a module of each type being used.
The Corn format makes heavy use of variables
to show how module configs can be easily referenced to improve readability
and reduce config length when using multiple bars.
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"all_monitors": false,
"name_map": {
"1": "ﭮ",
"2": "",
"3": "",
"Code": "",
"Games": ""
},
"type": "workspaces"
},
{
"favorites": [
"firefox",
"discord",
"Steam"
],
"icon_theme": "Paper",
"show_icons": true,
"show_names": false,
"type": "launcher"
}
],
"end": [
{
"music_dir": "/home/jake/Music",
"type": "mpd"
},
{
"host": "chloe:6600",
"type": "mpd"
},
{
"path": "/home/jake/bin/phone-battery",
"type": "script"
},
{
"format": [
"{cpu-percent}% ",
"{memory-percent}% "
],
"type": "sys-info"
},
{
"type": "clock"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
all_monitors = false
type = 'workspaces'
[start.name_map]
1 = 'ﭮ'
2 = ''
3 = ''
Code = ''
Games = ''
[[start]]
icon_theme = 'Paper'
show_icons = true
show_names = false
type = 'launcher'
favorites = [
'firefox',
'discord',
'Steam',
]
[[end]]
music_dir = '/home/jake/Music'
type = 'mpd'
[[end]]
host = 'chloe:6600'
type = 'mpd'
[[end]]
path = '/home/jake/bin/phone-battery'
type = 'script'
[[end]]
type = 'sys-info'
format = [
'{cpu-percent}% ',
'{memory-percent}% ',
]
[[end]]
type = 'clock'
```
</details>
<details>
<summary>YAML</summary>
```yaml
---
start:
- all_monitors: false
name_map:
"1":
"2":
"3":
Code:
Games:
type: workspaces
- favorites:
- firefox
- discord
- Steam
icon_theme: Paper
show_icons: true
show_names: false
type: launcher
end:
- music_dir: /home/jake/Music
type: mpd
- host: "chloe:6600"
type: mpd
- path: /home/jake/bin/phone-battery
type: script
- format:
- "{cpu-percent}% "
- "{memory-percent}% "
type: sys-info
- type: clock
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$workspaces = {
type = "workspaces"
all_monitors = false
name_map = {
1 = "ﭮ"
2 = ""
3 = ""
Games = ""
Code = ""
}
}
$launcher = {
type = "launcher"
favorites = ["firefox" "discord" "Steam"]
show_names = false
show_icons = true
icon_theme = "Paper"
}
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
$mpd_server = { type = "mpd" host = "chloe:6600" }
$sys_info = {
type = "sys-info"
format = ["{cpu-percent}% " "{memory-percent}% "]
}
$tray = { type = "tray" }
$clock = { type = "clock" }
$phone_battery = {
type = "script"
path = "/home/jake/bin/phone-battery"
}
$start = [ $workspaces $launcher ]
$end = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
}
in {
start = $start
end = $end
}
```
</details>
- [JSON](https://github.com/JakeStanger/ironbar/blob/master/examples/config.json)
- [TOML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.toml)
- [YAML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.yaml)
- [Corn](https://github.com/JakeStanger/ironbar/blob/master/examples/config.corn)

View File

@@ -1,142 +0,0 @@
The below example is a full stylesheet for all modules:
```css
* {
/* a nerd font is required to be installed for icons */
font-family: Noto Sans Nerd Font, sans-serif;
font-size: 16px;
border: none;
}
#bar {
border-top: 1px solid #424242;
}
.container {
background-color: #2d2d2d;
}
.container#end > * + * {
margin-left: 20px;
}
.popup {
background-color: #2d2d2d;
border: 1px solid #424242;
}
#workspaces .item {
color: white;
background-color: #2d2d2d;
border-radius: 0;
}
#workspaces .item.focused {
box-shadow: inset 0 -3px;
background-color: #1c1c1c;
}
#workspaces *:not(.focused):hover {
box-shadow: inset 0 -3px;
}
#launcher .item {
border-radius: 0;
background-color: #2d2d2d;
margin-right: 4px;
}
#launcher .item:not(.focused):hover {
background-color: #1c1c1c;
}
#launcher .open {
border-bottom: 2px solid #6699cc;
}
#launcher .focused {
color: white;
background-color: black;
border-bottom: 4px solid #6699cc;
}
#launcher .urgent {
color: white;
background-color: #8f0a0a;
}
#script {
color: white;
}
#sysinfo {
color: white;
}
#tray .item {
background-color: #2d2d2d;
}
#mpd {
background-color: #2d2d2d;
color: white;
}
#popup-mpd {
color: white;
padding: 1em;
}
#popup-mpd #album-art {
margin-right: 1em;
}
#popup-mpd #title .icon, #popup-mpd #title .label {
font-size: 1.7em;
}
#popup-mpd #controls * {
border-radius: 0;
background-color: #2d2d2d;
color: white;
}
#popup-mpd #controls *:disabled {
color: #424242;
}
#clock {
color: white;
background-color: #2d2d2d;
font-weight: bold;
}
#popup-clock {
padding: 1em;
}
#popup-clock #calendar-clock {
color: white;
font-size: 2.5em;
padding-bottom: 0.1em;
}
#popup-clock #calendar {
background-color: #2d2d2d;
color: white;
}
#popup-clock #calendar .header {
padding-top: 1em;
border-top: 1px solid #424242;
font-size: 1.5em;
}
#popup-clock #calendar:selected {
background-color: #6699cc;
}
#focused {
color: white;
}
```

View File

@@ -1,6 +1,6 @@
Creates a button on the bar, which opens a popup. The popup contains a header, shutdown button, restart button, and uptime.
![A screenshot of the custom power menu module open, with some other modules present on the bar](../../_imgs/custom-power-menu.png)
![A screenshot of the custom power menu module open, with some other modules present on the bar](https://f.jstanger.dev/github/ironbar/custom-power-menu.png)
## Configuration
@@ -16,9 +16,9 @@ Creates a button on the bar, which opens a popup. The popup contains a header, s
{
"bar": [
{
"on_click": "popup:toggle",
"label": "",
"name": "power-btn",
"on_click": "popup:toggle",
"type": "button"
}
],
@@ -38,26 +38,27 @@ Creates a button on the bar, which opens a popup. The popup contains a header, s
"widgets": [
{
"class": "power-btn",
"on_click": "!shutdown now",
"label": "<span font-size='40pt'></span>",
"on_click": "!shutdown now",
"type": "button"
},
{
"class": "power-btn",
"on_click": "!reboot",
"label": "<span font-size='40pt'></span>",
"on_click": "!reboot",
"type": "button"
}
]
},
{
"label": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
}
]
}
],
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
"type": "custom"
}
]
@@ -75,12 +76,13 @@ type = 'clock'
[[end]]
class = 'power-menu'
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
type = 'custom'
[[end.bar]]
on_click = 'popup:toggle'
label = ''
name = 'power-btn'
on_click = 'popup:toggle'
type = 'button'
[[end.popup]]
@@ -97,18 +99,18 @@ type = 'box'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!shutdown now'
label = '''<span font-size='40pt'></span>'''
on_click = '!shutdown now'
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!reboot'
label = '''<span font-size='40pt'></span>'''
on_click = '!reboot'
type = 'button'
[[end.popup.widgets]]
label = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
```
@@ -121,10 +123,11 @@ type = 'label'
```yaml
end:
- type: clock
- bar:
- on_click: popup:toggle
label:
- label:
name: power-btn
on_click: popup:toggle
type: button
class: power-menu
popup:
@@ -137,16 +140,17 @@ end:
- type: box
widgets:
- class: power-btn
on_click: '!shutdown now'
label: <span font-size='40pt'></span>
on_click: '!shutdown now'
type: button
- class: power-btn
on_click: '!reboot'
label: <span font-size='40pt'></span>
on_click: '!reboot'
type: button
- label: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
type: custom
```
@@ -157,30 +161,37 @@ end:
```corn
let {
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
$popup = {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" 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 = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
}
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
bar = [ $button ]
popup = [ $popup ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" 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 = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
$clock = { type = "clock" }
} in {
end = [ $power_menu ]
end = [ $power_menu $clock ]
}
```
</details>

View File

@@ -8,8 +8,8 @@ Clicking on the widget opens a popup with the time and a calendar.
> Type: `clock`
| Name | Type | Default | Description |
|----------|--------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| Name | Type | Default | Description |
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `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> |
<details>

View File

@@ -1,7 +1,7 @@
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.
![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://user-images.githubusercontent.com/5057870/196058785-042ef171-7e77-4d5c-921a-eca03c6424bd.png)
![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://f.jstanger.dev/github/ironbar/custom-power-menu.png)
## Configuration
@@ -18,15 +18,17 @@ It is well worth looking at the examples.
### `Widget`
| Name | Type | Default | Description |
|---------------|------------------------------|--------------|---------------------------------------------------------------------------|
| `widget_type` | `box` or `label` or `button` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
| Name | Type | Default | Description |
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------|
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
### Labels
@@ -56,6 +58,8 @@ The following bar commands are supported:
- `popup:open`
- `popup:close`
---
XML is arguably better-suited and easier to read for this sort of markup,
but currently is not supported.
Nonetheless, it may be worth comparing the examples to the below equivalent

View File

@@ -7,12 +7,14 @@ Displays the title and/or icon of the currently focused window.
> Type: `focused`
| Name | Type | Default | Description |
|--------------|-----------|---------|---------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `icon_theme` | `string` | `null` | GTK icon theme to use |
| Name | Type | Default | Description |
|-------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>
@@ -25,7 +27,7 @@ Displays the title and/or icon of the currently focused window.
"show_icon": true,
"show_title": true,
"icon_size": 32,
"icon_theme": "Paper"
"truncate": "end"
}
]
}
@@ -43,7 +45,7 @@ type = "focused"
show_icon = true
show_title = true
icon_size = 32
icon_theme = "Paper"
truncate = "end"
```
</details>
@@ -57,7 +59,7 @@ end:
show_icon: true
show_title: true
icon_size: 32
icon_theme: "Paper"
truncate: "end"
```
</details>
@@ -73,7 +75,7 @@ end:
show_icon = true
show_title = true
icon_size = 32
icon_theme = "Paper"
truncate = "end"
}
]
}

View File

@@ -3,7 +3,7 @@ Hovering over a program with multiple windows open shows a popup with each windo
Clicking an icon/popup item focuses or launches the program.
Optionally displays a launchable set of favourites.
![Screenshot showing several open applications, including a focused terminal.](https://user-images.githubusercontent.com/5057870/184540058-120e190e-2a45-4167-99c7-ed76482d1f16.png)
![Screenshot showing several open applications, including a popup showing multiple terminal windows.](https://f.jstanger.dev/github/ironbar/launcher.png)
## Configuration
@@ -14,7 +14,6 @@ Optionally displays a launchable set of favourites.
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_theme` | `string` | `null` | GTK icon theme to use. |
<details>
<summary>JSON</summary>
@@ -29,8 +28,7 @@ Optionally displays a launchable set of favourites.
"discord"
],
"show_names": false,
"show_icons": true,
"icon_theme": "Paper"
"show_icons": true
}
]
}
@@ -49,7 +47,6 @@ type = "launcher"
favorites = ["firefox", "discord"]
show_names = false
show_icons = true
icon_theme = "Paper"
```
</details>
@@ -65,7 +62,6 @@ start:
- discord
show_names: false
show_icons: true
icon_theme: "Paper"
```
</details>
@@ -78,10 +74,9 @@ start:
start = [
{
type = "launcher"
favorites = ["firefox" "discord"]
favorites = [ "firefox" "discord" ]
show_names = false
show_icons = true
icon_theme = "Paper"
}
]

View File

@@ -5,21 +5,31 @@ and playback controls.
in MPRIS mode, the widget will listen to all players and automatically detect/display the active one.
![Screenshot showing MPD widget with track playing with popout open](https://user-images.githubusercontent.com/5057870/184539664-a8f3ad5b-69c0-492d-a27d-82303c09a347.png)
![Screenshot showing MPD widget with track playing with popout open](https://f.jstanger.dev/github/ironbar/music.png)
## Configuration
> Type: `music`
| | Type | Default | Description |
|----------------|------------------|-----------------------------|----------------------------------------------------------------------------------|
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
| `icons.play` | `string` | `` | Icon to show when playing. |
| `icons.pause` | `string` | `` | Icon to show when paused. |
| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. |
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
| | Type | Default | Description |
|-------------------|---------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string/image` | `` | Icon to show when playing. |
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `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.
<details>
<summary>JSON</summary>
@@ -30,7 +40,8 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
{
"type": "music",
"player_type": "mpd",
"format": "{icon} {title} / {artist}",
"format": "{title} / {artist}",
"truncate": "end",
"icons": {
"play": "",
"pause": ""
@@ -50,8 +61,9 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
[[start]]
type = "music"
player_type = "mpd"
format = "{icon} {title} / {artist}"
format = "{title} / {artist}"
music_dir = "/home/jake/Music"
truncate = "end"
[[start.icons]]
play = ""
@@ -67,7 +79,8 @@ pause = ""
start:
- type: "music"
player_type: "mpd"
format: "{icon} {title} / {artist}"
format: "{title} / {artist}"
truncate: "end"
icons:
play: ""
pause: ""
@@ -85,7 +98,8 @@ start:
{
type = "music"
player_type = "mpd"
format = "{icon} {title} / {artist}"
format = "{title} / {artist}"
truncate = "end"
icons.play = ""
icons.pause = ""
music_dir = "/home/jake/Music"
@@ -103,7 +117,6 @@ and will be replaced with values from the currently playing track:
| Token | Description |
|--------------|--------------------------------------|
| `{icon}` | Either `icons.play` or `icons.pause` |
| `{title}` | Title |
| `{album}` | Album name |
| `{artist}` | Artist name |
@@ -116,24 +129,25 @@ and will be replaced with values from the currently playing track:
## Styling
| Selector | Description |
|------------------------------------------|------------------------------------------|
| `#music` | Tray widget button |
| `#popup-music` | Popup box |
| `#popup-music #album-art` | Album art image inside popup box |
| `#popup-music #title` | Track title container inside popup box |
| `#popup-music #title .icon` | Track title icon label inside popup box |
| `#popup-music #title .label` | Track title label inside popup box |
| `#popup-music #album` | Track album container inside popup box |
| `#popup-music #album .icon` | Track album icon label inside popup box |
| `#popup-music #album .label` | Track album label inside popup box |
| `#popup-music #artist` | Track artist container inside popup box |
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
| `#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-pause` | Play/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` | Volume slider popup box |
| `#popup-music #volume .icon` | Volume icon label inside popup box |
| Selector | Description |
|-------------------------------------|------------------------------------------|
| `#music` | Tray widget button |
| `#popup-music` | Popup box |
| `#popup-music #album-art` | Album art image inside popup box |
| `#popup-music #title` | Track title container inside popup box |
| `#popup-music #title .icon` | Track title icon label inside popup box |
| `#popup-music #title .label` | Track title label inside popup box |
| `#popup-music #album` | Track album container inside popup box |
| `#popup-music #album .icon` | Track album icon label inside popup box |
| `#popup-music #album .label` | Track album label inside popup box |
| `#popup-music #artist` | Track artist container inside popup box |
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
| `#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` | Volume slider popup box |
| `#popup-music #volume .icon` | Volume icon label inside popup box |

View File

@@ -8,11 +8,11 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string>` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
| Name | Type | Default | Description |
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string/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. |
| `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>
<summary>JSON</summary>
@@ -72,15 +72,15 @@ end:
```corn
{
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
all_monitors = false
}
]
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
all_monitors = false
}
]
}
```

View File

@@ -4,19 +4,20 @@ let {
all_monitors = false
name_map = {
1 = "ﭮ"
2 = ""
2 = "icon:firefox"
3 = ""
Games = ""
Games = "icon:steam"
Code = ""
}
}
$focused = { type = "focused" }
$launcher = {
type = "launcher"
favorites = ["firefox" "discord" "Steam"]
show_names = false
show_icons = true
icon_theme = "Paper"
}
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
@@ -24,52 +25,65 @@ let {
$sys_info = {
type = "sys_info"
format = ["{cpu_percent}% " "{memory_percent}% "]
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
" {uptime}"
]
}
$tray = { type = "tray" }
$clock = {
type = "clock"
// show-if = "500:[ $(($(date +%s) % 2)) -eq 0 ]"
show_if.cmd = "exit 0"
show_if.interval = 500
}
$clock = { type = "clock" }
$phone_battery = {
type = "script"
cmd = "/home/jake/bin/phone-battery"
show_if.cmd = "/home/jake/bin/phone-connected"
show_if.interval = 500
}
$log_tail = {
type = "script"
path = "tail -f /home/jake/.local/share/ironbar/error.log"
mode = "watch"
// -- begin custom --
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
$popup = {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" 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 = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
}
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" 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 = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
bar = [ $button ]
popup = [ $popup ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
// -- end custom --
$left = [ $workspaces $launcher ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $power_menu $clock ]

View File

@@ -1,43 +1,133 @@
{
"anchor_to_edges": true,
"end": [
{
"music_dir": "/home/jake/Music",
"player_type": "mpd",
"truncate": {
"length": 100,
"mode": "end"
},
"type": "music"
},
{
"host": "chloe:6600",
"player_type": "mpd",
"truncate": "end",
"type": "music"
},
{
"cmd": "/home/jake/bin/phone-battery",
"show_if": {
"cmd": "/home/jake/bin/phone-connected",
"interval": 500
},
"type": "script"
},
{
"format": [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
" {uptime}"
],
"interval": {
"cpu": 1,
"disks": 300,
"memory": 30,
"networks": 3,
"temps": 5
},
"type": "sys_info"
},
{
"bar": [
{
"label": "",
"name": "power-btn",
"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"
},
{
"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",
"position": "bottom",
"start": [
{
"bar": [
{
"size": 32,
"src": "file:///path/to/image.jpg",
"type": "image"
}
],
"type": "custom"
},
{
"all_monitors": false,
"name_map": {
"1": "ﭮ",
"2": "icon:firefox",
"3": "",
"Code": "",
"Games": "icon:steam"
},
"type": "workspaces"
},
{
"type": "launcher",
"icon_theme": "Paper",
"favorites": [
"firefox",
"discord",
"Steam"
],
"show_names": false
}
],
"end": [
{
"type": "mpd"
},
{
"type": "mpd",
"host": "chloe:6600"
},
{
"path": "/home/jake/bin/phone-battery",
"type": "script"
},
{
"format": [
"{cpu_percent}% ",
"{memory_percent}% "
],
"type": "sys_info"
},
{
"type": "tray"
},
{
"type": "clock"
"show_icons": true,
"show_names": false,
"type": "launcher"
}
]
}
}

118
examples/config.toml Normal file
View File

@@ -0,0 +1,118 @@
anchor_to_edges = true
icon_theme = 'Paper'
position = 'bottom'
[[end]]
music_dir = '/home/jake/Music'
player_type = 'mpd'
type = 'music'
[end.truncate]
length = 100
mode = 'end'
[[end]]
host = 'chloe:6600'
player_type = 'mpd'
truncate = 'end'
type = 'music'
[[end]]
cmd = '/home/jake/bin/phone-battery'
type = 'script'
[end.show_if]
cmd = '/home/jake/bin/phone-connected'
interval = 500
[[end]]
type = 'sys_info'
format = [
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
' {memory_used} / {memory_total} GB ({memory_percent}%)',
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
' {uptime}',
]
[end.interval]
cpu = 1
disks = 300
memory = 30
networks = 3
temps = 5
[[end]]
class = 'power-menu'
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
type = 'custom'
[[end.bar]]
label = ''
name = 'power-btn'
on_click = 'popup:toggle'
type = 'button'
[[end.popup]]
orientation = 'vertical'
type = 'box'
[[end.popup.widgets]]
label = 'Power menu'
name = 'header'
type = 'label'
[[end.popup.widgets]]
type = 'box'
[[end.popup.widgets.widgets]]
class = 'power-btn'
label = '''<span font-size='40pt'></span>'''
on_click = '!shutdown now'
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
label = '''<span font-size='40pt'></span>'''
on_click = '!reboot'
type = 'button'
[[end.popup.widgets]]
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
[[end]]
type = 'clock'
[[start]]
type = 'custom'
[[start.bar]]
size = 32
src = 'file:///path/to/image.jpg'
type = 'image'
[[start]]
all_monitors = false
type = 'workspaces'
[start.name_map]
1 = 'ﭮ'
2 = 'icon:firefox'
3 = ''
Code = ''
Games = 'icon:steam'
[[start]]
show_icons = true
show_names = false
type = 'launcher'
favorites = [
'firefox',
'discord',
'Steam',
]

97
examples/config.yaml Normal file
View File

@@ -0,0 +1,97 @@
anchor_to_edges: true
icon_theme: Paper
position: bottom
start:
- bar:
- size: 32
src: file:///path/to/image.jpg
type: image
type: custom
- all_monitors: false
name_map:
'1':
'2': icon:firefox
'3':
Code:
Games: icon:steam
type: workspaces
- favorites:
- firefox
- discord
- Steam
show_icons: true
show_names: false
type: launcher
end:
- music_dir: /home/jake/Music
player_type: mpd
truncate:
length: 100
mode: end
type: music
- host: chloe:6600
player_type: mpd
truncate: end
type: music
- cmd: /home/jake/bin/phone-battery
show_if:
cmd: /home/jake/bin/phone-connected
interval: 500
type: script
- format:
-  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
-  {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
- 李 {net_down:enp39s0} / {net_up:enp39s0} Mbps
- 猪 {load_average:1} | {load_average:5} | {load_average:15}
-  {uptime}
interval:
cpu: 1
disks: 300
memory: 30
networks: 3
temps: 5
type: sys_info
- bar:
- label:
name: power-btn
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
- 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

View File

@@ -1,29 +0,0 @@
let {
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
$popup = {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" 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 = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
}
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ $button ]
popup = [ $popup ]
}
} in {
end = [ $power_menu { type = "clock" } ]
}

View File

@@ -1,31 +1,19 @@
* {
/* `otf-font-awesome` is required to be installed for icons */
font-family: Noto Sans Nerd Font, sans-serif;
/* font-family: 'Jetbrains Mono', monospace;*/
font-size: 16px;
/*color: white;*/
/*background-color: #2d2d2d;*/
/*background-color: red;*/
border: none;
/*opacity: 0.4;*/
}
#bar {
border-top: 1px solid #424242;
}
.container {
.background, .container {
background-color: #2d2d2d;
}
/* test 34543*/
#right > * + * {
margin-left: 20px;
}
#workspaces .item {
color: white;
background-color: #2d2d2d;
@@ -57,7 +45,7 @@
#launcher .focused {
color: white;
background-color: black;
background-color: #1c1c1c;
border-bottom: 4px solid #6699cc;
}
@@ -66,25 +54,54 @@
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;
margin-left: 5px;
}
#clock:hover {
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;
}
#mpd {
#music {
background-color: #2d2d2d;
color: white;
}
@@ -119,30 +136,77 @@
background-color: #6699cc;
}
#popup-mpd {
#music:hover {
background-color: #1c1c1c;
}
#popup-music {
color: white;
padding: 1em;
}
#popup-mpd #album-art {
/*border: 1px solid #424242;*/
#popup-music #album-art {
margin-right: 1em;
}
#popup-mpd #title .icon, #popup-mpd #title .label {
#popup-music #title .icon *, #popup-music #title .label {
font-size: 1.7em;
}
#popup-mpd #controls * {
#popup-music #controls * {
border-radius: 0;
background-color: #2d2d2d;
color: white;
}
#popup-mpd #controls *:disabled {
#popup-music #controls *:disabled {
color: #424242;
}
#popup-music #volume > box:last-child label {
margin-left: 6px;
}
#focused {
color: white;
}
.power-menu {
margin-left: 10px;
}
.power-menu #power-btn {
color: white;
background-color: #2d2d2d;
}
.power-menu #power-btn:hover {
background-color: #1c1c1c;
}
.popup-power-menu {
padding: 1em;
}
.popup-power-menu #header {
color: white;
font-size: 1.4em;
border-bottom: 1px solid white;
padding-bottom: 0.4em;
margin-bottom: 0.8em;
}
.popup-power-menu .power-btn {
color: white;
background-color: #2d2d2d;
border: 1px solid white;
padding: 0.6em 1em;
}
.popup-power-menu .power-btn + .power-btn {
margin-left: 1em;
}
.popup-power-menu .power-btn:hover {
background-color: #1c1c1c;
}

View File

@@ -1,23 +0,0 @@
{
end = [
{
type = "sys_info"
interval.memory = 30
interval.cpu = 1
interval.temps = 5
interval.disks = 300
interval.networks = 3
format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
" {uptime}"
]
}
]
}

12
flake.lock generated
View File

@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672350804,
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
"lastModified": 1675115703,
"narHash": "sha256-4zetAPSyY0D77x+Ww9QBe8RHn1akvIvHJ/kgg8kGDbk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
"rev": "2caf4ef5005ecc68141ecb4aac271079f7371c44",
"type": "github"
},
"original": {
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1672453260,
"narHash": "sha256-ruR2xo30Vn7kY2hAgg2Z2xrCvNePxck6mgR5a8u+zow=",
"lastModified": 1675132198,
"narHash": "sha256-izOVjdIfdv0OzcfO9rXX0lfGkQn4tdJ0eNm3P3LYo/o=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "176b6fd3dd3d7cea8d22ab1131364a050228d94c",
"rev": "48b1403150c3f5a9aeee8bc4c77c8926f29c6501",
"type": "github"
},
"original": {

View File

@@ -50,7 +50,7 @@
cargoDeps = rustPlatform.importCargoLock {lockFile = ./Cargo.lock;};
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with prev; [pkg-config];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl];
};
};
packages = genSystems (
@@ -74,6 +74,7 @@
gtk3
gtk-layer-shell
pkg-config
openssl
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";

View File

@@ -8,7 +8,7 @@ use crate::{await_sync, read_lock, send, write_lock, Config};
use color_eyre::Result;
use gtk::gdk::{EventMask, Monitor, ScrollDirection};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, EventBox, Orientation, Widget};
use gtk::{Application, ApplicationWindow, EventBox, IconTheme, Orientation, Widget};
use std::sync::{Arc, RwLock};
use tokio::spawn;
use tokio::sync::mpsc;
@@ -140,6 +140,11 @@ fn load_modules(
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 {
@@ -148,6 +153,7 @@ fn load_modules(
monitor,
output_name,
location: $location,
icon_theme: &icon_theme,
}
};
}
@@ -189,15 +195,20 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
for (id, config) in modules.into_iter().enumerate() {
match config {
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
ModuleConfig::Tray(mut module) => add_module!(module, id),
ModuleConfig::Music(mut module) => add_module!(module, id),
ModuleConfig::Launcher(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Launcher(mut module) => add_module!(module, id),
#[cfg(feature = "music")]
ModuleConfig::Music(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
#[cfg(feature = "tray")]
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}
}

View File

@@ -1,5 +1,6 @@
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::error::{ERR_CHANNEL_SEND, ERR_MUTEX_LOCK};
use crate::{lock, send};
use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListenerMutable as EventListener;
@@ -9,10 +10,9 @@ use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::task::spawn_blocking;
use tracing::{error, info};
use tracing::{debug, error, info};
pub struct EventClient {
workspaces: Arc<Mutex<Vec<Workspace>>>,
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
@@ -21,12 +21,7 @@ impl EventClient {
fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16);
let workspaces = Arc::new(Mutex::new(vec![]));
// load initial list
Self::refresh_workspaces(&workspaces);
Self {
workspaces,
workspace_tx,
_workspace_rx: workspace_rx,
}
@@ -35,164 +30,198 @@ impl EventClient {
fn listen_workspace_events(&self) {
info!("Starting Hyprland event listener");
let workspaces = self.workspaces.clone();
let tx = self.workspace_tx.clone();
spawn_blocking(move || {
let mut event_listener = EventListener::new();
// we need a lock to ensure events don't run at the same time
let lock = Arc::new(Mutex::new(()));
// cache the active workspace since Hyprland doesn't give us the prev active
let active = Self::get_active_workspace().expect("Failed to get active workspace");
let active = Arc::new(Mutex::new(Some(active)));
{
let workspaces = workspaces.clone();
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_added_handler(move |workspace_type, _state| {
Self::refresh_workspaces(&workspaces);
let _lock = lock!(lock);
debug!("Added workspace: {workspace_type:?}");
let workspace = Self::get_workspace(&workspaces, workspace_type);
workspace.map_or_else(
|| error!("Unable to locate workspace"),
|workspace| {
tx.send(WorkspaceUpdate::Add(workspace))
.expect(ERR_CHANNEL_SEND);
},
);
let workspace_name = get_workspace_name(workspace_type);
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, focused);
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Add(workspace));
}
});
}
{
let workspaces = workspaces.clone();
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type, _state| {
let prev_workspace = Self::get_focused_workspace(&workspaces);
let _lock = lock!(lock);
Self::refresh_workspaces(&workspaces);
let mut prev_workspace = lock!(active);
let workspace = Self::get_workspace(&workspaces, workspace_type);
if let (Some(prev_workspace), Some(workspace)) = (prev_workspace, workspace) {
if prev_workspace.id != workspace.id {
tx.send(WorkspaceUpdate::Focus {
old: prev_workspace,
new: workspace.clone(),
})
.expect(ERR_CHANNEL_SEND);
}
// there may be another type of update so dispatch that regardless of focus change
tx.send(WorkspaceUpdate::Update(workspace))
.expect(ERR_CHANNEL_SEND);
} else {
error!("Unable to locate workspace");
}
});
}
{
let workspaces = workspaces.clone();
let tx = tx.clone();
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
let workspace = Self::get_workspace(&workspaces, workspace_type);
workspace.map_or_else(
|| error!("Unable to locate workspace"),
|workspace| {
tx.send(WorkspaceUpdate::Remove(workspace))
.expect(ERR_CHANNEL_SEND);
},
debug!(
"Received workspace change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.id)
);
Self::refresh_workspaces(&workspaces);
});
}
let workspace_name = get_workspace_name(workspace_type);
let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
{
let workspaces = workspaces.clone();
let tx = tx.clone();
event_listener.add_workspace_moved_handler(move |event_data, _state| {
let workspace_type = event_data.1;
Self::refresh_workspaces(&workspaces);
let workspace = Self::get_workspace(&workspaces, workspace_type);
workspace.map_or_else(
|| error!("Unable to locate workspace"),
|| {
error!("Unable to locate workspace");
},
|workspace| {
tx.send(WorkspaceUpdate::Move(workspace))
.expect(ERR_CHANNEL_SEND);
// there may be another type of update so dispatch that regardless of focus change
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
if !focused {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
},
);
});
}
{
let workspaces = workspaces.clone();
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data, _state| {
let _lock = lock!(lock);
let workspace_type = event_data.1;
let prev_workspace = Self::get_focused_workspace(&workspaces);
let mut prev_workspace = lock!(active);
Self::refresh_workspaces(&workspaces);
debug!(
"Received active monitor change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.name)
);
let workspace = Self::get_workspace(&workspaces, workspace_type);
let workspace_name = get_workspace_name(workspace_type);
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(prev_workspace), Some(workspace)) = (prev_workspace, workspace) {
if prev_workspace.id != workspace.id {
tx.send(WorkspaceUpdate::Focus {
old: prev_workspace,
new: workspace,
})
.expect(ERR_CHANNEL_SEND);
}
if let (Some(workspace), false) = (workspace, focused) {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
} else {
error!("Unable to locate workspace");
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_workspace_moved_handler(move |event_data, _state| {
let _lock = lock!(lock);
let workspace_type = event_data.1;
debug!("Received workspace move: {workspace_type:?}");
let mut prev_workspace = lock!(active);
let workspace_name = get_workspace_name(workspace_type);
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 {
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
if !focused {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
}
});
}
{
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
let _lock = lock!(lock);
debug!("Received workspace destroy: {workspace_type:?}");
let name = get_workspace_name(workspace_type);
send!(tx, WorkspaceUpdate::Remove(name));
});
}
event_listener
.start_listener()
.expect("Failed to start listener");
});
}
fn refresh_workspaces(workspaces: &Mutex<Vec<Workspace>>) {
let mut workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
/// Sends a `WorkspaceUpdate::Focus` event
/// and updates the active workspace cache.
fn send_focus_change(
prev_workspace: &mut Option<Workspace>,
workspace: Workspace,
tx: &Sender<WorkspaceUpdate>,
) {
let old = prev_workspace
.as_ref()
.map(|w| w.name.clone())
.unwrap_or_default();
let active = HWorkspace::get_active().expect("Failed to get active workspace");
let new_workspaces = Workspaces::get()
send!(
tx,
WorkspaceUpdate::Focus {
old,
new: workspace.name.clone(),
}
);
prev_workspace.replace(workspace);
}
/// Gets a workspace by name from the server.
///
/// 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()
.expect("Failed to get workspaces")
.collect()
.into_iter()
.map(|workspace| Workspace::from((workspace.id == active.id, workspace)));
workspaces.clear();
workspaces.extend(new_workspaces);
.find_map(|w| {
if w.name == name {
Some(Workspace::from((focused, w)))
} else {
None
}
})
}
fn get_workspace(workspaces: &Mutex<Vec<Workspace>>, id: WorkspaceType) -> Option<Workspace> {
let id_string = id_to_string(id);
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
workspaces
.iter()
.find(|workspace| workspace.id == id_string)
.cloned()
}
fn get_focused_workspace(workspaces: &Mutex<Vec<Workspace>>) -> Option<Workspace> {
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
workspaces
.iter()
.find(|workspace| workspace.focused)
.cloned()
/// Gets the active workspace from the server.
fn get_active_workspace() -> Result<Workspace> {
let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
Ok(w)
}
}
impl WorkspaceClient for EventClient {
fn focus(&self, id: String) -> color_eyre::Result<()> {
fn focus(&self, id: String) -> Result<()> {
Dispatch::call(DispatchType::Workspace(
WorkspaceIdentifierWithSpecial::Name(&id),
))?;
@@ -205,13 +234,16 @@ impl WorkspaceClient for EventClient {
{
let tx = self.workspace_tx.clone();
let workspaces = self.workspaces.clone();
Self::refresh_workspaces(&workspaces);
let active_name = HWorkspace::get_active()
.map(|active| active.name)
.unwrap_or_default();
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.map(|w| Workspace::from((w.name == active_name, w)))
.collect();
tx.send(WorkspaceUpdate::Init(workspaces.clone()))
.expect(ERR_CHANNEL_SEND);
send!(tx, WorkspaceUpdate::Init(workspaces));
}
rx
@@ -230,10 +262,9 @@ pub fn get_client() -> &'static EventClient {
&CLIENT
}
fn id_to_string(id: WorkspaceType) -> String {
match id {
WorkspaceType::Unnamed(id) => id.to_string(),
WorkspaceType::Named(name) => name,
fn get_workspace_name(name: WorkspaceType) -> String {
match name {
WorkspaceType::Regular(name) => name,
WorkspaceType::Special(name) => name.unwrap_or_default(),
}
}
@@ -241,7 +272,7 @@ fn id_to_string(id: WorkspaceType) -> String {
impl From<(bool, hyprland::data::Workspace)> for Workspace {
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
Self {
id: id_to_string(workspace.id),
id: workspace.id.to_string(),
name: workspace.name,
monitor: workspace.monitor,
focused,

View File

@@ -1,13 +1,18 @@
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use std::fmt::{Display, Formatter};
use tokio::sync::broadcast;
use tracing::debug;
#[cfg(feature = "workspaces+hyprland")]
pub mod hyprland;
#[cfg(feature = "workspaces+sway")]
pub mod sway;
pub enum Compositor {
#[cfg(feature = "workspaces+sway")]
Sway,
#[cfg(feature = "workspaces+hyprland")]
Hyprland,
Unsupported,
}
@@ -18,7 +23,9 @@ impl Display for Compositor {
f,
"{}",
match self {
#[cfg(feature = "workspaces+sway")]
Self::Sway => "Sway",
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => "Hyprland",
Self::Unsupported => "Unsupported",
}
@@ -31,9 +38,15 @@ impl Compositor {
/// This is done by checking system env vars.
fn get_current() -> Self {
if std::env::var("SWAYSOCK").is_ok() {
Self::Sway
cfg_if! {
if #[cfg(feature = "workspaces+sway")] { Self::Sway }
else { tracing::error!("Not compiled with Sway support"); Self::Unsupported }
}
} else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
Self::Hyprland
cfg_if! {
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland}
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
}
} else {
Self::Unsupported
}
@@ -44,7 +57,9 @@ impl Compositor {
let current = Self::get_current();
debug!("Getting workspace client for: {current}");
match current {
#[cfg(feature = "workspaces+sway")]
Self::Sway => Ok(sway::get_sub_client()),
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => Ok(hyprland::get_client()),
Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently workspaces are only supported by Sway and Hyprland")),
@@ -70,13 +85,13 @@ pub enum WorkspaceUpdate {
/// This is re-sent to all subscribers when a new subscription is created.
Init(Vec<Workspace>),
Add(Workspace),
Remove(Workspace),
Remove(String),
Update(Workspace),
Move(Workspace),
/// Declares focus moved from the old workspace to the new.
Focus {
old: Workspace,
new: Workspace,
old: String,
new: String,
},
}

View File

@@ -1,6 +1,5 @@
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::await_sync;
use crate::error::ERR_CHANNEL_SEND;
use crate::{await_sync, send};
use async_once::AsyncOnce;
use color_eyre::Report;
use futures_util::StreamExt;
@@ -75,7 +74,7 @@ impl WorkspaceClient for SwayEventClient {
let event =
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
tx.send(event).expect(ERR_CHANNEL_SEND);
send!(tx, event);
});
}
@@ -132,12 +131,24 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
WorkspaceChange::Init => {
Self::Add(event.current.expect("Missing current workspace").into())
}
WorkspaceChange::Empty => {
Self::Remove(event.current.expect("Missing current workspace").into())
}
WorkspaceChange::Empty => Self::Remove(
event
.current
.expect("Missing current workspace")
.name
.unwrap_or_default(),
),
WorkspaceChange::Focus => Self::Focus {
old: event.old.expect("Missing old workspace").into(),
new: event.current.expect("Missing current workspace").into(),
old: event
.old
.expect("Missing old workspace")
.name
.unwrap_or_default(),
new: event
.current
.expect("Missing current workspace")
.name
.unwrap_or_default(),
},
WorkspaceChange::Move => {
Self::Move(event.current.expect("Missing current workspace").into())

View File

@@ -1,4 +1,7 @@
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "tray")]
pub mod system_tray;
pub mod wayland;

View File

@@ -4,7 +4,9 @@ use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
#[cfg(feature = "music+mpd")]
pub mod mpd;
#[cfg(feature = "music+mpris")]
pub mod mpris;
#[derive(Clone, Debug)]
@@ -22,7 +24,7 @@ pub struct Track {
pub disc: Option<u64>,
pub genre: Option<String>,
pub track: Option<u64>,
pub cover_path: Option<PathBuf>,
pub cover_path: Option<String>,
}
#[derive(Clone, Debug)]

View File

@@ -121,12 +121,16 @@ impl MpdClient {
fn convert_song(song: &Song, music_dir: &Path) -> Track {
let (track, disc) = song.number();
let cover_path = music_dir.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
let cover_path = music_dir
.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
)
.into_os_string()
.into_string()
.ok();
Track {
title: song.title().map(std::string::ToString::to_string),
@@ -136,7 +140,7 @@ impl MpdClient {
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
disc: Some(disc),
track: Some(track),
cover_path: Some(cover_path),
cover_path,
}
}
}

View File

@@ -1,11 +1,10 @@
use super::{MusicClient, PlayerUpdate, Status, Track};
use crate::clients::music::PlayerState;
use crate::error::ERR_MUTEX_LOCK;
use crate::{lock, send};
use color_eyre::Result;
use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
@@ -44,7 +43,7 @@ impl Client {
.find_all()
.expect("Failed to connect to D-Bus");
let mut players_list_val = players_list.lock().expect(ERR_MUTEX_LOCK);
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
@@ -57,8 +56,7 @@ impl Client {
.expect("Failed to connect to D-Bus");
{
let mut current_player =
current_player.lock().expect(ERR_MUTEX_LOCK);
let mut current_player = lock!(current_player);
if status == PlaybackStatus::Playing || current_player.is_none() {
debug!("Setting active player to '{identity}'");
@@ -108,22 +106,19 @@ impl Client {
trace!("Received player event from '{identity}': {event:?}");
match event {
Ok(Event::PlayerShutDown) => {
current_player.lock().expect(ERR_MUTEX_LOCK).take();
players.lock().expect(ERR_MUTEX_LOCK).remove(identity);
lock!(current_player).take();
lock!(players).remove(identity);
break;
}
Ok(Event::Playing) => {
current_player
.lock()
.expect(ERR_MUTEX_LOCK)
.replace(identity.to_string());
lock!(current_player).replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
Ok(_) => {
let current_player = current_player.lock().expect(ERR_MUTEX_LOCK);
let current_player = lock!(current_player);
let current_player = current_player.as_ref();
if let Some(current_player) = current_player {
if current_player == identity {
@@ -171,15 +166,13 @@ impl Client {
let track = Track::from(metadata);
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
tx.send(player_update)
.expect("Failed to send player update");
send!(tx, player_update);
Ok(())
}
fn get_player(&self) -> Option<Player> {
let player_name = self.current_player.lock().expect(ERR_MUTEX_LOCK);
let player_name = lock!(self.current_player);
let player_name = player_name.as_ref();
player_name.and_then(|player_name| {
@@ -266,10 +259,7 @@ impl From<Metadata> for Track {
.and_then(mpris::MetadataValue::as_str_array)
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
track: value.track_number().map(|track| track as u64),
cover_path: value
.art_url()
.map(|path| path.replace("file://", ""))
.map(PathBuf::from),
cover_path: value.art_url().map(|s| s.to_string()),
}
}
}

View File

@@ -1,15 +1,25 @@
use crate::{lock, send};
use async_once::AsyncOnce;
use color_eyre::Report;
use lazy_static::lazy_static;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use stray::message::menu::TrayMenu;
use stray::message::tray::StatusNotifierItem;
use stray::message::{NotifierItemCommand, NotifierItemMessage};
use stray::StatusNotifierWatcher;
use tokio::spawn;
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use tracing::error;
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
pub struct TrayEventReceiver {
tx: mpsc::Sender<NotifierItemCommand>,
b_tx: broadcast::Sender<NotifierItemMessage>,
_b_rx: broadcast::Receiver<NotifierItemMessage>,
tray: Arc<Mutex<Tray>>,
}
impl TrayEventReceiver {
@@ -20,19 +30,39 @@ impl TrayEventReceiver {
let tray = StatusNotifierWatcher::new(rx).await?;
let mut host = tray.create_notifier_host("ironbar").await?;
let b_tx2 = b_tx.clone();
spawn(async move {
while let Ok(message) = host.recv().await {
b_tx2.send(message)?;
}
let tray = Arc::new(Mutex::new(BTreeMap::new()));
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
});
{
let b_tx = b_tx.clone();
let tray = tray.clone();
spawn(async move {
while let Ok(message) = host.recv().await {
send!(b_tx, message.clone());
let mut tray = lock!(tray);
match message {
NotifierItemMessage::Update {
address,
item,
menu,
} => {
tray.insert(address, (item, menu));
}
NotifierItemMessage::Remove { address } => {
tray.remove(&address);
}
}
}
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
});
}
Ok(Self {
tx,
b_tx,
_b_rx: b_rx,
tray,
})
}
@@ -42,7 +72,20 @@ impl TrayEventReceiver {
mpsc::Sender<NotifierItemCommand>,
broadcast::Receiver<NotifierItemMessage>,
) {
(self.tx.clone(), self.b_tx.subscribe())
let tx = self.tx.clone();
let b_rx = self.b_tx.subscribe();
let tray = lock!(self.tray).clone();
for (address, (item, menu)) in tray {
let update = NotifierItemMessage::Update {
address,
item,
menu,
};
send!(self.b_tx, update);
}
(tx, b_rx)
}
}
@@ -58,11 +101,14 @@ lazy_static! {
let tray = TrayEventReceiver::new().await;
if tray.is_ok() || retries == MAX_RETRIES {
break tray;
match tray {
Ok(tray) => break Some(tray),
Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})")))
}
debug!("Failed to create StatusNotifierWatcher (attempt {retries})");
if retries == MAX_RETRIES {
break None;
}
};
value.expect("Failed to create StatusNotifierWatcher")

View File

@@ -123,6 +123,9 @@ impl Config {
/// and parses it into `Self` based on its extension.
fn load_file(path: &Path) -> Result<Self> {
let file = fs::read(path).wrap_err("Failed to read config file")?;
let str = std::str::from_utf8(&file)?;
let extension = path
.extension()
.unwrap_or_default()
@@ -130,11 +133,16 @@ impl Config {
.unwrap_or_default();
match extension {
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
"corn" => libcorn::from_slice(&file).wrap_err("Invalid Corn config"),
_ => unreachable!(),
#[cfg(feature = "config+json")]
"json" => serde_json::from_str(str).wrap_err("Invalid JSON config"),
#[cfg(feature = "config+toml")]
"toml" => toml::from_str(str).wrap_err("Invalid TOML config"),
#[cfg(feature = "config+yaml")]
"yaml" | "yml" => serde_yaml::from_str(str).wrap_err("Invalid YAML config"),
#[cfg(feature = "config+corn")]
"corn" => libcorn::from_str(str).wrap_err("Invalid Corn config"),
_ => Err(Report::msg(format!("Unsupported config type: {extension}"))
.note("You may need to recompile with support if available")),
}
}
}

View File

@@ -1,18 +1,26 @@
mod r#impl;
mod truncate;
#[cfg(feature = "clock")]
use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule;
use crate::modules::focused::FocusedModule;
use crate::modules::launcher::LauncherModule;
#[cfg(feature = "music")]
use crate::modules::music::MusicModule;
use crate::modules::script::ScriptModule;
#[cfg(feature = "sys_info")]
use crate::modules::sysinfo::SysInfoModule;
#[cfg(feature = "tray")]
use crate::modules::tray::TrayModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use crate::script::ScriptInput;
use serde::Deserialize;
use std::collections::HashMap;
pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)]
pub struct CommonConfig {
pub show_if: Option<ScriptInput>,
@@ -29,15 +37,20 @@ pub struct CommonConfig {
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "clock")]
Clock(ClockModule),
Music(MusicModule),
Tray(TrayModule),
Workspaces(WorkspacesModule),
SysInfo(SysInfoModule),
Launcher(LauncherModule),
Script(ScriptModule),
Focused(FocusedModule),
Custom(CustomModule),
Focused(FocusedModule),
Launcher(LauncherModule),
#[cfg(feature = "music")]
Music(MusicModule),
Script(ScriptModule),
#[cfg(feature = "sys_info")]
SysInfo(SysInfoModule),
#[cfg(feature = "tray")]
Tray(TrayModule),
#[cfg(feature = "workspaces")]
Workspaces(WorkspacesModule),
}
#[derive(Debug, Clone)]
@@ -70,6 +83,9 @@ pub struct Config {
#[serde(default = "default_bar_height")]
pub height: i32,
/// GTK icon theme to use.
pub icon_theme: Option<String>,
pub start: Option<Vec<ModuleConfig>>,
pub center: Option<Vec<ModuleConfig>>,
pub end: Option<Vec<ModuleConfig>>,

54
src/config/truncate.rs Normal file
View File

@@ -0,0 +1,54 @@
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
use gtk::prelude::*;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum EllipsizeMode {
Start,
Middle,
End,
}
impl From<EllipsizeMode> for GtkEllipsizeMode {
fn from(value: EllipsizeMode) -> Self {
match value {
EllipsizeMode::Start => Self::Start,
EllipsizeMode::Middle => Self::Middle,
EllipsizeMode::End => Self::End,
}
}
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(untagged)]
pub enum TruncateMode {
Auto(EllipsizeMode),
MaxLength {
mode: EllipsizeMode,
length: Option<i32>,
},
}
impl TruncateMode {
const fn mode(&self) -> EllipsizeMode {
match self {
Self::MaxLength { mode, .. } | Self::Auto(mode) => *mode,
}
}
const fn length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::MaxLength { length, .. } => *length,
}
}
pub fn truncate_label(&self, label: &gtk::Label) {
label.set_ellipsize(self.mode().into());
if let Some(max_length) = self.length() {
label.set_max_width_chars(max_length);
}
}
}

View File

@@ -1,6 +1,3 @@
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::collections::HashMap;
use std::fs::File;
use std::io;
@@ -67,7 +64,7 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
}
/// Attempts to get the icon name from the app's `.desktop` file.
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
find_desktop_file(app_id).and_then(|file| {
let map = parse_desktop_file(file);
map.map_or(None, |map| {
@@ -75,65 +72,3 @@ fn get_desktop_icon_name(app_id: &str) -> Option<String> {
})
})
}
enum IconLocation {
Theme(String),
File(PathBuf),
}
/// Attempts to get the location of an icon.
///
/// Handles icons that are part of a GTK theme, icons specified as path
/// and icons for steam games.
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
let has_icon = theme
.lookup_icon(app_id, size, IconLookupFlags::empty())
.is_some();
if has_icon {
return Some(IconLocation::Theme(app_id.to_string()));
}
let is_steam_game = app_id.starts_with("steam_app_");
if is_steam_game {
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
return match dirs::data_dir() {
Some(dir) => {
let path = dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
));
return Some(IconLocation::File(path));
}
None => None,
};
}
let icon_name = get_desktop_icon_name(app_id);
if let Some(icon_name) = icon_name {
let is_path = PathBuf::from(&icon_name).exists();
return if is_path {
Some(IconLocation::File(PathBuf::from(icon_name)))
} else {
return Some(IconLocation::Theme(icon_name));
};
}
None
}
/// Gets the icon associated with an app.
pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
let icon_location = get_icon_location(theme, app_id, size);
match icon_location {
Some(IconLocation::Theme(icon_name)) => {
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
icon.map_or(None, |icon| icon)
}
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
None => None,
}
}

50
src/image/gtk.rs Normal file
View File

@@ -0,0 +1,50 @@
use super::ImageProvider;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error;
#[cfg(any(feature = "music", feature = "workspaces"))]
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
let button = Button::new();
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone()))
{
Ok(_) => {
button.set_image(Some(&image));
button.set_always_show_image(true);
}
Err(err) => {
error!("{err:?}");
button.set_label(input);
}
}
} else {
button.set_label(input);
}
button
}
#[cfg(feature = "music")]
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
let container = gtk::Box::new(Orientation::Horizontal, 0);
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
container.add(&image);
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image))
{
error!("{err:?}");
}
} else {
let label = Label::new(Some(input));
container.add(&label);
}
container
}

7
src/image/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
#[cfg(any(feature = "music", feature = "workspaces"))]
mod gtk;
mod provider;
#[cfg(any(feature = "music", feature = "workspaces"))]
pub use self::gtk::*;
pub use provider::ImageProvider;

199
src/image/provider.rs Normal file
View File

@@ -0,0 +1,199 @@
use crate::desktop_file::get_desktop_icon_name;
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf};
cfg_if!(
if #[cfg(feature = "http")] {
use crate::send;
use gtk::gio::{Cancellable, MemoryInputStream};
use tokio::spawn;
use tracing::error;
}
);
#[derive(Debug)]
enum ImageLocation<'a> {
Icon {
name: String,
theme: &'a IconTheme,
},
Local(PathBuf),
Steam(String),
#[cfg(feature = "http")]
Remote(reqwest::Url),
}
pub struct ImageProvider<'a> {
location: ImageLocation<'a>,
size: i32,
}
impl<'a> ImageProvider<'a> {
/// Attempts to parse the image input to find its location.
/// Errors if no valid location type can be found.
///
/// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
let location = Self::get_location(input, theme, size)?;
Ok(Self { location, size })
}
/// Returns true if the input starts with a prefix
/// that is supported by the parser
/// (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 {
input.starts_with("icon:")
|| input.starts_with("file://")
|| input.starts_with("http://")
|| input.starts_with("https://")
}
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
let (input_type, input_name) = input
.split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n));
match input_type {
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
name: input_name.to_string(),
theme,
}),
Some(input_type) if input_type == "file" => Ok(ImageLocation::Local(PathBuf::from(
input_name[2..].to_string(),
))),
#[cfg(feature = "http")]
Some(input_type) if input_type == "http" || input_type == "https" => {
Ok(ImageLocation::Remote(input.parse()?))
}
None if input.starts_with("steam_app_") => Ok(ImageLocation::Steam(
input_name.chars().skip("steam_app_".len()).collect(),
)),
None if theme
.lookup_icon(input, size, IconLookupFlags::empty())
.is_some() =>
{
Ok(ImageLocation::Icon {
name: input_name.to_string(),
theme,
})
}
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available")),
None if PathBuf::from(input_name).is_file() => {
Ok(ImageLocation::Local(PathBuf::from(input_name)))
}
None => get_desktop_icon_name(input_name).map_or_else(
|| Err(Report::msg("Unknown image type")),
|input| Self::get_location(&input, theme, size),
),
}
}
/// Attempts to fetch the image from the location
/// and load it into the provided `GTK::Image` widget.
pub fn load_into_image(&self, image: gtk::Image) -> Result<()> {
// handle remote locations async to avoid blocking UI thread while downloading
#[cfg(feature = "http")]
if let ImageLocation::Remote(url) = &self.location {
let url = url.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
let bytes = Self::get_bytes_from_http(url).await;
if let Ok(bytes) = bytes {
send!(tx, bytes);
}
});
{
let size = self.size;
rx.attach(None, move |bytes| {
let stream = MemoryInputStream::from_bytes(&bytes);
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
size,
size,
true,
Some(&Cancellable::new()),
);
match pixbuf {
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
Err(err) => error!("{err:?}"),
}
Continue(false)
});
}
} else {
self.load_into_image_sync(image)?;
};
#[cfg(not(feature = "http"))]
self.load_into_image_sync(image)?;
Ok(())
}
fn load_into_image_sync(&self, image: gtk::Image) -> Result<()> {
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
ImageLocation::Local(path) => self.get_from_file(path),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
#[cfg(feature = "http")]
_ => unreachable!(), // handled above
}?;
image.set_pixbuf(Some(&pixbuf));
Ok(())
}
/// Attempts to get a `Pixbuf` from the GTK icon theme.
fn get_from_icon(&self, name: &str, theme: &IconTheme) -> Result<Pixbuf> {
let pixbuf = match theme.lookup_icon(name, self.size, IconLookupFlags::empty()) {
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
None => Ok(None),
}?;
pixbuf.map_or_else(
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
Ok,
)
}
/// Attempts to get a `Pixbuf` from a local file.
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
Ok(pixbuf)
}
/// Attempts to get a `Pixbuf` from a local file,
/// using the Steam game ID to look it up.
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
let path = dirs::data_dir().map_or_else(
|| Err(Report::msg("Missing XDG data dir")),
|dir| {
Ok(dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
)))
},
)?;
self.get_from_file(&path)
}
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
#[cfg(feature = "http")]
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
let bytes = reqwest::get(url).await?.bytes().await?;
Ok(glib::Bytes::from_owned(bytes))
}
}

View File

@@ -2,9 +2,10 @@ mod bar;
mod bridge_channel;
mod clients;
mod config;
mod desktop_file;
mod dynamic_string;
mod error;
mod icon;
mod image;
mod logging;
mod macros;
mod modules;

View File

@@ -82,7 +82,7 @@ impl Module<Button> for ClockModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: button,
@@ -94,6 +94,7 @@ impl Module<Button> for ClockModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
@@ -119,6 +120,8 @@ impl Module<Button> for ClockModule {
});
}
container.show_all();
Some(container)
}
}

View File

@@ -1,12 +1,13 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::{ButtonGeometry, Popup};
use crate::script::Script;
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, Label, Orientation};
use gtk::{Button, IconTheme, Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
@@ -46,6 +47,8 @@ pub struct Widget {
class: Option<String>,
on_click: Option<String>,
orientation: Option<String>,
src: Option<String>,
size: Option<i32>,
}
/// Supported GTK widget types
@@ -55,20 +58,33 @@ pub enum WidgetType {
Box,
Label,
Button,
Image,
}
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
fn add_to(
self,
parent: &gtk::Box,
tx: Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) {
match self.widget_type {
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)),
WidgetType::Label => parent.add(&self.into_label()),
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
WidgetType::Image => parent.add(&self.into_image(icon_theme)),
}
}
/// Creates a `gtk::Box` from this widget
fn into_box(self, tx: &Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
fn into_box(
self,
tx: &Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) -> gtk::Box {
let mut builder = gtk::Box::builder();
if let Some(name) = self.name {
@@ -87,9 +103,9 @@ impl Widget {
}
if let Some(widgets) = self.widgets {
widgets
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation));
for widget in widgets {
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
}
}
container
@@ -157,6 +173,31 @@ impl Widget {
button
}
fn into_image(self, icon_theme: &IconTheme) -> gtk::Image {
let mut builder = gtk::Image::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
let gtk_image = builder.build();
if let Some(src) = self.src {
let size = self.size.unwrap_or(32);
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
.and_then(|image| image.load_into_image(gtk_image.clone()))
{
error!("{err:?}");
}
}
if let Some(class) = self.class {
gtk_image.style_context().add_class(&class);
}
gtk_image
}
}
#[derive(Debug)]
@@ -217,10 +258,15 @@ impl Module<gtk::Box> for CustomModule {
}
self.bar.clone().into_iter().for_each(|widget| {
widget.add_to(&container, context.controller_tx.clone(), orientation);
widget.add_to(
&container,
context.controller_tx.clone(),
orientation,
info.icon_theme,
);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
@@ -232,6 +278,7 @@ impl Module<gtk::Box> for CustomModule {
self,
tx: Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
@@ -245,11 +292,18 @@ impl Module<gtk::Box> for CustomModule {
}
if let Some(popup) = self.popup {
popup
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
for widget in popup {
widget.add_to(
&container,
tx.clone(),
Orientation::Horizontal,
info.icon_theme,
);
}
}
container.show_all();
Some(container)
}
}

View File

@@ -1,14 +1,16 @@
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig;
use crate::config::{CommonConfig, TruncateMode};
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, icon, read_lock, send_async};
use crate::{await_sync, read_lock, send_async};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{IconTheme, Image, Label};
use gtk::Label;
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
@@ -22,8 +24,8 @@ pub struct FocusedModule {
/// Icon size in pixels.
#[serde(default = "default_icon_size")]
icon_size: i32,
/// GTK icon theme to use.
icon_theme: Option<String>,
truncate: Option<TruncateMode>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
@@ -91,26 +93,29 @@ impl Module<gtk::Box> for FocusedModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme));
}
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = Image::builder().name("icon").build();
let icon = gtk::Image::builder().name("icon").build();
let label = Label::builder().name("label").build();
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
container.add(&icon);
container.add(&label);
{
let icon_theme = icon_theme.clone();
context.widget_rx.attach(None, move |(name, id)| {
let pixbuf = icon::get_icon(&icon_theme, &id, self.icon_size);
if self.show_icon {
icon.set_pixbuf(pixbuf.as_ref());
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
.and_then(|image| image.load_into_image(icon.clone()))
{
error!("{err:?}");
}
}
if self.show_title {

View File

@@ -1,16 +1,17 @@
use super::open_state::OpenState;
use crate::clients::wayland::ToplevelInfo;
use crate::icon::get_icon;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Orientation};
use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap;
use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
use tracing::error;
#[derive(Debug, Clone)]
pub struct Item {
@@ -151,16 +152,24 @@ impl ItemButton {
button = button.label(&item.name);
}
if show_icons {
let icon = get_icon(icon_theme, &item.app_id, 32);
if icon.is_some() {
let image = Image::from_pixbuf(icon.as_ref());
button = button.image(&image).always_show_image(true);
}
}
let button = button.build();
if show_icons {
let gtk_image = gtk::Image::new();
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32);
match image {
Ok(image) => {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
if let Err(err) = image.load_into_image(gtk_image) {
error!("{err:?}");
}
}
Err(err) => error!("{err:?}"),
};
}
let style_context = button.style_context();
style_context.add_class("item");

View File

@@ -5,13 +5,13 @@ use self::item::{Item, ItemButton, Window};
use self::open_state::OpenState;
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig;
use crate::icon::find_desktop_file;
use crate::desktop_file::find_desktop_file;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, read_lock, try_send, write_lock};
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use gtk::{Button, Orientation};
use indexmap::IndexMap;
use serde::Deserialize;
use std::process::{Command, Stdio};
@@ -33,9 +33,6 @@ pub struct LauncherModule {
#[serde(default = "crate::config::default_true")]
show_icons: bool,
/// Name of the GTK icon theme to use.
icon_theme: Option<String>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@@ -297,8 +294,6 @@ impl Module<gtk::Box> for LauncherModule {
}
}
}
Ok::<(), swayipc_async::Error>(())
});
Ok(())
@@ -309,15 +304,15 @@ impl Module<gtk::Box> for LauncherModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> crate::Result<ModuleWidget<gtk::Box>> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
{
let container = container.clone();
let icon_theme = icon_theme.clone();
let controller_tx = context.controller_tx.clone();
let show_names = self.show_names;
let show_icons = self.show_icons;
@@ -325,7 +320,6 @@ impl Module<gtk::Box> for LauncherModule {
let mut buttons = IndexMap::<String, ItemButton>::new();
let controller_tx2 = context.controller_tx.clone();
context.widget_rx.attach(None, move |event| {
match event {
LauncherUpdate::AddItem(item) => {
@@ -341,7 +335,7 @@ impl Module<gtk::Box> for LauncherModule {
orientation,
&icon_theme,
&context.tx,
&controller_tx2,
&controller_tx,
);
container.add(&button.button);
@@ -400,7 +394,7 @@ impl Module<gtk::Box> for LauncherModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
popup,
@@ -411,6 +405,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
controller_tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250;

View File

@@ -4,14 +4,19 @@
///
/// Clicking the widget opens a popup containing the current time
/// with second-level precision and a calendar.
#[cfg(feature = "clock")]
pub mod clock;
pub mod custom;
pub mod focused;
pub mod launcher;
#[cfg(feature = "music")]
pub mod music;
pub mod script;
#[cfg(feature = "sys_info")]
pub mod sysinfo;
#[cfg(feature = "tray")]
pub mod tray;
#[cfg(feature = "workspaces")]
pub mod workspaces;
use crate::config::BarPosition;
@@ -19,7 +24,7 @@ use crate::popup::ButtonGeometry;
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::Monitor;
use gtk::{Application, Widget};
use gtk::{Application, IconTheme, Widget};
use tokio::sync::mpsc;
#[derive(Clone)]
@@ -34,6 +39,7 @@ pub struct ModuleInfo<'a> {
pub bar_position: BarPosition,
pub monitor: &'a Monitor,
pub output_name: &'a str,
pub icon_theme: &'a IconTheme,
}
#[derive(Debug)]
@@ -88,6 +94,7 @@ where
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,

140
src/modules/music/config.rs Normal file
View File

@@ -0,0 +1,140 @@
use crate::config::{CommonConfig, TruncateMode};
use dirs::{audio_dir, home_dir};
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Clone)]
pub struct Icons {
/// Icon to display when playing.
#[serde(default = "default_icon_play")]
pub(crate) play: String,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")]
pub(crate) pause: String,
/// Icon to display for previous button.
#[serde(default = "default_icon_prev")]
pub(crate) prev: String,
/// Icon to display for next button.
#[serde(default = "default_icon_next")]
pub(crate) next: String,
/// Icon to display under volume slider
#[serde(default = "default_icon_volume")]
pub(crate) volume: String,
/// Icon to display nex to track title
#[serde(default = "default_icon_track")]
pub(crate) track: String,
/// Icon to display nex to album name
#[serde(default = "default_icon_album")]
pub(crate) album: String,
/// Icon to display nex to artist name
#[serde(default = "default_icon_artist")]
pub(crate) artist: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
pause: default_icon_pause(),
play: default_icon_play(),
prev: default_icon_prev(),
next: default_icon_next(),
volume: default_icon_volume(),
track: default_icon_track(),
album: default_icon_album(),
artist: default_icon_artist(),
}
}
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum PlayerType {
Mpd,
Mpris,
}
impl Default for PlayerType {
fn default() -> Self {
Self::Mpris
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct MusicModule {
/// Type of player to connect to
#[serde(default)]
pub(crate) player_type: PlayerType,
/// Format of current song info to display on the bar.
#[serde(default = "default_format")]
pub(crate) format: String,
/// Player state icons
#[serde(default)]
pub(crate) icons: Icons,
// -- MPD --
/// TCP or Unix socket address.
#[serde(default = "default_socket")]
pub(crate) host: String,
/// Path to root of music directory.
#[serde(default = "default_music_dir")]
pub(crate) music_dir: PathBuf,
// -- Common --
pub(crate) truncate: Option<TruncateMode>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_socket() -> String {
String::from("localhost:6600")
}
fn default_format() -> String {
String::from("{title} / {artist}")
}
fn default_icon_play() -> String {
String::from("")
}
fn default_icon_pause() -> String {
String::from("")
}
fn default_icon_prev() -> String {
String::from("\u{f9ad}")
}
fn default_icon_next() -> String {
String::from("\u{f9ac}")
}
fn default_icon_volume() -> String {
String::from("")
}
fn default_icon_track() -> String {
String::from("\u{f886}")
}
fn default_icon_album() -> String {
String::from("\u{f524}")
}
fn default_icon_artist() -> String {
String::from("\u{fd01}")
}
fn default_music_dir() -> PathBuf {
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
}

View File

@@ -1,17 +1,15 @@
mod config;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::config::CommonConfig;
use crate::error::ERR_CHANNEL_SEND;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::try_send;
use crate::{send_async, try_send};
use color_eyre::Result;
use dirs::{audio_dir, home_dir};
use glib::Continue;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{Button, Image, Label, Orientation, Scale};
use gtk::{Button, IconTheme, Label, Orientation, Scale};
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -19,6 +17,9 @@ use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
pub use self::config::MusicModule;
use self::config::PlayerType;
#[derive(Debug)]
pub enum PlayerCommand {
Previous,
@@ -28,93 +29,6 @@ pub enum PlayerCommand {
Volume(u8),
}
#[derive(Debug, Deserialize, Clone)]
pub struct Icons {
/// Icon to display when playing.
#[serde(default = "default_icon_play")]
play: String,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")]
pause: String,
/// Icon to display under volume slider
#[serde(default = "default_icon_volume")]
volume: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
pause: default_icon_pause(),
play: default_icon_play(),
volume: default_icon_volume(),
}
}
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum PlayerType {
// Auto,
Mpd,
Mpris,
}
impl Default for PlayerType {
fn default() -> Self {
Self::Mpris
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct MusicModule {
/// Type of player to connect to
#[serde(default)]
player_type: PlayerType,
/// Format of current song info to display on the bar.
#[serde(default = "default_format")]
format: String,
/// Player state icons
#[serde(default)]
icons: Icons,
// -- MPD --
/// TCP or Unix socket address.
#[serde(default = "default_socket")]
host: String,
/// Path to root of music directory.
#[serde(default = "default_music_dir")]
music_dir: PathBuf,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_socket() -> String {
String::from("localhost:6600")
}
fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
fn default_icon_play() -> String {
String::from("")
}
fn default_icon_pause() -> String {
String::from("")
}
fn default_icon_volume() -> String {
String::from("")
}
fn default_music_dir() -> PathBuf {
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
}
/// Formats a duration given in seconds
/// in hh:mm format
fn format_time(duration: Duration) -> String {
@@ -166,7 +80,6 @@ impl Module<Button> for MusicModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let format = self.format.clone();
let icons = self.icons.clone();
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
@@ -188,13 +101,8 @@ impl Module<Button> for MusicModule {
match update {
PlayerUpdate::Update(track, status) => match *track {
Some(track) => {
let display_string = replace_tokens(
format.as_str(),
&tokens,
&track,
&status,
&icons,
);
let display_string =
replace_tokens(format.as_str(), &tokens, &track, &status);
let update = SongUpdate {
song: track,
@@ -202,14 +110,9 @@ impl Module<Button> for MusicModule {
display_string,
};
tx.send(ModuleUpdateEvent::Update(Some(update)))
.await
.expect(ERR_CHANNEL_SEND);
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
}
None => tx
.send(ModuleUpdateEvent::Update(None))
.await
.expect(ERR_CHANNEL_SEND),
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
},
PlayerUpdate::Disconnect => break,
}
@@ -251,9 +154,22 @@ impl Module<Button> for MusicModule {
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
button.add(&button_contents);
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, 24);
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, 24);
let label = Label::new(None);
label.set_angle(info.bar_position.get_angle());
button.add(&label);
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
button_contents.add(&icon_pause);
button_contents.add(&icon_play);
button_contents.add(&label);
let orientation = info.bar_position.get_orientation();
@@ -275,6 +191,21 @@ impl Module<Button> for MusicModule {
context.widget_rx.attach(None, move |mut event| {
if let Some(event) = event.take() {
label.set_label(&event.display_string);
match event.status.state {
PlayerState::Playing => {
icon_play.show();
icon_pause.hide();
}
PlayerState::Paused => {
icon_pause.show();
icon_play.hide();
}
PlayerState::Stopped => {
button.hide();
}
}
button.show();
} else {
button.hide();
@@ -285,7 +216,7 @@ impl Module<Button> for MusicModule {
});
};
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: button,
@@ -297,23 +228,28 @@ impl Module<Button> for MusicModule {
self,
tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;
let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-music")
.build();
let album_image = Image::builder()
let album_image = gtk::Image::builder()
.width_request(128)
.height_request(128)
.name("album-art")
.build();
let icons = self.icons;
let info_box = gtk::Box::new(Orientation::Vertical, 10);
let title_label = IconLabel::new("\u{f886}", None);
let album_label = IconLabel::new("\u{f524}", None);
let artist_label = IconLabel::new("\u{fd01}", None);
let title_label = IconLabel::new(&icons.track, None, icon_theme);
let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
title_label.container.set_widget_name("title");
album_label.container.set_widget_name("album");
@@ -324,12 +260,22 @@ impl Module<Button> for MusicModule {
info_box.add(&artist_label.container);
let controls_box = gtk::Box::builder().name("controls").build();
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
let btn_prev = new_icon_button(&icons.prev, icon_theme, 24);
btn_prev.set_widget_name("btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, 24);
btn_play.set_widget_name("btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, 24);
btn_pause.set_widget_name("btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, 24);
btn_next.set_widget_name("btn-next");
controls_box.add(&btn_prev);
controls_box.add(&btn_play_pause);
controls_box.add(&btn_play);
controls_box.add(&btn_pause);
controls_box.add(&btn_next);
info_box.add(&controls_box);
@@ -344,7 +290,7 @@ impl Module<Button> for MusicModule {
volume_slider.set_inverted(true);
volume_slider.set_widget_name("slider");
let volume_icon = Label::new(Some(&self.icons.volume));
let volume_icon = new_icon_label(&icons.volume, icon_theme, 24);
volume_icon.style_context().add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
@@ -359,13 +305,14 @@ impl Module<Button> for MusicModule {
try_send!(tx_prev, PlayerCommand::Previous);
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |button| {
if button.style_context().has_class("playing") {
try_send!(tx_toggle, PlayerCommand::Pause);
} else {
try_send!(tx_toggle, PlayerCommand::Play);
}
let tx_play = tx.clone();
btn_play.connect_clicked(move |_| {
try_send!(tx_play, PlayerCommand::Play);
});
let tx_pause = tx.clone();
btn_pause.connect_clicked(move |_| {
try_send!(tx_pause, PlayerCommand::Pause);
});
let tx_next = tx.clone();
@@ -382,6 +329,8 @@ impl Module<Button> for MusicModule {
container.show_all();
{
let icon_theme = icon_theme.clone();
let mut prev_cover = None;
rx.attach(None, move |update| {
if let Some(update) = update {
@@ -389,16 +338,22 @@ impl Module<Button> for MusicModule {
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
match new_cover.map(|cover_path| {
Pixbuf::from_file_at_scale(cover_path, 128, 128, true)
}) {
Some(Ok(pixbuf)) => album_image.set_from_pixbuf(Some(&pixbuf)),
let res = match new_cover
.map(|cover_path| ImageProvider::parse(&cover_path, &icon_theme, 128))
{
Some(Ok(image)) => image.load_into_image(album_image.clone()),
Some(Err(err)) => {
error!("{:?}", err);
album_image.set_from_pixbuf(None);
Err(err)
}
None => {
album_image.set_from_pixbuf(None);
Ok(())
}
None => album_image.set_from_pixbuf(None),
};
if let Err(err) = res {
error!("{err:?}");
}
}
title_label
@@ -413,23 +368,23 @@ impl Module<Button> for MusicModule {
match update.status.state {
PlayerState::Stopped => {
btn_play_pause.set_sensitive(false);
btn_pause.hide();
btn_play.show();
btn_play.set_sensitive(false);
}
PlayerState::Playing => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label(&self.icons.pause);
btn_play.set_sensitive(false);
btn_play.hide();
let style_context = btn_play_pause.style_context();
style_context.add_class("playing");
style_context.remove_class("paused");
btn_pause.set_sensitive(true);
btn_pause.show();
}
PlayerState::Paused => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label(&self.icons.play);
btn_pause.set_sensitive(false);
btn_pause.hide();
let style_context = btn_play_pause.style_context();
style_context.add_class("paused");
style_context.remove_class("playing");
btn_play.set_sensitive(true);
btn_play.show();
}
}
@@ -459,11 +414,10 @@ fn replace_tokens(
tokens: &Vec<String>,
song: &Track,
status: &Status,
icons: &Icons,
) -> String {
let mut compiled_string = format_string.to_string();
for token in tokens {
let value = get_token_value(song, status, icons, token);
let value = get_token_value(song, status, token);
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
}
compiled_string
@@ -471,14 +425,8 @@ fn replace_tokens(
/// Converts a string format token value
/// into its respective value.
fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) -> String {
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
match token {
"icon" => match status.state {
PlayerState::Stopped => None,
PlayerState::Playing => Some(&icons.play),
PlayerState::Paused => Some(&icons.pause),
}
.map(std::string::ToString::to_string),
"title" => song.title.clone(),
"album" => song.album.clone(),
"artist" => song.artist.clone(),
@@ -493,17 +441,17 @@ fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) ->
.unwrap_or_default()
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct IconLabel {
label: Label,
container: gtk::Box,
}
impl IconLabel {
fn new(icon: &str, label: Option<&str>) -> Self {
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = Label::new(Some(icon));
let icon = new_icon_label(icon_input, icon_theme, 32);
let label = Label::new(label);
icon.style_context().add_class("icon");

View File

@@ -1,6 +1,7 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::try_send;
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
@@ -63,8 +64,8 @@ impl Module<Label> for ScriptModule {
spawn(async move {
script.run(move |(out, _)| match out {
OutputStream::Stdout(stdout) => {
tx.try_send(ModuleUpdateEvent::Update(stdout))
.expect("Failed to send stdout"); }
try_send!(tx, ModuleUpdateEvent::Update(stdout));
},
OutputStream::Stderr(stderr) => {
error!("{:?}", Report::msg(stderr)
.wrap_err("Watched script error:")

View File

@@ -1,10 +1,11 @@
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Button;
use gtk::{Button, IconTheme};
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
@@ -49,12 +50,13 @@ fn create_button(
name: &str,
focused: bool,
name_map: &HashMap<String, String>,
icon_theme: &IconTheme,
tx: &Sender<String>,
) -> Button {
let button = Button::builder()
.label(name_map.get(name).map_or(name, String::as_str))
.name(name)
.build();
let label = name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, icon_theme, 32);
button.set_widget_name(name);
let style_context = button.style_context();
style_context.add_class("item");
@@ -154,6 +156,7 @@ impl Module<gtk::Box> for WorkspacesModule {
{
let container = container.clone();
let output_name = info.output_name.to_string();
let icon_theme = info.icon_theme.clone();
// keep track of whether init event has fired previously
// since it fires for every workspace subscriber
@@ -170,6 +173,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&workspace.name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
container.add(&item);
@@ -187,12 +191,12 @@ impl Module<gtk::Box> for WorkspacesModule {
}
}
WorkspaceUpdate::Focus { old, new } => {
let old = button_map.get(&old.name);
let old = button_map.get(&old);
if let Some(old) = old {
old.style_context().remove_class("focused");
}
let new = button_map.get(&new.name);
let new = button_map.get(&new);
if let Some(new) = new {
new.style_context().add_class("focused");
}
@@ -204,6 +208,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
@@ -227,6 +232,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
@@ -247,7 +253,7 @@ impl Module<gtk::Box> for WorkspacesModule {
}
}
WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace.name);
let button = button_map.get(&workspace);
if let Some(item) = button {
container.remove(item);
}

View File

@@ -133,7 +133,7 @@ impl Popup {
/// Shows the popup
pub fn show(&self, geometry: ButtonGeometry) {
self.window.show_all();
self.window.show();
self.set_pos(geometry);
}