567 Commits

Author SHA1 Message Date
Jake Stanger
3fad8c6a16 wip output events rework 2023-09-25 22:52:17 +01:00
Jake Stanger
0c0163cfa1 Merge pull request #317 from JakeStanger/dependabot/cargo/hyprland-0.3.12
build(deps): bump hyprland from 0.3.11 to 0.3.12
2023-09-25 20:06:23 +01:00
Jake Stanger
e1b1d6f465 Merge pull request #315 from JakeStanger/dependabot/cargo/clap-4.4.4
build(deps): bump clap from 4.4.2 to 4.4.4
2023-09-25 20:06:06 +01:00
dependabot[bot]
1f824edd70 build(deps): bump hyprland from 0.3.11 to 0.3.12
Bumps [hyprland](https://github.com/hyprland-community/hyprland-rs) from 0.3.11 to 0.3.12.
- [Release notes](https://github.com/hyprland-community/hyprland-rs/releases)
- [Commits](https://github.com/hyprland-community/hyprland-rs/compare/0.3.11...0.3.12)

---
updated-dependencies:
- dependency-name: hyprland
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 14:53:06 +00:00
dependabot[bot]
088ac971c1 build(deps): bump clap from 4.4.2 to 4.4.4
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.2 to 4.4.4.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.2...v4.4.4)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 14:52:09 +00:00
Jake Stanger
59f66df079 Merge pull request #311 from JakeStanger/dependabot/cargo/serde_json-1.0.107
build(deps): bump serde_json from 1.0.105 to 1.0.107
2023-09-18 20:03:44 +01:00
Jake Stanger
2d5d1b450c Merge pull request #314 from JakeStanger/dependabot/cargo/chrono-0.4.31
build(deps): bump chrono from 0.4.30 to 0.4.31
2023-09-18 20:03:34 +01:00
Jake Stanger
7a2edcdffc Merge pull request #310 from JakeStanger/dependabot/cargo/sysinfo-0.29.10
build(deps): bump sysinfo from 0.29.9 to 0.29.10
2023-09-18 20:02:36 +01:00
dependabot[bot]
8f4ad02c75 build(deps): bump chrono from 0.4.30 to 0.4.31
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.30 to 0.4.31.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.30...v0.4.31)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 14:28:54 +00:00
dependabot[bot]
c5a1694b10 build(deps): bump serde_json from 1.0.105 to 1.0.107
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.105 to 1.0.107.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.105...v1.0.107)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 14:27:13 +00:00
dependabot[bot]
f8cc634cc9 build(deps): bump sysinfo from 0.29.9 to 0.29.10
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.9 to 0.29.10.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 14:26:43 +00:00
Jake Stanger
ccc6ff2d94 docs(readme): add nixpkgs details
Resolves #303.
2023-09-16 22:59:03 +01:00
Jake Stanger
71ea185a65 Merge pull request #305 from JakeStanger/dependabot/cargo/walkdir-2.4.0
build(deps): bump walkdir from 2.3.3 to 2.4.0
2023-09-11 21:54:34 +01:00
Jake Stanger
c793cd3c1a Merge pull request #306 from JakeStanger/dependabot/cargo/chrono-0.4.30
build(deps): bump chrono from 0.4.28 to 0.4.30
2023-09-11 21:36:32 +01:00
dependabot[bot]
1f4b349423 build(deps): bump walkdir from 2.3.3 to 2.4.0
Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.3.3 to 2.4.0.
- [Commits](https://github.com/BurntSushi/walkdir/compare/2.3.3...2.4.0)

---
updated-dependencies:
- dependency-name: walkdir
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 20:35:49 +00:00
Jake Stanger
4755e2c827 Merge pull request #307 from JakeStanger/dependabot/cargo/regex-1.9.5
build(deps): bump regex from 1.9.4 to 1.9.5
2023-09-11 21:35:13 +01:00
Jake Stanger
b548e3ec01 Merge pull request #308 from JakeStanger/dependabot/cargo/serde-1.0.188
build(deps): bump serde from 1.0.185 to 1.0.188
2023-09-11 21:34:45 +01:00
Jake Stanger
7033d29c6a Merge pull request #309 from JakeStanger/dependabot/cargo/hyprland-0.3.11
build(deps): bump hyprland from 0.3.9 to 0.3.11
2023-09-11 21:33:18 +01:00
dependabot[bot]
0335aa4470 build(deps): bump hyprland from 0.3.9 to 0.3.11
Bumps [hyprland](https://github.com/hyprland-community/hyprland-rs) from 0.3.9 to 0.3.11.
- [Release notes](https://github.com/hyprland-community/hyprland-rs/releases)
- [Commits](https://github.com/hyprland-community/hyprland-rs/compare/0.3.9...0.3.11)

---
updated-dependencies:
- dependency-name: hyprland
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 14:41:24 +00:00
dependabot[bot]
dce245ef5f build(deps): bump serde from 1.0.185 to 1.0.188
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.185 to 1.0.188.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.185...v1.0.188)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 14:41:14 +00:00
dependabot[bot]
bd002e278b build(deps): bump regex from 1.9.4 to 1.9.5
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.4 to 1.9.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.4...1.9.5)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 14:41:00 +00:00
dependabot[bot]
ae1bc2ef84 build(deps): bump chrono from 0.4.28 to 0.4.30
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.28 to 0.4.30.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.28...v0.4.30)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 14:40:52 +00:00
Jake Stanger
4e67b73a83 refactor(wlr data control): update to new nix epoll bindings 2023-09-05 22:43:29 +01:00
Jake Stanger
60bb69feec feat: add widget and widget-container css classes on all widgets 2023-09-05 22:37:58 +01:00
Jake Stanger
5bc7b7f317 Merge pull request #300 from JakeStanger/dependabot/cargo/nix-0.27.1
build(deps): bump nix from 0.26.2 to 0.27.1
2023-09-04 19:59:50 +01:00
dependabot[bot]
f88bec93ff build(deps): bump nix from 0.26.2 to 0.27.1
Bumps [nix](https://github.com/nix-rust/nix) from 0.26.2 to 0.27.1.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.26.2...v0.27.1)

---
updated-dependencies:
- dependency-name: nix
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 18:43:49 +00:00
Jake Stanger
0d0447bbd2 Merge pull request #301 from JakeStanger/dependabot/cargo/clap-4.4.2
build(deps): bump clap from 4.4.1 to 4.4.2
2023-09-04 19:43:16 +01:00
Jake Stanger
49a79bb011 Merge pull request #299 from JakeStanger/dependabot/cargo/ctrlc-3.4.1
build(deps): bump ctrlc from 3.4.0 to 3.4.1
2023-09-04 19:42:33 +01:00
Jake Stanger
1dcda6c90e Merge pull request #298 from JakeStanger/dependabot/cargo/chrono-0.4.28
build(deps): bump chrono from 0.4.26 to 0.4.28
2023-09-04 19:35:13 +01:00
dependabot[bot]
4e9c05761e build(deps): bump clap from 4.4.1 to 4.4.2
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.1...v4.4.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 14:57:51 +00:00
dependabot[bot]
af674bb769 build(deps): bump ctrlc from 3.4.0 to 3.4.1
Bumps [ctrlc](https://github.com/Detegr/rust-ctrlc) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/Detegr/rust-ctrlc/releases)
- [Commits](https://github.com/Detegr/rust-ctrlc/compare/3.4.0...3.4.1)

---
updated-dependencies:
- dependency-name: ctrlc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 14:56:47 +00:00
dependabot[bot]
a31a07e451 build(deps): bump chrono from 0.4.26 to 0.4.28
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.26 to 0.4.28.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.26...v0.4.28)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 14:56:16 +00:00
Jake Stanger
0df3d4c725 Merge pull request #297 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-09-01 17:33:36 +01:00
github-actions[bot]
50ab2e4742 flake.lock: Update
Flake lock file updates:

• Updated input 'crane':
    'github:ipetkov/crane/8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e' (2023-07-07)
  → 'github:ipetkov/crane/174604795d316b75777e28185c3a4918bc69b399' (2023-08-30)
• Updated input 'crane/flake-utils':
    'github:numtide/flake-utils/dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7' (2023-06-25)
  → 'github:numtide/flake-utils/919d646de7be200f3bf08cb76ae1f09402b6f9b4' (2023-07-11)
• Updated input 'crane/rust-overlay':
    'github:oxalica/rust-overlay/f9b92316727af9e6c7fee4a761242f7f46880329' (2023-07-03)
  → 'github:oxalica/rust-overlay/b520a3889b24aaf909e287d19d406862ced9ffc9' (2023-08-07)
• Updated input 'naersk':
    'github:nix-community/naersk/d9a33d69a9c421d64c8d925428864e93be895dcc' (2023-07-26)
  → 'github:nix-community/naersk/78789c30d64dea2396c9da516bbcc8db3a475207' (2023-08-18)
• Updated input 'naersk/nixpkgs':
    'github:NixOS/nixpkgs/9418167277f665de6f4a29f414d438cf39c55b9e' (2023-07-31)
  → 'github:NixOS/nixpkgs/a63a64b593dcf2fe05f7c5d666eb395950f36bc9' (2023-08-30)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/fb942492b7accdee4e6d17f5447091c65897dde4' (2023-07-31)
  → 'github:nixos/nixpkgs/e7f38be3775bab9659575f192ece011c033655f0' (2023-08-30)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/05d480a7aef1aae1bfb67a39134dcf48c5322528' (2023-07-30)
  → 'github:oxalica/rust-overlay/40e851593ef4f9f8cd0b69c8cae7b722b9953a23' (2023-08-31)
2023-09-01 00:51:26 +00:00
Jake Stanger
abd1f80548 docs(examples): update discord icon, temporarily disable random label 2023-08-30 21:45:53 +01:00
Jake Stanger
3ddf799739 build: update universal-config/corn 2023-08-30 21:37:46 +01:00
Jake Stanger
6fbe0d5e71 chore: bump version to 0.14.0-pre 2023-08-29 22:49:26 +01:00
Jake Stanger
bc553b4918 Merge pull request #292 from JakeStanger/dependabot/cargo/clap-4.4.1
build(deps): bump clap from 4.3.23 to 4.4.1
2023-08-28 21:36:54 +01:00
Jake Stanger
4ee2ce4d67 Merge pull request #290 from JakeStanger/dependabot/cargo/regex-1.9.4
build(deps): bump regex from 1.9.3 to 1.9.4
2023-08-28 21:08:08 +01:00
dependabot[bot]
cf3dc17ad3 build(deps): bump clap from 4.3.23 to 4.4.1
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.23 to 4.4.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.23...v4.4.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 20:04:35 +00:00
Jake Stanger
8b8ccf7be7 Merge pull request #288 from JakeStanger/dependabot/cargo/reqwest-0.11.20
build(deps): bump reqwest from 0.11.18 to 0.11.20
2023-08-28 21:03:47 +01:00
Jake Stanger
1035ce670f Merge pull request #287 from JakeStanger/dependabot/cargo/sysinfo-0.29.9
build(deps): bump sysinfo from 0.29.8 to 0.29.9
2023-08-28 21:03:30 +01:00
Jake Stanger
43b446d266 Merge pull request #286 from JakeStanger/dependabot/cargo/notify-6.1.1
build(deps): bump notify from 6.0.1 to 6.1.1
2023-08-28 21:02:38 +01:00
dependabot[bot]
93eb4c6472 build(deps): bump regex from 1.9.3 to 1.9.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.3 to 1.9.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.3...1.9.4)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 14:30:34 +00:00
dependabot[bot]
40432c8704 build(deps): bump reqwest from 0.11.18 to 0.11.20
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.18 to 0.11.20.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.18...v0.11.20)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 14:29:54 +00:00
dependabot[bot]
9ced8597e4 build(deps): bump sysinfo from 0.29.8 to 0.29.9
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.8 to 0.29.9.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 14:29:40 +00:00
dependabot[bot]
10f8bbae3f build(deps): bump notify from 6.0.1 to 6.1.1
Bumps [notify](https://github.com/notify-rs/notify) from 6.0.1 to 6.1.1.
- [Release notes](https://github.com/notify-rs/notify/releases)
- [Changelog](https://github.com/notify-rs/notify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/notify-rs/notify/compare/notify-6.0.1...notify-6.1.1)

---
updated-dependencies:
- dependency-name: notify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 14:29:26 +00:00
Jake Stanger
af1f9e39b0 Merge pull request #283 from malicean/feat/workspaces-visible
feat(workspaces): visible CSS selector
2023-08-26 22:20:40 +01:00
Alice Janik
25c490b8b4 feat(workspaces): visible CSS selector 2023-08-25 20:50:51 -05:00
Jake Stanger
6c38ff29c4 Merge pull request #281 from JakeStanger/dependabot/cargo/serde-1.0.185
build(deps): bump serde from 1.0.183 to 1.0.185
2023-08-25 23:03:01 +01:00
Jake Stanger
fea1f18524 refactor: fix new clippy warnings, fmt 2023-08-25 22:55:12 +01:00
Jake Stanger
b9c41af0f7 docs(workspaces): add missing .inactive selector 2023-08-24 23:29:16 +01:00
dependabot[bot]
2a8a62eea6 build(deps): bump serde from 1.0.183 to 1.0.185
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.183 to 1.0.185.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.183...v1.0.185)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 20:37:58 +00:00
Jake Stanger
d010fd6398 Merge pull request #280 from JakeStanger/dependabot/cargo/hyprland-0.3.9
build(deps): bump hyprland from 0.3.8 to 0.3.9
2023-08-21 21:26:18 +01:00
Jake Stanger
57cca121bf Merge pull request #279 from JakeStanger/dependabot/cargo/serde_json-1.0.105
build(deps): bump serde_json from 1.0.104 to 1.0.105
2023-08-21 21:24:48 +01:00
dependabot[bot]
7f6ba90bfd build(deps): bump serde_json from 1.0.104 to 1.0.105
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.104 to 1.0.105.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.104...v1.0.105)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 20:07:26 +00:00
Jake Stanger
9431d09de7 Merge pull request #278 from JakeStanger/dependabot/cargo/clap-4.3.23
build(deps): bump clap from 4.3.21 to 4.3.23
2023-08-21 21:06:44 +01:00
Jake Stanger
69bd650810 Merge pull request #277 from JakeStanger/dependabot/cargo/tokio-1.32.0
build(deps): bump tokio from 1.31.0 to 1.32.0
2023-08-21 21:02:24 +01:00
dependabot[bot]
5f9ac64892 build(deps): bump hyprland from 0.3.8 to 0.3.9
Bumps [hyprland](https://github.com/hyprland-community/hyprland-rs) from 0.3.8 to 0.3.9.
- [Release notes](https://github.com/hyprland-community/hyprland-rs/releases)
- [Commits](https://github.com/hyprland-community/hyprland-rs/compare/0.3.8...0.3.9)

---
updated-dependencies:
- dependency-name: hyprland
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 14:50:45 +00:00
dependabot[bot]
3f7904fefb build(deps): bump clap from 4.3.21 to 4.3.23
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.21 to 4.3.23.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.21...v4.3.23)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 14:50:26 +00:00
dependabot[bot]
84e0065c0c build(deps): bump tokio from 1.31.0 to 1.32.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.31.0 to 1.32.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.31.0...tokio-1.32.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 14:50:17 +00:00
JakeStanger
e5281e9619 docs: update CHANGELOG.md for v0.13.0 [skip ci] 2023-08-16 19:32:13 +00:00
Jake Stanger
1b476eb9f9 chore(wayland): downgrade some logs from debug to trace 2023-08-16 20:27:24 +01:00
Jake Stanger
50741941fb Merge pull request #147 from yavko/add-always-workspaces
feat: Add favorite and hidden workspaces
2023-08-15 20:25:49 +01:00
yavko
19c684e49f feat(nix): automatic development environment with direnv 2023-08-15 20:09:32 +01:00
yavko
d1be6100d6 build: update hyprland-rs to 0.3.8, enable silent feature 2023-08-15 20:09:32 +01:00
yavko
9f65cf293d feat(workspaces): add favorites and hidden options 2023-08-15 20:09:32 +01:00
Jake Stanger
7e877f6631 Merge pull request #276 from JakeStanger/dependabot/cargo/strip-ansi-escapes-0.2.0
build(deps): bump strip-ansi-escapes from 0.1.1 to 0.2.0
2023-08-14 19:31:13 +01:00
Jake Stanger
6203447ec5 Merge pull request #275 from JakeStanger/dependabot/cargo/sysinfo-0.29.8
build(deps): bump sysinfo from 0.29.7 to 0.29.8
2023-08-14 19:30:35 +01:00
Jake Stanger
3621414843 Merge pull request #274 from JakeStanger/dependabot/cargo/tokio-1.31.0
build(deps): bump tokio from 1.29.1 to 1.31.0
2023-08-14 19:29:22 +01:00
Jake Stanger
e75616c547 Merge pull request #273 from JakeStanger/dependabot/cargo/clap-4.3.21
build(deps): bump clap from 4.3.19 to 4.3.21
2023-08-14 19:28:47 +01:00
dependabot[bot]
71002c4250 build(deps): bump strip-ansi-escapes from 0.1.1 to 0.2.0
Bumps [strip-ansi-escapes](https://github.com/luser/strip-ansi-escapes) from 0.1.1 to 0.2.0.
- [Commits](https://github.com/luser/strip-ansi-escapes/compare/0.1.1...v0.2.0)

---
updated-dependencies:
- dependency-name: strip-ansi-escapes
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-14 14:49:32 +00:00
dependabot[bot]
eb58d9caf8 build(deps): bump sysinfo from 0.29.7 to 0.29.8
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.7 to 0.29.8.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-14 14:49:08 +00:00
dependabot[bot]
6f9b61865d build(deps): bump tokio from 1.29.1 to 1.31.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.31.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.31.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-14 14:48:46 +00:00
dependabot[bot]
fec07c6407 build(deps): bump clap from 4.3.19 to 4.3.21
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.19 to 4.3.21.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.19...v4.3.21)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-14 14:48:17 +00:00
Jake Stanger
8ec0237bc5 amend 54f0f232 2023-08-13 20:38:49 +01:00
Jake Stanger
9e2ac0f43d Merge pull request #272 from JakeStanger/build/stray
build: replace `stray` with `system-tray` fork
2023-08-13 15:25:30 +01:00
Jake Stanger
baeb4eae74 Merge pull request #271 from JakeStanger/fix/launcher-popup
fix(launcher): popup not closing when hover leaves widget
2023-08-13 15:21:47 +01:00
Jake Stanger
b6e4ed6608 build: replace stray with system-tray fork
Fully resolves #166
2023-08-13 15:11:29 +01:00
Jake Stanger
54f0f232f2 fix(launcher): popup not closing when hover leaves widget
Fixes #224
2023-08-13 15:07:31 +01:00
Jake Stanger
5255ddffbb Merge pull request #270 from JakeStanger/build/hyprland
build: update to latest hyprland-rs
2023-08-11 21:49:11 +01:00
Jake Stanger
9fe6d49195 build: update to latest hyprland-rs
Resolves #269
2023-08-11 21:15:45 +01:00
Jake Stanger
b649525a2c Merge pull request #266 from JakeStanger/dependabot/cargo/serde-1.0.183
build(deps): bump serde from 1.0.180 to 1.0.183
2023-08-09 20:59:10 +01:00
dependabot[bot]
fb2e30c839 build(deps): bump serde from 1.0.180 to 1.0.183
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.180 to 1.0.183.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.180...v1.0.183)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-07 14:39:41 +00:00
Jake Stanger
82030c7d51 Merge pull request #265 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-08-01 21:31:13 +01:00
Jake Stanger
4679d8b842 Merge pull request #263 from JakeStanger/dependabot/cargo/serde-1.0.180
build(deps): bump serde from 1.0.175 to 1.0.180
2023-08-01 21:30:11 +01:00
Jake Stanger
901a86caa4 fix(custom): crash when clicking non-popup button 2023-08-01 21:29:00 +01:00
github-actions[bot]
1da8c90415 flake.lock: Update
Flake lock file updates:

• Updated input 'naersk':
    'github:nix-community/naersk/abca1fb7a6cfdd355231fc220c3d0302dbb4369a' (2023-07-05)
  → 'github:nix-community/naersk/d9a33d69a9c421d64c8d925428864e93be895dcc' (2023-07-26)
• Updated input 'naersk/nixpkgs':
    'path:/nix/store/s1z7nb9n6r5n0r34fabp6yybwkbr8mjk-source?lastModified=1687412861&narHash=sha256-Z/g0wbL68C%2BmSGerYS2quv9FXQ1RRP082cAC0Bh4vcs%3D&rev=e603dc5f061ca1d8a19b3ede6a8cf9c9fcba6cdc' (2023-06-22)
  → 'github:NixOS/nixpkgs/9418167277f665de6f4a29f414d438cf39c55b9e' (2023-07-31)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/b12803b6d90e2e583429bb79b859ca53c348b39a' (2023-07-24)
  → 'github:nixos/nixpkgs/fb942492b7accdee4e6d17f5447091c65897dde4' (2023-07-31)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/8d64353ca827002fb8459e44d49116c78d868eba' (2023-07-25)
  → 'github:oxalica/rust-overlay/05d480a7aef1aae1bfb67a39134dcf48c5322528' (2023-07-30)
2023-08-01 01:02:50 +00:00
dependabot[bot]
9fc3c5302f build(deps): bump serde from 1.0.175 to 1.0.180
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.175 to 1.0.180.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.175...v1.0.180)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 20:54:28 +00:00
Jake Stanger
22ab33d194 Merge pull request #261 from JakeStanger/dependabot/cargo/sysinfo-0.29.7
build(deps): bump sysinfo from 0.29.6 to 0.29.7
2023-07-31 21:53:36 +01:00
Jake Stanger
78ba508fb3 Merge pull request #260 from JakeStanger/dependabot/cargo/serde_json-1.0.104
build(deps): bump serde_json from 1.0.103 to 1.0.104
2023-07-31 21:53:20 +01:00
dependabot[bot]
b6895b9312 build(deps): bump sysinfo from 0.29.6 to 0.29.7
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.6 to 0.29.7.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 15:01:21 +00:00
dependabot[bot]
c59d4267c8 build(deps): bump serde_json from 1.0.103 to 1.0.104
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.103 to 1.0.104.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.103...v1.0.104)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 15:01:04 +00:00
Jake Stanger
2902331af0 fix(dynamic string): incorrectly handling strings containing multipoint utf-8 chars 2023-07-30 23:30:24 +01:00
Jake Stanger
a47ec69a07 chore(cargo): add keywords 2023-07-29 22:00:24 +01:00
Jake Stanger
c094179fca Merge pull request #258 from yavko/use-crane
ci(nix): Add Crane and Naersk builders
2023-07-26 22:51:16 +01:00
Yavor Kolev
311284590f build(nix): add crane and naersk builders 2023-07-26 22:23:41 +01:00
Jake Stanger
ff04be70cc Merge pull request #259 from JakeStanger/fix/images
Image fixes
2023-07-26 22:15:19 +01:00
Jake Stanger
89ec06fc7b fix(music): hide album art widget when no image 2023-07-26 22:03:27 +01:00
Jake Stanger
7f6fef6338 fix(image): matching desktop file names too eagerly
Fixes #228
2023-07-26 22:03:27 +01:00
Jake Stanger
36f3db7411 refactor(image): do not try to read desktop files where definitely not necessary 2023-07-26 22:03:27 +01:00
Jake Stanger
2367faab04 fix(image): using fallback in places it shouldn't 2023-07-26 21:49:45 +01:00
Jake Stanger
1c68a97d33 chore(nix): add shell.nix file 2023-07-26 21:22:19 +01:00
Jake Stanger
00d5606f06 Merge pull request #257 from JakeStanger/fix/missing-icon-crash
feat(image resolver): add fallback image
2023-07-24 21:31:44 +01:00
Jake Stanger
ef443e6978 feat(image resolver): add fallback image
Puts a cap on the recursion, and falls back to a question mark image from the icon theme if no image could be resolved.

Fixes #250
2023-07-24 21:04:41 +01:00
Jake Stanger
4a8c823590 Merge pull request #254 from JakeStanger/dependabot/cargo/serde-1.0.175
build(deps): bump serde from 1.0.171 to 1.0.175
2023-07-24 18:17:05 +01:00
dependabot[bot]
1114f67b5d build(deps): bump serde from 1.0.171 to 1.0.175
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.171 to 1.0.175.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.171...v1.0.175)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 16:36:00 +00:00
Jake Stanger
0d388d91f9 Merge pull request #256 from JakeStanger/dependabot/cargo/sysinfo-0.29.6
build(deps): bump sysinfo from 0.29.5 to 0.29.6
2023-07-24 17:34:53 +01:00
Jake Stanger
bf682d84ff Merge pull request #253 from JakeStanger/dependabot/cargo/clap-4.3.19
build(deps): bump clap from 4.3.12 to 4.3.19
2023-07-24 17:34:38 +01:00
dependabot[bot]
2cd8be72a3 build(deps): bump sysinfo from 0.29.5 to 0.29.6
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.5 to 0.29.6.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 14:47:44 +00:00
dependabot[bot]
eb1347f20b build(deps): bump clap from 4.3.12 to 4.3.19
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.12 to 4.3.19.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.12...v4.3.19)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 14:46:49 +00:00
Jake Stanger
ef446f084a Merge pull request #252 from JakeStanger/ci/nix-cache
ci(build): add nix caching
2023-07-22 15:39:10 +01:00
Jake Stanger
4bb75282b4 ci(build): add nix caching 2023-07-22 14:58:16 +01:00
Jake Stanger
5a6b14e5f8 Merge pull request #243 from JakeStanger/dependabot/cargo/serde_json-1.0.103
build(deps): bump serde_json from 1.0.100 to 1.0.103
2023-07-18 21:41:00 +01:00
Jake Stanger
806cfc61aa Merge pull request #242 from JakeStanger/dependabot/cargo/wayland-protocols-0.30.1
build(deps): bump wayland-protocols from 0.30.0 to 0.30.1
2023-07-17 23:38:17 +01:00
dependabot[bot]
782fbf102c build(deps): bump serde_json from 1.0.100 to 1.0.103
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.100 to 1.0.103.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.100...v1.0.103)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-17 22:37:29 +00:00
Jake Stanger
c823bcc83b Merge pull request #241 from JakeStanger/dependabot/cargo/sysinfo-0.29.5
build(deps): bump sysinfo from 0.29.4 to 0.29.5
2023-07-17 23:37:09 +01:00
Jake Stanger
0654f146a3 Merge pull request #240 from JakeStanger/dependabot/cargo/clap-4.3.12
build(deps): bump clap from 4.3.10 to 4.3.12
2023-07-17 23:36:42 +01:00
dependabot[bot]
caecee6875 build(deps): bump wayland-protocols from 0.30.0 to 0.30.1
Bumps [wayland-protocols](https://github.com/smithay/wayland-rs) from 0.30.0 to 0.30.1.
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/compare/v0.30.0...wayland-egl-v0.30.1)

---
updated-dependencies:
- dependency-name: wayland-protocols
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-17 14:49:10 +00:00
dependabot[bot]
3853c953a6 build(deps): bump sysinfo from 0.29.4 to 0.29.5
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.4 to 0.29.5.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-17 14:48:57 +00:00
dependabot[bot]
f21d473b5a build(deps): bump clap from 4.3.10 to 4.3.12
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.10 to 4.3.12.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.10...v4.3.12)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-17 14:48:46 +00:00
Jake Stanger
3d949874de docs(ipc): add link to luajit library 2023-07-16 21:25:03 +01:00
Jake Stanger
c09bec2d2b Merge pull request #238 from JakeStanger/fix/launcher-focus
Launcher focus fixes
2023-07-16 20:53:20 +01:00
Jake Stanger
6f57ad47ac fix(launcher): not setting focus state when opening favourite
Fixes partially #225.
2023-07-16 20:41:53 +01:00
Jake Stanger
87dd7646fc fix(launcher): not clearing focused state when closing window
Fixes #213.
Fixes partially #225.
2023-07-16 20:24:23 +01:00
Jake Stanger
06251e293e refactor: fix new pedantic clippy warnings 2023-07-16 20:17:32 +01:00
Jake Stanger
36abe4073e Merge pull request #237 from JakeStanger/feat/ipc-popup
feat(ipc): commands for opening/closing popups
2023-07-16 20:06:13 +01:00
Jake Stanger
b7ee794bfc feat(ipc): commands for opening/closing popups
Also includes some refactoring around related GTK helper code
2023-07-16 19:15:55 +01:00
Jake Stanger
c582bc3390 fix(cli): set-visible command causing panic 2023-07-16 18:47:44 +01:00
Jake Stanger
2a8313a9b4 Merge pull request #236 from A-Cloud-Ninja/master
[Feature] Add basic bar visibility change to IPC
2023-07-16 18:35:17 +01:00
A-Cloud-Ninja
2ccb2633c6 feat: IPC for get_visible, set_visible, new bar name config option 2023-07-16 18:21:44 +01:00
Jake Stanger
2f8443f349 Merge pull request #234 from JakeStanger/dependabot/cargo/serde_json-1.0.100
build(deps): bump serde_json from 1.0.96 to 1.0.100
2023-07-10 17:52:03 +01:00
Jake Stanger
fbb41f33a3 Merge pull request #232 from JakeStanger/dependabot/cargo/sysinfo-0.29.4
build(deps): bump sysinfo from 0.29.2 to 0.29.4
2023-07-10 17:51:54 +01:00
Jake Stanger
cc2669a3a6 Merge pull request #230 from JakeStanger/dependabot/cargo/serde-1.0.171
build(deps): bump serde from 1.0.165 to 1.0.171
2023-07-10 17:51:45 +01:00
dependabot[bot]
bf470cdadd build(deps): bump serde_json from 1.0.96 to 1.0.100
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.96 to 1.0.100.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.96...v1.0.100)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 15:00:33 +00:00
dependabot[bot]
ab67a0a414 build(deps): bump sysinfo from 0.29.2 to 0.29.4
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.2 to 0.29.4.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 14:59:28 +00:00
dependabot[bot]
4305f3e3bc build(deps): bump serde from 1.0.165 to 1.0.171
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.165 to 1.0.171.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.165...v1.0.171)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 14:58:32 +00:00
Jake Stanger
eee2182ab9 fix(ipc): command/response casing 2023-07-09 19:59:17 +01:00
Jake Stanger
abf490b04e Merge pull request #226 from JakeStanger/fix/desktop-file
fix(launcher): incorrectly resolving some applications
2023-07-06 09:17:36 +01:00
Jake Stanger
4ca17d1337 fix(launcher): incorrectly resolving some applications
Potentially also fixes some mismatches with icons.

Fixes #222.
2023-07-05 23:32:56 +01:00
Jake Stanger
fc820746a4 Merge pull request #220 from JakeStanger/feat/default-config
Default configuration
2023-07-04 18:08:33 +01:00
Jake Stanger
738b9e3da7 feat(config): use default fallback with config instructions
When no config file is found, the bar will now automatically instead load a hard-coded default consisting of the `focused` and `clock` modules, and a `label` informing you the bar is not configured. Instructions are also printed to the log.
2023-07-04 17:38:39 +01:00
Jake Stanger
1a272e00fb fix(label): not using markup 2023-07-04 17:38:08 +01:00
Jake Stanger
f8d8c06300 Merge pull request #210 from christoph00/XDG_DATA_DIRS
Find Additional Applications dirs in XDG_DATA_DIR
2023-07-04 12:53:22 +01:00
Christoph Asche
c711dd8585 fix: failing to resolve icons with home_manager 2023-07-04 01:53:17 +02:00
Jake Stanger
68432b066b Merge pull request #221 from JakeStanger/feat/clock-localization
feat(clock): localization support
2023-07-03 23:35:27 +01:00
Jake Stanger
b310ea7636 feat(clock): localization support 2023-07-03 23:20:37 +01:00
Jake Stanger
7c8d4668bc build: update universal-config (for corn v0.8) 2023-07-03 20:42:42 +01:00
Jake Stanger
f1231384c1 Merge pull request #217 from JakeStanger/dependabot/cargo/zbus-3.14.1
build(deps): bump zbus from 3.13.1 to 3.14.1
2023-07-03 16:04:01 +01:00
Jake Stanger
c2cf41a8b0 Merge pull request #214 from JakeStanger/dependabot/cargo/mpd_client-1.2.0
build(deps): bump mpd_client from 1.1.0 to 1.2.0
2023-07-03 16:03:23 +01:00
Jake Stanger
aa635291c3 Merge pull request #215 from JakeStanger/dependabot/cargo/clap-4.3.10
build(deps): bump clap from 4.3.4 to 4.3.10
2023-07-03 16:02:08 +01:00
Jake Stanger
5b2a1787ef Merge pull request #218 from JakeStanger/dependabot/cargo/serde-1.0.165
build(deps): bump serde from 1.0.164 to 1.0.165
2023-07-03 16:01:04 +01:00
Jake Stanger
95786a1c90 Merge pull request #216 from JakeStanger/dependabot/cargo/tokio-1.29.1
build(deps): bump tokio from 1.28.2 to 1.29.1
2023-07-03 15:57:31 +01:00
dependabot[bot]
d8121de9c5 build(deps): bump serde from 1.0.164 to 1.0.165
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.164 to 1.0.165.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.164...v1.0.165)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 14:47:41 +00:00
dependabot[bot]
697aac1161 build(deps): bump zbus from 3.13.1 to 3.14.1
Bumps [zbus](https://github.com/dbus2/zbus) from 3.13.1 to 3.14.1.
- [Release notes](https://github.com/dbus2/zbus/releases)
- [Commits](https://github.com/dbus2/zbus/compare/zbus-3.13.1...zbus-3.14.1)

---
updated-dependencies:
- dependency-name: zbus
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 14:47:28 +00:00
dependabot[bot]
b695eeb415 build(deps): bump tokio from 1.28.2 to 1.29.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.28.2 to 1.29.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.28.2...tokio-1.29.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 14:47:15 +00:00
dependabot[bot]
f41afce91d build(deps): bump clap from 4.3.4 to 4.3.10
Bumps [clap](https://github.com/clap-rs/clap) from 4.3.4 to 4.3.10.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.3.4...v4.3.10)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 14:47:01 +00:00
dependabot[bot]
c99c5de0d6 build(deps): bump mpd_client from 1.1.0 to 1.2.0
Bumps [mpd_client](https://github.com/elomatreb/mpd_client) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/elomatreb/mpd_client/releases)
- [Commits](https://github.com/elomatreb/mpd_client/compare/mpd_client-v1.1.0...mpd_client-v1.2.0)

---
updated-dependencies:
- dependency-name: mpd_client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 14:46:48 +00:00
Jake Stanger
7f4a816379 Merge pull request #209 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-07-01 11:23:34 +01:00
github-actions[bot]
b785b4d4ae flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/04af42f3b31dba0ef742d254456dc4c14eedac86' (2023-06-17)
  → 'github:nixos/nixpkgs/4bc72cae107788bf3f24f30db2e2f685c9298dc9' (2023-06-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/01d84cd842e48e89be67e4c2d9dc46aa7709adc5' (2023-06-17)
  → 'github:oxalica/rust-overlay/4c31223801dd0f28ac15d60f2e5ddbd4d51ce17a' (2023-06-30)
2023-07-01 01:15:59 +00:00
Jake Stanger
3502c23817 Merge pull request #208 from JakeStanger/feat/config-reload
feat(ipc): reload config command
2023-07-01 00:33:03 +01:00
Jake Stanger
7d3bb02b46 feat(ipc): reload config command 2023-07-01 00:05:12 +01:00
Jake Stanger
4620f29d38 docs(examples): update stylesheet 2023-06-30 23:01:11 +01:00
Jake Stanger
a9ac29d885 fix: clipboard partially behind wrong feature flag 2023-06-30 23:00:52 +01:00
Jake Stanger
6ae15f44bd Merge pull request #206 from JakeStanger/feat/music-progress
Resolves #128.

Also includes a fix to ensure elements in the popup are hidden if the connected player is not capable of providing the info.
2023-06-30 20:07:16 +01:00
Jake Stanger
1759945912 fix(music): correctly show/hide popup elements based on player capabilities 2023-06-30 19:27:00 +01:00
Jake Stanger
12053f111a feat(music): progress/seek bar in popup
Resolves #128.
2023-06-30 19:26:49 +01:00
Jake Stanger
bd90167f4e feat(clock): format option for popup header 2023-06-30 11:10:19 +01:00
Jake Stanger
7016f7f79e refactor: use new smart pointer macros throughout codebase 2023-06-29 23:16:31 +01:00
Jake Stanger
ac04cc27ce Merge pull request #191 from body20002/flatpak_icons
Add Support For Flatpak Icons
2023-06-29 23:06:51 +01:00
Abdallah Gamal
f78c7f9b98 fix: not resolving flatpak application icons 2023-06-29 22:40:49 +01:00
Jake Stanger
6db7742e06 fix: crash on startup introduced by recent refactors 2023-06-29 18:02:51 +01:00
Jake Stanger
4b88079561 docs: fix header 2023-06-29 17:38:41 +01:00
Jake Stanger
9a68dc99bd build: fix error 2023-06-29 17:37:10 +01:00
Jake Stanger
cc181a8b6d refactor: fix new clippy warnings 2023-06-29 16:57:47 +01:00
Jake Stanger
27f920d012 feat(launcher): slightly improve focus logic when clicking item with multiple windows 2023-06-29 16:42:03 +01:00
Jake Stanger
4a9410abac Merge pull request #199 from JakeStanger/feat/cli-ipc
IPC, CLI and Dynamic Variables
2023-06-29 16:39:56 +01:00
Jake Stanger
607c7285d7 docs: update for ipc/cli, tidy a bit 2023-06-29 16:26:55 +01:00
Jake Stanger
c6319b78fd feat(ipc): support for injecting additional stylesheets 2023-06-29 16:26:55 +01:00
Jake Stanger
ded50cca6f feat: support for 'ironvar' dynamic variables 2023-06-29 16:26:54 +01:00
Jake Stanger
f5bdc5a027 feat: ipc server and cli 2023-06-29 16:19:19 +01:00
Jake Stanger
44313bfc75 Merge pull request #203 from JakeStanger/dependabot/cargo/notify-6.0.1
build(deps): bump notify from 6.0.0 to 6.0.1
2023-06-26 17:16:05 +01:00
Jake Stanger
9e5f72087f Merge pull request #201 from JakeStanger/dependabot/cargo/indexmap-2.0.0
build(deps): bump indexmap from 1.9.3 to 2.0.0
2023-06-26 17:15:44 +01:00
Jake Stanger
449795b4e9 Merge pull request #200 from JakeStanger/dependabot/cargo/mpris-2.0.1
build(deps): bump mpris from 2.0.0 to 2.0.1
2023-06-26 17:14:31 +01:00
dependabot[bot]
a67bf38faa build(deps): bump notify from 6.0.0 to 6.0.1
Bumps [notify](https://github.com/notify-rs/notify) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/notify-rs/notify/releases)
- [Changelog](https://github.com/notify-rs/notify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/notify-rs/notify/compare/notify-6.0.0...notify-6.0.1)

---
updated-dependencies:
- dependency-name: notify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 15:06:12 +00:00
dependabot[bot]
e539eadd8d build(deps): bump indexmap from 1.9.3 to 2.0.0
Bumps [indexmap](https://github.com/bluss/indexmap) from 1.9.3 to 2.0.0.
- [Changelog](https://github.com/bluss/indexmap/blob/master/RELEASES.md)
- [Commits](https://github.com/bluss/indexmap/compare/1.9.3...2.0.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 15:05:37 +00:00
dependabot[bot]
592213d8af build(deps): bump mpris from 2.0.0 to 2.0.1
Bumps [mpris](https://github.com/Mange/mpris-rs) from 2.0.0 to 2.0.1.
- [Changelog](https://github.com/Mange/mpris-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Mange/mpris-rs/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: mpris
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 15:05:17 +00:00
Jake Stanger
d121dc3d1e refactor: fix unused var warning 2023-06-22 23:21:02 +01:00
Jake Stanger
0e8c8a1770 chore: bump version to 0.13.0 2023-06-18 23:10:15 +01:00
Jake Stanger
93baf8f568 Merge pull request #197 from JakeStanger/ci/cache
ci: add caching support
2023-06-18 17:51:32 +01:00
Jake Stanger
1ef32059da ci: add caching support 2023-06-18 17:41:10 +01:00
Jake Stanger
5be0750792 Merge pull request #160 from JakeStanger/feat/upower-icon-size
feat(upower): icon size option
2023-06-18 17:01:57 +01:00
JakeStanger
aea8de2552 docs: update CHANGELOG.md for v0.12.1 [skip ci] 2023-06-18 15:44:32 +00:00
Jake Stanger
c8c84446d6 Merge pull request #196 from JakeStanger/fix/launcher-crash
fix(launcher): crash when focusing newly opened window in popup
2023-06-18 16:33:04 +01:00
Jake Stanger
103a224355 fix(launcher): crash when focusing newly opened window in popup
Attempting to focus a newly opened window from the launcher popup attempted to close the popup directly in an invalid manner, which caused the bar to hard crash. The controller already handles this correctly, so removed this code.

Resolves #41 🎉
2023-06-18 16:21:35 +01:00
Jake Stanger
18b36423e2 Merge pull request #193 from JakeStanger/build/stray-patch
Use patched version of `stray`
2023-06-18 00:07:49 +01:00
Jake Stanger
09a26d04bc chore: update flake lock 2023-06-17 23:53:00 +01:00
Jake Stanger
96323801d9 build: use patched version of stray 2023-06-17 23:52:52 +01:00
Jake Stanger
de98cf3dae fix(tray): (maybe?) sometimes bus name is taken
No idea if this fix actually does anything but no harm in it I guess.
2023-06-17 21:29:33 +01:00
Jake Stanger
b3b96673b0 Merge pull request #192 from JakeStanger/fix/focused-icon
Fix focused icon issues
2023-06-17 17:11:14 +01:00
Jake Stanger
de3aa5d7b1 fix(focused): previous icon does not clear if new icon fails to load
Fixes #169
2023-06-17 16:43:58 +01:00
Jake Stanger
ac34c05d2e fix(focused): empty icon rendered when show_icon = false
Fixes #184
2023-06-17 16:43:38 +01:00
Jake Stanger
6f7af07cdd Merge pull request #189 from JakeStanger/fix/icon-classes
fix: add sensible class names for icon labels
2023-06-14 20:34:13 +01:00
Jake Stanger
96d36c43d4 docs: add missing icon/image selectors 2023-06-12 22:25:40 +01:00
Jake Stanger
e11177fea3 fix: add sensible class names for icon labels
BREAKING CHANGE: Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.

Resolves #185.
2023-06-12 22:25:23 +01:00
Jake Stanger
cbba2bc614 build: update deps, temporarily pin hyprland 2023-06-12 22:21:33 +01:00
Jake Stanger
658d040607 Merge pull request #188 from JakeStanger/dependabot/cargo/serde-1.0.164
build(deps): bump serde from 1.0.163 to 1.0.164
2023-06-12 19:47:31 +01:00
Jake Stanger
81c69f644e Merge pull request #187 from JakeStanger/dependabot/cargo/sysinfo-0.29.2
build(deps): bump sysinfo from 0.29.0 to 0.29.2
2023-06-12 19:46:59 +01:00
Jake Stanger
55327c6f98 Merge pull request #186 from JakeStanger/dependabot/cargo/glib-0.17.10
build(deps): bump glib from 0.17.9 to 0.17.10
2023-06-12 19:46:10 +01:00
dependabot[bot]
fc2e93491f build(deps): bump serde from 1.0.163 to 1.0.164
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.163 to 1.0.164.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.163...v1.0.164)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 15:04:54 +00:00
dependabot[bot]
a10cbd6287 build(deps): bump sysinfo from 0.29.0 to 0.29.2
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.29.0 to 0.29.2.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 15:04:04 +00:00
dependabot[bot]
1991662bc4 build(deps): bump glib from 0.17.9 to 0.17.10
Bumps [glib](https://github.com/gtk-rs/gtk-rs-core) from 0.17.9 to 0.17.10.
- [Release notes](https://github.com/gtk-rs/gtk-rs-core/releases)
- [Changelog](https://github.com/gtk-rs/gtk-rs-core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gtk-rs/gtk-rs-core/compare/0.17.9...0.17.10)

---
updated-dependencies:
- dependency-name: glib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 15:03:53 +00:00
Jake Stanger
b4f1c7ac2d Merge pull request #182 from JakeStanger/dependabot/cargo/reqwest-0.11.18
build(deps): bump reqwest from 0.11.16 to 0.11.18
2023-06-05 18:20:08 +01:00
Jake Stanger
1e799f7635 Merge pull request #181 from JakeStanger/dependabot/cargo/notify-6.0.0
build(deps): bump notify from 5.1.0 to 6.0.0
2023-06-05 18:20:00 +01:00
dependabot[bot]
a9fe75b2f7 build(deps): bump notify from 5.1.0 to 6.0.0
Bumps [notify](https://github.com/notify-rs/notify) from 5.1.0 to 6.0.0.
- [Release notes](https://github.com/notify-rs/notify/releases)
- [Changelog](https://github.com/notify-rs/notify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/notify-rs/notify/commits)

---
updated-dependencies:
- dependency-name: notify
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 17:08:20 +00:00
dependabot[bot]
8ce7f8cb80 build(deps): bump reqwest from 0.11.16 to 0.11.18
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.16 to 0.11.18.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.16...v0.11.18)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 17:08:16 +00:00
Jake Stanger
e709200242 Merge pull request #180 from JakeStanger/dependabot/cargo/chrono-0.4.26
build(deps): bump chrono from 0.4.25 to 0.4.26
2023-06-05 18:06:51 +01:00
Jake Stanger
f8a2c0f002 Merge pull request #179 from JakeStanger/dependabot/cargo/wayland-client-0.30.2
build(deps): bump wayland-client from 0.30.1 to 0.30.2
2023-06-05 18:06:29 +01:00
Jake Stanger
3b18e1d8a1 Merge pull request #178 from JakeStanger/dependabot/cargo/regex-1.8.4
build(deps): bump regex from 1.8.3 to 1.8.4
2023-06-05 17:47:45 +01:00
dependabot[bot]
f37bc80292 build(deps): bump chrono from 0.4.25 to 0.4.26
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.25 to 0.4.26.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.25...v0.4.26)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 15:04:42 +00:00
dependabot[bot]
e0bc05acb5 build(deps): bump wayland-client from 0.30.1 to 0.30.2
Bumps [wayland-client](https://github.com/smithay/wayland-rs) from 0.30.1 to 0.30.2.
- [Release notes](https://github.com/smithay/wayland-rs/releases)
- [Changelog](https://github.com/Smithay/wayland-rs/blob/master/historical_changelog.md)
- [Commits](https://github.com/smithay/wayland-rs/compare/wayland-client-v0.30.1...wayland-client-v0.30.2)

---
updated-dependencies:
- dependency-name: wayland-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 15:04:15 +00:00
dependabot[bot]
5a675153b4 build(deps): bump regex from 1.8.3 to 1.8.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.8.3...1.8.4)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-05 15:04:00 +00:00
Jake Stanger
090a6669b8 Merge pull request #177 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-06-01 10:31:10 +01:00
github-actions[bot]
80655883ef flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b' (2023-04-29)
  → 'github:nixos/nixpkgs/5e871d8aa6f57cc8e0dc087d1c5013f6e212b4ce' (2023-05-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/1be440e9119e69b68151cd9c84876ff3063a2e45' (2023-04-30)
  → 'github:oxalica/rust-overlay/9651f0beee6e7a9783cc02eac722854851c65ae7' (2023-05-31)
2023-06-01 01:16:59 +00:00
Jake Stanger
ce015f8d19 Merge pull request #175 from JakeStanger/dependabot/cargo/tracing-subscriber-0.3.17
build(deps): bump tracing-subscriber from 0.3.16 to 0.3.17
2023-05-29 16:35:58 +01:00
dependabot[bot]
5a09e46b28 build(deps): bump tracing-subscriber from 0.3.16 to 0.3.17
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.16 to 0.3.17.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.16...tracing-subscriber-0.3.17)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-29 15:25:15 +00:00
Jake Stanger
0fce762eef Merge pull request #174 from JakeStanger/dependabot/cargo/tokio-1.28.2
build(deps): bump tokio from 1.27.0 to 1.28.2
2023-05-29 16:24:31 +01:00
Jake Stanger
0a6da15bb2 Merge pull request #173 from JakeStanger/dependabot/cargo/chrono-0.4.25
build(deps): bump chrono from 0.4.24 to 0.4.25
2023-05-29 16:23:45 +01:00
Jake Stanger
19d1414daa Merge pull request #172 from JakeStanger/dependabot/cargo/sysinfo-0.29.0
build(deps): bump sysinfo from 0.28.4 to 0.29.0
2023-05-29 16:23:09 +01:00
dependabot[bot]
4456bb5d20 build(deps): bump tokio from 1.27.0 to 1.28.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.27.0 to 1.28.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.27.0...tokio-1.28.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-29 15:05:45 +00:00
dependabot[bot]
942d401472 build(deps): bump chrono from 0.4.24 to 0.4.25
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.24 to 0.4.25.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.24...v0.4.25)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-29 15:05:30 +00:00
dependabot[bot]
952fef270e build(deps): bump sysinfo from 0.28.4 to 0.29.0
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.28.4 to 0.29.0.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-29 15:05:13 +00:00
Jake Stanger
a5ecb363fd fix: popups occasionally getting jumbled with multiple bars 2023-05-29 14:01:42 +01:00
Jake Stanger
e036ff03c1 Merge pull request #165 from JakeStanger/dependabot/cargo/regex-1.8.3
build(deps): bump regex from 1.7.3 to 1.8.3
2023-05-26 22:53:23 +01:00
dependabot[bot]
6c48d40e5b build(deps): bump regex from 1.7.3 to 1.8.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.7.3 to 1.8.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.7.3...1.8.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 21:00:06 +00:00
Jake Stanger
a0881fc909 Merge pull request #164 from JakeStanger/dependabot/cargo/glib-0.17.9
build(deps): bump glib from 0.17.8 to 0.17.9
2023-05-26 21:59:43 +01:00
Jake Stanger
d938298e7b Merge pull request #163 from JakeStanger/dependabot/cargo/dirs-5.0.1
build(deps): bump dirs from 5.0.0 to 5.0.1
2023-05-26 21:59:19 +01:00
Jake Stanger
e18fb0661d Merge pull request #162 from JakeStanger/dependabot/cargo/zbus-3.13.1
build(deps): bump zbus from 3.11.1 to 3.13.1
2023-05-26 21:58:56 +01:00
Jake Stanger
6950c79906 Merge pull request #161 from JakeStanger/dependabot/cargo/serde-1.0.163
build(deps): bump serde from 1.0.160 to 1.0.163
2023-05-26 21:58:41 +01:00
dependabot[bot]
c4af6a8069 build(deps): bump glib from 0.17.8 to 0.17.9
Bumps [glib](https://github.com/gtk-rs/gtk-rs-core) from 0.17.8 to 0.17.9.
- [Release notes](https://github.com/gtk-rs/gtk-rs-core/releases)
- [Changelog](https://github.com/gtk-rs/gtk-rs-core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gtk-rs/gtk-rs-core/compare/0.17.8...0.17.9)

---
updated-dependencies:
- dependency-name: glib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 18:50:14 +00:00
dependabot[bot]
7ede33c9c1 build(deps): bump dirs from 5.0.0 to 5.0.1
Bumps [dirs](https://github.com/soc/dirs-rs) from 5.0.0 to 5.0.1.
- [Commits](https://github.com/soc/dirs-rs/commits)

---
updated-dependencies:
- dependency-name: dirs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 18:50:02 +00:00
dependabot[bot]
7f0fdf2391 build(deps): bump zbus from 3.11.1 to 3.13.1
Bumps [zbus](https://github.com/dbus2/zbus) from 3.11.1 to 3.13.1.
- [Release notes](https://github.com/dbus2/zbus/releases)
- [Commits](https://github.com/dbus2/zbus/compare/zbus-3.11.1...zbus-3.13.1)

---
updated-dependencies:
- dependency-name: zbus
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 18:49:51 +00:00
dependabot[bot]
807a31bf92 build(deps): bump serde from 1.0.160 to 1.0.163
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.163.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.160...v1.0.163)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 18:49:37 +00:00
Jake Stanger
e1b0c9b43d ci: add dependabot job 2023-05-26 19:48:55 +01:00
Jake Stanger
c3e9654cd3 feat(upower): icon size option
Adds missing `icon_size` config option to upower module.
2023-05-26 19:41:02 +01:00
Jake Stanger
e6a70f7663 Merge pull request #159 from JakeStanger/fix/upower-icon
Upower icon fixes
2023-05-26 19:39:53 +01:00
Jake Stanger
b4d7344200 Merge pull request #155 from JakeStanger/fix/nerd-icons
fix: broken nerd font icons
2023-05-26 19:00:22 +01:00
Jake Stanger
a6b686624b fix(upower): icon outside button
This moves the upower battery icon to inside the button,
moving it before the label for consistency.
2023-05-26 18:58:30 +01:00
Jake Stanger
b9740cba8f fix: upower icon too large 2023-05-26 18:40:52 +01:00
Jake Stanger
1f980ca783 Merge pull request #156 from JakeStanger/fix/mpris-visibility
fix(music): showing when no mpris player found
2023-05-21 20:57:59 +01:00
Jake Stanger
48d6af0281 fix(music): showing when no mpris player found
THe music module expects an event to be sent on subscription to the
client to set up the initial state. This ensures an event is sent when
in MPRIS mode when no player is initially found.
2023-05-21 20:44:31 +01:00
Jake Stanger
22b630a10b fix: broken nerd font icons
Fixes #152.
2023-05-21 14:04:18 +01:00
Jake Stanger
5877f773aa Merge pull request #154 from JakeStanger/fix/image-errors
fix: poor error handling for missing images
2023-05-20 15:29:40 +01:00
Jake Stanger
87ca399220 fix: poor error handling for missing images
Previously images that could not be located were handled by throwing a
full report error, which incorrectly stated it was an invalid image
*type*.

This changes the image handling to instead log a single-line warning
directly in the image provider code, reducing the error handling
required by each consumer.

Resolves #146.
2023-05-20 14:38:39 +01:00
Jake Stanger
960da55a05 Merge pull request #153 from JakeStanger/fix/excess-windows
fix: excess popup windows
2023-05-20 13:49:29 +01:00
Jake Stanger
0e65f93a23 fix: excess popup windows
Previously a popup window was created for each section of each bar.
This reduces it to a single popup window per bar.

Fixes #150.
2023-05-20 13:20:55 +01:00
Jake Stanger
91ed1ee384 Merge pull request #144 from JakeStanger/fix/hidpi-image
fix(image): still blurry on hidpi
2023-05-09 12:01:25 +01:00
Jake Stanger
9012feee4f fix(image): still blurry on hidpi
Adds in proper HiDPI image support into the image provider, using calls to Cairo.

Resolves #96 at last! (I hope...)
2023-05-08 16:12:01 +01:00
Jake Stanger
3b54d527b2 Merge pull request #143 from JakeStanger/docs/readme-overhaul
README Overhaul / Contribution Guidelines Update
2023-05-08 14:51:32 +01:00
Jake Stanger
242b70ed39 docs(contributing): enforce conventional commits 2023-05-08 14:13:33 +01:00
Jake Stanger
bd144e87a8 docs(readme): make prettier 2023-05-08 14:13:33 +01:00
Jake Stanger
3ccb54b49c Merge pull request #136 from JakeStanger/fix/systemd-scripts
fix: scripts don't work while running ironbar under a systemd service
2023-05-07 21:39:45 +01:00
Jake Stanger
ff315ff5db docs(music): fix incorrect type for host/music_dir options 2023-05-07 17:53:15 +01:00
Jake Stanger
cdeafbdc72 docs(sys info): add typical temperature sensors for intel/amd cpus 2023-05-07 17:50:17 +01:00
Jake Stanger
13d39235ad docs(examples): fix casing of steam in launcher favourites 2023-05-07 16:52:05 +01:00
Jake Stanger
327e345630 docs(examples): fix css button styles 2023-05-07 16:25:49 +01:00
Jake Stanger
f82f897982 fix(upower): popup always empty 2023-05-07 16:13:32 +01:00
Jake Stanger
fe12251af8 chore: bump version to 1.12.1 2023-05-07 16:09:39 +01:00
JakeStanger
d116a51083 docs: update CHANGELOG.md for v0.12.0 [skip ci] 2023-05-06 12:38:37 +00:00
Jake Stanger
6836abefd1 chore(release): v0.12.0 2023-05-06 13:37:22 +01:00
Jake Stanger
91e766d6ab Merge pull request #140 from JakeStanger/feat/better-styles
[BREAKING] module-level `name` and `class` options
2023-05-06 13:36:27 +01:00
Jake Stanger
dea66415c2 feat: module-level name and class options
BREAKING CHANGE: To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.

Resolves #75.
2023-05-06 13:22:35 +01:00
Jake Stanger
528a8d6dd6 Merge pull request #130 from JakeStanger/refactor/wayland-0.30
Update Wayland libraries
2023-05-05 22:41:31 +01:00
Jake Stanger
e1abadcf39 fix(clipboard): copying large images filling write pipe
Fixes partially #86
2023-05-05 22:30:16 +01:00
Jake Stanger
cf32870f8a docs(compiling): add ron feature flag 2023-05-05 22:29:26 +01:00
Jake Stanger
139bc5d23f docs(compiling): improve requirements list 2023-05-05 22:29:16 +01:00
Jake Stanger
735f5cc9f1 fix(launcher): crash when focusing window
Fixes #41 🎉
2023-05-04 20:07:46 +01:00
Jake Stanger
aed04c1ccf chore: add trace logging for mutex locks 2023-05-04 20:07:46 +01:00
Jake Stanger
c1ea5fad7e feat(logging): include line numbers 2023-05-04 20:07:46 +01:00
Jake Stanger
38da59cd41 refactor: fix a few pedantic clippy warnings 2023-05-04 20:07:46 +01:00
Jake Stanger
7f46cb4976 refactor(wayland): update to 0.30.0
This is pretty much a rewrite of the Wayland client code for `wayland-client` and `wayland-protocols` v0.30.0, and `smithay-client-toolkit` v0.17.0
2023-05-04 20:07:42 +01:00
Jake Stanger
5c18ec8ba0 Merge pull request #138 from JakeStanger/build/ron-support
build: enable support for `ron` config lang
2023-05-03 21:49:39 +01:00
Jake Stanger
81acc176ed build: enable support for ron config lang 2023-05-03 20:15:37 +01:00
Jake Stanger
618b7ef552 docs: improve example css 2023-05-02 23:08:49 +01:00
Jake Stanger
2a155b9aa8 feat(music): add css selector for button contents 2023-05-02 23:08:13 +01:00
Jake Stanger
31a57ae637 fix: scripts don't work while running ironbar under a systemd service
Fixes #134
2023-05-01 20:57:08 +01:00
Jake Stanger
bc87c7f0d4 chore: fix docs typo 2023-05-01 14:17:21 +01:00
Jake Stanger
bde469816a Merge pull request #135 from JakeStanger/oknozor/master
fix: fallback to default icon theme for notifier items
2023-05-01 14:11:05 +01:00
Paul Delafosse
98aaaa0d14 fix: fallback to default icon theme for notifier items 2023-05-01 14:10:43 +01:00
Jake Stanger
51e95d9e01 Merge remote-tracking branch 'origin/master' 2023-05-01 13:31:31 +01:00
Jake Stanger
ea9f7caaf7 docs: add missing upower feature flag 2023-05-01 13:30:57 +01:00
Jake Stanger
338829e275 Merge pull request #133 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-05-01 13:26:08 +01:00
Jake Stanger
610c3528af docs: add missing link to upower page 2023-05-01 13:24:38 +01:00
github-actions[bot]
f95e1e8f74 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/e3652e0735fbec227f342712f180f4f21f0594f2' (2023-03-30)
  → 'github:nixos/nixpkgs/08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b' (2023-04-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/aa480d799023141e1b9e5d6108700de63d9ad002' (2023-03-31)
  → 'github:oxalica/rust-overlay/1be440e9119e69b68151cd9c84876ff3063a2e45' (2023-04-30)
• Updated input 'rust-overlay/flake-utils':
    'github:numtide/flake-utils/c0e246b9b83f637f4681389ecabcb2681b4f3af0' (2022-08-07)
  → 'github:numtide/flake-utils/cfacdce06f30d2b68473a46042957675eebb3401' (2023-04-11)
• Added input 'rust-overlay/flake-utils/systems':
    'github:nix-systems/default/da67096a3b9bf56a91d16901293e51ba5b49a27e' (2023-04-09)
2023-05-01 00:58:57 +00:00
Jake Stanger
35dfbbf91d Merge pull request #132 from JakeStanger/fix/multiple-instances
fix: bars duplicate when starting second instance
2023-04-30 19:55:25 +01:00
Jake Stanger
14b6c1a69f fix: bars duplicate when starting second instance
This ensures that starting `ironbar` while an instance already running causes the 2nd instance to cleanly exit, and avoids launching the init code a second time.
2023-04-30 19:43:58 +01:00
Jake Stanger
0e3102de8c Merge pull request #83 from p00f/upower-string
implement upower module
2023-04-30 00:26:34 +01:00
Chinmay Dalal
ad3c171eca feat: implement upower module 2023-04-30 00:15:04 +01:00
Jake Stanger
e5bc44168f Merge pull request #125 from JakeStanger/feat/custom-slider-label
feat(custom): option to toggle slider label
2023-04-23 17:35:40 +01:00
Jake Stanger
cc62927f15 Merge pull request #124 from JakeStanger/feat/music-status-icon
feat(music): option to hide status icon on widget
2023-04-23 17:35:19 +01:00
Jake Stanger
76e2b7ba3e feat(music): option to hide status icon on widget
Adds new `show_status_icon` option.

Resolves #97.
2023-04-23 13:00:37 +01:00
Jake Stanger
033d0f7e6e feat(custom): option to toggle slider label
Adds new `show_label` option.

Resolves #115 (for real this time).
2023-04-23 12:59:55 +01:00
Jake Stanger
dc16b1e15a Merge pull request #122 from yavko/fix-nix-pixbuf-loader
Attempt to fix image blurriness on nix
2023-04-23 11:10:04 +01:00
Jake Stanger
03cd263095 Merge pull request #121 from JakeStanger/fix/icon-scale
fix(image): not scaling images for hidpi
2023-04-23 11:07:49 +01:00
Jake Stanger
db0868a3fc fix(image): not scaling icons for hidpi 2023-04-23 11:07:19 +01:00
Yavor Kolev
0382b50cf4 Merge branch 'JakeStanger:master' into fix-nix-pixbuf-loader 2023-04-22 16:49:19 -07:00
yavko
338f5a0e1b fix(nix): Attempt to fix image blurriness 2023-04-22 16:47:04 -07:00
Jake Stanger
20949a7744 Merge pull request #120 from JakeStanger/feat/icon-sizes
feat: ability to configure image icon sizes
2023-04-22 22:35:21 +01:00
Jake Stanger
2da28b9bf5 feat: ability to configure image icon sizes
Adds `icon_size` option to following widgets:

- `clipboard`
- `launcher`
- `music`
- `workspaces`

Also adds `cover_image_size` option to `music`.
2023-04-22 22:22:49 +01:00
Jake Stanger
618e97f1e8 Merge pull request #119 from JakeStanger/feat/slider-step
Custom slider widget step option
2023-04-22 21:39:50 +01:00
Jake Stanger
dd7c9f30db docs: add transition module-level options 2023-04-22 21:29:47 +01:00
Jake Stanger
1fa0c0e977 feat(custom): support mouse wheel on slider 2023-04-22 21:29:47 +01:00
Jake Stanger
74d18aedfb Merge pull request #118 from JakeStanger/fix/dynamic-string
fix(dynamic string): crash when last segment is static and a single char
2023-04-22 16:40:33 +01:00
Jake Stanger
2c88c99cb6 fix(dynamic string): crash when last segment is static and a single char
Resolves #117.
2023-04-22 16:29:54 +01:00
Jake Stanger
236bb09170 Merge pull request #116 from JakeStanger/feat/revealer
feat: wrap modules in a revealer to support animated show/hide
2023-04-22 15:24:20 +01:00
Jake Stanger
83f44fd92f feat: wrap modules in a revealer to support animated show/hide
Resolves #72.
2023-04-22 14:49:15 +01:00
Jake Stanger
1855416db4 Merge pull request #114 from JakeStanger/feat/common-in-custom
feat(custom): support common options in widgets
2023-04-22 13:48:08 +01:00
Jake Stanger
e63509a3a7 refactor: fix a few new clippy warnings 2023-04-22 13:45:44 +01:00
Jake Stanger
4a09b70854 feat(custom): support common options in widgets 2023-04-22 13:34:39 +01:00
Jake Stanger
9d09855fce Merge pull request #109 from JakeStanger/fix/tray-icons
fix(tray): icons sometimes not showing
2023-04-22 11:01:06 +01:00
Jake Stanger
e9d0273176 Merge pull request #113 from yavko/fix-nix-run
Fix `nix run` support
2023-04-22 10:47:37 +01:00
yavko
7926bb07eb fix(nix): Fix nix run support 2023-04-21 21:43:55 -07:00
Jake Stanger
6fd69d657c refactor: move module creation code to module module 2023-04-21 23:51:54 +01:00
Jake Stanger
27d11de661 refactor(config): split common code into separate file 2023-04-21 23:51:29 +01:00
Jake Stanger
07df51c249 docs: include readme in rust docs 2023-04-21 23:50:49 +01:00
Jake Stanger
b038e7671a fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-21 23:02:53 +01:00
Jake Stanger
e5ab9f33b5 Merge remote-tracking branch 'origin/fix/tray-icons' into fix/tray-icons 2023-04-21 22:33:59 +01:00
Jake Stanger
68bc8230dd fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-21 22:31:09 +01:00
Jake Stanger
246313136f Merge pull request #111 from JakeStanger/fix/script-parsing
fix(script): parser incorrectly handling colons
2023-04-21 20:37:13 +01:00
Jake Stanger
15a9d8d42c fix(script): parser incorrectly handling colons
The short input parser was previously splitting colons, and incorrectly handling situations where the `cmd` section contained colons. The parser now properly checks input in the `mode:interval:cmd` format, moving onto the next section regardless of whether the previous was found.

This means unless your script literally starts with `poll:` or `5000:` you won't hit this issue anymore.
2023-04-20 21:59:23 +01:00
Jake Stanger
a87d8d5c30 fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-16 21:37:47 +01:00
Jake Stanger
8e99fd4d0f chore(system tray): add debug logging 2023-04-16 19:55:17 +01:00
Jake Stanger
1e1d65ae49 chore(script): add debug logging 2023-04-13 12:47:26 +01:00
Jake Stanger
2815cef440 Merge pull request #106 from JakeStanger/feat/custom-dynamic-image
Custom image dynamic src support
2023-04-10 20:17:58 +01:00
Jake Stanger
138b5b3903 docs(custom): fix potential error in progress example 2023-04-10 20:05:37 +01:00
Jake Stanger
7355db74ec fix(image): http provider not handling non-success codes 2023-04-10 20:05:13 +01:00
Jake Stanger
c214f65ecb refactor: fix strict clippy warnings 2023-04-10 20:04:59 +01:00
Jake Stanger
3d308ab572 feat(custom): support dynamic string in image source
Resolves #94.
2023-04-10 20:04:36 +01:00
Jake Stanger
b770ae716c Merge pull request #104 from JakeStanger/feat/custom-widgets
Custom module improvements
2023-04-10 14:02:42 +01:00
Jake Stanger
3613aef5c5 refactor(custom): reduce a lot of repeated code 2023-04-10 13:51:07 +01:00
Jake Stanger
a9d1233909 feat(custom): support dynamic strings on buttons 2023-04-10 13:49:09 +01:00
Jake Stanger
72b14b6c4e feat(custom): progress bar widget.
Resolves partially #68.
2023-04-10 12:59:24 +01:00
Jake Stanger
910945306c fix(dynamic string): parser issue related to incorrectly matching braces 2023-04-10 00:17:09 +01:00
Jake Stanger
dfe1964abf feat(custom): slider widget
Resolves partially #68.
2023-04-10 00:17:09 +01:00
Jake Stanger
e928b30f99 docs(custom): rewrite widget options to be clearer 2023-04-10 00:16:44 +01:00
Jake Stanger
2ab06f044e refactor(custom): split into enum with separate file per widget 2023-04-07 20:24:41 +01:00
Jake Stanger
4b4f1ffc21 Merge pull request #103 from JakeStanger/feat/popup-gap-config
feat: ability to configure popup gap
2023-04-07 15:02:58 +01:00
Jake Stanger
0691db3b87 Merge pull request #102 from JakeStanger/feat/labels
feat: new label module
2023-04-07 14:53:56 +01:00
Jake Stanger
cac064f479 feat: ability to configure popup gap 2023-04-07 14:53:18 +01:00
Jake Stanger
6c622864b3 feat: new label module
Takes a text label, with the ability to include embedded scripts.

Resolves #80.
2023-04-07 14:29:07 +01:00
Jake Stanger
55c06c4766 chore: bash script for regenerating examples 2023-04-07 14:26:17 +01:00
JakeStanger
1b0287becc docs: update CHANGELOG.md for v0.11.0 [skip ci] 2023-04-01 17:44:26 +00:00
Jake Stanger
7bf44ca75d chore(release): v0.11.0 2023-04-01 18:36:24 +01:00
Jake Stanger
fb04ceab7d Merge pull request #95 from JakeStanger/feat/module-hover
feat: module hover options
2023-04-01 18:17:52 +01:00
Jake Stanger
102d2478a9 feat: module hover options
Resolves #70.
2023-04-01 13:29:40 +01:00
Jake Stanger
80a414ab67 build: update deps
Resolves #93
2023-04-01 13:12:26 +01:00
Jake Stanger
72ba17add3 Merge pull request #92 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-04-01 11:14:03 +01:00
github-actions[bot]
2b07620847 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/7f5639fa3b68054ca0b062866dc62b22c3f11505' (2023-02-26)
  → 'github:nixos/nixpkgs/e3652e0735fbec227f342712f180f4f21f0594f2' (2023-03-30)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c1df023b1aaded1b65a1f4ad604a98a58ab4db97' (2023-02-28)
  → 'github:oxalica/rust-overlay/aa480d799023141e1b9e5d6108700de63d9ad002' (2023-03-31)
2023-04-01 00:57:22 +00:00
Jake Stanger
ba488ad38f Merge pull request #89 from yavko/fix-hm-module
Fix home manager module, and features
2023-03-29 12:51:55 +01:00
yavko
d0b7bdbafc fix(nix): home manager module, and features 2023-03-29 01:45:40 -07:00
Jake Stanger
0f5ec1fe34 Merge pull request #85 from JakeStanger/refactor/config
Use `universal-config` crate for config
2023-03-19 16:37:20 +00:00
Jake Stanger
6221f7454a refactor: fix new clippy warnings 2023-03-19 16:22:40 +00:00
Jake Stanger
ecdd71a43d refactor(config): use universal-config crate.
XML config is not supported.
2023-03-19 16:22:40 +00:00
Jake Stanger
01a36a9476 build: update gtk deps 2023-03-19 00:14:59 +00:00
Jake Stanger
d4dd8c41ea chore: improve image provider logging 2023-03-04 23:13:35 +00:00
Jake Stanger
83c5dceaa7 chore: clean up println calls 2023-03-04 23:13:22 +00:00
Jake Stanger
711644e190 Merge pull request #81 from JakeStanger/fix/dynamic-string-ordering
Fix dynamic string ordering
2023-03-01 23:20:41 +00:00
Jake Stanger
8cbb73b75e fix(dynamic string): dynamic sections not respecting ordering
Fixes #69.
2023-03-01 23:09:34 +00:00
Jake Stanger
7212bbcf61 refactor(dynamic string): use vec instead of indexmap 2023-03-01 23:09:01 +00:00
Jake Stanger
0125ce5916 docs(examples): update styles example 2023-03-01 20:35:41 +00:00
Jake Stanger
2b26eaf410 docs(clipboard): fix incorrect setting description 2023-03-01 20:35:31 +00:00
Jake Stanger
33676fc4dc ci(nix): fix cachix error 2023-03-01 20:35:12 +00:00
Jake Stanger
7978c48d5c Merge pull request #79 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-03-01 12:55:52 +00:00
Jake Stanger
1d37e010c8 Merge pull request #66 from yavko/add-nix-flags
Add initial nix flags impl
2023-03-01 12:55:25 +00:00
yavko
54b9b28c75 fix: make readme more concise 2023-02-28 19:11:18 -08:00
yavko
3a44d74cf3 style(nix): fmt flake.nix 2023-02-28 19:08:43 -08:00
yavko
b1475a1aff feat(nix): use cargo default features 2023-02-28 19:07:12 -08:00
yavko
b2749fee92 style(nix): fmt flake.nix 2023-02-28 19:07:03 -08:00
yavko
9984b638b5 feat(nix): initial nix feature flags impl 2023-02-28 19:06:53 -08:00
github-actions[bot]
207b60db7e flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/2caf4ef5005ecc68141ecb4aac271079f7371c44' (2023-01-30)
  → 'github:nixos/nixpkgs/7f5639fa3b68054ca0b062866dc62b22c3f11505' (2023-02-26)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/48b1403150c3f5a9aeee8bc4c77c8926f29c6501' (2023-01-31)
  → 'github:oxalica/rust-overlay/c1df023b1aaded1b65a1f4ad604a98a58ab4db97' (2023-02-28)
2023-03-01 01:09:08 +00:00
Jake Stanger
7779c33e0c Merge pull request #77 from JakeStanger/feat/clipboard-manager
Clipboard manager module
2023-02-28 17:54:06 +00:00
Jake Stanger
575d6cc30f feat: new clipboard manager module 2023-02-26 13:42:53 +00:00
Jake Stanger
5bbe64bb86 docs(clock): format table 2023-02-25 14:29:38 +00:00
Jake Stanger
83a49165c4 docs(compiling): add info about build deps 2023-02-25 14:29:38 +00:00
Jake Stanger
d84139a914 refactor: general tidy up
fix clippy warnings from latest stable rust
2023-02-25 14:26:02 +00:00
Jake Stanger
ca4fe422f2 feat(truncate): ability to set fixed length
BREAKING CHANGE: This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
2023-02-25 14:26:02 +00:00
Jake Stanger
1ad1961396 Merge pull request #67 from ttoino/feature/margin
Add configurable margins around bar
2023-02-08 19:42:15 +00:00
toino
d253c4bd7f feat: add configurable margins around bar 2023-02-08 18:47:21 +00:00
Jake Stanger
fbee6e8bd4 style: run fmt 2023-02-08 17:30:09 +00:00
Jake Stanger
7c36f5cb0c docs: fix a couple of issues 2023-02-02 20:37:16 +00:00
Jake Stanger
7dff3e6f8b fix(image): widgets missing names 2023-02-02 20:37:02 +00:00
Jake Stanger
2ac507144b fix: not setting layer shell namespace 2023-02-02 20:36:31 +00:00
JakeStanger
82875cde68 docs: update CHANGELOG.md for v0.10.0 [skip ci] 2023-02-01 22:22:19 +00:00
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
Jake Stanger
c906dd40fb chore(release): v0.9.0 2023-01-28 14:49:53 +00:00
Jake Stanger
eb30105fc2 style: fix 1.67 clippy warnings 2023-01-28 14:40:31 +00:00
Jake Stanger
90cd078973 fix(mpd): stops working if connection lost
The client will now attempt to reconnect when a connection loss is detected.

Fixes #21.
2023-01-28 14:40:12 +00:00
Jake Stanger
1cdfebf8db Merge pull request #53 from JakeStanger/feat/hyprland-workspaces
feat(workspaces): hyprland support
2023-01-28 00:53:23 +00:00
Jake Stanger
0cefcbd02b fix(music): wrong widget name on vol slider 2023-01-28 00:51:24 +00:00
Jake Stanger
08cfbbc2ea fix(music): unable to go to prev with mpris 2023-01-28 00:43:02 +00:00
Jake Stanger
e1f523cf2a fix(music): popup artist label using wrong name 2023-01-28 00:27:22 +00:00
Jake Stanger
c223892a57 docs(workspaces): update for hyprland/new ordering option 2023-01-27 23:18:59 +00:00
Jake Stanger
9ba28fe7fa feat(workspaces): better ordering
Includes option to revert to previous (lack of) ordering method if preferred.
2023-01-27 23:18:59 +00:00
Jake Stanger
0d7ab54160 refactor: remove redundant clone 2023-01-27 23:18:59 +00:00
Jake Stanger
6e5d0c1e8c feat(workspaces): hyprland support
Resolves #18.

The bar will now automatically detect whether running under Sway or Hyprland and use the correct IPC client depending.
2023-01-27 23:18:59 +00:00
Jake Stanger
a79900d842 Merge pull request #52 from JakeStanger/feat/mpris
feat: mpris support
2023-01-25 23:20:31 +00:00
Jake Stanger
6d8e647f12 feat: mpris support
Resolves #25.

Completely refactors the MPD module to be the 'music' module. This now supports both MPD and MPRIS with the same UI for both.

BREAKING CHANGE: 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.
2023-01-25 23:09:49 +00:00
Jake Stanger
1949d07721 chore(github): update issue templates 2023-01-04 17:36:19 +00:00
Jake Stanger
f779520545 Merge pull request #48 from colemickens/master
s/pkgs.system/pkgs.hostPlatform.system/g
2023-01-03 21:30:35 +00:00
Cole Mickens
df7c447e9c s/pkgs.system/pkgs.hostPlatform.system/g 2023-01-02 23:52:06 -08:00
Jake Stanger
90b9d70941 Merge pull request #47 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-01-01 12:07:52 +00:00
github-actions[bot]
da806d38c6 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/e76c78d20685a043d23f5f9e0ccd2203997f1fb1' (2022-11-30)
  → 'github:nixos/nixpkgs/677ed08a50931e38382dbef01cba08a8f7eac8f6' (2022-12-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/bfdf688742cf984c4837dbbe1c6cbca550365613' (2022-12-01)
  → 'github:oxalica/rust-overlay/176b6fd3dd3d7cea8d22ab1131364a050228d94c' (2022-12-31)
2023-01-01 01:06:03 +00:00
Jake Stanger
8076412bfc Merge pull request #46 from JakeStanger/feat/mouse-events
feat: mouse event config options
2022-12-15 22:10:40 +00:00
Jake Stanger
fa67d077b1 feat: mouse event config options
Adds `on_click_middle`, `on_click_right`, `on_scroll_up`, `on_scroll_down`.

BREAKING CHANGE: `on_click` is now called `on_click_left` for consistency with new options.

Resolves #44.
2022-12-15 21:37:08 +00:00
Jake Stanger
b2afe78c07 Merge pull request #45 from JakeStanger/feat/config-loading-improvements
Feat/config loading improvements
2022-12-13 18:47:07 +00:00
Jake Stanger
1dd5863431 feat: better surface some config error messages
Resolves #39
2022-12-12 23:28:49 +00:00
Jake Stanger
0a341f6673 build: update libcorn 2022-12-12 23:15:57 +00:00
Jake Stanger
bb81f8e583 Merge pull request #43 from JakeStanger/refactor/general
General refactoring and tidy-up
2022-12-12 19:56:34 +00:00
Jake Stanger
a45ebfc1f5 build: update dependencies 2022-12-11 23:30:42 +00:00
Jake Stanger
ea2c84d1bd refactor: general code tidy-up 2022-12-11 23:17:15 +00:00
Jake Stanger
5e21cbcca6 refactor: macros to reduce repeated code 2022-12-11 22:45:52 +00:00
Jake Stanger
9d5049dde0 refactor: standardise error messages 2022-12-11 21:31:45 +00:00
Jake Stanger
fd2d7e5c7a refactor: move startup logging code to logging module 2022-12-11 21:31:23 +00:00
Jake Stanger
2c1b2924d4 refactor: move most of the horrible add_module macro content into proper functions 2022-12-04 23:23:22 +00:00
Jake Stanger
490f3f3f65 Merge pull request #40 from JakeStanger/update_flake_lock_action
Update flake.lock
2022-12-01 20:58:30 +00:00
github-actions[bot]
843e40ef45 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/af50806f7c6ab40df3e6b239099e8f8385f6c78b' (2022-11-21)
  → 'github:nixos/nixpkgs/e76c78d20685a043d23f5f9e0ccd2203997f1fb1' (2022-11-30)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/9652ef34c7439eca9f86cee11e94dbef5c9adb09' (2022-11-22)
  → 'github:oxalica/rust-overlay/bfdf688742cf984c4837dbbe1c6cbca550365613' (2022-12-01)
2022-12-01 20:57:34 +00:00
Jake Stanger
d8c60d9d47 ci(nix flake lock): fix invalid action version
whoops
2022-12-01 20:56:41 +00:00
JakeStanger
b97f018e81 docs: update CHANGELOG.md for v0.8.0 [skip ci] 2022-11-30 23:01:46 +00:00
Jake Stanger
c1e1743b5e chore(release): v0.8.0 2022-11-30 22:58:31 +00:00
Jake Stanger
37458642df ci(build): reverse order of fmt/clippy/build 2022-11-30 22:56:44 +00:00
Jake Stanger
862c46c7ec style: run rustfmt
d'oh
2022-11-30 22:49:49 +00:00
Jake Stanger
afedf0214d docs: add link to new custom power menu example 2022-11-30 22:42:31 +00:00
Jake Stanger
64f54040ef refactor: move dynamic_label.rs to dynamic_string.rs and fix failing test 2022-11-30 22:40:53 +00:00
Jake Stanger
d20972cb32 feat: dynamic tooltips
Resolves #36
2022-11-30 22:27:56 +00:00
Jake Stanger
1320639d4e docs: add custom power menu example 2022-11-30 22:09:46 +00:00
Jake Stanger
907a565f3d test(dynamic label): do not run if cannot initialise gtk 2022-11-28 22:46:32 +00:00
Jake Stanger
ec69649a04 docs: update example configs 2022-11-28 22:43:12 +00:00
Jake Stanger
c4cdf4be8b docs: update example configs 2022-11-28 22:30:32 +00:00
Jake Stanger
00f973c3a4 style: run rustfmt 2022-11-28 22:30:32 +00:00
Jake Stanger
5d153a02fc feat(custom): ability to embed scripts in labels for dynamic content
Fully resolves #34.
2022-11-28 22:30:31 +00:00
Jake Stanger
e274ba39cd refactor(custom): rename exec to on_click for consistency
BREAKING CHANGE: This changes the option on buttons in the `custom` module. Any uses of the module must be updated to use the new custom widget attribute name.
2022-11-28 22:26:14 +00:00
Jake Stanger
8c75bc46ac refactor(script): rename path to cmd for consistency
BREAKING CHANGE: This changes the option in the `script` module. Any uses of the module must be updated to use the new option name.
2022-11-28 22:25:14 +00:00
Jake Stanger
df77020c52 refactor(sys_info): use snake_case for module tokens for consistency
BREAKING CHANGE: This renames the module from `sys-info` to `sys_info`, and almost every formatting token from `kebab-case` to `snake_case`. Any use of this module will need to be updated.
2022-11-28 22:23:33 +00:00
Jake Stanger
0fb5fa8c2a refactor: use latest libcorn with serde support
This should speed Corn config loading up a bit :)
2022-11-28 22:23:11 +00:00
Jake Stanger
cf87bb4e8d ci(build): run cargo tests 2022-11-28 22:21:47 +00:00
Jake Stanger
badfcc0c2d Merge pull request #38 from JakeStanger/feat/common-options
feat: common module options (`show_if`, `on_click`, `tooltip`)
2022-11-28 22:20:40 +00:00
Jake Stanger
c9e66d4664 feat: common module options (show_if, on_click, tooltip)
The first three of many options that are common to all modules.

Resolves #36. Resolves partially #34.
2022-11-28 22:09:18 +00:00
Yavor Kolev
a3f90adaf1 feat: add nix flake support
* Add nix flake

* Fix readme syntax issue

* Format nix flake

* ci(build): add nix flake support

* ci(build): fix workflow_dispatch casing

* ci(build): fix nix flake lock update job

* ci: add nix flake lock update timer job

old method didn't work

* improve example and add cachix info

Co-authored-by: Jake Stanger <mail@jstanger.dev>
2022-11-26 21:29:16 +00:00
Jake Stanger
47420d83bf Merge pull request #35 from JakeStanger/feat/script-watch
feat(script): new watch mode
2022-11-07 21:26:38 +00:00
Jake Stanger
4662f60ac5 refactor: move various clients to own folder 2022-11-06 23:38:51 +00:00
Jake Stanger
94693c92e3 ci(sync wiki): overhaul with bash script 2022-11-06 23:25:11 +00:00
Jake Stanger
8c774100f1 docs(script): add information on new mode options 2022-11-06 23:04:24 +00:00
Jake Stanger
b4db0226cd Merge branch 'master' into feat/script-watch 2022-11-06 22:55:16 +00:00
Jake Stanger
ff17ec1996 refactor: various changes based on rust 1.65 clippy 2022-11-06 22:53:48 +00:00
Jake Stanger
c48029664d docs(script): improve doc comment 2022-11-06 22:53:29 +00:00
Jake Stanger
58d55db660 docs: migrate wiki into main repo 2022-11-06 22:52:21 +00:00
Jake Stanger
73158c2fce feat(script): new watch mode
Resolves #30
2022-11-06 17:40:01 +00:00
JakeStanger
1c032ae8e3 docs: update CHANGELOG.md for v0.7.0 [skip ci] 2022-11-05 17:34:48 +00:00
Jake Stanger
3b04642148 chore(release): v0.7.0 2022-11-05 17:33:36 +00:00
Jake Stanger
0a331f3138 docs(readme): remove warning about outdated cargo package 2022-11-05 17:33:02 +00:00
Jake Stanger
bc625b929b refactor: clippy & fmt 2022-11-05 17:32:42 +00:00
Jake Stanger
ad77dc4e4c feat: improved logging & error handling 2022-11-05 17:32:29 +00:00
Jake Stanger
3a83bd31ab fix: able to insert duplicate keys into collection
This replaces the custom `Collection` implementation with `IndexMap` from the crate of the same name.

Fixes #28.
2022-11-05 17:32:01 +00:00
Jake Stanger
5ebc84c7b9 refactor(logging): consts for default log levels 2022-11-05 17:29:17 +00:00
Jake Stanger
51d1cd4a16 build: update deps 2022-11-01 22:56:47 +00:00
Jake Stanger
b7792a415e feat: env var to set custom css location
Set `IRONBAR_CSS` to load CSS from that path instead of regular path.
2022-11-01 13:25:46 +00:00
Jake Stanger
9f82ba58cd chore: cleanup println 2022-10-23 17:18:49 +01:00
Jake Stanger
a93700e8fd Merge pull request #27 from JakeStanger/feat/custom-widget
feat: new custom module
2022-10-23 17:11:17 +01:00
Jake Stanger
2a3fe33446 build: remove no longer required patch, reduce build times 2022-10-23 17:01:35 +01:00
Jake Stanger
3750124d8c feat: new custom module
Allows basic modules to be created from a config object, including popup content.
2022-10-23 17:01:35 +01:00
Jake Stanger
e693c1c166 fix(mpd): volume slider causing mpd server errors 2022-10-16 22:23:40 +01:00
Jake Stanger
cbd0c49e25 fix: css watcher not working 2022-10-16 22:21:51 +01:00
Jake Stanger
e23e691bc6 build: use latest release version of stray 2022-10-16 16:44:51 +01:00
Jake Stanger
be0f4c6366 chore: run fmt 2022-10-16 16:44:32 +01:00
Jake Stanger
493df6bb49 feat(mpd): add volume slider to popup 2022-10-16 16:00:18 +01:00
Jake Stanger
b4ac1c9850 Merge pull request #26 from JakeStanger/feat/sysinfo-tokens
Add loads more tokens & interval options to `sysinfo`, optimise info refreshing
2022-10-16 14:04:11 +01:00
Jake Stanger
27f6abad67 chore: format and fix clippy warnings 2022-10-16 13:54:48 +01:00
Jake Stanger
ec1d59677b feat(logging): IRONBAR_LOG and IRONBAR_FILE_LOG env vars 2022-10-16 13:42:59 +01:00
Jake Stanger
70e1b526a9 fix(logging): file log not capturing panics 2022-10-16 13:42:35 +01:00
Jake Stanger
3c43c20c6a fix: weird behaviour when config does not exist 2022-10-16 12:58:11 +01:00
Jake Stanger
b66bd788b2 fix: logging for creating bar incorrect still 2022-10-16 12:57:37 +01:00
Jake Stanger
f17ae7a415 fix(script): not parsing pango markup 2022-10-16 12:56:39 +01:00
Jake Stanger
a06c4bccca docs(examples): add full system info config 2022-10-16 01:03:36 +01:00
Jake Stanger
e4e72d8008 style(sys-info): fix clippy warnings & run fmt 2022-10-16 01:03:20 +01:00
Jake Stanger
9e6dbbd131 fix(sys-info): tokens not replaced if more than one in string 2022-10-16 01:00:43 +01:00
Jake Stanger
91c57edc73 feat(sys-info): pango markup support 2022-10-16 01:00:14 +01:00
Jake Stanger
dec402edd9 feat(sys-info): config options for refresh intervals 2022-10-16 00:59:28 +01:00
Jake Stanger
fad90fdad6 feat(sys-info): add loads more formatting tokens 2022-10-16 00:58:47 +01:00
Jake Stanger
35ce3b4d45 chore: use cargo patch block 2022-10-16 00:56:47 +01:00
Jake Stanger
27d04795af docs(readme): add warning about crate being outdated 2022-10-15 18:51:45 +01:00
JakeStanger
9d9c275313 docs: update CHANGELOG.md for v0.6.0 [skip ci] 2022-10-15 17:27:37 +00:00
Jake Stanger
0669504519 chore(release): v0.6.0 2022-10-15 18:26:51 +01:00
Jake Stanger
eb5170ff6a style: run fmt 2022-10-15 18:08:56 +01:00
Jake Stanger
b7b64886e3 fix: sometimes panicking on startup 2022-10-15 18:01:09 +01:00
Jake Stanger
75339f07ed fix: vertical bars ignoring height config option 2022-10-15 16:35:31 +01:00
Jake Stanger
06cfad62e2 feat: more positioning options (#23)
* feat: more positioning options

Can now display the bar on the left/right, and avoid anchoring to edges to centre the bar.

BREAKING CHANGE: The `left` and `right` config options have been renamed to `start` and `end`
2022-10-15 16:27:25 +01:00
Jake Stanger
1b853bcb71 refactor: fix clippy warning 2022-10-15 16:19:21 +01:00
Jake Stanger
bd5bdf5af5 fix: logging for creating bar incorrect 2022-10-15 15:36:23 +01:00
Jake Stanger
8536ad719a fix(mpd): incorrectly checking for unix sockets 2022-10-15 00:17:40 +01:00
Jake Stanger
006c242f49 style: run fmt 2022-10-15 00:09:38 +01:00
Jake Stanger
2cd59ef5ff build: fix compilation errors caused by package update 2022-10-15 00:07:10 +01:00
Jake Stanger
f411b7c451 chore: update lockfile 2022-10-14 23:53:13 +01:00
Jake Stanger
1dd0a9e52f feat(launcher): add popup css selectors 2022-10-14 23:49:11 +01:00
Jake Stanger
5523e9af46 fix(popup): often opening in wrong place
Fixes #16.
2022-10-14 23:48:28 +01:00
Jake Stanger
9e31107251 chore: update lockfile 2022-10-14 23:46:56 +01:00
Jake Stanger
668fe4a308 Merge pull request #24 from JakeStanger/feat/wayland-protocols
Implement Wayland protocol support
2022-10-14 22:24:57 +01:00
Jake Stanger
994d0f580b docs(readme): update references to sway 2022-10-14 22:14:04 +01:00
Jake Stanger
5ce50b0987 refactor: tidy and format 2022-10-10 21:59:44 +01:00
Jake Stanger
b1c66b9117 feat: wlroots-agnostic support for launcher module 2022-10-10 20:15:24 +01:00
Jake Stanger
bb4fe7f7f5 docs(readme): credit smithay client toolkit
Could not have got the Wayland stuff working without.
2022-10-04 23:26:26 +01:00
Jake Stanger
324f00cdf9 feat: wlroots-agnostic support for focused module 2022-10-04 23:26:07 +01:00
Jake Stanger
b188bc7146 feat: initial support for running outside sway
Progress is being tracked in #18. Currently the workspaces, focused and launcher modules are not supported.
2022-09-27 20:24:16 +01:00
Jake Stanger
d22d954e83 ci: alter changelog categories 2022-09-25 22:50:17 +01:00
Jake Stanger
45e44d7913 chore(intellij): update run configs 2022-09-25 22:50:05 +01:00
Jake Stanger
b352181b3d docs: update json example 2022-09-25 22:49:54 +01:00
Jake Stanger
720ba7bfb0 Major module refactor (#19)
* refactor: major module restructuring

Modules now implement a "controller", which allows for separation of logic from UI code and enforces a tighter structure around how modules should be written. The introduction of this change required major refactoring or even rewriting of all modules.

This also better integrates the popup into modules, making it easier for data to be passed around without fetching the same thing twice

The refactor also improves some client code, switching from `ksway` to the much more stable `swayipc-async`. Partial multi-monitor for the tray module has been added.

BREAKING CHANGE: The `mpd` module config has changed, moving the icons to their own object.
2022-09-25 22:49:00 +01:00
JakeStanger
daafa0943e docs: update CHANGELOG.md for v0.5.2 [skip ci] 2022-09-07 21:49:04 +00:00
Jake Stanger
b801751bda chore(release): v0.5.2 2022-09-07 22:47:57 +01:00
Jake Stanger
ee67b3be28 build: update deps
new corn version allows unicode keys :)
2022-09-07 22:47:47 +01:00
Jake Stanger
6442667961 docs(readme): fix links in install section 2022-09-06 22:50:27 +01:00
JakeStanger
68574d4327 docs: update CHANGELOG.md for v0.5.1 [skip ci] 2022-09-06 21:39:15 +00:00
Jake Stanger
6871126bd8 chore(release): v0.5.1 2022-09-06 22:38:16 +01:00
Jake Stanger
481adfcaa4 chore(intellij): update run configs 2022-09-06 22:37:55 +01:00
Jake Stanger
64650fbf3a Merge pull request #14 from JakeStanger/fix/launcher-state
Fix launcher state issues
2022-09-06 21:56:21 +01:00
Jake Stanger
a35d25520c fix(launcher): item state changes not handled correctly
This completely rewrites the item open state handling code (again) in a more logical way that should prevent incorrect states, and removes some locking issues.
2022-09-06 21:46:02 +01:00
Jake Stanger
78e30b39fe docs: add some rustdoc comments throughout 2022-08-28 16:57:41 +01:00
Jake Stanger
b81927e3a5 fix(launcher): opening new instances when focused/urgent 2022-08-25 22:08:08 +01:00
JakeStanger
5d319e91f2 docs: update CHANGELOG.md for v0.5.0 [skip ci] 2022-08-25 20:55:43 +00:00
Jake Stanger
015dcd3204 chore(release): v0.5.0 2022-08-25 21:54:32 +01:00
Jake Stanger
1e38719996 feat: introduce logging in some areas 2022-08-25 21:53:57 +01:00
Jake Stanger
6dcae66570 fix: avoid creating loads of sway/mpd clients 2022-08-25 21:53:42 +01:00
Jake Stanger
649b0efb19 style: run rustfmt 2022-08-24 21:27:30 +01:00
Jake Stanger
023c2fb118 fix(workspaces): not listening to move event 2022-08-24 21:27:19 +01:00
Jake Stanger
ea57f5e18d Merge remote-tracking branch 'origin/master' 2022-08-24 18:05:38 +01:00
Jake Stanger
53142d1bea ci(release): fix missing build deps [skip ci] 2022-08-22 23:13:45 +01:00
JakeStanger
7e0f2cad1c docs: update CHANGELOG.md for v0.4.0 [skip ci] 2022-08-22 22:09:27 +00:00
141 changed files with 18696 additions and 3425 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

50
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
> A clear and concise description of what you want to happen.
> The more info here about what you are trying to achieve, the better - there's likely more than one way to go about implementing a solution.
**Describe alternatives you've considered**
> A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
> Add any other context or screenshots about the feature request here.

10
.github/ISSUE_TEMPLATE/other.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Other
about: Any other issue type
title: ''
labels: ''
assignees: ''
---

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

43
.github/scripts/sync-wiki.sh vendored Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
set -eu
TEMP_REPO_DIR="wiki_action_$GITHUB_REPOSITORY$GITHUB_SHA"
TEMP_WIKI_DIR="temp_wiki_$GITHUB_SHA"
WIKI_DIR='docs'
if [ -z "$GH_TOKEN" ]; then
echo "Token is not specified"
exit 1
fi
#Clone repo
echo "Cloning repo https://github.com/$GITHUB_REPOSITORY"
git clone "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY" "$TEMP_REPO_DIR"
#Clone wiki repo
echo "Cloning wiki repo https://github.com/$GITHUB_REPOSITORY.wiki.git"
cd "$TEMP_REPO_DIR"
git clone "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.wiki.git" "$TEMP_WIKI_DIR"
#Get commit details
author='Jake Stanger'
email='mail@jstanger.dev'
message='action: sync wiki'
echo "Copying edited wiki"
cp -R "$TEMP_WIKI_DIR/.git" "$WIKI_DIR/"
echo "Checking if wiki has changes"
cd "$WIKI_DIR"
git config --local user.email "$email"
git config --local user.name "$author"
git add .
if git diff-index --quiet HEAD; then
echo "Nothing changed"
exit 0
fi
echo "Pushing changes to wiki"
git commit -m "$message" && git push "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.wiki.git"

View File

@@ -1,6 +1,7 @@
name: Build name: Build
on: on:
workflow_dispatch:
push: push:
branches: [ "master" ] branches: [ "master" ]
pull_request: pull_request:
@@ -21,17 +22,57 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps - name: Install build deps
run: 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: Build - name: Check formatting
run: cargo build --verbose 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 uses: actions-rs/clippy-check@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features args: --all-features
- name: Check formatting - name: Build
run: cargo fmt --check run: cargo build --verbose
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
build-nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with:
name: jakestanger
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build --print-build-logs

View File

@@ -17,12 +17,18 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
- name: Update CHANGELOG - name: Update CHANGELOG
id: changelog id: changelog
uses: Requarks/changelog-action@v1 uses: Requarks/changelog-action@v1
with: with:
token: ${{ github.token }} token: ${{ github.token }}
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
excludeTypes: 'build,chore,style'
- name: Create release - name: Create release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1

View File

@@ -0,0 +1,27 @@
name: update-nix-flake-lock
on:
workflow_dispatch: # allows manual triggering
schedule:
- cron: '0 0 1 * *' # first day of every month
jobs:
update-nix-flake-lock:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Update flake.lock
uses: DeterminateSystems/update-flake-lock@v15
with:
pr-title: "Update flake.lock" # Title of PR to be created
pr-labels: | # Labels to be set on the PR
dependencies
automated

17
.github/workflows/wiki.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Sync Wiki
on:
push:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Sync Wiki
run: ./.github/scripts/sync-wiki.sh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

17
.idea/runConfigurations/Format.xml generated Normal file
View File

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

View File

@@ -12,6 +12,7 @@
<envs> <envs>
<env name="IRONBAR_CONFIG" value="examples/config.json" /> <env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" /> <env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="debug" />
</envs> </envs>
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run (Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <configuration default="false" name="Run (GTK Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package ironbar --bin ironbar" /> <option name="command" value="run --package ironbar --bin ironbar" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" /> <option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" /> <option name="channel" value="DEFAULT" />
@@ -12,6 +12,8 @@
<envs> <envs>
<env name="GTK_DEBUG" value="interactive" /> <env name="GTK_DEBUG" value="interactive" />
<env name="IRONBAR_CONFIG" value="examples/config.json" /> <env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="RUST_LOG" value="debug" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
</envs> </envs>
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />

View File

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

428
CHANGELOG.md Normal file
View File

@@ -0,0 +1,428 @@
# Changelog
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.13.0] - 2023-08-16
### :sparkles: New Features
- [`c3e9654`](https://github.com/JakeStanger/ironbar/commit/c3e9654cd3dfcea4276f5b114112b7541dd847fd) - **upower**: icon size option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f5bdc5a`](https://github.com/JakeStanger/ironbar/commit/f5bdc5a0272fefca4c91336699ea63913cdf3c08) - ipc server and cli *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ded50cc`](https://github.com/JakeStanger/ironbar/commit/ded50cca6f01f08a8e44257394fdde634d421e8e) - support for 'ironvar' dynamic variables *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c6319b7`](https://github.com/JakeStanger/ironbar/commit/c6319b78fd3992ad43327e90b6326ab653238f2e) - **ipc**: support for injecting additional stylesheets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27f920d`](https://github.com/JakeStanger/ironbar/commit/27f920d01217bedba279003291ad48c1aaa56bb0) - **launcher**: slightly improve focus logic when clicking item with multiple windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bd90167`](https://github.com/JakeStanger/ironbar/commit/bd90167f4ea90cb97984b9f3b5e6f65b375d0c4a) - **clock**: format option for popup header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`12053f1`](https://github.com/JakeStanger/ironbar/commit/12053f111a6f05a59e33396b9042821b98b9bc5c) - **music**: progress/seek bar in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7d3bb02`](https://github.com/JakeStanger/ironbar/commit/7d3bb02b4612f278bcc8a268a48c61b239c63e82) - **ipc**: reload config command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b310ea7`](https://github.com/JakeStanger/ironbar/commit/b310ea76362bcdf10e187d6b00cd2b8ed2870c41) - **clock**: localization support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`738b9e3`](https://github.com/JakeStanger/ironbar/commit/738b9e3da714c9b998030e9f60b9b6f50c62ec76) - **config**: use default fallback with config instructions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2ccb263`](https://github.com/JakeStanger/ironbar/commit/2ccb2633c6c4d7f6eb264a6c49951853b728c9f3) - IPC for get_visible, set_visible, new bar `name` config option *(commit by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja))*
- [`b7ee794`](https://github.com/JakeStanger/ironbar/commit/b7ee794bfc86730e7921c8a930cf8d8bb44474ad) - **ipc**: commands for opening/closing popups *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ef443e6`](https://github.com/JakeStanger/ironbar/commit/ef443e6978949479388129760dabc3f8930c0b0f) - **image resolver**: add fallback image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9f65cf2`](https://github.com/JakeStanger/ironbar/commit/9f65cf293d9527a2c536847f0005957421a9be33) - **workspaces**: add `favorites` and `hidden` options *(commit by [@yavko](https://github.com/yavko))*
- [`19c684e`](https://github.com/JakeStanger/ironbar/commit/19c684e49facb57e3e2acf9cafecf177c2e1bfbf) - **nix**: automatic development environment with direnv *(commit by [@yavko](https://github.com/yavko))*
### :bug: Bug Fixes
- [`6db7742`](https://github.com/JakeStanger/ironbar/commit/6db7742e068dc03d67dbf35e0d9db27f900392fe) - crash on startup introduced by recent refactors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f78c7f9`](https://github.com/JakeStanger/ironbar/commit/f78c7f9b981c98676e855ff6a63e33a51927c709) - not resolving flatpak application icons *(commit by [@body20002](https://github.com/body20002))*
- [`1759945`](https://github.com/JakeStanger/ironbar/commit/1759945912e376581e5fcd5ed2916f89e2090f2b) - **music**: correctly show/hide popup elements based on player capabilities *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a9ac29d`](https://github.com/JakeStanger/ironbar/commit/a9ac29d8857256d13e14104db235117e3c752972) - clipboard partially behind wrong feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c711dd8`](https://github.com/JakeStanger/ironbar/commit/c711dd858554140bcfb6df515a43b40ee2baee67) - failing to resolve icons with home_manager *(commit by [@christoph00](https://github.com/christoph00))*
- [`1a272e0`](https://github.com/JakeStanger/ironbar/commit/1a272e00fbeca4b5e39b527ffed439bc51fd4080) - **label**: not using markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4ca17d1`](https://github.com/JakeStanger/ironbar/commit/4ca17d1337acfbbc21c04058d97f689a1cce60a6) - **launcher**: incorrectly resolving some applications *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`eee2182`](https://github.com/JakeStanger/ironbar/commit/eee2182ab90fdc22cd05da9417cbee17e4c67088) - **ipc**: command/response casing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c582bc3`](https://github.com/JakeStanger/ironbar/commit/c582bc33905702a9ebe323e6dfa9413485f48fb7) - **cli**: `set-visible` command causing panic *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`87dd764`](https://github.com/JakeStanger/ironbar/commit/87dd7646fc9223ac7e67842934f3bd104b4eea80) - **launcher**: not clearing focused state when closing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6f57ad4`](https://github.com/JakeStanger/ironbar/commit/6f57ad47ac30348c0ae2b7dba35d5ffdbf96f72d) - **launcher**: not setting focus state when opening favourite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2367faa`](https://github.com/JakeStanger/ironbar/commit/2367faab0440327620052de845c6a0d3032f2f05) - **image**: using fallback in places it shouldn't *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7f6fef6`](https://github.com/JakeStanger/ironbar/commit/7f6fef6338d7a8c909f3224b32426dfc2aacc295) - **image**: matching desktop file names too eagerly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`89ec06f`](https://github.com/JakeStanger/ironbar/commit/89ec06fc7b252052f96e45f5d0f6d6504878a13a) - **music**: hide album art widget when no image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2902331`](https://github.com/JakeStanger/ironbar/commit/2902331af00f2e52fdea06964fbd89d72fe2ebbb) - **dynamic string**: incorrectly handling strings containing multipoint utf-8 chars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`901a86c`](https://github.com/JakeStanger/ironbar/commit/901a86caa491648268f0618e17a25b978552db0c) - **custom**: crash when clicking non-popup button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`54f0f23`](https://github.com/JakeStanger/ironbar/commit/54f0f232f208602699e5021942c3d0f3947ca6de) - **launcher**: popup not closing when hover leaves widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`d121dc3`](https://github.com/JakeStanger/ironbar/commit/d121dc3d1e9468a67deb528c35fc3897c3840f77) - fix unused var warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cc181a8`](https://github.com/JakeStanger/ironbar/commit/cc181a8b6d0ac1cccd4ed2f9f420c138ed5383d2) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7016f7f`](https://github.com/JakeStanger/ironbar/commit/7016f7f79e7e29a3318b535ba224aa78bd91688a) - use new smart pointer macros throughout codebase *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`06251e2`](https://github.com/JakeStanger/ironbar/commit/06251e293e8f56e1897fed80335f114fdea57183) - fix new pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3d94987`](https://github.com/JakeStanger/ironbar/commit/3d949874de90b0e5c06cb62726629133d0ea76e3) - **ipc**: add link to luajit library *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.12.1] - 2023-06-18
### :boom: BREAKING CHANGES
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.
### :bug: Bug Fixes
- [`31a57ae`](https://github.com/JakeStanger/ironbar/commit/31a57ae637fa5918f163c8b191916867395912f3) - scripts don't work while running ironbar under a systemd service *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f82f897`](https://github.com/JakeStanger/ironbar/commit/f82f897982e87906e2c9156d4115013bc8e99763) - **upower**: popup always empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9012fee`](https://github.com/JakeStanger/ironbar/commit/9012feee4f9b60b2c22a956de732847892331222) - **image**: still blurry on hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0e65f93`](https://github.com/JakeStanger/ironbar/commit/0e65f93a230cb5ab010b43962fd2e829945c291b) - excess popup windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`87ca399`](https://github.com/JakeStanger/ironbar/commit/87ca399220e5d48eefe2f295d1dba1b9452c4472) - poor error handling for missing images *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`22b630a`](https://github.com/JakeStanger/ironbar/commit/22b630a10b9836531a8b03eb904e6f9fcf839fe6) - broken nerd font icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`48d6af0`](https://github.com/JakeStanger/ironbar/commit/48d6af0281f460d3ed3745a2ffb2b61848430ecb) - **music**: showing when no mpris player found *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b9740cb`](https://github.com/JakeStanger/ironbar/commit/b9740cba8f2fa9dfa18a57345027283610f6487e) - upower icon too large *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a6b6866`](https://github.com/JakeStanger/ironbar/commit/a6b686624b750863aa1c26ca4f1688dfa8c81a61) - **upower**: icon outside button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a5ecb36`](https://github.com/JakeStanger/ironbar/commit/a5ecb363fdb2eb3ab543ad56c55c186414500469) - popups occasionally getting jumbled with multiple bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ac34c05`](https://github.com/JakeStanger/ironbar/commit/ac34c05d2ecb07fd871ed03ef6ee545dc2e6743d) - **focused**: empty icon rendered when `show_icon = false` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`de3aa5d`](https://github.com/JakeStanger/ironbar/commit/de3aa5d7b10e0bf6d5ff3a39b009ff53a3316a5e) - **focused**: previous icon does not clear if new icon fails to load *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`de98cf3`](https://github.com/JakeStanger/ironbar/commit/de98cf3daee816a0ff72d1f6ba6bc0e15ec53fca) - **tray**: (maybe?) sometimes bus name is taken *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ff315ff`](https://github.com/JakeStanger/ironbar/commit/ff315ff5dbd545d8b72b6aa10087c940cb8a5eee) - **music**: fix incorrect type for `host`/`music_dir` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bd144e8`](https://github.com/JakeStanger/ironbar/commit/bd144e87a8f6668c877d42697ebbedbe5a374c3d) - **readme**: make prettier *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`242b70e`](https://github.com/JakeStanger/ironbar/commit/242b70ed3988b85455b0dbbcb3243b31f89d2ee1) - **contributing**: enforce conventional commits *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`96d36c4`](https://github.com/JakeStanger/ironbar/commit/96d36c43d43ba2f9e9d9441ae01c0743cc56f627) - add missing icon/image selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.12.0] - 2023-05-06
### :boom: BREAKING CHANGES
- due to [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.
### :sparkles: New Features
- [`6c62286`](https://github.com/JakeStanger/ironbar/commit/6c622864b388548eaaa595f41993606cc151d585) - new label module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cac064f`](https://github.com/JakeStanger/ironbar/commit/cac064f4795e9f418cc0820f04944f91121c426a) - ability to configure popup gap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dfe1964`](https://github.com/JakeStanger/ironbar/commit/dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d) - **custom**: slider widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`72b14b6`](https://github.com/JakeStanger/ironbar/commit/72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2) - **custom**: progress bar widget. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a9d1233`](https://github.com/JakeStanger/ironbar/commit/a9d12339097cbe0fef1628460ef538319a048223) - **custom**: support dynamic strings on buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3d308ab`](https://github.com/JakeStanger/ironbar/commit/3d308ab572a39ada2501ddc1b822e50e1f8a8363) - **custom**: support dynamic string in image source *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4a09b70`](https://github.com/JakeStanger/ironbar/commit/4a09b70854dad33bf890a3fe766f854d9195e786) - **custom**: support common options in widgets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`83f44fd`](https://github.com/JakeStanger/ironbar/commit/83f44fd92fe74b45fcdfc242fb90fc932dd2b00b) - wrap modules in a revealer to support animated show/hide *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1fa0c0e`](https://github.com/JakeStanger/ironbar/commit/1fa0c0e9774c302727d414f5aef999ab71a7acb8) - **custom**: support mouse wheel on slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2da28b9`](https://github.com/JakeStanger/ironbar/commit/2da28b9bf5790adfc46c58b6f6d5fdd13cc17195) - ability to configure image icon sizes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`033d0f7`](https://github.com/JakeStanger/ironbar/commit/033d0f7e6e450b3f2d62d9a75210d52611cf346d) - **custom**: option to toggle slider label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`76e2b7b`](https://github.com/JakeStanger/ironbar/commit/76e2b7ba3e788f273039d74635881ddb96264258) - **music**: option to hide status icon on widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ad3c171`](https://github.com/JakeStanger/ironbar/commit/ad3c171ecacaebf10408c2583ed7361ed029075e) - implement upower module *(commit by [@p00f](https://github.com/p00f))*
- [`2a155b9`](https://github.com/JakeStanger/ironbar/commit/2a155b9aa8a3634908512d9b83680925962d478f) - **music**: add css selector for button contents *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c1ea5fa`](https://github.com/JakeStanger/ironbar/commit/c1ea5fad7ec308895f0454b6de05a3177563626c) - **logging**: include line numbers *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`9109453`](https://github.com/JakeStanger/ironbar/commit/910945306c3261190a16300da2ed28efb945a6ac) - **dynamic string**: parser issue related to incorrectly matching braces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7355db7`](https://github.com/JakeStanger/ironbar/commit/7355db74ec9118c2cb46899534a3adac8d7165d9) - **image**: http provider not handling non-success codes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a87d8d5`](https://github.com/JakeStanger/ironbar/commit/a87d8d5c3071a1d8ab149deae17d261ae97368ea) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`15a9d8d`](https://github.com/JakeStanger/ironbar/commit/15a9d8d42c9319a7062e6a90086e0c1c3323f5d8) - **script**: parser incorrectly handling colons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`68bc823`](https://github.com/JakeStanger/ironbar/commit/68bc8230ddf3352cc0de9f8cc770632744c22747) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b038e76`](https://github.com/JakeStanger/ironbar/commit/b038e7671af4bfa41060adf724deb8c6151fac1f) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7926bb0`](https://github.com/JakeStanger/ironbar/commit/7926bb07eb181edaf6da2f11a7dc00f8be2240eb) - **nix**: Fix `nix run` support *(commit by [@yavko](https://github.com/yavko))*
- [`2c88c99`](https://github.com/JakeStanger/ironbar/commit/2c88c99cb605d312e2d76d620f502c7e7cd8866e) - **dynamic string**: crash when last segment is static and a single char *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`338f5a0`](https://github.com/JakeStanger/ironbar/commit/338f5a0e1b58dc9b52caee61d6a9748cf13153c5) - **nix**: Attempt to fix image blurriness *(commit by [@yavko](https://github.com/yavko))*
- [`db0868a`](https://github.com/JakeStanger/ironbar/commit/db0868a3fc0734daa61067e377018c692599ebff) - **image**: not scaling icons for hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`14b6c1a`](https://github.com/JakeStanger/ironbar/commit/14b6c1a69f28836ed9e3b74eeb97a42ea60ffc27) - bars duplicate when starting second instance *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`98aaaa0`](https://github.com/JakeStanger/ironbar/commit/98aaaa0d1407681b3d790c933c4972b8122f8007) - fallback to default icon theme for notifier items *(commit by [@oknozor](https://github.com/oknozor))*
- [`735f5cc`](https://github.com/JakeStanger/ironbar/commit/735f5cc9f1518c256785d42f3d21ed5c68b11711) - **launcher**: crash when focusing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e1abadc`](https://github.com/JakeStanger/ironbar/commit/e1abadcf39a2d39078e75179a167e9277ee5e550) - **clipboard**: copying large images filling write pipe *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`2ab06f0`](https://github.com/JakeStanger/ironbar/commit/2ab06f044ec300628d6648852d395889b6752b76) - **custom**: split into enum with separate file per widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3613aef`](https://github.com/JakeStanger/ironbar/commit/3613aef5c5a4051b5a44e33342c0eaaab3d4a690) - **custom**: reduce a lot of repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c214f65`](https://github.com/JakeStanger/ironbar/commit/c214f65ecb86a0da6559025203701661924f65bb) - fix strict clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27d11de`](https://github.com/JakeStanger/ironbar/commit/27d11de6616c410422d7abd579d09b3abc02f43a) - **config**: split common code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6fd69d6`](https://github.com/JakeStanger/ironbar/commit/6fd69d657c6224bc47c9b3cb5affcf74b63a6aa6) - move module creation code to module module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e63509a`](https://github.com/JakeStanger/ironbar/commit/e63509a3a7673ea41b4c937089a1cf6d2362fed3) - fix a few new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7f46cb4`](https://github.com/JakeStanger/ironbar/commit/7f46cb49767bd722be8d42999a9ba69887efcd40) - **wayland**: update to 0.30.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dd7c9f3`](https://github.com/JakeStanger/ironbar/commit/dd7c9f30db6e4e1ede4d57255122b359636b8f58) - add transition module-level options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`610c352`](https://github.com/JakeStanger/ironbar/commit/610c3528af98b8c6b02af7ce5c07190776522c3a) - add missing link to upower page *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ea9f7ca`](https://github.com/JakeStanger/ironbar/commit/ea9f7caaf7a35eebd603ce2854672d5af2901018) - add missing `upower` feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`618b7ef`](https://github.com/JakeStanger/ironbar/commit/618b7ef5520de6f3796b66e42422a36c5a191ab0) - improve example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`139bc5d`](https://github.com/JakeStanger/ironbar/commit/139bc5d23f7f887b7b65d50adc21fa6679ea291e) - **compiling**: improve requirements list *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cf32870`](https://github.com/JakeStanger/ironbar/commit/cf32870f8a380c305a436593950c3da524a2296f) - **compiling**: add ron feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.11.0] - 2023-04-01
### :boom: BREAKING CHANGES
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
### :sparkles: New Features
- [`d253c4b`](https://github.com/JakeStanger/ironbar/commit/d253c4bd7f306c7b8fef223d1beb7b1f6e77629b) - add configurable margins around bar *(commit by [@ttoino](https://github.com/ttoino))*
- [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - **truncate**: ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`575d6cc`](https://github.com/JakeStanger/ironbar/commit/575d6cc30f9e28079aed8425566048abd3d9e022) - new clipboard manager module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9984b63`](https://github.com/JakeStanger/ironbar/commit/9984b638b55adea11ba90412346fbb8220f05682) - **nix**: initial nix feature flags impl *(commit by [@yavko](https://github.com/yavko))*
- [`b1475a1`](https://github.com/JakeStanger/ironbar/commit/b1475a1affd2f101f1f707ab1a0e8e5509a1d99f) - **nix**: use cargo default features *(commit by [@yavko](https://github.com/yavko))*
- [`102d247`](https://github.com/JakeStanger/ironbar/commit/102d2478a9d0ecc8be12c5ea6019a5a5411cc6ab) - module hover options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`2ac5071`](https://github.com/JakeStanger/ironbar/commit/2ac507144b42a80507f8d2df214889c114c069df) - not setting layer shell namespace *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7dff3e6`](https://github.com/JakeStanger/ironbar/commit/7dff3e6f8b989132ff0c4406caa72f063dd57c9f) - **image**: widgets missing names *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`54b9b28`](https://github.com/JakeStanger/ironbar/commit/54b9b28c75b2fe300e2bad1436d315da1950953e) - make readme more concise *(commit by [@yavko](https://github.com/yavko))*
- [`8cbb73b`](https://github.com/JakeStanger/ironbar/commit/8cbb73b75e7aca1aa163406f4583273e6ff4bac2) - **dynamic string**: dynamic sections not respecting ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`d0b7bdb`](https://github.com/JakeStanger/ironbar/commit/d0b7bdbafcc34967dd5b048ea12e6267ba293566) - **nix**: home manager module, and features *(commit by [@yavko](https://github.com/yavko))*
### :recycle: Refactors
- [`d84139a`](https://github.com/JakeStanger/ironbar/commit/d84139a914f9b35054dc6048715e1ed7e79d7441) - general tidy up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7212bbc`](https://github.com/JakeStanger/ironbar/commit/7212bbcf61e097b35a7ab341e19e9daefd2edf95) - **dynamic string**: use vec instead of indexmap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ecdd71a`](https://github.com/JakeStanger/ironbar/commit/ecdd71a43d267161f84e3c4a3c22e9454c0f7184) - **config**: use `universal-config` crate. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`82875cd`](https://github.com/JakeStanger/ironbar/commit/82875cde687628f3ee3436343068825440128599) - update CHANGELOG.md for v0.10.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7c36f5c`](https://github.com/JakeStanger/ironbar/commit/7c36f5cb0cf03191c9b03e2455b63829a64e402e) - fix a couple of issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`83a4916`](https://github.com/JakeStanger/ironbar/commit/83a49165c42fa793ef1224f93cbc147bc69de894) - **compiling**: add info about build deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5bbe64b`](https://github.com/JakeStanger/ironbar/commit/5bbe64bb86fb2db0921e284a1560db2f6c1a1920) - **clock**: format table *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2b26eaf`](https://github.com/JakeStanger/ironbar/commit/2b26eaf41036609be4dfc57689ca8d770dcb6b9b) - **clipboard**: fix incorrect setting description *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0125ce5`](https://github.com/JakeStanger/ironbar/commit/0125ce5916c003d1ea9a141fe5a0f6a54b2778ab) - **examples**: update styles example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.10.0] - 2023-02-01
### :boom: BREAKING CHANGES
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
This removes the `icon_theme` option from `launcher` and `focused`. You will need to set this at the top of your config instead.
- due to [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
(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.
### :sparkles: New Features
- [`8691824`](https://github.com/JakeStanger/ironbar/commit/8691824db1a12c3f3589ff8b5315b8dba5cb8aec) - **music**: ability to truncate button text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`07dbf78`](https://github.com/JakeStanger/ironbar/commit/07dbf780105027b533b0bb34c9ae3e4e96f29f4a) - **focused**: ability to truncate label text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`393800a`](https://github.com/JakeStanger/ironbar/commit/393800aaa2093b9257c43fde8bdb8399f26ebc74) - **custom**: image widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b054c17`](https://github.com/JakeStanger/ironbar/commit/b054c17d14628c9188bfa9aed506ea1de3051f9c) - **workspaces**: support for using images in `name_map` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`96141d4`](https://github.com/JakeStanger/ironbar/commit/96141d49907412ea26d23ef30c10ade8b32b89b9) - **music**: support for using images in `name_map`, additional icon options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c347b6c`](https://github.com/JakeStanger/ironbar/commit/c347b6c9449ce4e16e2e133d7dd35544ab9a533c) - add feature flags *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`5772711`](https://github.com/JakeStanger/ironbar/commit/57727111923a419f9b7613103283aa4cf6bd082c) - **music**: remote mpris album art not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5fb4125`](https://github.com/JakeStanger/ironbar/commit/5fb412572f3da60ac482a1960d891f70bc29287b) - **tray**: some init issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`058c8f4`](https://github.com/JakeStanger/ironbar/commit/058c8f4228f9f7faa66cda9dd1636ea32e9de68b) - **hyprland**: issues with tracking workspaces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`51d2c22`](https://github.com/JakeStanger/ironbar/commit/51d2c2279f50add992def0d58cfaa9890ea3d041) - **images**: incorrectly resolving non-files *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`012762e`](https://github.com/JakeStanger/ironbar/commit/012762e10203fb2d58160acdae4dc7ca7689b131) - swap out some code for existing macros *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9750255`](https://github.com/JakeStanger/ironbar/commit/97502559b30c51e77c1dd9a7d794a88756294c83) - **music**: split config code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`15f0857`](https://github.com/JakeStanger/ironbar/commit/15f0857859d5d4a590b60b6b1a4347b4b84a58a1) - replace icon loading with improved general image loading *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1ed3220`](https://github.com/JakeStanger/ironbar/commit/1ed3220733c2dcb7c5e5cbf377b3324d3183609e) - update CHANGELOG.md for v0.9.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - **music**: remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6a39905`](https://github.com/JakeStanger/ironbar/commit/6a39905b4333582fbcda81a66a9b91055333d698) - **compiling**: add missing full stop *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7b23e61`](https://github.com/JakeStanger/ironbar/commit/7b23e61e7dedf2736a30580b6c1aa84e002c462c) - **wiki**: update screenshots and examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [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))*:
This renames the module from `sys-info` to `sys_info`, and almost every formatting token from `kebab-case` to `snake_case`. Any use of this module will need to be updated.
- due to [`8c75bc4`](https://github.com/JakeStanger/ironbar/commit/8c75bc46ac2885a748d31df9261d988cc797e916) - rename `path` to `cmd` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
This changes the option in the `script` module. Any uses of the module must be updated to use the new option name.
- due to [`e274ba3`](https://github.com/JakeStanger/ironbar/commit/e274ba39cd6d8f1c73033ac1e60e5bce89205ce2) - rename `exec` to `on_click` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
This changes the option on buttons in the `custom` module. Any uses of the module must be updated to use the new custom widget attribute name.
### :sparkles: New Features
- [`73158c2`](https://github.com/JakeStanger/ironbar/commit/73158c2fce2880347b88d58541dea000534996c8) - **script**: new watch mode *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a3f90ad`](https://github.com/JakeStanger/ironbar/commit/a3f90adaf19aebed7020eeb44b91250af080d313) - add nix flake support *(commit by [@yavko](https://github.com/yavko))*
- [`c9e66d4`](https://github.com/JakeStanger/ironbar/commit/c9e66d4664137c50aba4aecdc3a3ba43d3da11fe) - common module options (`show_if`, `on_click`, `tooltip`) *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`5d153a0`](https://github.com/JakeStanger/ironbar/commit/5d153a02fc9b113bb77a04596b806edd182fc5d3) - **custom**: ability to embed scripts in labels for dynamic content *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`d20972c`](https://github.com/JakeStanger/ironbar/commit/d20972cb32714627d0cca947021453979c76dd03) - dynamic tooltips *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`ff17ec1`](https://github.com/JakeStanger/ironbar/commit/ff17ec1996cf344663e84e79d11b08dc84b97635) - various changes based on rust 1.65 clippy *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4662f60`](https://github.com/JakeStanger/ironbar/commit/4662f60ac54165be6fb7aea12c245309db0fe5d6) - move various clients to own folder *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0fb5fa8`](https://github.com/JakeStanger/ironbar/commit/0fb5fa8c2a166c3d46b006ceb0d53af076824ff4) - use latest `libcorn` with serde support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`df77020`](https://github.com/JakeStanger/ironbar/commit/df77020c5277ae9e379bb4fd67c221be5cb20426) - **sys_info**: use snake_case for module tokens for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8c75bc4`](https://github.com/JakeStanger/ironbar/commit/8c75bc46ac2885a748d31df9261d988cc797e916) - **script**: rename `path` to `cmd` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e274ba3`](https://github.com/JakeStanger/ironbar/commit/e274ba39cd6d8f1c73033ac1e60e5bce89205ce2) - **custom**: rename `exec` to `on_click` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`64f5404`](https://github.com/JakeStanger/ironbar/commit/64f54040ef626157af6b6a9ce5258507a10a23fb) - move dynamic_label.rs to dynamic_string.rs and fix failing test *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :white_check_mark: Tests
- [`907a565`](https://github.com/JakeStanger/ironbar/commit/907a565f3d418a276dfb454e1189ddede1814291) - **dynamic label**: do not run if cannot initialise gtk *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`1c032ae`](https://github.com/JakeStanger/ironbar/commit/1c032ae8e3a38b82c286bab7d102842f14b708e1) - update CHANGELOG.md for v0.7.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`58d55db`](https://github.com/JakeStanger/ironbar/commit/58d55db6600fe2f9b23ae8ec6a50a686d2acaf65) - migrate wiki into main repo *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c480296`](https://github.com/JakeStanger/ironbar/commit/c48029664d5f58bf73faa2931f34b38b8b184d25) - **script**: improve doc comment *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8c77410`](https://github.com/JakeStanger/ironbar/commit/8c774100f1c8ea051284c6950339a2c8ed59a52a) - **script**: add information on new mode options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c4cdf4b`](https://github.com/JakeStanger/ironbar/commit/c4cdf4be8ba83f3669158a1552eab4a840085204) - update example configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ec69649`](https://github.com/JakeStanger/ironbar/commit/ec69649a04f6199953836e51c2efe1fe2a19e320) - update example configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1320639`](https://github.com/JakeStanger/ironbar/commit/1320639d4e6b7c8cd8f861b26b2b854504775ef0) - add custom power menu example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`afedf02`](https://github.com/JakeStanger/ironbar/commit/afedf0214d3a71f6283c70bd3a110d24f68d2fdf) - add link to new custom power menu example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.7.0] - 2022-11-05
### :sparkles: New Features
- [`fad90fd`](https://github.com/JakeStanger/ironbar/commit/fad90fdad683a612497ac7822a66a90f43fce0a2) - **sys-info**: add loads more formatting tokens *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dec402e`](https://github.com/JakeStanger/ironbar/commit/dec402edd9d6c5b8677ff337699ad99ebc69b776) - **sys-info**: config options for refresh intervals *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`91c57ed`](https://github.com/JakeStanger/ironbar/commit/91c57edc73f15397ea0de70c4a6a6532c35caf2a) - **sys-info**: pango markup support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ec1d596`](https://github.com/JakeStanger/ironbar/commit/ec1d59677b13c9654a98d78f909ba2d0fcfbb72d) - **logging**: `IRONBAR_LOG` and `IRONBAR_FILE_LOG` env vars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`493df6b`](https://github.com/JakeStanger/ironbar/commit/493df6bb49fec8c465706d3f9b395728ba73a621) - **mpd**: add volume slider to popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3750124`](https://github.com/JakeStanger/ironbar/commit/3750124d8cfb4783932a6b3359384f245fcd2394) - new custom module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b7792a4`](https://github.com/JakeStanger/ironbar/commit/b7792a415e09fc535750ea5af530f91aa791c4bc) - env var to set custom css location *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ad77dc4`](https://github.com/JakeStanger/ironbar/commit/ad77dc4e4c2f80fcb4c9604c796be0f981e895ee) - improved logging & error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`9e6dbbd`](https://github.com/JakeStanger/ironbar/commit/9e6dbbd131a09f101b0d490265fe7d4ec564e38c) - **sys-info**: tokens not replaced if more than one in string *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f17ae7a`](https://github.com/JakeStanger/ironbar/commit/f17ae7a415b931c64942de085e8889f37b3f9b11) - **script**: not parsing pango markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b66bd78`](https://github.com/JakeStanger/ironbar/commit/b66bd788b23256a2127a1352693fdd3f929d9c4b) - logging for creating bar incorrect still *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3c43c20`](https://github.com/JakeStanger/ironbar/commit/3c43c20c6ae53a9aa6b67770b0c489806784f4ac) - weird behaviour when config does not exist *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`70e1b52`](https://github.com/JakeStanger/ironbar/commit/70e1b526a9681b16545d7f05d77470d76bd8819e) - **logging**: file log not capturing panics *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cbd0c49`](https://github.com/JakeStanger/ironbar/commit/cbd0c49e251b5c8e0289ca6200a393d89994992d) - css watcher not working *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e693c1c`](https://github.com/JakeStanger/ironbar/commit/e693c1c166eef0b5edcdcd033bb12d572e4e5f04) - **mpd**: volume slider causing mpd server errors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3a83bd3`](https://github.com/JakeStanger/ironbar/commit/3a83bd31ab165869f7f274b054b2f16485261fd1) - able to insert duplicate keys into collection *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`5ebc84c`](https://github.com/JakeStanger/ironbar/commit/5ebc84c7b98cc648a659ca37fdc0f041057f0ea4) - **logging**: consts for default log levels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bc625b9`](https://github.com/JakeStanger/ironbar/commit/bc625b929b8644ce92f275b5d98cdf74b93fe067) - clippy & fmt *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`9d9c275`](https://github.com/JakeStanger/ironbar/commit/9d9c2753137331ae85ac8ab7d75a6de9a9c82042) - update CHANGELOG.md for v0.6.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27d0479`](https://github.com/JakeStanger/ironbar/commit/27d04795af1c25fe5f765c7480d5dd5d096a8ab7) - **readme**: add warning about crate being outdated *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a06c4bc`](https://github.com/JakeStanger/ironbar/commit/a06c4bccca6cb51935605ac9239e63024fb7c663) - **examples**: add full system info config *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0a331f3`](https://github.com/JakeStanger/ironbar/commit/0a331f31381f0d967793c0d8b7a14e2a43bf666f) - **readme**: remove warning about outdated cargo package *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.6.0] - 2022-10-15
### :sparkles: New Features
- [`b188bc7`](https://github.com/JakeStanger/ironbar/commit/b188bc714614406935d8bb88a719adab2dfce32f) - initial support for running outside sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`324f00c`](https://github.com/JakeStanger/ironbar/commit/324f00cdf9200e3e3ecedfa68ab4c99b170242e2) - wlroots-agnostic support for `focused` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b1c66b9`](https://github.com/JakeStanger/ironbar/commit/b1c66b9117cf8a10350cdb857a5267a1a72ad914) - wlroots-agnostic support for `launcher` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1dd0a9e`](https://github.com/JakeStanger/ironbar/commit/1dd0a9e52f69e672d9ac313c1da0e201c911e6c2) - **launcher**: add popup css selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`5523e9a`](https://github.com/JakeStanger/ironbar/commit/5523e9af46e457f9d45902debaaacf26b586e457) - **popup**: often opening in wrong place *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8536ad7`](https://github.com/JakeStanger/ironbar/commit/8536ad719a92aec4166e35b75cb029075ad3ae34) - **mpd**: incorrectly checking for unix sockets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bd5bdf5`](https://github.com/JakeStanger/ironbar/commit/bd5bdf5af548304958663d593fccb454afa6c8ff) - logging for creating bar incorrect *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`75339f0`](https://github.com/JakeStanger/ironbar/commit/75339f07ed164fa94838036a604a1dcb6d53564c) - vertical bars ignoring height config option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b7b6488`](https://github.com/JakeStanger/ironbar/commit/b7b64886e3c48ace3faffbb1e277275aeeac3adf) - sometimes panicking on startup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`5ce50b0`](https://github.com/JakeStanger/ironbar/commit/5ce50b0987812a1ade2d1262e8d7df6916cfc39a) - tidy and format *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`daafa09`](https://github.com/JakeStanger/ironbar/commit/daafa0943e5b9886b09fd18d6fff04558fb02335) - update CHANGELOG.md for v0.5.2 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b352181`](https://github.com/JakeStanger/ironbar/commit/b352181b3d232ccc79ffc1d9e22a633729d01a47) - update json example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bb4fe7f`](https://github.com/JakeStanger/ironbar/commit/bb4fe7f7f58fa2a6d0a2259bd9442700d2c884f7) - **readme**: credit smithay client toolkit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :boom: BREAKING CHANGES
- due to [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*:
The `left` and `right` config options have been renamed to `start` and `end`
## [v0.5.2] - 2022-09-07
### :wrench: Chores
- [`b801751`](https://github.com/JakeStanger/ironbar/commit/b801751bdabd8416084f46e6b6d803ea28a259ec) - **release**: v0.5.2 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.5.1] - 2022-09-06
### :bug: Bug Fixes
- [`b81927e`](https://github.com/JakeStanger/ironbar/commit/b81927e3a57808188e31419695a36aa4ea3f2830) - **launcher**: opening new instances when focused/urgent *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a35d255`](https://github.com/JakeStanger/ironbar/commit/a35d25520cd3fd235cdc77ec6209d88499ca3639) - **launcher**: item state changes not handled correctly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`481adfc`](https://github.com/JakeStanger/ironbar/commit/481adfcaa41c0d3a1ba7d61edb68db49d959c78f) - **intellij**: update run configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6871126`](https://github.com/JakeStanger/ironbar/commit/6871126bd8def89ccbf2934180d615e781ec32c7) - **release**: v0.5.1 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.5.0] - 2022-08-25
### :sparkles: New Features
- [`1e38719`](https://github.com/JakeStanger/ironbar/commit/1e387199962b81caeb40ffbd99a956f24abdf4e3) - introduce logging in some areas *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`023c2fb`](https://github.com/JakeStanger/ironbar/commit/023c2fb118f46f3592f1dfe1a6704014c062ab3f) - **workspaces**: not listening to move event *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6dcae66`](https://github.com/JakeStanger/ironbar/commit/6dcae66570cf5434e077ec823cded33771b4239c) - avoid creating loads of sway/mpd clients *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`015dcd3`](https://github.com/JakeStanger/ironbar/commit/015dcd3204dfa6a1ebcef1b4f3b345ed733fee2f) - **release**: v0.5.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.4.0] - 2022-08-22
### :sparkles: New Features
- [`ab8f7ec`](https://github.com/JakeStanger/ironbar/commit/ab8f7ecfc8fa4b96fce78518af75794641950140) - logging support and proper error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`f2ee2df`](https://github.com/JakeStanger/ironbar/commit/f2ee2dfe7a0f5575d0c3ec09644ca990b088cd85) - error when using with `swaybar_command` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`1d7c377`](https://github.com/JakeStanger/ironbar/commit/1d7c3772e4b97c7198043cb55fe9c71695a211ab) - **release**: v0.4.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1
[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.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0

View File

@@ -4,7 +4,8 @@ I welcome contributions of any kind with open arms. That said, please do stick t
- Fix any `cargo clippy` warnings, using at least the default configuration. - Fix any `cargo clippy` warnings, using at least the default configuration.
- Make sure your code is formatted using `cargo fmt`. - Make sure your code is formatted using `cargo fmt`.
- Keep any documentation up to date. - Keep any documentation up to date.
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages. - Please use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
This ensures your contributions are automatically included in the changelog.
- For PRs: - For PRs:
@@ -14,3 +15,4 @@ I welcome contributions of any kind with open arms. That said, please do stick t
- For issues: - For issues:
- Please provide as much information as you can - share your config, any logs, steps to reproduce... - Please provide as much information as you can - share your config, any logs, steps to reproduce...
- If reporting an error, please ensure you use `IRONBAR_LOG` or `IRONBAR_FILE_LOG` set to `debug`.

3086
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,140 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.4.0" version = "0.14.0-pre"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable wlroots/sway bar" description = "Customisable GTK Layer Shell wlroots/sway bar"
repository = "https://github.com/jakestanger/ironbar"
categories = ["gui"]
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features]
default = [
"cli",
"ipc",
"http",
"config+all",
"clipboard",
"clock",
"music+all",
"sys_info",
"tray",
"upower",
"workspaces+all"
]
cli = ["dep:clap", "ipc"]
ipc = ["dep:serde_json"]
http = ["dep:reqwest"]
"config+all" = [
"config+json",
"config+yaml",
"config+toml",
"config+corn",
"config+ron",
]
"config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"]
"config+toml" = ["universal-config/toml"]
"config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
clipboard = ["nix"]
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 = ["system-tray"]
upower = ["upower_dbus", "zbus", "futures-lite"]
workspaces = ["futures-util"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
[dependencies] [dependencies]
gtk = "0.15.5" # core
gtk-layer-shell = "0.4.1" gtk = "0.17.0"
glib = "0.15.12" gtk-layer-shell = "0.6.0"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] } glib = "0.17.10"
tracing = "0.1.36" tokio = { version = "1.32.0", features = [
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } "macros",
"rt-multi-thread",
"time",
"process",
"sync",
"io-util",
"net",
] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-appender = "0.2.2" tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1" strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2" color-eyre = "0.6.2"
futures-util = "0.3.21" serde = { version = "1.0.188", features = ["derive"] }
chrono = "0.4.19" indexmap = "2.0.0"
serde = { version = "1.0.141", features = ["derive"] } dirs = "5.0.1"
serde_json = "1.0.82" walkdir = "2.4.0"
serde_yaml = "0.9.4" notify = { version = "6.1.1", default-features = false }
toml = "0.5.9" wayland-client = "0.30.2"
cornfig = "0.2.0" wayland-protocols = { version = "0.30.1", features = ["unstable", "client"] }
regex = "1.6.0" wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
stray = "0.1.1" smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
dirs = "4.0.0" "calloop",
walkdir = "2.3.2" ] }
notify = "4.0.17" universal-config = { version = "0.4.3", default_features = false }
mpd_client = "0.7.5" ctrlc = "3.4.1"
ksway = "0.1.0"
sysinfo = "0.25.1" lazy_static = "1.4.0"
# required for wrapping ksway async_once = "0.2.6"
crossbeam-channel = "0.3.9" cfg-if = "1.0.0"
# cli
clap = { version = "4.4.4", optional = true, features = ["derive"] }
# ipc
serde_json = { version = "1.0.107", optional = true }
# http
reqwest = { version = "0.11.20", optional = true }
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
# clock
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
# music
mpd_client = { version = "1.2.0", optional = true }
mpris = { version = "2.0.1", optional = true }
# sys_info
sysinfo = { version = "0.29.10", optional = true }
# tray
system-tray = { version = "0.1.4", optional = true }
# upower
upower_dbus = { version = "0.3.2", optional = true }
futures-lite = { version = "1.12.0", optional = true }
zbus = { version = "3.14.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
futures-util = { version = "0.3.21", optional = true }
# shared
regex = { version = "1.9.5", default-features = false, features = [
"std",
], optional = true } # music, sys_info

164
README.md
View File

@@ -1,33 +1,140 @@
# Ironbar <h1 align="center" >--- Ironbar ---</h1>
Ironbar is a customisable and feature-rich bar targeting the Sway compositor, written in Rust. <div align="center">
It uses GTK3 and gtk-layer-shell. <a href="https://github.com/JakeStanger/ironbar/releases">
<img src="https://img.shields.io/crates/v/ironbar?label=version&style=for-the-badge" alt="Current version" />
</a>
<a href="https://github.com/JakeStanger/ironbar/actions/workflows/build.yml">
<img src="https://img.shields.io/github/actions/workflow/status/jakestanger/ironbar/build.yml?style=for-the-badge" alt="Build status" />
</a>
<a href="https://github.com/JakeStanger/ironbar/issues">
<img src="https://img.shields.io/github/issues/jakestanger/ironbar?style=for-the-badge" alt="Open issues" />
</a>
<a href="https://github.com/JakeStanger/ironbar/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/jakestanger/ironbar?style=for-the-badge" alt="License" />
</a>
<a href="https://crates.io/crates/ironbar">
<img src="https://img.shields.io/crates/d/ironbar?label=crates.io%20downloads&style=for-the-badge" alt="Crates.io downloads" />
</a>
</div>
The bar can be styled to your liking using CSS and hot-loads style changes. ---
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png) <div align="center">
A customisable and feature-rich GTK bar for wlroots compositors, written in Rust.
Ironbar is designed to support anything from a lightweight bar to a full desktop panel with ease.
---
## Getting Started
[Wiki](https://github.com/JakeStanger/ironbar/wiki)
|
[Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide)
|
[Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide)
---
![Screenshot of fully configured bar with MPD widget open](https://f.jstanger.dev/github/ironbar/bar.png?raw)
✨ Looking for a starting point, or want to show off? Head to [Show and tell](https://github.com/JakeStanger/ironbar/discussions/categories/show-and-tell) ✨
</div>
---
## Features
- First-class support for Sway and Hyprland
- Fully themeable with hot-loaded CSS
- Popups to show rich content
- Ability to create custom widgets, run scripts and embed dynamic content
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
- Support for multiple config languages
## Installation ## Installation
Run using `ironbar`.
### Cargo ### Cargo
[crate](https://crates.io/crates/ironbar)
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh ```sh
cargo install ironbar cargo install ironbar
``` ```
[crate](https://crates.io/crates/ironbar)
### Arch Linux ### Arch Linux
[aur package](https://aur.archlinux.org/packages/ironbar-git)
```sh ```sh
yay -S ironbar-git yay -S ironbar-git
``` ```
### Nix
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
```sh
nix-shell -p ironbar
```
#### Flake
A flake is included with the repo which can be used with Home Manager.
<details>
<summary>Example usage</summary>
```nix
{
# Add the ironbar flake input
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
inputs.ironbar = {
url = "github:JakeStanger/ironbar";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.hm = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs: {
homeManagerConfigurations."USER@HOSTNAME" = inputs.hm.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [
# And add the home-manager module
inputs.ironbar.homeManagerModules.default
{
# And configure
programs.ironbar = {
enable = true;
config = {};
style = "";
package = inputs.ironbar;
features = ["feature" "another_feature"];
};
}
];
};
};
}
```
</details>
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
### Source ### Source
[repo](https://github.com/jakestanger/ironbar)
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh ```sh
git clone https://github.com/jakestanger/ironbar.git git clone https://github.com/jakestanger/ironbar.git
cd ironbar cd ironbar
@@ -36,42 +143,39 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar install target/release/ironbar ~/.local/bin/ironbar
``` ```
[aur package](https://aur.archlinux.org/packages/ironbar-git) By default, all features are enabled.
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
## Configuration ## Running
Ironbar gives a lot of flexibility when configuring, including multiple file formats Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
and options for scaling complexity: you can use a single config across all monitors, See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
or configure different/multiple bars per monitor.
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide). Ironbar can be launched using the `ironbar` binary.
## Styling Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the file. These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide). File output can be found at `~/.local/share/ironbar/error.log`.
## Project Status ## Status
This project is in alpha, but should be usable. Ironbar is an **alpha** project.
Everything that is implemented works and should be documented. It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
There is currently room for lots more modules, and lots more configuration options for the existing modules. If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
The current configuration schema is not set in stone and breaking changes could come along at any point;
until the project matures I am more interested in ease of use than backwards compatibility.
A few bugs do exist, and I am sure there are plenty more to be found. I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
The project will be *actively developed* as I am using it on my daily driver.
Bugs will be fixed, features will be added, code will be refactored.
## Contribution Guidelines ## Contribution Guidelines
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md). All are welcome, but I ask a few basic things to help make things easier. Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md) for details.
## Acknowledgements ## Acknowledgements
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar. - [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust - [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this

81
docs/Compiling.md Normal file
View File

@@ -0,0 +1,81 @@
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
```
## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
You also need rust; only the latest stable version is supported.
### Arch
```shell
pacman -S gtk3 gtk-layer-shell
```
### Ubuntu/Debian
```shell
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
# for http support
apt install libssl-dev
```
### Fedora
```shell
dnf install gtk3 gtk-layer-shell
```
## 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. |
| ipc | Enables the IPC server. |
| cli | Enables the CLI. Will also enable `ipc`. |
| 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). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | |
| clipboard | Enables the `clipboard` module. |
| 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. |
| upower | Enables the `upower` 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. |

323
docs/Configuration guide.md Normal file
View File

@@ -0,0 +1,323 @@
By default, you get a single bar at the bottom of all your screens.
To change that, you'll unsurprisingly need a config file.
This page details putting together the skeleton for your config to get you to a stage where you can start configuring
modules.
It may look long and overwhelming, but that is just because the bar supports a lot of scenarios!
If you want to see some ready-to-go config files check
the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples)
and the example pages in the sidebar.
## 1. Create config file
The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`.
Ironbar supports a range of configuration formats, so you can pick your favourite:
- `config.json`
- `config.toml`
- `config.yaml`
- `config.corn` (Experimental, includes variable support for re-using blocks.
See [here](https://github.com/jakestanger/corn) for info)
You can also override the default config path using the `IRONBAR_CONFIG` environment variable.
## 2. Pick your use-case
Ironbar gives you a few ways to configure the bar to suit your needs.
This allows you to keep your config simple and relatively flat if your use-case is simple,
and make it more complex if required.
### a) I want the same bar across all monitors
Place the bar config inside the top-level object. This is automatically applied to each of your monitors.
<details>
<summary>JSON</summary>
```json
{
"position": "bottom",
"height": 42,
"start": [],
"center": [],
"end": []
}
```
</details>
<details>
<summary>TOML</summary>
```toml
position = "bottom"
height = 42
start = []
center = []
end = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
position: "bottom"
height: 42
start: [ ]
center: [ ]
end: [ ]
```
</details>
<details>
<summary>Corn</summary>
```
{
position = "bottom"
height = 42
start = []
center = []
end = []
}
```
</details>
### b) I want my config to differ across one or more monitors
Create a map/object called `monitors` inside the top-level object.
Each of the map's keys should be an output name,
and each value should be an object containing the bar config.
To find your output names, run `wayland-info | grep wl_output -A1`.
<details>
<summary>JSON</summary>
```json
{
"monitors": {
"DP-1": {
"start": []
},
"DP-2": {
"position": "bottom",
"height": 30,
"start": []
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[monitors]
[monitors.DP-1]
start = []
[monitors.DP-2]
position = "bottom"
height = 30
start = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
monitors:
DP-1:
start: [ ]
DP-2:
position: "bottom"
height: 30
start: [ ]
```
</details>
<details>
<summary>Corn</summary>
```
{
monitors.DP-1.start = []
monitors.DP-2 = {
position = "bottom"
height = 30
start = []
}
}
```
</details>
### c) I want one or more monitors to have multiple bars
Create a map/object called `monitors` inside the top-level object.
Each of the map's keys should be an output name.
If you want the screen to have multiple bars, use an array of bar config objects.
If you want the screen to have a single bar, use an object.
To find your output names, run `wayland-info | grep wl_output -A1`.
<details>
<summary>JSON</summary>
```json
{
"monitors": {
"DP-1": [
{
"start": []
},
{
"position": "top",
"start": []
}
],
"DP-2": {
"position": "bottom",
"height": 30,
"start": []
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[monitors]
[[monitors.DP-1]]
start = []
[[monitors.DP-2]]
position = "top"
start = []
[monitors.DP-2]
position = "bottom"
height = 30
start = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
monitors:
DP-1:
- start: [ ]
- position: "top"
start: [ ]
DP-2:
position: "bottom"
height: 30
start: [ ]
```
</details>
<details>
<summary>Corn</summary>
```corn
{
monitors.DP-1 = [
{ start = [] }
{ position = "top" start = [] }
]
monitors.DP-2 = {
position = "bottom"
height = 30
start = []
}
}
```
</details>
## 3. Write your bar config(s)
Once you have the basic config structure set up, it's time to actually configure your bar(s).
Check [here](config) for an example config file for a fully configured bar in each format.
### 3.1 Top-level options
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
### 3.2 Module-level options
The following table lists each of the module-level options that are present on **all** modules.
For details on available modules and each of their config options, check the sidebar.
For information on the `Script` type, and embedding scripts in strings, see [here](script).
#### Events
| Name | Type | Default | Description |
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
#### Visibility
| Name | Type | Default | Description |
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
#### Appearance
| Name | Type | Default | Description |
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
For more information on styling, please see the [styling guide](styling-guide).

224
docs/Controlling Ironbar.md Normal file
View File

@@ -0,0 +1,224 @@
Ironbar includes a simple IPC server which can be used to control it programmatically at runtime.
It also includes a command line interface, which can be used for interacting with the IPC server.
# CLI
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
You can also view help per-command, for example using `ironbar set --help`.
Responses are handled by writing their type to stdout, followed by any value starting on the next line.
Error responses are written to stderr in the same format.
Example:
```shell
$ ironbar set subject world
ok
$ ironbar get subject
ok
world
```
# IPC
The server listens on a Unix socket.
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
Commands and responses are sent as JSON objects, denoted by their `type` key.
The message buffer is currently limited to `1024` bytes.
Particularly large messages will be truncated or cause an error.
The full spec can be found below.
## Libraries
- [Luajit](https://github.com/A-Cloud-Ninja/ironbar-ipc-luajit) - Maintained by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja)
## Commands
### `ping`
Sends a ping request to the IPC.
Responds with `ok`.
```json
{
"type": "ping"
}
```
### `inspect`
Opens the GTK inspector window.
Responds with `ok`.
```json
{
"type": "inspect"
}
```
### `reload`
Restarts the bars, reloading the config in the process.
The IPC server and main GTK application are untouched.
Responds with `ok`.
```json
{
"type": "reload"
}
```
### `get`
Gets an [ironvar](ironvars) value.
Responds with `ok_value` if the value exists, otherwise `error`.
```json
{
"type": "get",
"key": "foo"
}
```
### `set`
Sets an [ironvar](ironvars) value.
Responds with `ok`.
```json
{
"type": "set",
"key": "foo",
"value": "bar"
}
```
### `load_css`
Loads an additional CSS stylesheet, with hot-reloading enabled.
Responds with `ok` if the stylesheet exists, otherwise `error`.
```json
{
"type": "load_css",
"path": "/path/to/style.css"
}
```
### `set_visible`
Sets a bar's visibility.
Responds with `ok` if the bar exists, otherwise `error`.
```json
{
"type": "set_visible",
"bar_name": "bar-123",
"visible": true
}
```
### `get_visible`
Gets a bar's visibility.
Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, otherwise `error`.
```json
{
"type": "get_visible",
"bar_name": "bar-123"
}
```
### `toggle_popup`
Toggles the open/closed state for a module's popup.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "toggle_popup",
"bar_name": "bar-123",
"name": "clock"
}
```
### `open_popup`
Sets a module's popup open, regardless of its current state.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "open_popup",
"bar_name": "bar-123",
"name": "clock"
}
```
### `close_popup`
Sets the popup on a bar closed, regardless of which module it is open for.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "close_popup",
"bar_name": "bar-123"
}
```
## Responses
### `ok`
The operation completed successfully, with no response data.
```json
{
"type": "ok"
}
```
### `ok_value`
The operation completed successfully, with response data.
```json
{
"type": "ok_value",
"value": "lorem ipsum"
}
```
### `error`
The operation failed.
Message is optional.
```json
{
"type": "error",
"message": "lorem ipsum"
}
```

39
docs/Dynamic values.md Normal file
View File

@@ -0,0 +1,39 @@
In some configuration locations, Ironbar supports dynamic values,
meaning you can inject content into the bar from an external source.
Currently two dynamic content sources are supported - scripts and ironvars.
## Dynamic String
Dynamic strings can contain any mixture of static string elements, scripts and variables.
Scripts should be placed inside `{{double braces}}`. Both polling and watching scripts are supported.
Variables use the standard `#name` syntax. Variables cannot be placed inside scripts.
To use a literal hash, use `##`. This is only necessary outside of scripts.
Example:
```toml
label = "{{cat greeting.txt}}, #subject"
```
## Dynamic Boolean
Dynamic booleans can use a single source of either a script or variable to control a true/false value.
For scripts, you can just write these directly with no notation.
Only polling scripts are supported.
The script exit code is used, where `0` is `true` and any other code is `false.
For variables, use the standard `#name` notation.
An empty string, `0` and `false` are treated as false.
Any other value is true.
Example:
```toml
show_if = "exit 0" # script
show_if = "#show_module" # variable
```

4
docs/Home.md Normal file
View File

@@ -0,0 +1,4 @@
Welcome to the Ironbar wiki.
Detail about each module, and their configuration and styling options can be found on the sidebar.
You can also find an example configuration and stylesheet there.

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.

9
docs/Ironvars.md Normal file
View File

@@ -0,0 +1,9 @@
Ironvars are runtime variables that can be referenced in several places in your config,
then set using the IPC server (such as via the CLI) using the `set` command.
Any UTF-8 string *without whitespace* is a valid key.
Any UTF-8 string is a valid value.
Reference values using `#my_variable`. These update as soon as the value changes.
You can set defaults using the `ironvar_defaults` key in your top-level config.

107
docs/Scripts.md Normal file
View File

@@ -0,0 +1,107 @@
There are various places inside the configuration (other than the `script` module)
that allow script input to dynamically set values.
Scripts are passed to `sh -c`.
Three types of scripts exist: polling, oneshot and watching:
- **Polling** scripts will run and wait for exit.
Normally they will repeat this at an interval, hence the name, although in some cases they may only run on a user
event.
If the script exited code 0, the `stdout` will be used. Otherwise, `stderr` will be printed to the log.
- **Oneshot** scripts are a variant of polling scripts.
They wait for script to exit, and may do something with the output, but are only fired by user events instead of the interval.
Generally options that accept oneshot scripts do not support the other types.
- **Watching** scripts start a long-running process. Every time the process writes to `stdout`, the last line is captured
and used.
One should prefer to use watch-mode where possible, as it removes the overhead of regularly spawning processes.
That said, there are some cases which only support polling. These are indicated by `Script [polling]` as the option
type.
## Writing script configs
There are two available config formats for scripts, shorthand as a string, or longhand as an object.
Shorthand can be used in all cases, but there are some cases (such as embedding scripts inside strings) where longhand
cannot be used.
In both formats, `mode` is one of `poll` or `watch` and `interval` is the number of milliseconds to wait between
spawning the script.
Both `mode` and `interval` are optional and can be excluded to fall back to their defaults of `poll` and `5000`
respectively.
For oneshot scripts, both the mode and interval are ignored.
### Shorthand (string)
Shorthand scripts should be written in the format:
```
mode:interval:script
```
For example:
```
poll:5000:uptime -p | cut -d ' ' -f2-
```
#### Embedding
Some string config options support "embedding scripts". This allows you to mix static/dynamic content.
An example of this is the common `tooltip` option.
Scripts can be embedded in these cases using `{{double braces}}` and the shorthand syntax:
```json
"Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
```
### Longhand (object)
An object consisting of the `cmd` key and optionally the `mode` and/or `interval` keys.
<details>
<summary>JSON</summary>
```json
{
"mode": "poll",
"interval": 5000,
"cmd": "uptime -p | cut -d ' ' -f2-"
}
```
</details>
<details>
<summary>YAML</summary>
```yaml
mode: poll
interval: 5000
cmd: "uptime -p | cut -d ' ' -f2-"
```
</details>
<details>
<summary>YAML</summary>
```toml
mode = "poll"
interval = 5000
cmd = "uptime -p | cut -d ' ' -f2-"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
mode = "poll"
interval = 5000
cmd = "uptime -p | cut -d ' ' -f2-"
}
```
</details>

42
docs/Styling guide.md Normal file
View File

@@ -0,0 +1,42 @@
Ironbar ships with no styles by default, so will fall back to the default GTK styles.
To style the bar, create a file at `~/.config/ironbar/style.css`.
Style changes are hot-loaded so there is no need to reload the bar.
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html),
which only includes a subset of the full web spec (plus a few non-standard properties).
The below table describes the selectors provided by the bar itself.
Information on styling individual modules can be found on their pages in the sidebar.
| Selector | Description |
|---------------------|--------------------------------------------|
| `.background` | Top-level window. |
| `#bar` | Bar root box. |
| `#bar #start` | Bar left or top modules container box. |
| `#bar #center` | Bar center modules container box. |
| `#bar #end` | Bar right or bottom modules container box. |
| `.container` | All of the above. |
| `.widget-container` | The `EventBox` wrapping any widget. |
| `.widget` | Any widget. |
| `.popup` | Any popup box. |
Every widget can be selected using a `kebab-case` class name matching its name.
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
```css
@define-color color_bg #2d2d2d;
box, menubar {
background-color: @color_bg;
}
```

37
docs/_Sidebar.md Normal file
View File

@@ -0,0 +1,37 @@
# Guides
- [Compiling from source](compiling)
- [Configuration guide](configuration-guide)
- [Images](images)
- [Styling guide](styling-guide)
# Dynamic content
- [Controlling Ironbar](controlling-ironbar)
- [Dynamic values](dynamic-values)
- [Scripts](scripts)
- [Ironvars](ironvars)
# Examples
- [Config](config)
- [Stylesheet](https://github.com/JakeStanger/ironbar/blob/master/examples/style.css)
## Custom
- [Power Menu](power-menu)
# Modules
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)
- [Focused](focused)
- [Label](label)
- [Launcher](launcher)
- [Music](music)
- [Script](script)
- [Sys_Info](sys-info)
- [Tray](tray)
- [Upower](upower)
- [Workspaces](workspaces)

10
docs/examples/Config.md Normal file
View File

@@ -0,0 +1,10 @@
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.
- [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

@@ -0,0 +1,241 @@
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](https://f.jstanger.dev/github/ironbar/custom-power-menu.png)
## Configuration
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock"
},
{
"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"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = 'clock'
[[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'
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: clock
- 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
```
</details>
<details>
<summary>Corn</summary>
```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 = [ $button ]
popup = [ $popup ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
$clock = { type = "clock" }
} in {
end = [ $power_menu $clock ]
}
```
</details>
## Styling
```css
.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;
}
```

97
docs/modules/Clipboard.md Normal file
View File

@@ -0,0 +1,97 @@
Shows recent clipboard items, allowing you to switch between them to re-copy previous values.
Clicking the icon button opens the popup containing all functionality.
Supports plain text and images.
![Screenshot of clipboard popup open, with two textual values and an image copied. Several other unrelated widgets are visible on the bar.](https://f.jstanger.dev/github/ironbar/clipboard.png?raw)
## Configuration
> Type: `clipboard`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string` or [image](images) | `󰨸` | Icon to show on the widget button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>
```json
{
"end": {
"type": "clipboard",
"max_items": 3,
"truncate": {
"mode": "end",
"length": 50
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "clipboard"
max_items = 3
[[end.truncate]]
mode = "end"
length = 50
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: 'clipboard'
max_items: 3
truncate:
mode: 'end'
length: 50
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [ {
type = "clipboard"
max_items = 3
truncate.mode = "end"
truncate.length = 50
} ]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------------------|------------------------------------------------------|
| `.clipboard` | Clipboard widget. |
| `.clipboard .btn` | Clipboard widget button. |
| `.clipboard .btn .icon` | Clipboard widget button icon (any type). |
| `.clipboard .btn .text-icon` | Clipboard widget button icon (textual only). |
| `.clipboard .btn .image` | Clipboard widget button icon (image only). |
| `.popup-clipboard` | Clipboard popup box. |
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
For more information on styling, please see the [styling guide](styling-guide).

83
docs/modules/Clock.md Normal file
View File

@@ -0,0 +1,83 @@
Displays the current date and time.
Clicking on the widget opens a popup with the time and a calendar.
![Screenshot of clock widget with popup open](https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png)
## Configuration
> Type: `clock`
| Name | Type | Default | Description |
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. |
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. |
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock",
"format": "%d/%m/%Y %H:%M"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "clock"
format = "%d/%m/%Y %H:%M"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "clock"
format: "%d/%m/%Y %H:%M"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "clock"
format = "%d/%m/%Y %H:%M"
}
]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------------|------------------------------------------------------------------------------------|
| `.clock` | Clock widget button |
| `.popup-clock` | Clock popup box |
| `.popup-clock .calendar-clock` | Clock inside the popup |
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
For more information on styling, please see the [styling guide](styling-guide).

403
docs/modules/Custom.md Normal file
View File

@@ -0,0 +1,403 @@
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.
If you only intend to run a single script, prefer the [script](script) module,
or [label](label) if you only need a single text label.
![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?raw)
## Configuration
> Type: `custom`
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
It is well worth looking at the examples.
### `Widget`
There are many widget types, each with their own config options.
You can think of these like HTML elements and their attributes.
Every widget has the following options available; `type` is mandatory.
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
| Name | Type | Default | Description |
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
#### Box
A container to place nested widgets inside.
> Type: `box`
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
#### Label
A text label. Pango markup is supported.
> Type `label`
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
#### Button
A clickable button, which can run a command when clicked.
> Type `button`
| Name | Type | Default | Description |
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
#### Image
An image or icon from disk or http.
> Type `image`
| Name | Type | Default | Description |
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
#### Slider
A draggable slider.
> Type: `slider`
Note that `on_change` will provide the **floating point** value as an argument.
If your input program requires an integer, you will need to round it.
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
| `min` | `float` | `0` | Minimum slider value. |
| `max` | `float` | `100` | Maximum slider value. |
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
The example slider widget below shows a volume control for MPC,
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
```corn
$slider = {
type = "custom"
bar = [
{
type = "slider"
length = 100
max = 100
on_change="!mpc volume ${0%.*}"
value = "200:mpc volume | cut -d ':' -f2 | cut -d '%' -f1"
}
]
}
```
#### Progress
A progress bar.
> Type: `progress`
Note that `value` expects a numeric value **between 0-`max`** as output.
| Name | Type | Default | Description |
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
| `max` | `float` | `100` | Maximum progress bar value. |
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
The example below shows progress for the current playing song in MPD,
and displays the elapsed/length timestamps as a label above:
```corn
$progress = {
type = "custom"
bar = [
{
type = "progress"
value = "500:mpc | sed -n 2p | awk '{ print $4 }' | grep -Eo '[0-9]+' || echo 0"
label = "{{500:mpc | sed -n 2p | awk '{ print $3 }'}} elapsed"
length = 200
}
]
}
```
### Label Attributes
> This is different to the `label` widget, although applies to it.
Any widgets with a `label` attribute support embedded scripts,
meaning you can interpolate text from scripts to dynamically show content.
This can be done by including scripts in `{{double braces}}` using the shorthand script syntax.
For example, the following label would output your system uptime, updated every 30 seconds.
```
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
```
Both polling and watching mode are supported. For more information on script syntax, see [here](scripts).
### Commands
Buttons can execute commands that interact with the bar,
as well as any arbitrary shell command.
To execute shell commands, prefix them with an `!`.
For example, if you want to run `~/.local/bin/my-script.sh` on click,
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
Some widgets provide a value when they run the command, such as `slider`.
This is passed as an argument and can be accessed using `$0`.
The following bar commands are supported:
- `popup:toggle`
- `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
to help get your head around what's going on:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<custom class="power-menu">
<bar>
<button name="power-btn" label="" on_click="popup:toggle"/>
</bar>
<popup>
<box orientation="vertical">
<label name="header" label="Power menu" />
<box>
<button class="power-btn" label="" on_click="!shutdown now" />
<button class="power-btn" label="" on_click="!reboot" />
</box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
</box>
</popup>
</custom>
```
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock"
},
{
"bar": [
{
"on_click": "popup:toggle",
"label": "",
"name": "power-btn",
"type": "button"
}
],
"class": "power-menu",
"popup": [
{
"orientation": "vertical",
"type": "box",
"widgets": [
{
"label": "Power menu",
"name": "header",
"type": "label"
},
{
"type": "box",
"widgets": [
{
"class": "power-btn",
"on_click": "!shutdown now",
"label": "<span font-size='40pt'></span>",
"type": "button"
},
{
"class": "power-btn",
"on_click": "!reboot",
"label": "<span font-size='40pt'></span>",
"type": "button"
}
]
},
{
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
}
]
}
],
"type": "custom"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = 'clock'
[[end]]
class = 'power-menu'
type = 'custom'
[[end.bar]]
on_click = 'popup:toggle'
label = ''
name = 'power-btn'
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'
on_click = '!shutdown now'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!reboot'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets]]
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: clock
- bar:
- on_click: popup:toggle
label:
name: power-btn
type: button
class: power-menu
popup:
- orientation: vertical
type: box
widgets:
- label: Power menu
name: header
type: label
- type: box
widgets:
- class: power-btn
on_click: '!shutdown now'
label: <span font-size='40pt'></span>
type: button
- class: power-btn
on_click: '!reboot'
label: <span font-size='40pt'></span>
type: button
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
type: custom
```
</details>
<details>
<summary>Corn</summary>
```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 = [ $button ]
popup = [ $popup ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
} in {
end = [ $power_menu ]
}
```
</details>
## Styling
Since the widgets are all custom, you can use their `name` and `class` attributes, then target them using `#name` and `.class`.
The following top-level selectors are always available:
| Selector | Description |
|-----------------|--------------------------------|
| `.custom` | Custom widget container. |
| `.popup-custom` | Custom widget popup container. |
For more information on styling, please see the [styling guide](styling-guide).

95
docs/modules/Focused.md Normal file
View File

@@ -0,0 +1,95 @@
Displays the title and/or icon of the currently focused window.
![Screenshot of focused widget, showing this page open on firefox](https://user-images.githubusercontent.com/5057870/184714118-c1fb1c67-cd8c-4cc0-b5cd-6faccff818ac.png)
## Configuration
> Type: `focused`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "focused",
"show_icon": true,
"show_title": true,
"icon_size": 32,
"truncate": "end"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "focused"
show_icon = true
show_title = true
icon_size = 32
truncate = "end"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "focused"
show_icon: true
show_title: true
icon_size: 32
truncate: "end"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "focused"
show_icon = true
show_title = true
icon_size = 32
truncate = "end"
}
]
}
```
</details>
## Styling
| Selector | Description |
|-------------------|--------------------|
| `.focused` | Focused widget box |
| `.focused .icon` | App icon |
| `.focused .label` | App name |
For more information on styling, please see the [styling guide](styling-guide).

75
docs/modules/Label.md Normal file
View File

@@ -0,0 +1,75 @@
Displays custom text, with markup support.
If you only intend to run a single script, prefer the [script](script) module.
For more advanced use-cases, use [custom](custom).
## Configuration
> Type: `label`
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "label",
"label": "random num: {{500:echo $RANDOM}}"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "label"
label = "random num: {{500:echo $RANDOM}}"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "label"
label: "random num: {{500:echo $RANDOM}}"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "label"
label = "random num: {{500:echo $RANDOM}}"
}
]
}
```
</details>
## Styling
| Selector | Description |
|----------|------------------------------------------------------------------------------------|
| `.label` | Label widget |
For more information on styling, please see the [styling guide](styling-guide).

101
docs/modules/Launcher.md Normal file
View File

@@ -0,0 +1,101 @@
Windows-style taskbar that displays running windows, grouped by program.
Hovering over a program with multiple windows open shows a popup with each window.
Clicking an icon/popup item focuses or launches the program.
Optionally displays a launchable set of favourites.
![Screenshot showing several open applications, including a popup showing multiple terminal windows.](https://f.jstanger.dev/github/ironbar/launcher.png)
## Configuration
> Type: `launcher`
| | Type | Default | Description |
|--------------|------------|---------|-----------------------------------------------------------------------------------------------------|
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "launcher",
"favourites": [
"firefox",
"discord"
],
"show_names": false,
"show_icons": true
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "launcher"
favorites = ["firefox", "discord"]
show_names = false
show_icons = true
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "launcher"
favorites:
- firefox
- discord
show_names: false
show_icons: true
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "launcher"
favorites = [ "firefox" "discord" ]
show_names = false
show_icons = true
}
]
}
```
</details>
## Styling
| Selector | Description |
|-------------------------------|--------------------------|
| `.launcher` | Launcher widget box |
| `.launcher .item` | App button |
| `.launcher .item.open` | App button (open app) |
| `.launcher .item.focused` | App button (focused app) |
| `.launcher .item.urgent` | App button (urgent app) |
| `.popup-launcher` | Popup container |
| `.popup-launcher .popup-item` | Window button in popup |
For more information on styling, please see the [styling guide](styling-guide).

173
docs/modules/Music.md Normal file
View File

@@ -0,0 +1,173 @@
Displays currently playing song from your music player.
This module supports both MPRIS players and MPD servers.
Clicking on the widget opens a popout displaying info about the current song, album art
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://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` | `{title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
See [here](images) for information on images.
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "music",
"player_type": "mpd",
"format": "{title} / {artist}",
"truncate": "end",
"icons": {
"play": "",
"pause": ""
},
"music_dir": "/home/jake/Music"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "music"
player_type = "mpd"
format = "{title} / {artist}"
music_dir = "/home/jake/Music"
truncate = "end"
[[start.icons]]
play = ""
pause = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "music"
player_type: "mpd"
format: "{title} / {artist}"
truncate: "end"
icons:
play: ""
pause: ""
music_dir: "/home/jake/Music"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "music"
player_type = "mpd"
format = "{title} / {artist}"
truncate = "end"
icons.play = ""
icons.pause = ""
music_dir = "/home/jake/Music"
}
]
}
```
</details>
### Formatting Tokens
The following tokens can be used in the `format` config option,
and will be replaced with values from the currently playing track:
| Token | Description |
|--------------|--------------------------------------|
| `{title}` | Title |
| `{album}` | Album name |
| `{artist}` | Artist name |
| `{date}` | Release date |
| `{track}` | Track number |
| `{disc}` | Disc number |
| `{genre}` | Genre |
## Styling
| Selector | Description |
|---------------------------------------------|-------------------------------------------------------|
| `.music` | Tray widget button |
| `.music .contents` | Tray widget button contents box |
| `.music .contents .icon` | Tray widget button icon (any type) |
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
| `.music .contents .image` | Tray widget button icon (image only) |
| `.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-box` | Track title icon container inside popup box |
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
| `.popup-music .title .label` | Track title label inside popup box |
| `.popup-music .album` | Track album container inside popup box |
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
| `.popup-music .album .label` | Track album label inside popup box |
| `.popup-music .artist` | Track artist container inside popup box |
| `.popup-music .artist .icon-box` | Track artist icon container inside popup box |
| `.popup-music .artist .icon-box .icon` | Track artist icon inside its container (any type) |
| `.popup-music .artist .icon-box .text-icon` | Track artist icon inside its container (textual only) |
| `.popup-music .artist .icon-box .image` | Track artist icon inside its container (image only) |
| `.popup-music .artist .label` | Track artist label inside popup box |
| `.popup-music .controls` | Controls container inside popup box |
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
| `.popup-music .controls .btn-play` | Play button inside popup box |
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
| `.popup-music .controls .btn-next` | Next button inside popup box |
| `.popup-music .volume` | Volume container inside popup box |
| `.popup-music .volume .slider` | Slider inside volume container |
| `.popup-music .volume .icon` | Icon inside volume container |
| `.popup-music .progress` | Progress (seek) bar container |
| `.popup-music .progress .slider` | Slider inside progress container |
| `.popup-music .progress .label` | Duration label inside progress container |
For more information on styling, please see the [styling guide](styling-guide).

92
docs/modules/Script.md Normal file
View File

@@ -0,0 +1,92 @@
Executes a script and shows the result of `stdout` on a label.
Pango markup is supported.
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
For more advanced use-cases, use [custom](custom).
## Configuration
> Type: `script`
| Name | Type | Default | Description |
|------------|-----------------------|---------|---------------------------------------------------------|
| `cmd` | `string` | `null` | Path to the script on disk |
| `mode` | `'poll'` or `'watch'` | `poll` | See [#modes](#modes) |
| `interval` | `number` | `5000` | Number of milliseconds to wait between executing script |
### Modes
- Use `poll` to run the script wait for it to exit. On exit, the label is updated to show everything the script wrote to `stdout`.
- Use `watch` to start a long-running script. Every time the script writes to `stdout`, the label is updated to show the latest line.
Note this does not work for all programs as they may use block-buffering instead of line-buffering when they detect output being piped.
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "script",
"cmd": "/home/jake/.local/bin/phone-battery",
"mode": "poll",
"interval": 5000
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "script"
cmd = "/home/jake/.local/bin/phone-battery"
mode = "poll"
interval = 5000
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "script"
cmd: "/home/jake/.local/bin/phone-battery"
mode: 'poll'
interval : 5000
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "script"
cmd = "/home/jake/.local/bin/phone-battery"
mode = "poll"
interval = 5000
}
]
}
```
</details>
## Styling
| Selector | Description |
|-----------|---------------------|
| `.script` | Script widget label |
For more information on styling, please see the [styling guide](styling-guide).

180
docs/modules/Sys-Info.md Normal file
View File

@@ -0,0 +1,180 @@
Displays one or more labels containing system information.
Separating information across several labels allows for styling each one independently.
Pango markup is supported.
![Screenshot showing sys-info module with widgets for all of the types of formatting tokens](https://user-images.githubusercontent.com/5057870/196059090-4056d083-69f0-4e6f-9673-9e35dc29d9f0.png)
## Configuration
> Type: `sys_info`
| Name | Type | Default | Description |
|--------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. |
| `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. |
| `interval.memory` | `integer` | `5` | Seconds between refreshing memory data |
| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data |
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"format": [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
" {uptime}"
],
"interval": {
"cpu": 1,
"disks": 300,
"memory": 30,
"networks": 3,
"temps": 5
},
"type": "sys_info"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[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
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- format:
- ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C'
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
- '李 {net_down:enp39s0} / {net_up:enp39s0} Mbps'
- '猪 {load_average:1} | {load_average:5} | {load_average:15}'
- ' {uptime}'
interval:
cpu: 1
disks: 300
memory: 30
networks: 3
temps: 5
type: sys_info
```
</details>
<details>
<summary>Corn</summary>
```corn
{
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}"
]
}
]
}
```
</details>
### Formatting Tokens
The following tokens can be used in the `format` configuration option:
| Token | Description |
|--------------------------|------------------------------------------------------------------------------------|
| **CPU** | |
| `{cpu_percent}` | Total CPU utilisation percentage |
| **Memory** | |
| `{memory_free}` | Memory free in GB. |
| `{memory_used}` | Memory used in GB. |
| `{memory_total}` | Memory total in GB. |
| `{memory_percent}` | Memory utilisation percentage. |
| `{swap_free}` | Swap free in GB. |
| `{swap_used}` | Swap used in GB. |
| `{swap_total}` | Swap total in GB. |
| `{swap_percent}` | Swap utilisation percentage. |
| **Temperature** | |
| `{temp_c:[sensor]}` | Temperature in degrees C. Replace `[sensor]` with the sensor label. |
| `{temp_f:[sensor]}` | Temperature in degrees F. Replace `[sensor]` with the sensor label. |
| **Disk** | |
| `{disk_free:[mount]}` | Disk free space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_used:[mount]}` | Disk used space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_total:[mount]}` | Disk total space in GB. Replace `[mount]` with the disk mountpoint. |
| `{disk_percent:[mount]}` | Disk utilisation percentage. Replace `[mount]` with the disk mountpoint. |
| **Network** | |
| `{net_down:[adapter]}` | Average network download speed in Mbps. Replace `[adapter]` with the adapter name. |
| `{net_up:[adapter]}` | Average network upload speed in Mbps. Replace `[adapter]` with the adapter name. |
| **System** | |
| `{load_average:1}` | 1-minute load average. |
| `{load_average:5}` | 5-minute load average. |
| `{load_average:15}` | 15-minute load average. |
| `{uptime}` | System uptime formatted as `HH:mm`. |
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
## Styling
| Selector | Description |
|------------------|------------------------------|
| `.sysinfo` | Sysinfo widget box |
| `.sysinfo .item` | Individual information label |
For more information on styling, please see the [styling guide](styling-guide).

66
docs/modules/Tray.md Normal file
View File

@@ -0,0 +1,66 @@
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
![Screenshot showing icon tray widget](https://user-images.githubusercontent.com/5057870/184540135-78ffd79d-f802-4c79-b09a-05a733dadc55.png)
## Configuration
> Type: `tray`
***This module provides no configuration options.***
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "tray"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "tray"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "tray"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{ type = "tray" }
]
}
```
</details>
## Styling
| Selector | Description |
|---------------|------------------|
| `.tray` | Tray widget box |
| `.tray .item` | Tray icon button |
For more information on styling, please see the [styling guide](styling-guide).

84
docs/modules/Upower.md Normal file
View File

@@ -0,0 +1,84 @@
Displays system power information such as the battery percentage, and estimated time to empty.
`TODO: ADD SCREENSHOT`
[//]: # (![Screenshot]&#40;https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png&#41;)
## Configuration
> Type: `upower`
| Name | Type | Default | Description |
|-------------|-----------|-----------------|---------------------------------------------------|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
| `icon_size` | `integer` | `24` | Size to render icon at. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "upower",
"format": "{percentage}%"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "upower"
format = "{percentage}%"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "upower"
format: "{percentage}%"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "upower"
format = "{percentage}%"
}
]
}
```
</details>
## Styling
| Selector | Description |
|---------------------------------|--------------------------------|
| `.upower` | Upower widget container. |
| `.upower .button` | Upower widget button. |
| `.upower .button .contents` | Upower widget button contents. |
| `.upower .button .icon` | Upower widget battery icon. |
| `.upower .button .label` | Upower widget button label. |
| `.popup-upower` | Upower popup box. |
| `.popup-upower .upower-details` | Label inside the popup. |
For more information on styling, please see the [styling guide](styling-guide).

112
docs/modules/Workspaces.md Normal file
View File

@@ -0,0 +1,112 @@
> ⚠ **This module is currently only supported on Sway and Hyprland**
Shows all current workspaces. Clicking a workspace changes focus to it.
![Screenshot showing workspaces widget using custom icons with browser workspace focused](https://user-images.githubusercontent.com/5057870/184540156-26cfe4ec-ab8d-4e0f-a883-8b641025366b.png)
## Configuration
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "workspaces",
"name_map": {
"1": "",
"2": "",
"3": ""
},
"favorites": ["1", "2", "3"],
"all_monitors": false
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "workspaces"
all_monitors = false
favorites = ["1", "2", "3"]
[[end.name_map]]
1 = ""
2 = ""
3 = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "workspaces"
name_map:
1: ""
2: ""
3: ""
favorites:
- "1"
- "2"
- "3"
all_monitors: false
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
favorites = [ "1" "2" "3" ]
all_monitors = false
}
]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------------|--------------------------------------|
| `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) |
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
| `.workspaces .item .icon` | Workspace button icon (any type) |
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -3,41 +3,110 @@ let {
type = "workspaces" type = "workspaces"
all_monitors = false all_monitors = false
name_map = { name_map = {
1 = "" 1 = "󰙯"
2 = "" 2 = "icon:firefox"
3 = "" 3 = ""
Games = "" Games = "icon:steam"
Code = "" Code = ""
} }
} }
$focused = { type = "focused" }
$launcher = { $launcher = {
type = "launcher" type = "launcher"
favorites = ["firefox" "discord" "Steam"] favorites = ["firefox" "discord" "steam"]
show_names = false show_names = false
show_icons = true show_icons = true
icon_theme = "Paper"
} }
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" } $mpris = {
$mpd_server = { type = "mpd" host = "chloe:6600" } type = "music"
player_type = "mpris"
on_click_middle = "playerctl play-pause"
on_scroll_up = "playerctl volume +5"
on_scroll_down = "playerctl volume -5"
}
$mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
$mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
$sys_info = { $sys_info = {
type = "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" } $tray = { type = "tray" }
$clock = { type = "clock" } $clock = { type = "clock" }
$phone_battery = { $phone_battery = {
type = "script" type = "script"
path = "/home/jake/bin/phone-battery" cmd = "/home/jake/bin/phone-battery"
show_if.cmd = "/home/jake/bin/phone-connected"
show_if.interval = 500
} }
$left = [ $workspaces $launcher ] $clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
// -- 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 = [ $button ]
popup = [ $popup ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
// -- end custom --
$left = [ $workspaces $launcher $label ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
} }
in { in {
left = $left right = $right anchor_to_edges = true
position = "bottom"
icon_theme = "Paper"
start = $left
end = $right
} }

View File

@@ -1,18 +1,135 @@
{ {
"monitors": { "anchor_to_edges": true,
"DP-1": [ "end": [
{ {
"left": [{"type": "clock"}] "music_dir": "/home/jake/Music",
"player_type": "mpd",
"truncate": {
"max_length": 100,
"mode": "end"
}, },
{ "type": "music"
"position": "top", },
"left": [] {
} "host": "chloe:6600",
], "player_type": "mpd",
"DP-2": { "truncate": "end",
"position": "bottom", "type": "music"
"height": 30, },
"left": [] {
"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"
},
{
"max_items": 3,
"truncate": {
"length": 50,
"mode": "end"
},
"type": "clipboard"
},
{
"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": [
{
"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"
},
{
"label": "random num: {{500:echo FIXME}}",
"type": "label"
}
]
}

122
examples/config.toml Normal file
View File

@@ -0,0 +1,122 @@
anchor_to_edges = true
icon_theme = "Paper"
position = "bottom"
[[end]]
music_dir = "/home/jake/Music"
player_type = "mpd"
type = "music"
[end.truncate]
max_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]]
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}",
]
type = "sys_info"
[end.interval]
cpu = 1
disks = 300
memory = 30
networks = 3
temps = 5
[[end]]
max_items = 3
type = "clipboard"
[end.truncate]
length = 50
mode = "end"
[[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]]
all_monitors = false
type = "workspaces"
[start.name_map]
1 = "󰙯"
2 = "icon:firefox"
3 = ""
Code = ""
Games = "icon:steam"
[[start]]
favorites = [
"firefox",
"discord",
"steam",
]
show_icons = true
show_names = false
type = "launcher"
[[start]]
label = "random num: {{500:echo FIXME}}"
type = "label"

87
examples/config.yaml Normal file
View File

@@ -0,0 +1,87 @@
anchor_to_edges: true
end:
- music_dir: /home/jake/Music
player_type: mpd
truncate:
max_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
- max_items: 3
truncate:
length: 50
mode: end
type: clipboard
- 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:
- 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
- label: 'random num: {{500:echo FIXME}}'
type: label

View File

@@ -1,148 +1,201 @@
@define-color color_bg #2d2d2d;
@define-color color_bg_dark #1c1c1c;
@define-color color_border #424242;
@define-color color_border_active #6699cc;
@define-color color_text #ffffff;
@define-color color_urgent #8f0a0a;
/* -- base styles -- */
* { * {
/* `otf-font-awesome` is required to be installed for icons */
font-family: Noto Sans Nerd Font, sans-serif; font-family: Noto Sans Nerd Font, sans-serif;
/* font-family: 'Jetbrains Mono', monospace;*/
font-size: 16px; font-size: 16px;
/*color: white;*/
/*background-color: #2d2d2d;*/
/*background-color: red;*/
border: none; border: none;
border-radius: 0;
}
/*opacity: 0.4;*/ box, menubar, button {
background-color: @color_bg;
background-image: none;
}
button, label {
color: @color_text;
}
button:hover {
background-color: @color_bg_dark;
} }
#bar { #bar {
border-top: 1px solid #424242; border-top: 1px solid @color_border;
}
.container {
background-color: #2d2d2d;
}
/* test 34543*/
#right > * + * {
margin-left: 20px;
}
#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;
}
#clock {
color: white;
background-color: #2d2d2d;
font-weight: bold;
}
#script {
color: white;
}
#sysinfo {
color: white;
}
#tray .item {
background-color: #2d2d2d;
}
#mpd {
background-color: #2d2d2d;
color: white;
} }
.popup { .popup {
background-color: #2d2d2d; border: 1px solid @color_border;
border: 1px solid #424242;
}
#popup-clock {
padding: 1em; padding: 1em;
} }
#calendar-clock {
color: white; /* -- clipboard -- */
.clipboard {
margin-left: 5px;
font-size: 1.1em;
}
.popup-clipboard .item {
padding-bottom: 0.3em;
border-bottom: 1px solid @color_border;
}
/* -- clock -- */
.clock {
font-weight: bold;
margin-left: 5px;
}
.popup-clock .calendar-clock {
color: @color_text;
font-size: 2.5em; font-size: 2.5em;
padding-bottom: 0.1em; padding-bottom: 0.1em;
} }
#calendar { .popup-clock .calendar {
background-color: #2d2d2d; background-color: @color_bg;
color: white; color: @color_text;
} }
#calendar .header { .popup-clock .calendar .header {
padding-top: 1em; padding-top: 1em;
border-top: 1px solid #424242; border-top: 1px solid @color_border;
font-size: 1.5em; font-size: 1.5em;
} }
#calendar:selected { .popup-clock .calendar:selected {
background-color: #6699cc; background-color: @color_border_active;
} }
#popup-mpd {
color: white; /* -- launcher -- */
padding: 1em;
.launcher .item {
margin-right: 4px;
} }
#popup-mpd #album-art { .launcher .item:not(.focused):hover {
/*border: 1px solid #424242;*/ background-color: @color_bg_dark;
}
.launcher .open {
border-bottom: 1px solid @color_text;
}
.launcher .focused {
border-bottom: 2px solid @color_border_active;
}
.launcher .urgent {
border-bottom-color: @color_urgent;
}
.popup-launcher {
padding: 0;
}
.popup-launcher .popup-item:not(:first-child) {
border-top: 1px solid @color_border;
}
/* -- music -- */
.music:hover * {
background-color: @color_bg_dark;
}
.popup-music .album-art {
margin-right: 1em; margin-right: 1em;
} }
#popup-mpd #title .icon, #popup-mpd #title .label { .popup-music .icon-box {
margin-right: 0.4em;
}
.popup-music .title .icon, .popup-music .title .label {
font-size: 1.7em; font-size: 1.7em;
} }
#popup-mpd #controls * { .popup-music .controls *:disabled {
border-radius: 0; color: @color_border;
background-color: #2d2d2d;
color: white;
} }
#popup-mpd #controls *:disabled { .popup-music .volume .slider slider {
color: #424242; border-radius: 100%;
} }
#focused { .popup-music .volume .icon {
color: white; margin-left: 4px;
} }
.popup-music .progress .slider slider {
border-radius: 100%;
}
/* -- script -- */
.script {
padding-left: 10px;
}
/* -- sys_info -- */
.sysinfo {
margin-left: 10px;
}
.sysinfo .item {
margin-left: 5px;
}
/* -- tray -- */
.tray {
margin-left: 10px;
}
/* -- workspaces -- */
.workspaces .item.focused {
box-shadow: inset 0 -3px;
background-color: @color_bg_dark;
}
.workspaces .item:hover {
box-shadow: inset 0 -3px;
}
/* -- custom: power menu -- */
.popup-power-menu #header {
font-size: 1.4em;
padding-bottom: 0.4em;
margin-bottom: 0.6em;
border-bottom: 1px solid @color_border;
}
.popup-power-menu .power-btn {
border: 1px solid @color_border;
padding: 0.6em 1em;
}
.popup-power-menu #buttons > *:nth-child(1) .power-btn {
margin-right: 1em;
}

213
flake.lock generated Normal file
View File

@@ -0,0 +1,213 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1693439040,
"narHash": "sha256-t2nOxBcP0Q/XJt6Ild4v0hJ49OSl9F3nE1cdIT4xsDg=",
"owner": "ipetkov",
"repo": "crane",
"rev": "174604795d316b75777e28185c3a4918bc69b399",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1692351612,
"narHash": "sha256-KTGonidcdaLadRnv9KFgwSMh1ZbXoR/OBmPjeNMhFwU=",
"owner": "nix-community",
"repo": "naersk",
"rev": "78789c30d64dea2396c9da516bbcc8db3a475207",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1693355128,
"narHash": "sha256-+ZoAny3ZxLcfMaUoLVgL9Ywb/57wP+EtsdNGuXUJrwg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a63a64b593dcf2fe05f7c5d666eb395950f36bc9",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1693377291,
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay_2"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1691374719,
"narHash": "sha256-HCodqnx1Mi2vN4f3hjRPc7+lSQy18vRn8xWW68GeQOg=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b520a3889b24aaf909e287d19d406862ced9ffc9",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1693447852,
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

207
flake.nix Normal file
View File

@@ -0,0 +1,207 @@
{
description = "Nix Flake for ironbar";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
};
outputs = {
self,
nixpkgs,
rust-overlay,
crane,
naersk,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-linux"
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
self.overlays.default
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
in {
overlays.default = final: prev: let
rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = let
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in
if builder == "crane"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
};
};
packages = genSystems (
system: let
pkgs = pkgsFor system;
in
(self.overlays.default pkgs pkgs)
// {
default = self.packages.${system}.ironbar;
}
);
apps = genSystems (system: let
pkgs = pkgsFor system;
in {
default = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
ironbar = {
type = "app";
program = "${pkgs.ironbar}/bin/ironbar";
};
});
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = {
config,
lib,
pkgs,
...
}: let
cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
jsonFormat = pkgs.formats.json {};
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use.";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar.";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar.";
};
config = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The config to pass to ironbar.";
};
features = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [];
description = "The features to be used.";
};
};
config = let
pkg = cfg.package.override {features = cfg.features;};
in
lib.mkIf cfg.enable {
home.packages = [pkg];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
};
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${pkg}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
];
};
};
};
};
nixConfig = {
extra-substituters = ["https://jakestanger.cachix.org"];
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
};
}

95
nix/default.nix Normal file
View File

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

5
scripts/generate-examples.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
corn examples/config.corn -t json > examples/config.json
corn examples/config.corn -t toml > examples/config.toml
corn examples/config.corn -t yaml > examples/config.yaml

72
scripts/migrate-styles.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Migrates CSS selectors from widget names to CSS classes.
# These changed as part of the 0.12 release.
# ⚠ This script will **NOT** check for custom styles and may mangle them!
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
style_path="$HOME/.config/ironbar/style.css"
# general
sed -i 's/#icon/.icon/g' "$style_path"
sed -i 's/#label/.label/g' "$style_path"
sed -i 's/#image/.image/g' "$style_path"
# clipboard
sed -i 's/#clipboard/.clipboard/g' "$style_path"
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
# clock
sed -i 's/#clock/.clock/g' "$style_path"
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
sed -i 's/#calendar/.calendar/g' "$style_path"
# custom
sed -i 's/#custom/.custom/g' "$style_path"
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
# focused
sed -i 's/#focused/.focused/g' "$style_path"
# launcher
sed -i 's/#launcher/.launcher/g' "$style_path"
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
# music
sed -i 's/#music/.music/g' "$style_path"
sed -i 's/#contents/.contents/g' "$style_path"
sed -i 's/#popup-music/.popup-music/g' "$style_path"
sed -i 's/#album-art/.album-art/g' "$style_path"
sed -i 's/#title/.title/g' "$style_path"
sed -i 's/#album/.album/g' "$style_path"
sed -i 's/#artist/.artist/g' "$style_path"
sed -i 's/#controls/.controls/g' "$style_path"
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
sed -i 's/#btn-play/.btn-play/g' "$style_path"
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
sed -i 's/#btn-next/.btn-next/g' "$style_path"
sed -i 's/#volume/.volume/g' "$style_path"
sed -i 's/#slider/.slider/g' "$style_path"
# script
sed -i 's/#script/.script/g' "$style_path"
# sys_info
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
sed -i 's/#item/.item/g' "$style_path"
# tray
sed -i 's/#tray/.tray/g' "$style_path"
# upower
sed -i 's/#upower/.upower/g' "$style_path"
sed -i 's/#button/.button/g' "$style_path"
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
sed -i 's/#upower-details/.upower-details/g' "$style_path"
# workspaces
sed -i 's/#workspaces/.workspaces/g' "$style_path"
sed -i 's/#item/.item/g' "$style_path"

17
shell.nix Normal file
View File

@@ -0,0 +1,17 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
cargo
clippy
rustfmt
gtk3
gtk-layer-shell
gcc
openssl
];
nativeBuildInputs = with pkgs; [
pkg-config
];
}

View File

@@ -1,55 +1,161 @@
use crate::config::{BarPosition, ModuleConfig}; use crate::config::{BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{Module, ModuleInfo, ModuleLocation}; use crate::modules::{
use crate::Config; create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::popup::Popup;
use crate::unique_id::get_unique_usize;
use crate::{Config, GlobalState};
use color_eyre::Result; use color_eyre::Result;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Orientation}; use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::cell::RefCell;
use std::rc::Rc;
use tracing::{debug, info};
/// Creates a new window for a bar,
/// sets it up and adds its widgets.
pub fn create_bar( pub fn create_bar(
app: &Application, app: &Application,
monitor: &Monitor, monitor: &Monitor,
monitor_name: &str, monitor_name: &str,
config: Config, config: Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> { ) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build(); let win = ApplicationWindow::builder().application(app).build();
let bar_name = config
.name
.clone()
.unwrap_or_else(|| format!("bar-{}", get_unique_usize()));
setup_layer_shell(&win, monitor, &config.position); win.set_widget_name(&bar_name);
info!("Creating bar {}", bar_name);
setup_layer_shell(
&win,
monitor,
config.position,
config.anchor_to_edges,
config.margin,
);
let orientation = config.position.get_orientation();
let content = gtk::Box::builder() let content = gtk::Box::builder()
.orientation(Orientation::Horizontal) .orientation(orientation)
.spacing(0) .spacing(0)
.hexpand(false) .hexpand(false)
.height_request(config.height) .name("bar");
.name("bar")
.build();
let left = gtk::Box::builder().spacing(0).name("left").build(); let content = if orientation == Orientation::Horizontal {
let center = gtk::Box::builder().spacing(0).name("center").build(); content.height_request(config.height)
let right = gtk::Box::builder().spacing(0).name("right").build(); } else {
content.width_request(config.height)
}
.build();
content.style_context().add_class("container"); content.style_context().add_class("container");
left.style_context().add_class("container");
center.style_context().add_class("container");
right.style_context().add_class("container");
content.add(&left); let start = create_container("start", orientation);
let center = create_container("center", orientation);
let end = create_container("end", orientation);
content.add(&start);
content.set_center_widget(Some(&center)); content.set_center_widget(Some(&center));
content.pack_end(&right, false, false, 0); content.pack_end(&end, false, false, 0);
let load_result = load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
global_state
.borrow_mut()
.popups_mut()
.insert(bar_name.into(), load_result.popup);
load_modules(&left, &center, &right, app, config, monitor, monitor_name)?;
win.add(&content); win.add(&content);
win.connect_destroy_event(|_, _| { win.connect_destroy_event(|_, _| {
info!("Shutting down");
gtk::main_quit(); gtk::main_quit();
Inhibit(false) Inhibit(false)
}); });
win.show_all(); debug!("Showing bar");
// show each box but do not use `show_all`.
// this ensures `show_if` option works as intended.
start.show();
center.show();
end.show();
content.show();
win.show();
Ok(()) Ok(())
} }
/// Sets up GTK layer shell for a provided application window.
fn setup_layer_shell(
win: &ApplicationWindow,
monitor: &Monitor,
position: BarPosition,
anchor_to_edges: bool,
margin: MarginConfig,
) {
gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor);
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
gtk_layer_shell::auto_exclusive_zone_enable(win);
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
let bar_orientation = position.get_orientation();
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Top,
position == BarPosition::Top
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Bottom,
position == BarPosition::Bottom
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Left,
position == BarPosition::Left
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Right,
position == BarPosition::Right
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
);
}
/// Creates a `gtk::Box` container to place widgets inside.
fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
let container = gtk::Box::builder()
.orientation(orientation)
.spacing(0)
.name(name)
.build();
container.style_context().add_class("container");
container
}
#[derive(Debug)]
struct BarLoadResult {
popup: Rc<RefCell<Popup>>,
}
/// Loads the configured modules onto a bar.
fn load_modules( fn load_modules(
left: &gtk::Box, left: &gtk::Box,
center: &gtk::Box, center: &gtk::Box,
@@ -58,92 +164,100 @@ fn load_modules(
config: Config, config: Config,
monitor: &Monitor, monitor: &Monitor,
output_name: &str, output_name: &str,
) -> Result<()> { ) -> Result<BarLoadResult> {
if let Some(modules) = config.left { let icon_theme = IconTheme::new();
let info = ModuleInfo { if let Some(ref theme) = config.icon_theme {
app, icon_theme.set_custom_theme(Some(theme));
location: ModuleLocation::Left, }
bar_position: &config.position,
monitor,
output_name,
};
add_modules(left, modules, &info)?; macro_rules! info {
($location:expr) => {
ModuleInfo {
app,
bar_position: config.position,
monitor,
output_name,
location: $location,
icon_theme: &icon_theme,
}
};
}
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = Rc::new(RefCell::new(popup));
if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left);
add_modules(left, modules, &info, &popup)?;
} }
if let Some(modules) = config.center { if let Some(modules) = config.center {
let info = ModuleInfo { let info = info!(ModuleLocation::Center);
app, add_modules(center, modules, &info, &popup)?;
location: ModuleLocation::Center,
bar_position: &config.position,
monitor,
output_name,
};
add_modules(center, modules, &info)?;
} }
if let Some(modules) = config.right { if let Some(modules) = config.end {
let info = ModuleInfo { let info = info!(ModuleLocation::Right);
app, add_modules(right, modules, &info, &popup)?;
location: ModuleLocation::Right,
bar_position: &config.position,
monitor,
output_name,
};
add_modules(right, modules, &info)?;
} }
Ok(()) let result = BarLoadResult { popup };
Ok(result)
} }
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> { /// Adds modules into a provided GTK box,
/// which should be one of its left, center or right containers.
fn add_modules(
content: &gtk::Box,
modules: Vec<ModuleConfig>,
info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>,
) -> Result<()> {
let orientation = info.bar_position.get_orientation();
macro_rules! add_module { macro_rules! add_module {
($module:expr, $name:literal) => {{ ($module:expr, $id:expr) => {{
let widget = $module.into_widget(&info)?; let common = $module.common.take().expect("common config to exist");
widget.set_widget_name($name); let widget_parts = create_module(
content.add(&widget); *$module,
$id,
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);
content.add(&container);
}}; }};
} }
for config in modules { for config in modules {
let id = get_unique_usize();
match config { match config {
ModuleConfig::Clock(module) => add_module!(module, "clock"), #[cfg(feature = "clipboard")]
ModuleConfig::Mpd(module) => add_module!(module, "mpd"), ModuleConfig::Clipboard(mut module) => add_module!(module, id),
ModuleConfig::Tray(module) => add_module!(module, "tray"), #[cfg(feature = "clock")]
ModuleConfig::Workspaces(module) => add_module!(module, "workspaces"), ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::SysInfo(module) => add_module!(module, "sysinfo"), ModuleConfig::Custom(mut module) => add_module!(module, id),
ModuleConfig::Launcher(module) => add_module!(module, "launcher"), ModuleConfig::Focused(mut module) => add_module!(module, id),
ModuleConfig::Script(module) => add_module!(module, "script"), ModuleConfig::Label(mut module) => add_module!(module, id),
ModuleConfig::Focused(module) => add_module!(module, "focused"), 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 = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
} }
} }
Ok(()) Ok(())
} }
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor);
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
gtk_layer_shell::auto_exclusive_zone_enable(win);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Top,
position == &BarPosition::Top,
);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Bottom,
position == &BarPosition::Bottom,
);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Left, true);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Right, true);
}

44
src/bridge_channel.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::send;
use tokio::spawn;
use tokio::sync::mpsc;
/// MPSC async -> GTK sync channel.
/// The sender uses `tokio::sync::mpsc`
/// while the receiver uses `glib::MainContext::channel`.
///
/// This makes it possible to send events asynchronously
/// and receive them on the main thread,
/// allowing UI updates to be handled on the receiving end.
pub struct BridgeChannel<T> {
async_tx: mpsc::Sender<T>,
sync_rx: glib::Receiver<T>,
}
impl<T: Send + 'static> BridgeChannel<T> {
/// Creates a new channel
pub fn new() -> Self {
let (async_tx, mut async_rx) = mpsc::channel(32);
let (sync_tx, sync_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
while let Some(val) = async_rx.recv().await {
send!(sync_tx, val);
}
});
Self { async_tx, sync_rx }
}
/// Gets a clone of the sender.
pub fn create_sender(&self) -> mpsc::Sender<T> {
self.async_tx.clone()
}
/// Attaches a callback to the receiver.
pub fn recv<F>(self, f: F) -> glib::SourceId
where
F: FnMut(T) -> glib::Continue + 'static,
{
self.sync_rx.attach(None, f)
}
}

145
src/cached_broadcast.rs Normal file
View File

@@ -0,0 +1,145 @@
use crate::{arc_rw, read_lock, send_async, write_lock};
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
// use std::thread::sleep;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
use tokio::time::sleep;
use tracing::trace;
pub trait Cacheable: Debug + Clone + Send + Sync {
type Key: Debug + Clone + Send + Sync + Eq;
fn get_key(&self) -> Self::Key;
}
pub type Sender<T> = mpsc::Sender<Event<T>>;
pub type Receiver<T> = mpsc::Receiver<Event<T>>;
pub struct CachedBroadcastChannel<T>
where
T: Cacheable,
{
capacity: usize,
data: Vec<T>,
channels: Arc<RwLock<Vec<mpsc::Sender<Event<T>>>>>,
base_tx: mpsc::Sender<Event<T>>,
}
#[derive(Debug, Clone)]
pub enum Event<T>
where
T: Cacheable,
{
Add(T),
Remove(T::Key),
Replace(T::Key, T),
}
impl<T> CachedBroadcastChannel<T>
where
T: Cacheable + 'static,
{
pub fn new(capacity: usize) -> Self {
let (tx, rx) = mpsc::channel::<Event<T>>(capacity);
let mut rx = DropDetector(rx);
// spawn_blocking(move || loop {
// let ev = rx.0.try_recv();
// println!("{ev:?}");
// sleep(Duration::from_secs(1))
// });
let channels = arc_rw!(Vec::<Sender<T>>::new());
let channels = Arc::clone(&channels);
spawn(async move {
println!("hello");
while let Some(event) = rx.0.recv().await {
println!("ev");
// trace!("{event:?}");
// let iter = read_lock!(channels).clone().into_iter();
// for channel in iter {
// send_async!(channel, event.clone());
// }
}
println!("goodbye");
});
Self {
capacity,
data: vec![],
channels,
base_tx: tx,
}
}
pub async fn send(&mut self, event: Event<T>) {
match event.clone() {
Event::Add(data) => {
self.data.push(data);
}
Event::Remove(key) => {
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
return;
};
self.data.remove(index);
}
Event::Replace(key, data) => {
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
return;
};
let _ = std::mem::replace(&mut self.data[index], data);
}
}
send_async!(self.base_tx, event);
// let mut closed = vec![];
// for (i, channel) in read_lock!(self.channels).iter().enumerate() {
// if channel.is_closed() {
// closed.push(i);
// } else {
// send_async!(channel, event.clone());
// }
// }
//
// for channel in closed.into_iter().rev() {
// write_lock!(self.channels).remove(channel);
// }
}
pub fn sender(&self) -> mpsc::Sender<Event<T>> {
self.base_tx.clone()
}
pub fn receiver(&mut self) -> mpsc::Receiver<Event<T>> {
let (tx, rx) = mpsc::channel(self.capacity);
write_lock!(self.channels).push(tx);
rx
}
pub fn data(&self) -> &Vec<T> {
&self.data
}
}
#[derive(Debug)]
struct DropDetector<T>(T);
impl<T> Drop for DropDetector<T> {
fn drop(&mut self) {
println!("DROPPED")
}
}
impl<T: Cacheable> Drop for CachedBroadcastChannel<T> {
fn drop(&mut self) {
println!("Channel DROPPED")
}
}

19
src/cli/mod.rs Normal file
View File

@@ -0,0 +1,19 @@
use crate::ipc::commands::Command;
use crate::ipc::responses::Response;
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Parser, Debug, Serialize, Deserialize)]
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
}
pub fn handle_response(response: Response) {
match response {
Response::Ok => println!("ok"),
Response::OkValue { value } => println!("ok\n{value}"),
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
}
}

239
src/clients/clipboard.rs Normal file
View File

@@ -0,0 +1,239 @@
use super::wayland::{self, ClipboardItem};
use crate::{arc_mut, lock, try_send};
use indexmap::map::Iter;
use indexmap::IndexMap;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::{debug, trace};
#[derive(Debug)]
pub enum ClipboardEvent {
Add(Arc<ClipboardItem>),
Remove(usize),
Activate(usize),
}
type EventSender = mpsc::Sender<ClipboardEvent>;
/// Clipboard client singleton,
/// to ensure bars don't duplicate requests to the compositor.
pub struct ClipboardClient {
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
cache: Arc<Mutex<ClipboardCache>>,
}
impl ClipboardClient {
fn new() -> Self {
trace!("Initializing clipboard client");
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
let cache = arc_mut!(ClipboardCache::new());
{
let senders = senders.clone();
let cache = cache.clone();
spawn(async move {
let (mut rx, item) = {
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_clipboard()
};
if let Some(item) = item {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
lock!(cache).insert(item, senders.len());
}
while let Ok(item) = rx.recv().await {
debug!("Received clipboard item (ID: {})", item.id);
let (existing_id, cache_size) = {
let cache = lock!(cache);
(cache.contains(&item), cache.len())
};
existing_id.map_or_else(
|| {
{
let mut cache = lock!(cache);
let senders = lock!(senders);
cache.insert(item.clone(), senders.len());
}
let senders = lock!(senders);
let iter = senders.iter();
for (tx, sender_cache_size) in iter {
if cache_size == *sender_cache_size {
let removed_id = lock!(cache)
.remove_ref_first()
.expect("Clipboard cache unexpectedly empty");
try_send!(tx, ClipboardEvent::Remove(removed_id));
}
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
},
|existing_id| {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(existing_id));
}
},
);
}
});
}
Self { senders, cache }
}
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
let (tx, rx) = mpsc::channel(16);
{
let cache = lock!(self.cache);
let iter = cache.iter();
for (_, (item, _)) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
}
lock!(self.senders).push((tx, cache_size));
rx
}
pub fn copy(&self, id: usize) {
debug!("Copying item with id {id}");
let item = {
let cache = lock!(self.cache);
cache.get(id)
};
if let Some(item) = item {
let wl = wayland::get_client();
let wl = lock!(wl);
wl.copy_to_clipboard(item);
}
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Activate(id));
}
}
pub fn remove(&self, id: usize) {
lock!(self.cache).remove(id);
let senders = lock!(self.senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Remove(id));
}
}
}
/// Shared clipboard item cache.
///
/// Items are stored with a number of references,
/// allowing different consumers to 'remove' cached items
/// at different times.
#[derive(Debug)]
struct ClipboardCache {
cache: IndexMap<usize, (Arc<ClipboardItem>, usize)>,
}
impl ClipboardCache {
/// Creates a new empty cache.
fn new() -> Self {
Self {
cache: IndexMap::new(),
}
}
/// Gets the entry with key `id` from the cache.
fn get(&self, id: usize) -> Option<Arc<ClipboardItem>> {
self.cache.get(&id).map(|(item, _)| item).cloned()
}
/// Inserts an entry with `ref_count` initial references.
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
self.cache
.insert(item.id, (item, ref_count))
.map(|(item, _)| item)
}
/// Removes the entry with key `id`.
/// This ignores references.
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
self.cache.shift_remove(&id).map(|(item, _)| item)
}
/// Removes a reference to the entry with key `id`.
///
/// If the reference count reaches zero, the entry
/// is removed from the cache.
fn remove_ref(&mut self, id: usize) {
if let Some(entry) = self.cache.get_mut(&id) {
entry.1 -= 1;
if entry.1 == 0 {
self.cache.shift_remove(&id);
}
}
}
/// Removes a reference to the first entry.
///
/// If the reference count reaches zero, the entry
/// is removed from the cache.
fn remove_ref_first(&mut self) -> Option<usize> {
if let Some((id, _)) = self.cache.first() {
let id = *id;
self.remove_ref(id);
Some(id)
} else {
None
}
}
/// Checks if an item with matching mime type and value
/// already exists in the cache.
fn contains(&self, item: &ClipboardItem) -> Option<usize> {
self.cache.values().find_map(|(it, _)| {
if it.mime_type == item.mime_type && it.value == item.value {
Some(it.id)
} else {
None
}
})
}
/// Gets the current number of items in the cache.
fn len(&self) -> usize {
self.cache.len()
}
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, usize)> {
self.cache.iter()
}
}
lazy_static! {
static ref CLIENT: ClipboardClient = ClipboardClient::new();
}
pub fn get_client() -> &'static ClipboardClient {
&CLIENT
}

View File

@@ -0,0 +1,295 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{arc_mut, lock, send};
use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
use hyprland::shared::WorkspaceType;
use lazy_static::lazy_static;
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::task::spawn_blocking;
use tracing::{debug, error, info};
pub struct EventClient {
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
impl EventClient {
fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16);
Self {
workspace_tx,
_workspace_rx: workspace_rx,
}
}
fn listen_workspace_events(&self) {
info!("Starting Hyprland event listener");
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_mut!(());
// cache the active workspace since Hyprland doesn't give us the prev active
let active = Self::get_active_workspace().expect("Failed to get active workspace");
let active = arc_mut!(Some(active));
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_added_handler(move |workspace_type| {
let _lock = lock!(lock);
debug!("Added workspace: {workspace_type:?}");
let workspace_name = get_workspace_name(workspace_type);
let prev_workspace = lock!(active);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Add(workspace));
}
});
}
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type| {
let _lock = lock!(lock);
let mut prev_workspace = lock!(active);
debug!(
"Received workspace change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.id)
);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
workspace.map_or_else(
|| {
error!("Unable to locate workspace");
},
|workspace| {
// there may be another type of update so dispatch that regardless of focus change
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
},
);
});
}
{
let tx = tx.clone();
let lock = lock.clone();
let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.workspace;
let mut prev_workspace = lock!(active);
debug!(
"Received active monitor change: {:?} -> {workspace_type:?}",
prev_workspace.as_ref().map(|w| &w.name)
);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some((false, workspace)) =
workspace.map(|w| (w.visibility.is_focused(), w))
{
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| {
let _lock = lock!(lock);
let workspace_type = event_data.workspace;
debug!("Received workspace move: {workspace_type:?}");
let mut prev_workspace = lock!(active);
let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
}
});
}
{
event_listener.add_workspace_destroy_handler(move |workspace_type| {
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");
});
}
/// 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>,
) {
send!(
tx,
WorkspaceUpdate::Focus {
old: prev_workspace.take(),
new: workspace.clone(),
}
);
prev_workspace.replace(workspace);
}
/// Gets a workspace by name from the server, given the active workspace if known.
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
Workspaces::get()
.expect("Failed to get workspaces")
.find_map(|w| {
if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
create_is_visible()(w)
}));
Some(Workspace::from((vis, w)))
} else {
None
}
})
}
/// Gets the active workspace from the server.
fn get_active_workspace() -> Result<Workspace> {
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?;
Ok(w)
}
}
impl WorkspaceClient for EventClient {
fn focus(&self, id: String) -> Result<()> {
let identifier = match id.parse::<i32>() {
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
};
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(())
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace_tx.subscribe();
{
let tx = self.workspace_tx.clone();
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
let is_visible = create_is_visible();
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect();
send!(tx, WorkspaceUpdate::Init(workspaces));
}
rx
}
}
lazy_static! {
static ref CLIENT: EventClient = {
let client = EventClient::new();
client.listen_workspace_events();
client
};
}
pub fn get_client() -> &'static EventClient {
&CLIENT
}
fn get_workspace_name(name: WorkspaceType) -> String {
match name {
WorkspaceType::Regular(name) => name,
WorkspaceType::Special(name) => name.unwrap_or_default(),
}
}
/// Creates a function which determines if a workspace is visible. This function makes a Hyprland call that allocates so it should be cached when possible, but it is only valid so long as workspaces do not change so it should not be stored long term
fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), |ms| ms.to_vec());
move |w| monitors.iter().any(|m| m.active_workspace.id == w.id)
}
impl From<(Visibility, HWorkspace)> for Workspace {
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
Self {
id: workspace.id.to_string(),
name: workspace.name,
monitor: workspace.monitor,
visibility,
}
}
}
impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility
where
F: FnOnce(&'f HWorkspace) -> bool,
'a: 'f,
{
fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self {
if Some(workspace.name.as_str()) == active_name {
Self::focused()
} else if is_visible(workspace) {
Self::visible()
} else {
Self::Hidden
}
}
}

View File

@@ -0,0 +1,134 @@
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,
}
impl Display for Compositor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
#[cfg(feature = "workspaces+sway")]
Self::Sway => "Sway",
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => "Hyprland",
Self::Unsupported => "Unsupported",
}
)
}
}
impl Compositor {
/// Attempts to get the current compositor.
/// This is done by checking system env vars.
fn get_current() -> Self {
if std::env::var("SWAYSOCK").is_ok() {
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() {
cfg_if! {
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland}
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
}
} else {
Self::Unsupported
}
}
/// Gets the workspace client for the current compositor
pub fn get_workspace_client() -> Result<&'static (dyn WorkspaceClient + Send)> {
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")),
}
}
}
#[derive(Debug, Clone)]
pub struct Workspace {
/// Unique identifier
pub id: String,
/// Workspace friendly name
pub name: String,
/// Name of the monitor (output) the workspace is located on
pub monitor: String,
/// How visible the workspace is
pub visibility: Visibility,
}
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
#[derive(Debug, Copy, Clone)]
pub enum Visibility {
Visible(bool),
Hidden,
}
impl Visibility {
pub fn visible() -> Self {
Self::Visible(false)
}
pub fn focused() -> Self {
Self::Visible(true)
}
pub fn is_visible(self) -> bool {
matches!(self, Self::Visible(_))
}
pub fn is_focused(self) -> bool {
if let Self::Visible(focused) = self {
focused
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub enum WorkspaceUpdate {
/// Provides an initial list of workspaces.
/// This is re-sent to all subscribers when a new subscription is created.
Init(Vec<Workspace>),
Add(Workspace),
Remove(String),
Update(Workspace),
Move(Workspace),
/// Declares focus moved from the old workspace to the new.
Focus {
old: Option<Workspace>,
new: Workspace,
},
}
pub trait WorkspaceClient {
/// Requests the workspace with this name is focused.
fn focus(&self, name: String) -> Result<()>;
/// Creates a new to workspace event receiver.
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}

View File

@@ -0,0 +1,179 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send};
use async_once::AsyncOnce;
use color_eyre::Report;
use futures_util::StreamExt;
use lazy_static::lazy_static;
use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::spawn;
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::sync::Mutex;
use tracing::{info, trace};
pub struct SwayEventClient {
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
}
impl SwayEventClient {
fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16);
{
let workspace_tx = workspace_tx.clone();
spawn(async move {
let client = Connection::new().await?;
info!("Sway IPC subscription client connected");
let event_types = [EventType::Workspace];
let mut events = client.subscribe(event_types).await?;
while let Some(event) = events.next().await {
trace!("event: {:?}", event);
if let Event::Workspace(ev) = event? {
workspace_tx.send(WorkspaceUpdate::from(*ev))?;
};
}
Ok::<(), Report>(())
});
}
Self {
workspace_tx,
_workspace_rx: workspace_rx,
}
}
}
impl WorkspaceClient for SwayEventClient {
fn focus(&self, id: String) -> color_eyre::Result<()> {
await_sync(async move {
let client = get_client().await;
let mut client = client.lock().await;
client.run_command(format!("workspace {id}")).await
})?;
Ok(())
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace_tx.subscribe();
{
let tx = self.workspace_tx.clone();
await_sync(async {
let client = get_client().await;
let mut client = client.lock().await;
let workspaces = client
.get_workspaces()
.await
.expect("Failed to get workspaces");
let event =
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
send!(tx, event);
});
}
rx
}
}
lazy_static! {
static ref CLIENT: AsyncOnce<Arc<Mutex<Connection>>> = AsyncOnce::new(async {
let client = Connection::new()
.await
.expect("Failed to connect to Sway socket");
Arc::new(Mutex::new(client))
});
static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new();
}
/// Gets the sway IPC client
async fn get_client() -> Arc<Mutex<Connection>> {
let client = CLIENT.get().await;
Arc::clone(client)
}
/// Gets the sway IPC event subscription client
pub fn get_sub_client() -> &'static SwayEventClient {
&SUB_CLIENT
}
impl From<Node> for Workspace {
fn from(node: Node) -> Self {
let visibility = Visibility::from(&node);
Self {
id: node.id.to_string(),
name: node.name.unwrap_or_default(),
monitor: node.output.unwrap_or_default(),
visibility,
}
}
}
impl From<swayipc_async::Workspace> for Workspace {
fn from(workspace: swayipc_async::Workspace) -> Self {
let visibility = Visibility::from(&workspace);
Self {
id: workspace.id.to_string(),
name: workspace.name,
monitor: workspace.output,
visibility,
}
}
}
impl From<&Node> for Visibility {
fn from(node: &Node) -> Self {
if node.focused {
Self::focused()
} else if node.visible.unwrap_or(false) {
Self::visible()
} else {
Self::Hidden
}
}
}
impl From<&swayipc_async::Workspace> for Visibility {
fn from(workspace: &swayipc_async::Workspace) -> Self {
if workspace.focused {
Self::focused()
} else if workspace.visible {
Self::visible()
} else {
Self::Hidden
}
}
}
impl From<WorkspaceEvent> for WorkspaceUpdate {
fn from(event: WorkspaceEvent) -> Self {
match event.change {
WorkspaceChange::Init => {
Self::Add(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.map(Workspace::from),
new: Workspace::from(event.current.expect("Missing current workspace")),
},
WorkspaceChange::Move => {
Self::Move(event.current.expect("Missing current workspace").into())
}
_ => Self::Update(event.current.expect("Missing current workspace").into()),
}
}
}

11
src/clients/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "tray")]
pub mod system_tray;
#[cfg(feature = "upower")]
pub mod upower;
pub mod wayland;

85
src/clients/music/mod.rs Normal file
View File

@@ -0,0 +1,85 @@
use color_eyre::Result;
use std::path::PathBuf;
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;
pub const TICK_INTERVAL_MS: u64 = 200;
#[derive(Clone, Debug)]
pub enum PlayerUpdate {
/// Triggered when the track or player state notably changes,
/// such as a new track playing, the player being paused, or a volume change.
Update(Box<Option<Track>>, Status),
/// Triggered at regular intervals while a track is playing.
/// Used to keep track of the progress through the current track.
ProgressTick(ProgressTick),
/// Triggered when the client disconnects from the player.
Disconnect,
}
#[derive(Clone, Debug)]
pub struct Track {
pub title: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub date: Option<String>,
pub disc: Option<u64>,
pub genre: Option<String>,
pub track: Option<u64>,
pub cover_path: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub enum PlayerState {
Playing,
Paused,
Stopped,
}
#[derive(Clone, Copy, Debug)]
pub struct Status {
pub state: PlayerState,
pub volume_percent: Option<u8>,
pub playlist_position: u32,
pub playlist_length: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct ProgressTick {
pub duration: Option<Duration>,
pub elapsed: Option<Duration>,
}
pub trait MusicClient {
fn play(&self) -> Result<()>;
fn pause(&self) -> Result<()>;
fn next(&self) -> Result<()>;
fn prev(&self) -> Result<()>;
fn set_volume_percent(&self, vol: u8) -> Result<()>;
fn seek(&self, duration: Duration) -> Result<()>;
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
}
pub enum ClientType<'a> {
Mpd { host: &'a str, music_dir: PathBuf },
Mpris,
}
pub async fn get_client(client_type: ClientType<'_>) -> Box<Arc<dyn MusicClient>> {
match client_type {
ClientType::Mpd { host, music_dir } => Box::new(
mpd::get_client(host, music_dir)
.await
.expect("Failed to connect to MPD client"),
),
ClientType::Mpris => Box::new(mpris::get_client()),
}
}

344
src/clients/music/mpd.rs Normal file
View File

@@ -0,0 +1,344 @@
use super::{
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
};
use crate::{await_sync, send};
use color_eyre::Result;
use lazy_static::lazy_static;
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
use mpd_client::commands::SeekMode;
use mpd_client::protocol::MpdProtocolError;
use mpd_client::responses::{PlayState, Song};
use mpd_client::tag::Tag;
use mpd_client::{commands, Client};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpStream, UnixStream};
use tokio::spawn;
use tokio::sync::broadcast;
use tokio::sync::Mutex;
use tokio::time::sleep;
use tracing::{debug, error, info};
lazy_static! {
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub struct MpdClient {
client: Client,
music_dir: PathBuf,
tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>,
}
#[derive(Debug)]
pub enum MpdConnectionError {
MaxRetries,
ProtocolError(MpdProtocolError),
}
impl Display for MpdConnectionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MaxRetries => write!(f, "Reached max retries"),
Self::ProtocolError(e) => write!(f, "{e:?}"),
}
}
}
impl std::error::Error for MpdConnectionError {}
impl MpdClient {
async fn new(host: &str, music_dir: PathBuf) -> Result<Self, MpdConnectionError> {
debug!("Creating new MPD connection to {}", host);
let (client, mut state_changes) =
wait_for_connection(host, Duration::from_secs(5), None).await?;
let (tx, rx) = broadcast::channel(16);
{
let music_dir = music_dir.clone();
let tx = tx.clone();
let client = client.clone();
spawn(async move {
while let Some(change) = state_changes.next().await {
debug!("Received state change: {:?}", change);
if let ConnectionEvent::SubsystemChange(
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
) = change
{
Self::send_update(&client, &tx, &music_dir)
.await
.expect("Failed to send update");
}
}
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
});
}
{
let client = client.clone();
let tx = tx.clone();
spawn(async move {
loop {
Self::send_tick_update(&client, &tx).await;
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
}
});
}
Ok(Self {
client,
music_dir,
tx,
_rx: rx,
})
}
async fn send_update(
client: &Client,
tx: &broadcast::Sender<PlayerUpdate>,
music_dir: &Path,
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
let current_song = client.command(commands::CurrentSong).await;
let status = client.command(commands::Status).await;
if let (Ok(current_song), Ok(status)) = (current_song, status) {
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
let status = Status::from(status);
let update = PlayerUpdate::Update(Box::new(track), status);
send!(tx, update);
}
Ok(())
}
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
let status = client.command(commands::Status).await;
if let Ok(status) = status {
if status.state == PlayState::Playing {
let update = PlayerUpdate::ProgressTick(ProgressTick {
duration: status.duration,
elapsed: status.elapsed,
});
send!(tx, update);
}
}
}
fn is_connected(&self) -> bool {
!self.client.is_connection_closed()
}
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
info!("Connection to MPD server lost");
self.tx.send(PlayerUpdate::Disconnect)?;
Ok(())
}
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"),
)
.into_os_string()
.into_string()
.ok();
Track {
title: song.title().map(std::string::ToString::to_string),
album: song.album().map(std::string::ToString::to_string),
artist: Some(song.artists().join(", ")),
date: try_get_first_tag(song, &Tag::Date).map(std::string::ToString::to_string),
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
disc: Some(disc),
track: Some(track),
cover_path,
}
}
}
macro_rules! async_command {
($client:expr, $command:expr) => {
await_sync(async {
$client
.command($command)
.await
.unwrap_or_else(|err| error!("Failed to send command: {err:?}"))
})
};
}
impl MusicClient for MpdClient {
fn play(&self) -> Result<()> {
async_command!(self.client, commands::SetPause(false));
Ok(())
}
fn pause(&self) -> Result<()> {
async_command!(self.client, commands::SetPause(true));
Ok(())
}
fn next(&self) -> Result<()> {
async_command!(self.client, commands::Next);
Ok(())
}
fn prev(&self) -> Result<()> {
async_command!(self.client, commands::Previous);
Ok(())
}
fn set_volume_percent(&self, vol: u8) -> Result<()> {
async_command!(self.client, commands::SetVolume(vol));
Ok(())
}
fn seek(&self, duration: Duration) -> Result<()> {
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
Ok(())
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
let rx = self.tx.subscribe();
await_sync(async {
Self::send_update(&self.client, &self.tx, &self.music_dir)
.await
.expect("Failed to send player update");
});
rx
}
}
pub async fn get_client(
host: &str,
music_dir: PathBuf,
) -> Result<Arc<MpdClient>, MpdConnectionError> {
let mut connections = CONNECTIONS.lock().await;
match connections.get(host) {
None => {
let client = MpdClient::new(host, music_dir).await?;
let client = Arc::new(client);
connections.insert(host.to_string(), Arc::clone(&client));
Ok(client)
}
Some(client) => {
if client.is_connected() {
Ok(Arc::clone(client))
} else {
client
.send_disconnect_update()
.expect("Failed to send disconnect update");
let client = MpdClient::new(host, music_dir).await?;
let client = Arc::new(client);
connections.insert(host.to_string(), Arc::clone(&client));
Ok(client)
}
}
}
}
async fn wait_for_connection(
host: &str,
interval: Duration,
max_retries: Option<usize>,
) -> Result<Connection, MpdConnectionError> {
let mut retries = 0;
let max_retries = max_retries.unwrap_or(usize::MAX);
loop {
if retries == max_retries {
break Err(MpdConnectionError::MaxRetries);
}
retries += 1;
match try_get_mpd_conn(host).await {
Ok(conn) => break Ok(conn),
Err(err) => {
if retries == max_retries {
break Err(MpdConnectionError::ProtocolError(err));
}
}
}
sleep(interval).await;
}
}
/// Cycles through each MPD host and
/// returns the first one which connects,
/// or none if there are none
async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
}
}
fn is_unix_socket(host: &str) -> bool {
let path = PathBuf::from(host);
path.exists()
&& path
.metadata()
.map_or(false, |metadata| metadata.file_type().is_socket())
}
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host).await?;
Client::connect(connection).await
}
/// Attempts to read the first value for a tag
/// (since the MPD client returns a vector of tags, or None)
pub fn try_get_first_tag<'a>(song: &'a Song, tag: &'a Tag) -> Option<&'a str> {
song.tags
.get(tag)
.and_then(|vec| vec.first().map(String::as_str))
}
impl From<mpd_client::responses::Status> for Status {
fn from(status: mpd_client::responses::Status) -> Self {
Self {
state: PlayerState::from(status.state),
volume_percent: Some(status.volume),
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
playlist_length: status.playlist_length as u32,
}
}
}
impl From<PlayState> for PlayerState {
fn from(value: PlayState) -> Self {
match value {
PlayState::Stopped => Self::Stopped,
PlayState::Playing => Self::Playing,
PlayState::Paused => Self::Paused,
}
}
}

346
src/clients/music/mpris.rs Normal file
View File

@@ -0,0 +1,346 @@
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
use crate::clients::music::ProgressTick;
use crate::{arc_mut, 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::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
use std::{cmp, string};
use tokio::sync::broadcast;
use tokio::task::spawn_blocking;
use tracing::{debug, error, trace};
lazy_static! {
static ref CLIENT: Arc<Client> = Arc::new(Client::new());
}
pub struct Client {
current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>,
}
impl Client {
fn new() -> Self {
let (tx, rx) = broadcast::channel(32);
let current_player = arc_mut!(None);
{
let players_list = arc_mut!(HashSet::new());
let current_player = current_player.clone();
let tx = tx.clone();
spawn_blocking(move || {
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
// D-Bus gives no event for new players,
// so we have to keep polling the player list
loop {
let players = player_finder
.find_all()
.expect("Failed to connect to D-Bus");
let mut players_list_val = lock!(players_list);
for player in players {
let identity = player.identity();
if !players_list_val.contains(identity) {
debug!("Adding MPRIS player '{identity}'");
players_list_val.insert(identity.to_string());
let status = player
.get_playback_status()
.expect("Failed to connect to D-Bus");
{
let mut current_player = lock!(current_player);
if status == PlaybackStatus::Playing || current_player.is_none() {
debug!("Setting active player to '{identity}'");
current_player.replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
}
Self::listen_player_events(
identity.to_string(),
players_list.clone(),
current_player.clone(),
tx.clone(),
);
}
}
// wait 1 second before re-checking players
sleep(Duration::from_secs(1));
}
});
}
{
let current_player = current_player.clone();
let tx = tx.clone();
spawn_blocking(move || {
let player_finder = PlayerFinder::new().expect("to get new player finder");
loop {
Self::send_tick_update(&player_finder, &current_player, &tx);
sleep(Duration::from_millis(TICK_INTERVAL_MS));
}
});
}
Self {
current_player,
tx,
_rx: rx,
}
}
fn listen_player_events(
player_id: String,
players: Arc<Mutex<HashSet<String>>>,
current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>,
) {
spawn_blocking(move || {
let player_finder = PlayerFinder::new()?;
if let Ok(player) = player_finder.find_by_name(&player_id) {
let identity = player.identity();
for event in player.events()? {
trace!("Received player event from '{identity}': {event:?}");
match event {
Ok(Event::PlayerShutDown) => {
lock!(current_player).take();
lock!(players).remove(identity);
break;
}
Ok(Event::Playing) => {
lock!(current_player).replace(identity.to_string());
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
Ok(_) => {
let current_player = lock!(current_player);
let current_player = current_player.as_ref();
if let Some(current_player) = current_player {
if current_player == identity {
if let Err(err) = Self::send_update(&player, &tx) {
error!("{err:?}");
}
}
}
}
Err(err) => error!("{err:?}"),
}
}
}
Ok::<(), DBusError>(())
});
}
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
debug!("Sending update using '{}'", player.identity());
let metadata = player.get_metadata()?;
let playback_status = player
.get_playback_status()
.unwrap_or(PlaybackStatus::Stopped);
let track_list = player.get_track_list();
let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok();
let status = Status {
// MRPIS doesn't seem to provide playlist info reliably,
// so we can just assume next/prev will work by bodging the numbers
playlist_position: 1,
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
state: PlayerState::from(playback_status),
volume_percent,
};
let track = Track::from(metadata);
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
send!(tx, player_update);
Ok(())
}
fn get_player(&self) -> Option<Player> {
let player_name = lock!(self.current_player);
let player_name = player_name.as_ref();
player_name.and_then(|player_name| {
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
player_finder.find_by_name(player_name).ok()
})
}
fn send_tick_update(
player_finder: &PlayerFinder,
current_player: &Mutex<Option<String>>,
tx: &broadcast::Sender<PlayerUpdate>,
) {
if let Some(player) = lock!(current_player)
.as_ref()
.and_then(|name| player_finder.find_by_name(name).ok())
{
if let Ok(metadata) = player.get_metadata() {
let update = PlayerUpdate::ProgressTick(ProgressTick {
elapsed: player.get_position().ok(),
duration: metadata.length(),
});
send!(tx, update);
}
}
}
}
macro_rules! command {
($self:ident, $func:ident) => {
if let Some(player) = Self::get_player($self) {
player.$func()?;
} else {
error!("Could not find player");
}
};
}
impl MusicClient for Client {
fn play(&self) -> Result<()> {
command!(self, play);
Ok(())
}
fn pause(&self) -> Result<()> {
command!(self, pause);
Ok(())
}
fn next(&self) -> Result<()> {
command!(self, next);
Ok(())
}
fn prev(&self) -> Result<()> {
command!(self, previous);
Ok(())
}
fn set_volume_percent(&self, vol: u8) -> Result<()> {
if let Some(player) = Self::get_player(self) {
player.set_volume(vol as f64 / 100.0)?;
} else {
error!("Could not find player");
}
Ok(())
}
fn seek(&self, duration: Duration) -> Result<()> {
if let Some(player) = Self::get_player(self) {
let pos = player.get_position().unwrap_or_default();
let duration = duration.as_micros() as i64;
let position = pos.as_micros() as i64;
let seek = cmp::max(duration, 0) - position;
player.seek(seek)?;
} else {
error!("Could not find player");
}
Ok(())
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
debug!("Creating new subscription");
let rx = self.tx.subscribe();
if let Some(player) = self.get_player() {
if let Err(err) = Self::send_update(&player, &self.tx) {
error!("{err:?}");
}
} else {
let status = Status {
playlist_position: 0,
playlist_length: 0,
state: PlayerState::Stopped,
volume_percent: None,
};
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
}
rx
}
}
pub fn get_client() -> Arc<Client> {
CLIENT.clone()
}
impl From<Metadata> for Track {
fn from(value: Metadata) -> Self {
const KEY_DATE: &str = "xesam:contentCreated";
const KEY_GENRE: &str = "xesam:genre";
Self {
title: value
.title()
.map(std::string::ToString::to_string)
.and_then(replace_empty_none),
album: value
.album_name()
.map(std::string::ToString::to_string)
.and_then(replace_empty_none),
artist: value
.artists()
.map(|artists| artists.join(", "))
.and_then(replace_empty_none),
date: value
.get(KEY_DATE)
.and_then(mpris::MetadataValue::as_string)
.map(std::string::ToString::to_string),
disc: value.disc_number().map(|disc| disc as u64),
genre: value
.get(KEY_GENRE)
.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(string::ToString::to_string),
}
}
}
impl From<PlaybackStatus> for PlayerState {
fn from(value: PlaybackStatus) -> Self {
match value {
PlaybackStatus::Playing => Self::Playing,
PlaybackStatus::Paused => Self::Paused,
PlaybackStatus::Stopped => Self::Stopped,
}
}
}
fn replace_empty_none(string: String) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
}
}

127
src/clients/system_tray.rs Normal file
View File

@@ -0,0 +1,127 @@
use crate::unique_id::get_unique_usize;
use crate::{arc_mut, 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 system_tray::message::menu::TrayMenu;
use system_tray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::StatusNotifierWatcher;
use tokio::spawn;
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error, trace};
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 {
async fn new() -> system_tray::error::Result<Self> {
let id = format!("ironbar-{}", get_unique_usize());
let (tx, rx) = mpsc::channel(16);
let (b_tx, b_rx) = broadcast::channel(16);
let tray = StatusNotifierWatcher::new(rx).await?;
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
let tray = arc_mut!(BTreeMap::new());
{
let b_tx = b_tx.clone();
let tray = tray.clone();
spawn(async move {
while let Ok(message) = host.recv().await {
trace!("Received message: {message:?} ");
send!(b_tx, message.clone());
let mut tray = lock!(tray);
match message {
NotifierItemMessage::Update {
address,
item,
menu,
} => {
debug!("Adding item with address '{address}'");
tray.insert(address, (item, menu));
}
NotifierItemMessage::Remove { address } => {
debug!("Removing item with address '{address}'");
tray.remove(&address);
}
}
}
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
});
}
Ok(Self {
tx,
b_tx,
_b_rx: b_rx,
tray,
})
}
pub fn subscribe(
&self,
) -> (
mpsc::Sender<NotifierItemCommand>,
broadcast::Receiver<NotifierItemMessage>,
) {
let tx = self.tx.clone();
let b_rx = self.b_tx.subscribe();
let tray = lock!(self.tray).clone();
for (address, (item, menu)) in tray {
let update = NotifierItemMessage::Update {
address,
item,
menu,
};
send!(self.b_tx, update);
}
(tx, b_rx)
}
}
lazy_static! {
static ref CLIENT: AsyncOnce<TrayEventReceiver> = AsyncOnce::new(async {
const MAX_RETRIES: i32 = 10;
// sometimes this can fail
let mut retries = 0;
let value = loop {
retries += 1;
let tray = Box::pin(TrayEventReceiver::new()).await;
match tray {
Ok(tray) => break Some(tray),
Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})")))
}
if retries == MAX_RETRIES {
break None;
}
};
value.expect("Failed to create StatusNotifierWatcher")
});
}
pub async fn get_tray_event_client() -> &'static TrayEventReceiver {
CLIENT.get().await
}

40
src/clients/upower.rs Normal file
View File

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

View File

@@ -0,0 +1,276 @@
use super::wlr_foreign_toplevel::handle::ToplevelHandle;
use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
use super::wlr_foreign_toplevel::ToplevelEvent;
use super::Environment;
use crate::cached_broadcast::CachedBroadcastChannel;
use crate::error::ERR_CHANNEL_RECV;
use crate::{cached_broadcast, send};
use cfg_if::cfg_if;
use color_eyre::Report;
use smithay_client_toolkit::output::{OutputInfo, OutputState};
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::registry::RegistryState;
use smithay_client_toolkit::seat::SeatState;
use std::collections::HashMap;
use std::sync::mpsc;
use tokio::sync::broadcast;
use tokio::task::spawn_blocking;
use tracing::{debug, error, trace};
use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, WaylandSource};
cfg_if! {
if #[cfg(feature = "clipboard")] {
use super::ClipboardItem;
use super::wlr_data_control::manager::DataControlDeviceManagerState;
use crate::lock;
use std::sync::Arc;
}
}
#[derive(Debug)]
pub enum Request {
/// Sends a request for all the seats.
/// These are then sent on the `seat` channel.
Seats,
/// Sends a request for all the toplevels.
/// These are then sent on the `toplevel_init` channel.
Toplevels,
/// Sends a request for the current clipboard item.
/// This is then sent on the `clipboard_init` channel.
#[cfg(feature = "clipboard")]
Clipboard,
/// Copies the value to the clipboard
#[cfg(feature = "clipboard")]
CopyToClipboard(Arc<ClipboardItem>),
/// Forces a dispatch, flushing any currently queued events
Roundtrip,
}
pub struct WaylandClient {
// External channels
output_channel: CachedBroadcastChannel<OutputInfo>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
#[cfg(feature = "clipboard")]
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
// Internal channels
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
#[cfg(feature = "clipboard")]
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
request_tx: Sender<Request>,
}
impl WaylandClient {
pub(super) fn new() -> Self {
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
let mut output_channel = CachedBroadcastChannel::new(8);
let output_tx = output_channel.sender();
let tx2 = output_tx.clone();
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
#[cfg(feature = "clipboard")]
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
let (seat_tx, seat_rx) = mpsc::channel();
let toplevel_tx2 = toplevel_tx.clone();
cfg_if! {
if #[cfg(feature = "clipboard")] {
let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
let clipboard_tx2 = clipboard_tx.clone();
}
}
let (ev_tx, ev_rx) = channel::<Request>();
// `queue` is not `Send` so we need to handle everything inside the task
spawn_blocking(move || {
let toplevel_tx = toplevel_tx2;
#[cfg(feature = "clipboard")]
let clipboard_tx = clipboard_tx2;
let conn =
Connection::connect_to_env().expect("Failed to connect to Wayland compositor");
let (globals, queue) =
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
let qh = queue.handle();
let mut event_loop =
EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
WaylandSource::new(queue)
.expect("Failed to create Wayland source from queue")
.insert(event_loop.handle())
.expect("Failed to insert Wayland event queue into event loop");
let loop_handle = event_loop.handle();
// Initialize the registry handling
// so other parts of Smithay's client toolkit may bind globals.
let registry_state = RegistryState::new(&globals);
let output_delegate = OutputState::new(&globals, &qh);
let seat_delegate = SeatState::new(&globals, &qh);
#[cfg(feature = "clipboard")]
let data_control_device_manager_delegate =
DataControlDeviceManagerState::bind(&globals, &qh)
.expect("data device manager is not available");
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
.expect("foreign toplevel manager is not available");
let mut env = Environment {
registry_state,
output_state: output_delegate,
seat_state: seat_delegate,
#[cfg(feature = "clipboard")]
data_control_device_manager_state: data_control_device_manager_delegate,
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
seats: vec![],
handles: HashMap::new(),
#[cfg(feature = "clipboard")]
clipboard: crate::arc_mut!(None),
output_tx,
toplevel_tx,
#[cfg(feature = "clipboard")]
clipboard_tx,
#[cfg(feature = "clipboard")]
data_control_devices: vec![],
#[cfg(feature = "clipboard")]
selection_offers: vec![],
#[cfg(feature = "clipboard")]
copy_paste_sources: vec![],
loop_handle: event_loop.handle(),
};
loop_handle
.insert_source(ev_rx, move |event, _metadata, env| {
trace!("{event:?}");
match event {
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
Event::Msg(Request::Seats) => {
trace!("Receive get seats request");
send!(seat_tx, env.seats.clone());
}
Event::Msg(Request::Toplevels) => {
trace!("Receive get toplevels request");
send!(toplevel_init_tx, env.handles.clone());
}
#[cfg(feature = "clipboard")]
Event::Msg(Request::Clipboard) => {
trace!("Receive get clipboard requests");
let clipboard = lock!(env.clipboard).clone();
send!(clipboard_init_tx, clipboard);
}
#[cfg(feature = "clipboard")]
Event::Msg(Request::CopyToClipboard(value)) => {
env.copy_to_clipboard(value, &qh);
}
Event::Closed => panic!("Channel unexpectedly closed"),
}
})
.expect("Failed to insert channel into event queue");
loop {
trace!("Dispatching event loop");
if let Err(err) = event_loop.dispatch(None, &mut env) {
error!(
"{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
);
}
}
});
Self {
output_channel,
toplevel_tx,
_toplevel_rx: toplevel_rx,
toplevel_init_rx,
#[cfg(feature = "clipboard")]
clipboard_init_rx,
seat_rx,
#[cfg(feature = "clipboard")]
clipboard_tx,
#[cfg(feature = "clipboard")]
_clipboard_rx: clipboard_rx,
request_tx: ev_tx,
}
}
pub fn subscribe_toplevels(
&self,
) -> (
broadcast::Receiver<ToplevelEvent>,
HashMap<usize, ToplevelHandle>,
) {
let rx = self.toplevel_tx.subscribe();
let receiver = &self.toplevel_init_rx;
send!(self.request_tx, Request::Toplevels);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
}
#[cfg(feature = "clipboard")]
pub fn subscribe_clipboard(
&self,
) -> (
broadcast::Receiver<Arc<ClipboardItem>>,
Option<Arc<ClipboardItem>>,
) {
let rx = self.clipboard_tx.subscribe();
let receiver = &self.clipboard_init_rx;
send!(self.request_tx, Request::Clipboard);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
}
pub fn subscribe_outputs(&mut self) -> cached_broadcast::Receiver<OutputInfo> {
self.output_channel.receiver()
}
/// Force a roundtrip on the wayland connection,
/// flushing any queued events and immediately receiving any new ones.
pub fn roundtrip(&self) {
trace!("Sending roundtrip request");
send!(self.request_tx, Request::Roundtrip);
}
/// Gets a list of all outputs.
///
/// This should only be used in a scenario
/// where you need a snapshot of outputs at the current time.
/// Prefer to listen to output events with `subscribe_output` where possible.
pub fn get_outputs(&self) -> &Vec<OutputInfo> {
self.output_channel.data()
}
pub fn get_seats(&self) -> Vec<WlSeat> {
trace!("Sending get seats request");
send!(self.request_tx, Request::Seats);
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
}
#[cfg(feature = "clipboard")]
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
send!(self.request_tx, Request::CopyToClipboard(item));
}
}

View File

@@ -0,0 +1,101 @@
/// It is necessary to store macros in a separate file due to a compilation error.
/// I believe this stems from the feature flags.
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
// --- Data Control Device --- \\
#[macro_export]
macro_rules! delegate_data_control_device_manager {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_device {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_offer {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_source {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
// --- Foreign Toplevel --- \\
#[macro_export]
macro_rules! delegate_foreign_toplevel_manager {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
}
#[macro_export]
macro_rules! delegate_foreign_toplevel_handle {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
}

114
src/clients/wayland/mod.rs Normal file
View File

@@ -0,0 +1,114 @@
mod client;
mod macros;
mod wl_output;
mod wl_seat;
mod wlr_foreign_toplevel;
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
use crate::{arc_mut, cached_broadcast, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
use cfg_if::cfg_if;
use lazy_static::lazy_static;
use smithay_client_toolkit::output::{OutputInfo, OutputState};
use smithay_client_toolkit::reexports::calloop::LoopHandle;
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::SeatState;
use smithay_client_toolkit::{
delegate_output, delegate_registry, delegate_seat, registry_handlers,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
use wayland_client::protocol::wl_seat::WlSeat;
pub use self::client::WaylandClient;
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
cfg_if! {
if #[cfg(feature = "clipboard")] {
mod wlr_data_control;
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
use self::wlr_data_control::device::DataControlDevice;
use self::wlr_data_control::manager::DataControlDeviceManagerState;
use self::wlr_data_control::source::CopyPasteSource;
use self::wlr_data_control::SelectionOfferItem;
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
pub struct DataControlDeviceEntry {
seat: WlSeat,
device: DataControlDevice,
}
}
}
pub struct Environment {
pub registry_state: RegistryState,
pub output_state: OutputState,
pub seat_state: SeatState,
pub foreign_toplevel_manager_state: ToplevelManagerState,
#[cfg(feature = "clipboard")]
pub data_control_device_manager_state: DataControlDeviceManagerState,
pub loop_handle: LoopHandle<'static, Self>,
pub seats: Vec<WlSeat>,
#[cfg(feature = "clipboard")]
pub data_control_devices: Vec<DataControlDeviceEntry>,
#[cfg(feature = "clipboard")]
pub selection_offers: Vec<SelectionOfferItem>,
#[cfg(feature = "clipboard")]
pub copy_paste_sources: Vec<CopyPasteSource>,
pub handles: HashMap<usize, ToplevelHandle>,
#[cfg(feature = "clipboard")]
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
output_tx: cached_broadcast::Sender<OutputInfo>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
}
// Now we need to say we are delegating the responsibility of output related events for our application data
// type to the requisite delegate.
delegate_output!(Environment);
delegate_seat!(Environment);
delegate_foreign_toplevel_manager!(Environment);
delegate_foreign_toplevel_handle!(Environment);
cfg_if! {
if #[cfg(feature = "clipboard")] {
delegate_data_control_device_manager!(Environment);
delegate_data_control_device!(Environment);
delegate_data_control_source!(Environment);
delegate_data_control_offer!(Environment);
}
}
// In order for our delegate to know of the existence of globals, we need to implement registry
// handling for the program. This trait will forward events to the RegistryHandler trait
// implementations.
delegate_registry!(Environment);
// In order for delegate_registry to work, our application data type needs to provide a way for the
// implementation to access the registry state.
//
// We also need to indicate which delegates will get told about globals being created. We specify
// the types of the delegates inside the array.
impl ProvidesRegistryState for Environment {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
lazy_static! {
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
}
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
CLIENT.clone()
}

View File

@@ -0,0 +1,72 @@
use super::Environment;
use crate::cached_broadcast::Cacheable;
use crate::{cached_broadcast, try_send};
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
use tracing::debug;
use wayland_client::protocol::wl_output;
use wayland_client::{Connection, QueueHandle};
impl Environment {
pub fn output_info(&mut self) -> Vec<OutputInfo> {
self.output_state
.outputs()
.filter_map(|output| self.output_state.info(&output))
.collect()
}
}
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
// output and to provide an instance of the output state to the delegate when dispatching events.
impl OutputHandler for Environment {
// First we need to provide a way to access the delegate.
//
// This is needed because delegate implementations for handling events use the application data type in
// their function signatures. This allows the implementation to access an instance of the type.
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
// Then there exist these functions that indicate the lifecycle of an output.
// These will be called as appropriate by the delegate implementation.
fn new_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
debug!("Handler received new output");
if let Some(info) = self.output_state.info(&output) {
try_send!(self.output_tx, cached_broadcast::Event::Add(info));
};
}
fn update_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
debug!("Handle received output update");
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
debug!("Handle received output destruction");
if let Some(info) = self.output_state.info(&output) {
try_send!(self.output_tx, cached_broadcast::Event::Remove(info.id));
};
}
}
impl Cacheable for OutputInfo {
type Key = u32;
fn get_key(&self) -> Self::Key {
self.id
}
}

View File

@@ -0,0 +1,63 @@
use super::Environment;
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use tracing::debug;
use wayland_client::protocol::wl_seat;
use wayland_client::{Connection, QueueHandle};
impl SeatHandler for Environment {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
debug!("Handler received new seat");
self.seats.push(seat);
}
fn new_capability(
&mut self,
_: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
_: Capability,
) {
debug!("Handler received new capability");
#[cfg(feature = "clipboard")]
if !self
.data_control_devices
.iter_mut()
.any(|entry| entry.seat == seat)
{
debug!("Adding new data control device");
// create the data device here for this seat
let data_control_device_manager = &self.data_control_device_manager_state;
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
self.data_control_devices
.push(super::DataControlDeviceEntry {
seat: seat.clone(),
device: data_control_device,
});
}
if !self.seats.iter().any(|s| s == &seat) {
self.seats.push(seat);
}
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
_: Capability,
) {
debug!("Handler received capability removal");
// Not applicable
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
debug!("Handler received seat removal");
self.seats.retain(|s| s != &seat);
}
}

View File

@@ -0,0 +1,166 @@
use super::manager::DataControlDeviceManagerState;
use super::offer::{
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
};
use crate::error::ERR_WAYLAND_DATA;
use crate::lock;
use std::sync::{Arc, Mutex};
use tracing::warn;
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
};
pub struct DataControlDevice {
pub device: ZwlrDataControlDeviceV1,
}
#[derive(Debug, Default)]
pub struct DataControlDeviceInner {
/// the active selection offer and its data
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
/// the active undetermined offers and their data
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
}
#[derive(Debug, Default)]
pub struct DataControlDeviceData {
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
}
pub trait DataControlDeviceDataExt: Send + Sync {
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
fn data_control_device_data(&self) -> &DataControlDeviceData;
fn selection_mime_types(&self) -> Vec<String> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
.as_ref()
.map(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.mime_types()
})
.unwrap_or_default()
}
/// Get the active selection offer if it exists.
fn selection_offer(&self) -> Option<SelectionOffer> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
.as_ref()
.and_then(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.as_selection_offer()
})
}
}
impl DataControlDeviceDataExt for DataControlDevice {
type DataControlOfferInner = DataControlOfferData;
fn data_control_device_data(&self) -> &DataControlDeviceData {
self.device.data().expect(ERR_WAYLAND_DATA)
}
}
impl DataControlDeviceDataExt for DataControlDeviceData {
type DataControlOfferInner = DataControlOfferData;
fn data_control_device_data(&self) -> &DataControlDeviceData {
self
}
}
/// Handler trait for `DataDevice` events.
///
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
pub trait DataControlDeviceHandler: Sized {
/// Advertises a new selection.
fn selection(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
data_device: DataControlDevice,
);
}
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
where
D: Dispatch<ZwlrDataControlDeviceV1, U>
+ Dispatch<ZwlrDataControlOfferV1, V>
+ DataControlDeviceHandler
+ DataControlOfferHandler
+ 'static,
U: DataControlDeviceDataExt,
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
{
event_created_child!(D, ZwlrDataControlDeviceV1, [
0 => (ZwlrDataControlOfferV1, V::default())
]);
fn event(
state: &mut D,
data_device: &ZwlrDataControlDeviceV1,
event: Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
let data = data.data_control_device_data();
let inner = lock!(data.inner);
match event {
Event::DataOffer { id } => {
// XXX Drop done here to prevent Mutex deadlocks.S
lock!(inner.undetermined_offers).push(id.clone());
let data = id
.data::<V>()
.expect(ERR_WAYLAND_DATA)
.data_control_offer_data();
data.init_undetermined_offer(&id);
// Append the data offer to our list of offers.
drop(inner);
}
Event::Selection { id } => {
let mut selection_offer = lock!(inner.selection_offer);
if let Some(offer) = id {
let mut undetermined = lock!(inner.undetermined_offers);
if let Some(i) = undetermined.iter().position(|o| o == &offer) {
undetermined.remove(i);
}
drop(undetermined);
let data = offer
.data::<V>()
.expect(ERR_WAYLAND_DATA)
.data_control_offer_data();
data.to_selection_offer();
// XXX Drop done here to prevent Mutex deadlocks.
*selection_offer = Some(offer.clone());
drop(selection_offer);
drop(inner);
state.selection(
conn,
qh,
DataControlDevice {
device: data_device.clone(),
},
);
} else {
*selection_offer = None;
}
}
Event::Finished => {
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
}
_ => {}
}
}
}

View File

@@ -0,0 +1,132 @@
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
use super::offer::DataControlOfferData;
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
use smithay_client_toolkit::error::GlobalError;
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use std::marker::PhantomData;
use tracing::debug;
use wayland_client::globals::{BindError, GlobalList};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
};
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
manager: ZwlrDataControlManagerV1,
_phantom: PhantomData<V>,
}
impl DataControlDeviceManagerState {
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
where
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
{
let manager = globals.bind(qh, 1..=2, GlobalData)?;
debug!("Bound to ZwlDataControlManagerV1 global");
Ok(Self {
manager,
_phantom: PhantomData,
})
}
/// creates a data source for copy paste
pub fn create_copy_paste_source<'s, D, I>(
&self,
qh: &QueueHandle<D>,
mime_types: I,
) -> CopyPasteSource
where
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
I: IntoIterator<Item = &'s str>,
{
CopyPasteSource {
inner: self.create_data_control_source(qh, mime_types),
}
}
/// creates a data source
fn create_data_control_source<'s, D, I>(
&self,
qh: &QueueHandle<D>,
mime_types: I,
) -> ZwlrDataControlSourceV1
where
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
I: IntoIterator<Item = &'s str>,
{
let source =
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
for mime in mime_types {
source.offer(mime.to_string());
}
source
}
/// create a new data source for a given seat with some user data
pub fn create_data_control_source_with_data<D, U>(
&self,
qh: &QueueHandle<D>,
data: U,
) -> ZwlrDataControlSourceV1
where
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
U: DataControlSourceDataExt + 'static,
{
self.manager.create_data_source(qh, data)
}
/// create a new data device for a given seat
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
where
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
{
DataControlDevice {
device: self.get_data_control_device_with_data(
qh,
seat,
DataControlDeviceData::default(),
),
}
}
/// create a new data device for a given seat with some user data
pub fn get_data_control_device_with_data<D, U>(
&self,
qh: &QueueHandle<D>,
seat: &WlSeat,
data: U,
) -> ZwlrDataControlDeviceV1
where
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
U: DataControlDeviceDataExt + 'static,
{
self.manager.get_data_device(seat, qh, data)
}
}
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState {
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> {
Ok(self.manager.clone())
}
}
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState
where
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>,
{
fn event(
_state: &mut D,
_proxy: &ZwlrDataControlManagerV1,
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
_data: &GlobalData,
_conn: &Connection,
_qhandle: &QueueHandle<D>,
) {
unreachable!()
}
}

View File

@@ -0,0 +1,375 @@
pub mod device;
pub mod manager;
pub mod offer;
pub mod source;
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
use self::source::DataControlSourceHandler;
use crate::clients::wayland::Environment;
use crate::unique_id::get_unique_usize;
use crate::{lock, send};
use device::DataControlDevice;
use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
use std::cmp::min;
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::{ErrorKind, Read, Write};
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::sync::Arc;
use std::{fs, io};
use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
pub struct SelectionOfferItem {
offer: SelectionOffer,
token: Option<RegistrationToken>,
}
#[derive(Debug, Clone, Eq)]
pub struct ClipboardItem {
pub id: usize,
pub value: ClipboardValue,
pub mime_type: String,
}
impl PartialEq<Self> for ClipboardItem {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum ClipboardValue {
Text(String),
Image(Bytes),
Other,
}
impl Debug for ClipboardValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Text(text) => text.clone(),
Self::Image(bytes) => {
format!("[{} Bytes]", bytes.len())
}
Self::Other => "[Unknown]".to_string(),
}
)
}
}
#[derive(Debug)]
struct MimeType {
value: String,
category: MimeTypeCategory,
}
#[derive(Debug)]
enum MimeTypeCategory {
Text,
Image,
}
impl MimeType {
fn parse(mime_type: &str) -> Option<Self> {
match mime_type.to_lowercase().as_str() {
"text"
| "string"
| "utf8_string"
| "text/plain"
| "text/plain;charset=utf-8"
| "text/plain;charset=iso-8859-1"
| "text/plain;charset=us-ascii"
| "text/plain;charset=unicode" => Some(Self {
value: mime_type.to_string(),
category: MimeTypeCategory::Text,
}),
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
| "image/x-bmp" | "image/icon" => Some(Self {
value: mime_type.to_string(),
category: MimeTypeCategory::Image,
}),
_ => None,
}
}
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
mime_types.iter().find_map(|mime| Self::parse(mime))
}
}
impl Environment {
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
debug!("Copying item to clipboard: {item:?}");
// TODO: Proper device tracking
let device = self.data_control_devices.first();
if let Some(device) = device {
let source = self
.data_control_device_manager_state
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
source.set_selection(&device.device);
self.copy_paste_sources.push(source);
lock!(self.clipboard).replace(item);
}
}
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
let value = match mime_type.category {
MimeTypeCategory::Text => {
let mut txt = String::new();
file.read_to_string(&mut txt)?;
ClipboardValue::Text(txt)
}
MimeTypeCategory::Image => {
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
let bytes = Bytes::from(&bytes);
ClipboardValue::Image(bytes)
}
};
Ok(ClipboardItem {
id: get_unique_usize(),
value,
mime_type: mime_type.value.clone(),
})
}
}
impl DataControlDeviceHandler for Environment {
fn selection(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
data_device: DataControlDevice,
) {
debug!("Handler received selection event");
let mime_types = data_device.selection_mime_types();
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
return;
}
if let Some(offer) = data_device.selection_offer() {
self.selection_offers
.push(SelectionOfferItem { offer, token: None });
let cur_offer = self
.selection_offers
.last_mut()
.expect("Failed to get current offer");
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
lock!(self.clipboard).take();
// send an event so the clipboard module is aware it's changed
send!(
self.clipboard_tx,
Arc::new(ClipboardItem {
id: usize::MAX,
mime_type: String::new(),
value: ClipboardValue::Other
})
);
return;
};
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
let offer_clone = cur_offer.offer.clone();
let tx = self.clipboard_tx.clone();
let clipboard = self.clipboard.clone();
let token = self
.loop_handle
.insert_source(read_pipe, move |_, file, state| {
let item = state
.selection_offers
.iter()
.position(|o| o.offer == offer_clone)
.map(|p| state.selection_offers.remove(p))
.expect("Failed to find selection offer item");
match Self::read_file(&mime_type, file) {
Ok(item) => {
let item = Arc::new(item);
lock!(clipboard).replace(item.clone());
send!(tx, item);
}
Err(err) => error!("{err:?}"),
}
state
.loop_handle
.remove(item.token.expect("Missing item token"));
});
match token {
Ok(token) => {
cur_offer.token.replace(token);
}
Err(err) => error!("{err:?}"),
}
}
}
}
}
impl DataControlOfferHandler for Environment {
fn offer(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_offer: &mut DataControlDeviceOffer,
_mime_type: String,
) {
trace!("Handler received offer");
}
}
impl DataControlSourceHandler for Environment {
fn accept_mime(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_source: &ZwlrDataControlSourceV1,
mime: Option<String>,
) {
debug!("Accepted mime type: {mime:?}");
}
fn send_request(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: String,
write_pipe: WritePipe,
) {
debug!("Handler received source send request event ({mime})");
if let Some(item) = lock!(self.clipboard).clone() {
let fd = OwnedFd::from(write_pipe);
if self
.copy_paste_sources
.iter_mut()
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
{
trace!("Source found, writing to file");
let mut bytes = match &item.value {
ClipboardValue::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",)
),
};
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
.expect("Failed to increase pipe size");
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
trace!("Num bytes: {}", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll_fd =
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
epoll_fd
.add(fd, epoll_event)
.expect("to send valid epoll operation");
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
epoll_fd
.wait(&mut events, 100)
.expect("Failed to wait to epoll");
match file.write(chunk) {
Ok(_) => bytes = &bytes[chunk.len()..],
Err(err) => {
error!("{err:?}");
break;
}
}
}
} else {
error!("Failed to find source");
}
}
}
fn cancelled(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
) {
debug!("Handler received source cancelled event");
self.copy_paste_sources
.iter()
.position(|s| s.inner() == source)
.map(|pos| self.copy_paste_sources.remove(pos));
source.destroy();
}
}
/// Attempts to increase the fd pipe size to the requested number of bytes.
/// The kernel will automatically round this up to the nearest page size.
/// If the requested size is larger than the kernel max (normally 1MB),
/// it will be clamped at this.
///
/// Returns the new size if succeeded
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
// clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
.expect("Failed to find pipe-max-size virtual kernel file")
.trim()
.parse::<usize>()
.expect("Failed to parse pipe-max-size contents");
let size = min(size, max_pipe_size);
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
trace!("Current pipe size: {curr_size}");
let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
trace!("New pipe size: {res}");
if res < size as i32 {
return Err(io::Error::last_os_error());
}
res
} else {
size as i32
};
Ok(new_size)
}

View File

@@ -0,0 +1,183 @@
use super::manager::DataControlDeviceManagerState;
use crate::lock;
use nix::fcntl::OFlag;
use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut;
use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex};
use tracing::{trace, warn};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1,
};
#[derive(Debug, Clone)]
pub struct UndeterminedOffer {
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>,
}
impl PartialEq for UndeterminedOffer {
fn eq(&self, other: &Self) -> bool {
self.data_offer == other.data_offer
}
}
#[derive(Debug, Clone)]
pub struct SelectionOffer {
pub data_offer: ZwlrDataControlOfferV1,
}
impl PartialEq for SelectionOffer {
fn eq(&self, other: &Self) -> bool {
self.data_offer == other.data_offer
}
}
impl SelectionOffer {
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DataControlDeviceOffer {
Selection(SelectionOffer),
Undetermined(UndeterminedOffer),
}
impl Default for DataControlDeviceOffer {
fn default() -> Self {
Self::Undetermined(UndeterminedOffer { data_offer: None })
}
}
#[derive(Debug, Default)]
pub struct DataControlOfferData {
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
}
#[derive(Debug, Default)]
pub struct DataControlDeviceOfferInner {
pub(crate) offer: DataControlDeviceOffer,
pub(crate) mime_types: Vec<String>,
}
impl DataControlOfferData {
pub(crate) fn push_mime_type(&self, mime_type: String) {
lock!(self.inner).mime_types.push(mime_type);
}
pub(crate) fn to_selection_offer(&self) {
let mut inner = lock!(self.inner);
match &mut inner.deref_mut().offer {
DataControlDeviceOffer::Selection(_) => {}
DataControlDeviceOffer::Undetermined(o) => {
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
data_offer: o.data_offer.clone().expect("Missing current data offer"),
});
}
}
}
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
let mut inner = lock!(self.inner);
match &mut inner.deref_mut().offer {
DataControlDeviceOffer::Selection(_) => {
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
data_offer: Some(offer.clone()),
});
}
DataControlDeviceOffer::Undetermined(o) => {
o.data_offer = Some(offer.clone());
}
}
}
}
pub trait DataControlOfferDataExt {
fn data_control_offer_data(&self) -> &DataControlOfferData;
fn mime_types(&self) -> Vec<String>;
fn as_selection_offer(&self) -> Option<SelectionOffer>;
}
impl DataControlOfferDataExt for DataControlOfferData {
fn data_control_offer_data(&self) -> &DataControlOfferData {
self
}
fn mime_types(&self) -> Vec<String> {
lock!(self.inner).mime_types.clone()
}
fn as_selection_offer(&self) -> Option<SelectionOffer> {
match &lock!(self.inner).offer {
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
DataControlDeviceOffer::Undetermined(_) => None,
}
}
}
/// Handler trait for `DataOffer` events.
///
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
pub trait DataControlOfferHandler: Sized {
// Called for each mime type the data offer advertises.
fn offer(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DataControlDeviceOffer,
mime_type: String,
);
}
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
where
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
U: DataControlOfferDataExt,
{
fn event(
state: &mut D,
_offer: &ZwlrDataControlOfferV1,
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
let data = data.data_control_offer_data();
if let Event::Offer { mime_type } = event {
trace!("Adding new offer with type '{mime_type}'");
data.push_mime_type(mime_type.clone());
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
}
}
}
/// Request to receive the data of a given mime type.
///
/// You can do this several times, as a reaction to motion of
/// the dnd cursor, or to inspect the data in order to choose your
/// response.
///
/// Note that you should *not* read the contents right away in a
/// blocking way, as you may deadlock your application doing so.
/// At least make sure you flush your events to the server before
/// doing so.
///
/// Fails if too many file descriptors were already open and a pipe
/// could not be created.
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
// create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
offer.receive(mime_type, writefd);
if let Err(err) = close(writefd) {
warn!("Failed to close write pipe: {}", err);
}
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
}

View File

@@ -0,0 +1,101 @@
use super::device::DataControlDevice;
use super::manager::DataControlDeviceManagerState;
use smithay_client_toolkit::data_device_manager::WritePipe;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
Event, ZwlrDataControlSourceV1,
};
#[derive(Debug, Default)]
pub struct DataControlSourceData {}
pub trait DataControlSourceDataExt: Send + Sync {
fn data_source_data(&self) -> &DataControlSourceData;
}
impl DataControlSourceDataExt for DataControlSourceData {
fn data_source_data(&self) -> &DataControlSourceData {
self
}
}
/// Handler trait for `DataSource` events.
///
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
pub trait DataControlSourceHandler: Sized {
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
fn accept_mime(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: Option<String>,
);
/// The client has requested the data for this source to be sent.
/// Send the data, then close the fd.
fn send_request(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: String,
fd: WritePipe,
);
/// The data source is no longer valid
/// Cleanup & destroy this resource
fn cancelled(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
);
}
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
where
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
U: DataControlSourceDataExt,
{
fn event(
state: &mut D,
source: &ZwlrDataControlSourceV1,
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
_data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
match event {
Event::Send { mime_type, fd } => {
state.send_request(conn, qh, source, mime_type, fd.into());
}
Event::Cancelled => {
state.cancelled(conn, qh, source);
}
_ => {}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct CopyPasteSource {
pub(crate) inner: ZwlrDataControlSourceV1,
}
impl CopyPasteSource {
/// Set the selection of the provided data device as a response to the event with with provided serial.
pub fn set_selection(&self, device: &DataControlDevice) {
device.device.set_selection(Some(&self.inner));
}
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
&self.inner
}
}
impl Drop for CopyPasteSource {
fn drop(&mut self) {
self.inner.destroy();
}
}

View File

@@ -0,0 +1,178 @@
use super::manager::ToplevelManagerState;
use crate::lock;
use crate::unique_id::get_unique_usize;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use tracing::trace;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
Event, ZwlrForeignToplevelHandleV1,
};
#[derive(Debug, Clone)]
pub struct ToplevelHandle {
pub handle: ZwlrForeignToplevelHandleV1,
}
impl PartialEq for ToplevelHandle {
fn eq(&self, other: &Self) -> bool {
self.handle == other.handle
}
}
impl ToplevelHandle {
pub fn info(&self) -> Option<ToplevelInfo> {
trace!("Retrieving handle info");
let data = self.handle.data::<ToplevelHandleData>()?;
data.info()
}
pub fn focus(&self, seat: &WlSeat) {
trace!("Activating handle");
self.handle.activate(seat);
}
}
#[derive(Debug, Default)]
pub struct ToplevelHandleData {
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
}
impl ToplevelHandleData {
fn info(&self) -> Option<ToplevelInfo> {
lock!(self.inner).current_info.clone()
}
}
#[derive(Debug, Default)]
pub struct ToplevelHandleDataInner {
initial_done: bool,
output: Option<WlOutput>,
current_info: Option<ToplevelInfo>,
pending_info: ToplevelInfo,
}
#[derive(Debug, Clone)]
pub struct ToplevelInfo {
pub id: usize,
pub app_id: String,
pub title: String,
pub fullscreen: bool,
pub focused: bool,
}
impl Default for ToplevelInfo {
fn default() -> Self {
Self {
id: get_unique_usize(),
app_id: String::new(),
title: String::new(),
fullscreen: false,
focused: false,
}
}
}
pub trait ToplevelHandleDataExt {
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
}
impl ToplevelHandleDataExt for ToplevelHandleData {
fn toplevel_handle_data(&self) -> &ToplevelHandleData {
self
}
}
pub trait ToplevelHandleHandler: Sized {
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
}
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
U: ToplevelHandleDataExt,
{
fn event(
state: &mut D,
handle: &ZwlrForeignToplevelHandleV1,
event: Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
const STATE_ACTIVE: u32 = 2;
const STATE_FULLSCREEN: u32 = 3;
let data = data.toplevel_handle_data();
trace!("Processing handle event: {event:?}");
match event {
Event::Title { title } => {
lock!(data.inner).pending_info.title = title;
}
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
Event::State { state } => {
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
// the u32 then represents a value in the `State` enum.
assert_eq!(state.len() % 4, 0);
let state = (0..state.len() / 4)
.map(|i| {
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
.try_into()
.expect("Received invalid state length");
u32::from_le_bytes(slice)
})
.collect::<HashSet<_>>();
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN);
}
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
Event::Closed => state.remove_handle(
conn,
qh,
ToplevelHandle {
handle: handle.clone(),
},
),
Event::Done => {
{
let pending_info = lock!(data.inner).pending_info.clone();
lock!(data.inner).current_info = Some(pending_info);
}
if lock!(data.inner).initial_done {
state.update_handle(
conn,
qh,
ToplevelHandle {
handle: handle.clone(),
},
);
} else {
lock!(data.inner).initial_done = true;
state.new_handle(
conn,
qh,
ToplevelHandle {
handle: handle.clone(),
},
);
}
}
_ => {}
}
trace!("Event processed");
}
}

View File

@@ -0,0 +1,86 @@
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
use smithay_client_toolkit::error::GlobalError;
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use std::marker::PhantomData;
use tracing::{debug, warn};
use wayland_client::globals::{BindError, GlobalList};
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
};
pub struct ToplevelManagerState<V = ToplevelHandleData> {
manager: ZwlrForeignToplevelManagerV1,
_phantom: PhantomData<V>,
}
impl ToplevelManagerState {
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
where
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
{
let manager = globals.bind(qh, 1..=3, GlobalData)?;
debug!("Bound to ZwlForeignToplevelManagerV1 global");
Ok(Self {
manager,
_phantom: PhantomData,
})
}
}
pub trait ToplevelManagerHandler: Sized {
/// Advertises a new toplevel.
fn toplevel(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
manager: ToplevelManagerState,
);
}
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
Ok(self.manager.clone())
}
}
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
where
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
+ ToplevelManagerHandler
+ ToplevelHandleHandler
+ 'static,
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
{
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
0 => (ZwlrForeignToplevelHandleV1, V::default())
]);
fn event(
state: &mut D,
toplevel_manager: &ZwlrForeignToplevelManagerV1,
event: Event,
_data: &GlobalData,
conn: &Connection,
qhandle: &QueueHandle<D>,
) {
match event {
Event::Toplevel { toplevel: _ } => {
state.toplevel(
conn,
qhandle,
ToplevelManagerState {
manager: toplevel_manager.clone(),
_phantom: PhantomData,
},
);
}
Event::Finished => {
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
}
_ => {}
}
}
}

View File

@@ -0,0 +1,84 @@
pub mod handle;
pub mod manager;
use self::handle::ToplevelHandleHandler;
use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
use crate::clients::wayland::Environment;
use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle};
use crate::send;
pub use handle::{ToplevelHandle, ToplevelInfo};
#[derive(Debug, Clone)]
pub enum ToplevelEvent {
New(ToplevelHandle),
Update(ToplevelHandle),
Remove(ToplevelHandle),
}
impl ToplevelManagerHandler for Environment {
fn toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_manager: ToplevelManagerState,
) {
debug!("Manager received new handle");
}
}
impl ToplevelHandleHandler for Environment {
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
trace!("Handler received new handle");
match handle.info() {
Some(info) => {
trace!("Adding new handle: {info:?}");
self.handles.insert(info.id, handle.clone());
send!(self.toplevel_tx, ToplevelEvent::New(handle));
}
None => {
error!("Handle is missing information!");
}
}
}
fn update_handle(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ToplevelHandle,
) {
trace!("Handler received handle update");
match handle.info() {
Some(info) => {
trace!("Updating handle: {info:?}");
self.handles.insert(info.id, handle.clone());
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
}
None => {
error!("Handle is missing information!");
}
}
}
fn remove_handle(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ToplevelHandle,
) {
debug!("Handler received handle close");
match handle.info() {
Some(info) => {
self.handles.remove(&info.id);
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
}
None => {
error!("Handle is missing information!");
}
}
}
}

View File

@@ -1,131 +0,0 @@
use serde::Serialize;
use std::slice::{Iter, IterMut};
/// An ordered map.
/// Internally this is just two vectors -
/// one for keys and one for values.
#[derive(Debug, Clone, Serialize)]
pub struct Collection<TKey, TData> {
keys: Vec<TKey>,
values: Vec<TData>,
}
impl<TKey: PartialEq, TData> Collection<TKey, TData> {
pub const fn new() -> Self {
Self {
keys: vec![],
values: vec![],
}
}
pub fn insert(&mut self, key: TKey, value: TData) {
self.keys.push(key);
self.values.push(value);
assert_eq!(self.keys.len(), self.values.len());
}
pub fn get(&self, key: &TKey) -> Option<&TData> {
let index = self.keys.iter().position(|k| k == key);
match index {
Some(index) => self.values.get(index),
None => None,
}
}
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
let index = self.keys.iter().position(|k| k == key);
match index {
Some(index) => self.values.get_mut(index),
None => None,
}
}
pub fn remove(&mut self, key: &TKey) -> Option<TData> {
assert_eq!(self.keys.len(), self.values.len());
let index = self.keys.iter().position(|k| k == key);
if let Some(index) = index {
self.keys.remove(index);
Some(self.values.remove(index))
} else {
None
}
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn first(&self) -> Option<&TData> {
self.values.first()
}
pub fn as_slice(&self) -> &[TData] {
self.values.as_slice()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn iter(&self) -> Iter<'_, TData> {
self.values.iter()
}
pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
self.values.iter_mut()
}
}
impl<TKey: PartialEq, TData> From<(TKey, TData)> for Collection<TKey, TData> {
fn from((key, value): (TKey, TData)) -> Self {
let mut collection = Self::new();
collection.insert(key, value);
collection
}
}
impl<TKey: PartialEq, TData> FromIterator<(TKey, TData)> for Collection<TKey, TData> {
fn from_iter<T: IntoIterator<Item = (TKey, TData)>>(iter: T) -> Self {
let mut collection = Self::new();
for (key, value) in iter {
collection.insert(key, value);
}
collection
}
}
impl<'a, TKey: PartialEq, TData> IntoIterator for &'a Collection<TKey, TData> {
type Item = &'a TData;
type IntoIter = CollectionIntoIterator<'a, TKey, TData>;
fn into_iter(self) -> Self::IntoIter {
CollectionIntoIterator {
collection: self,
index: 0,
}
}
}
pub struct CollectionIntoIterator<'a, TKey, TData> {
collection: &'a Collection<TKey, TData>,
index: usize,
}
impl<'a, TKey: PartialEq, TData> Iterator for CollectionIntoIterator<'a, TKey, TData> {
type Item = &'a TData;
fn next(&mut self) -> Option<Self::Item> {
let res = self.collection.values.get(self.index);
self.index += 1;
res
}
}
impl<TKey: PartialEq, TData> Default for Collection<TKey, TData> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,143 +0,0 @@
use crate::modules::clock::ClockModule;
use crate::modules::focused::FocusedModule;
use crate::modules::launcher::LauncherModule;
use crate::modules::mpd::MpdModule;
use crate::modules::script::ScriptModule;
use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule;
use crate::modules::workspaces::WorkspacesModule;
use color_eyre::eyre::{Context, ContextCompat};
use color_eyre::{eyre, Help, Report};
use dirs::config_dir;
use eyre::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::{env, fs};
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ModuleConfig {
Clock(ClockModule),
Mpd(MpdModule),
Tray(TrayModule),
Workspaces(WorkspacesModule),
SysInfo(SysInfoModule),
Launcher(LauncherModule),
Script(ScriptModule),
Focused(FocusedModule),
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum MonitorConfig {
Single(Config),
Multiple(Vec<Config>),
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum BarPosition {
Top,
Bottom,
}
impl Default for BarPosition {
fn default() -> Self {
Self::Bottom
}
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct Config {
#[serde(default = "default_bar_position")]
pub position: BarPosition,
#[serde(default = "default_bar_height")]
pub height: i32,
pub left: Option<Vec<ModuleConfig>>,
pub center: Option<Vec<ModuleConfig>>,
pub right: Option<Vec<ModuleConfig>>,
pub monitors: Option<HashMap<String, MonitorConfig>>,
}
const fn default_bar_position() -> BarPosition {
BarPosition::Bottom
}
const fn default_bar_height() -> i32 {
42
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
let path = PathBuf::from(config_path);
if path.exists() {
Ok(path)
} else {
Err(Report::msg("Specified config file does not exist")
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
}
} else {
Self::try_find_config()
}?;
Self::load_file(&config_path)
}
fn try_find_config() -> Result<PathBuf> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
let file = extensions.into_iter().find_map(|extension| {
let full_path = config_dir
.join("ironbar")
.join(format!("config.{extension}"));
if Path::exists(&full_path) {
Some(full_path)
} else {
None
}
});
match file {
Some(file) => Ok(file),
None => Err(Report::msg("Could not find config file")),
}
}
fn load_file(path: &Path) -> Result<Self> {
let file = fs::read(path).wrap_err("Failed to read config file")?;
let extension = path
.extension()
.unwrap_or_default()
.to_str()
.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" => {
// corn doesn't support deserialization yet
// so serialize the interpreted result then deserialize that
let file =
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
}
_ => unreachable!(),
}
}
}
pub const fn default_false() -> bool {
false
}
pub const fn default_true() -> bool {
true
}

151
src/config/common.rs Normal file
View File

@@ -0,0 +1,151 @@
use crate::dynamic_value::{dynamic_string, DynamicBool};
use crate::script::{Script, ScriptInput};
use gtk::gdk::ScrollDirection;
use gtk::prelude::*;
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
use serde::Deserialize;
use tracing::trace;
/// Common configuration options
/// which can be set on every module.
#[derive(Debug, Default, Deserialize, Clone)]
pub struct CommonConfig {
pub class: Option<String>,
pub name: Option<String>,
pub show_if: Option<DynamicBool>,
pub transition_type: Option<TransitionType>,
pub transition_duration: Option<u32>,
pub on_click_left: Option<ScriptInput>,
pub on_click_right: Option<ScriptInput>,
pub on_click_middle: Option<ScriptInput>,
pub on_scroll_up: Option<ScriptInput>,
pub on_scroll_down: Option<ScriptInput>,
pub on_mouse_enter: Option<ScriptInput>,
pub on_mouse_exit: Option<ScriptInput>,
pub tooltip: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TransitionType {
None,
Crossfade,
SlideStart,
SlideEnd,
}
impl TransitionType {
pub const fn to_revealer_transition_type(
&self,
orientation: Orientation,
) -> RevealerTransitionType {
match (self, orientation) {
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft,
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight,
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
(Self::Crossfade, _) => RevealerTransitionType::Crossfade,
_ => RevealerTransitionType::None,
}
}
}
impl CommonConfig {
/// Configures the module's container according to the common config options.
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) {
self.install_show_if(container, revealer);
let left_click_script = self.on_click_left.map(Script::new_polling);
let middle_click_script = self.on_click_middle.map(Script::new_polling);
let right_click_script = self.on_click_right.map(Script::new_polling);
container.connect_button_press_event(move |_, event| {
let script = match event.button() {
1 => left_click_script.as_ref(),
2 => middle_click_script.as_ref(),
3 => right_click_script.as_ref(),
_ => None,
};
if let Some(script) = script {
trace!("Running on-click script: {}", event.button());
script.run_as_oneshot(None);
}
Inhibit(false)
});
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
container.connect_scroll_event(move |_, event| {
let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(),
_ => None,
};
if let Some(script) = script {
trace!("Running on-scroll script: {}", event.direction());
script.run_as_oneshot(None);
}
Inhibit(false)
});
macro_rules! install_oneshot {
($option:expr, $method:ident) => {
$option.map(Script::new_polling).map(|script| {
container.$method(move |_, _| {
script.run_as_oneshot(None);
Inhibit(false)
});
})
};
}
install_oneshot!(self.on_mouse_enter, connect_enter_notify_event);
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
if let Some(tooltip) = self.tooltip {
let container = container.clone();
dynamic_string(&tooltip, move |string| {
container.set_tooltip_text(Some(&string));
Continue(true)
});
}
}
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) {
self.show_if.take().map_or_else(
|| {
container.show_all();
},
|show_if| {
let container = container.clone();
{
let revealer = revealer.clone();
let container = container.clone();
show_if.subscribe(move |success| {
if success {
container.show_all();
}
revealer.set_reveal_child(success);
Continue(true)
});
}
revealer.connect_child_revealed_notify(move |revealer| {
if !revealer.reveals_child() {
container.hide();
}
});
},
);
}
}

58
src/config/impl.rs Normal file
View File

@@ -0,0 +1,58 @@
use super::{BarPosition, Config, MonitorConfig};
use color_eyre::{Help, Report};
use gtk::Orientation;
use serde::{Deserialize, Deserializer};
// Manually implement for better untagged enum error handling:
// currently open pr: https://github.com/serde-rs/serde/pull/1544
impl<'de> Deserialize<'de> for MonitorConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let content =
<serde::__private::de::Content as serde::Deserialize>::deserialize(deserializer)?;
match <Config as serde::Deserialize>::deserialize(
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
) {
Ok(config) => Ok(Self::Single(config)),
Err(outer) => match <Vec<Config> as serde::Deserialize>::deserialize(
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
) {
Ok(config) => Ok(Self::Multiple(config)),
Err(inner) => {
let report = Report::msg(format!(" multi-bar (c): {inner}").replace("An error occurred when deserializing: ", ""))
.wrap_err(format!("single-bar (b): {outer}").replace("An error occurred when deserializing: ", ""))
.wrap_err("An invalid config was found. The following errors were encountered:")
.note("Both the single-bar (type b / error 1) and multi-bar (type c / error 2) config variants were tried. You can likely ignore whichever of these is not relevant to you.")
.suggestion("Please see https://github.com/JakeStanger/ironbar/wiki/configuration-guide#2-pick-your-use-case for more info on the above");
Err(serde::de::Error::custom(format!("{report:?}")))
}
},
}
}
}
impl BarPosition {
/// Gets the orientation the bar and widgets should use
/// based on this position.
pub fn get_orientation(self) -> Orientation {
if self == Self::Top || self == Self::Bottom {
Orientation::Horizontal
} else {
Orientation::Vertical
}
}
/// Gets the angle that label text should be displayed at
/// based on this position.
pub const fn get_angle(self) -> f64 {
match self {
Self::Top | Self::Bottom => 0.0,
Self::Left => 90.0,
Self::Right => 270.0,
}
}
}

158
src/config/mod.rs Normal file
View File

@@ -0,0 +1,158 @@
mod common;
mod r#impl;
mod truncate;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule;
use crate::modules::focused::FocusedModule;
use crate::modules::label::LabelModule;
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 = "upower")]
use crate::modules::upower::UpowerModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use cfg_if::cfg_if;
use serde::Deserialize;
use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType};
pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
Clock(Box<ClockModule>),
Custom(Box<CustomModule>),
Focused(Box<FocusedModule>),
Label(Box<LabelModule>),
Launcher(Box<LauncherModule>),
#[cfg(feature = "music")]
Music(Box<MusicModule>),
Script(Box<ScriptModule>),
#[cfg(feature = "sys_info")]
SysInfo(Box<SysInfoModule>),
#[cfg(feature = "tray")]
Tray(Box<TrayModule>),
#[cfg(feature = "upower")]
Upower(Box<UpowerModule>),
#[cfg(feature = "workspaces")]
Workspaces(Box<WorkspacesModule>),
}
#[derive(Debug, Clone)]
pub enum MonitorConfig {
Single(Config),
Multiple(Vec<Config>),
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarPosition {
Top,
Bottom,
Left,
Right,
}
impl Default for BarPosition {
fn default() -> Self {
Self::Bottom
}
}
#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
pub struct MarginConfig {
#[serde(default)]
pub bottom: i32,
#[serde(default)]
pub left: i32,
#[serde(default)]
pub right: i32,
#[serde(default)]
pub top: i32,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
pub position: BarPosition,
#[serde(default = "default_true")]
pub anchor_to_edges: bool,
#[serde(default = "default_bar_height")]
pub height: i32,
#[serde(default)]
pub margin: MarginConfig,
#[serde(default = "default_popup_gap")]
pub popup_gap: i32,
pub name: Option<String>,
/// GTK icon theme to use.
pub icon_theme: Option<String>,
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
pub start: Option<Vec<ModuleConfig>>,
pub center: Option<Vec<ModuleConfig>>,
pub end: Option<Vec<ModuleConfig>>,
pub monitors: Option<HashMap<String, MonitorConfig>>,
}
impl Default for Config {
fn default() -> Self {
cfg_if! {
if #[cfg(feature = "clock")] {
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
}
else {
let end = None;
}
}
Self {
position: BarPosition::default(),
height: default_bar_height(),
margin: MarginConfig::default(),
name: None,
popup_gap: default_popup_gap(),
icon_theme: None,
ironvar_defaults: None,
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
)]),
center: Some(vec![ModuleConfig::Focused(Box::default())]),
end,
anchor_to_edges: default_true(),
monitors: None,
}
}
}
const fn default_bar_height() -> i32 {
42
}
const fn default_popup_gap() -> i32 {
5
}
pub const fn default_false() -> bool {
false
}
pub const fn default_true() -> bool {
true
}

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

@@ -0,0 +1,66 @@
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),
Length {
mode: EllipsizeMode,
length: Option<i32>,
max_length: Option<i32>,
},
}
impl TruncateMode {
const fn mode(&self) -> EllipsizeMode {
match self {
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
}
}
const fn length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::Length { length, .. } => *length,
}
}
const fn max_length(&self) -> Option<i32> {
match self {
Self::Auto(_) => None,
Self::Length { max_length, .. } => *max_length,
}
}
pub fn truncate_label(&self, label: &gtk::Label) {
label.set_ellipsize(self.mode().into());
if let Some(length) = self.length() {
label.set_width_chars(length);
}
if let Some(length) = self.max_length() {
label.set_max_width_chars(length);
}
}
}

208
src/desktop_file.rs Normal file
View File

@@ -0,0 +1,208 @@
use lazy_static::lazy_static;
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tracing::warn;
use walkdir::{DirEntry, WalkDir};
use crate::lock;
type DesktopFile = HashMap<String, Vec<String>>;
lazy_static! {
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> =
Mutex::new(HashMap::new());
/// These are the keys that in the cache
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> =
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]);
}
/// Finds directories that should contain `.desktop` files
/// and exist on the filesystem.
fn find_application_dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/usr/share/applications"), // system installed apps
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
];
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
if let Some(xdg_dirs) = xdg_dirs {
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
xdg_dir.push("applications");
dirs.push(xdg_dir);
}
}
let user_dir = dirs::data_local_dir(); // user installed apps
if let Some(mut user_dir) = user_dir {
user_dir.push("applications");
dirs.push(user_dir);
}
dirs.into_iter().filter(|dir| dir.exists()).collect()
}
/// Finds all the desktop files
fn find_desktop_files() -> Vec<PathBuf> {
let dirs = find_application_dirs();
dirs.into_iter()
.flat_map(|dir| {
WalkDir::new(dir)
.max_depth(5)
.into_iter()
.filter_map(Result::ok)
.map(DirEntry::into_path)
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
})
.collect()
}
/// Attempts to locate a `.desktop` file for an app id
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
// this is necessary to invalidate the cache
let files = find_desktop_files();
find_desktop_file_by_filename(app_id, &files)
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
}
/// Finds the correct desktop file using a simple condition check
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let with_names = files
.iter()
.map(|f| {
(
f,
f.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase(),
)
})
.collect::<Vec<_>>();
with_names
.iter()
// first pass - check for exact match
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
// second pass - check for substring
.or_else(|| {
with_names.iter().find(|(_, name)| {
// this will attempt to find flatpak apps that are in the format
// `com.company.app` or `com.app.something`
app_id
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part))
})
})
.map(|(file, _)| file.into())
}
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let app_id = &app_id.to_lowercase();
let mut desktop_files_cache = lock!(DESKTOP_FILES);
let files = files
.iter()
.filter_map(|file| {
let Some(parsed_desktop_file) = parse_desktop_file(file) else {
return None;
};
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
Some((file.clone(), parsed_desktop_file))
})
.collect::<Vec<_>>();
let file = files
.iter()
// first pass - check name key for exact match
.find(|(_, desktop_file)| {
desktop_file
.get("Name")
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
.unwrap_or_default()
})
// second pass - check name key for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file
.get("Name")
.map(|names| {
names
.iter()
.any(|name| name.to_lowercase().contains(app_id))
})
.unwrap_or_default()
})
})
// third pass - check all keys for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file
.values()
.flatten()
.any(|value| value.to_lowercase().contains(app_id))
})
});
file.map(|(path, _)| path).cloned()
}
/// Parses a desktop file into a hashmap of keys/vector(values).
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
let Ok(file) = fs::read_to_string(path) else {
warn!("Couldn't Open File: {}", path.display());
return None;
};
let mut desktop_file: DesktopFile = DesktopFile::new();
file.lines()
.filter_map(|line| {
let Some((key, value)) = line.split_once('=') else {
return None;
};
let key = key.trim();
let value = value.trim();
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
Some((key, value))
} else {
None
}
})
.for_each(|(key, value)| {
desktop_file
.entry(key.to_string())
.or_insert_with(Vec::new)
.push(value.to_string());
});
Some(desktop_file)
}
/// Attempts to get the icon name from the app's `.desktop` file.
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
let Some(path) = find_desktop_file(app_id) else {
return None;
};
let mut desktop_files_cache = lock!(DESKTOP_FILES);
let desktop_file = match desktop_files_cache.get(&path) {
Some(desktop_file) => desktop_file,
_ => desktop_files_cache
.entry(path.clone())
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
};
let mut icons = desktop_file.get("Icon").into_iter().flatten();
icons.next().map(std::string::ToString::to_string)
}

View File

@@ -0,0 +1,78 @@
#[cfg(feature = "ipc")]
use crate::ironvar::get_variable_manager;
use crate::script::Script;
use crate::send;
use cfg_if::cfg_if;
use glib::Continue;
use serde::Deserialize;
use tokio::spawn;
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum DynamicBool {
/// Either a script or variable, to be determined.
Unknown(String),
Script(Script),
#[cfg(feature = "ipc")]
Variable(Box<str>),
}
impl DynamicBool {
pub fn subscribe<F>(self, f: F)
where
F: FnMut(bool) -> Continue + 'static,
{
let value = match self {
Self::Unknown(input) => {
if input.starts_with('#') {
cfg_if! {
if #[cfg(feature = "ipc")] {
Self::Variable(input.into())
} else {
Self::Unknown(input)
}
}
} else {
let script = Script::from(input.as_str());
Self::Script(script)
}
}
_ => self,
};
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
rx.attach(None, f);
spawn(async move {
match value {
DynamicBool::Script(script) => {
script
.run(None, |_, success| {
send!(tx, success);
})
.await;
}
#[cfg(feature = "ipc")]
DynamicBool::Variable(variable) => {
let variable_manager = get_variable_manager();
let variable_name = variable[1..].into(); // remove hash
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
while let Ok(value) = rx.recv().await {
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
send!(tx, has_value);
}
}
DynamicBool::Unknown(_) => unreachable!(),
}
});
}
}
/// Check if a string ironvar is 'truthy'
#[cfg(feature = "ipc")]
fn is_truthy(string: &str) -> bool {
!(string.is_empty() || string == "0" || string == "false")
}

View File

@@ -0,0 +1,321 @@
#[cfg(feature = "ipc")]
use crate::ironvar::get_variable_manager;
use crate::script::{OutputStream, Script};
use crate::{arc_mut, lock, send};
use gtk::prelude::*;
use tokio::spawn;
/// A segment of a dynamic string,
/// containing either a static string
/// or a script.
#[derive(Debug)]
enum DynamicStringSegment {
Static(String),
Script(Script),
#[cfg(feature = "ipc")]
Variable(Box<str>),
}
/// Creates a new dynamic string, based off the input template.
/// Runs `f` with the compiled string each time one of the scripts or variables updates.
///
/// # Example
///
/// ```rs
/// dynamic_string(&text, move |string| {
/// label.set_markup(&string);
/// Continue(true)
/// });
/// ```
pub fn dynamic_string<F>(input: &str, f: F)
where
F: FnMut(String) -> Continue + 'static,
{
let tokens = parse_input(input);
let label_parts = arc_mut!(vec![]);
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in tokens.into_iter().enumerate() {
match segment {
DynamicStringSegment::Static(str) => {
lock!(label_parts).push(str);
}
DynamicStringSegment::Script(script) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
// insert blank value to preserve segment order
lock!(label_parts).push(String::new());
spawn(async move {
script
.run(None, |out, _| {
if let OutputStream::Stdout(out) = out {
let mut label_parts = lock!(label_parts);
let _: String = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join("");
send!(tx, string);
}
})
.await;
});
}
#[cfg(feature = "ipc")]
DynamicStringSegment::Variable(name) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
// insert blank value to preserve segment order
lock!(label_parts).push(String::new());
spawn(async move {
let variable_manager = get_variable_manager();
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
while let Ok(value) = rx.recv().await {
if let Some(value) = value {
let mut label_parts = lock!(label_parts);
let _: String = std::mem::replace(&mut label_parts[i], value);
let string = label_parts.join("");
send!(tx, string);
}
}
});
}
}
}
rx.attach(None, f);
// initialize
{
let label_parts = lock!(label_parts).join("");
send!(tx, label_parts);
}
}
/// Parses the input string into static and dynamic segments
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
// short-circuit parser if it's all static
if !input.contains("{{") && !input.contains('#') {
return vec![DynamicStringSegment::Static(input.to_string())];
}
let mut tokens = vec![];
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char_pair = if chars.len() > 1 {
Some(&chars[..=1])
} else {
None
};
let (token, skip) = match char_pair {
Some(['{', '{']) => parse_script(&chars),
Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2),
#[cfg(feature = "ipc")]
Some(['#', _]) => parse_variable(&chars),
_ => parse_static(&chars),
};
// quick runtime check to make sure the parser is working as expected
assert_ne!(skip, 0);
tokens.push(token);
chars.drain(..skip);
}
tokens
}
fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
const SKIP_BRACKETS: usize = 4; // two braces either side
let str = chars
.windows(2)
.skip(2)
.take_while(|win| win != &['}', '}'])
.map(|w| w[0])
.collect::<String>();
let len = str.chars().count() + SKIP_BRACKETS;
let script = Script::from(str.as_str());
(DynamicStringSegment::Script(script), len)
}
#[cfg(feature = "ipc")]
fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
const SKIP_HASH: usize = 1;
let str = chars
.iter()
.skip(1)
.take_while(|&c| !c.is_whitespace())
.collect::<String>();
let len = str.chars().count() + SKIP_HASH;
let value = str.into();
(DynamicStringSegment::Variable(value), len)
}
fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
let mut str = chars
.windows(2)
.take_while(|&win| win != ['{', '{'] && win[0] != '#')
.map(|w| w[0])
.collect::<String>();
let mut char_count = str.chars().count();
// if segment is at end of string, last char gets missed above due to uneven window.
if chars.len() == char_count + 1 {
let remaining_char = *chars.get(char_count).expect("Failed to find last char");
str.push(remaining_char);
char_count += 1;
}
(DynamicStringSegment::Static(str), char_count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static() {
const INPUT: &str = "hello world";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
}
#[test]
fn test_static_odd_char_count() {
const INPUT: &str = "hello";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
}
#[test]
fn test_script() {
const INPUT: &str = "{{echo hello}}";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 1);
assert!(
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
);
}
#[test]
fn test_variable() {
const INPUT: &str = "#variable";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 1);
assert!(
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
);
}
#[test]
fn test_static_script() {
const INPUT: &str = "hello {{echo world}}";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 2);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
assert!(
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
);
}
#[test]
fn test_static_variable() {
const INPUT: &str = "hello #subject";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 2);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
assert!(
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
);
}
#[test]
fn test_static_script_static() {
const INPUT: &str = "hello {{echo world}} foo";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 3);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
assert!(
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
);
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
}
#[test]
fn test_static_variable_static() {
const INPUT: &str = "hello #subject foo";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 3);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
assert!(
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
);
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
}
#[test]
fn test_static_script_variable() {
const INPUT: &str = "hello {{echo world}} #foo";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 4);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
assert!(
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
);
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
assert!(
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
);
}
#[test]
fn test_escape_hash() {
const INPUT: &str = "number ###num";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 3);
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
assert!(
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
);
}
#[test]
fn test_script_with_hash() {
const INPUT: &str = "{{echo #hello}}";
let tokens = parse_input(INPUT);
assert_eq!(tokens.len(), 1);
assert!(
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
);
}
}

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

@@ -0,0 +1,7 @@
#![doc = include_str!("../../docs/Dynamic values.md")]
mod dynamic_bool;
mod dynamic_string;
pub use dynamic_bool::DynamicBool;
pub use dynamic_string::dynamic_string;

14
src/error.rs Normal file
View File

@@ -0,0 +1,14 @@
#[repr(i32)]
pub enum ExitCode {
GtkDisplay = 1,
CreateBars = 2,
}
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";

43
src/global_state.rs Normal file
View File

@@ -0,0 +1,43 @@
use crate::popup::Popup;
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
/// Global application state shared across all bars.
///
/// Data that needs to be accessed from anywhere
/// that is not otherwise accessible should be placed on here.
#[derive(Debug)]
pub struct GlobalState {
popups: HashMap<Box<str>, Rc<RefCell<Popup>>>,
}
impl GlobalState {
pub(crate) fn new() -> Self {
Self {
popups: HashMap::new(),
}
}
pub fn popups(&self) -> &HashMap<Box<str>, Rc<RefCell<Popup>>> {
&self.popups
}
pub fn popups_mut(&mut self) -> &mut HashMap<Box<str>, Rc<RefCell<Popup>>> {
&mut self.popups
}
pub fn with_popup_mut<F, T>(&self, monitor_name: &str, f: F) -> Option<T>
where
F: FnOnce(RefMut<Popup>) -> T,
{
let popup = self.popups().get(monitor_name);
if let Some(popup) = popup {
let popup = popup.borrow_mut();
Some(f(popup))
} else {
None
}
}
}

77
src/gtk_helpers.rs Normal file
View File

@@ -0,0 +1,77 @@
use glib::IsA;
use gtk::prelude::*;
use gtk::{Orientation, Widget};
/// Represents a widget's size
/// and location relative to the bar's start edge.
#[derive(Debug, Copy, Clone)]
pub struct WidgetGeometry {
/// Position of the start edge of the widget
/// from the start edge of the bar.
pub position: i32,
/// The length of the widget.
pub size: i32,
/// The length of the bar.
pub bar_size: i32,
}
pub trait IronbarGtkExt {
/// Adds a new CSS class to the widget.
fn add_class(&self, class: &str);
/// Gets the geometry for the widget
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
/// Gets a data tag on a widget, if it exists.
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
/// Sets a data tag on a widget.
fn set_tag<V: 'static>(&self, key: &str, value: V);
}
impl<W: IsA<Widget>> IronbarGtkExt for W {
fn add_class(&self, class: &str) {
self.style_context().add_class(class);
}
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
let allocation = self.allocation();
let widget_size = if orientation == Orientation::Horizontal {
allocation.width()
} else {
allocation.height()
};
let top_level = self.toplevel().expect("Failed to get top-level widget");
let top_level_allocation = top_level.allocation();
let bar_size = if orientation == Orientation::Horizontal {
top_level_allocation.width()
} else {
top_level_allocation.height()
};
let (widget_x, widget_y) = self
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_pos = if orientation == Orientation::Horizontal {
widget_x
} else {
widget_y
};
WidgetGeometry {
position: widget_pos,
size: widget_size,
bar_size,
}
}
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
unsafe { self.data(key).map(|val| val.as_ref()) }
}
fn set_tag<V: 'static>(&self, key: &str, value: V) {
unsafe { self.set_data(key, value) }
}
}

View File

@@ -1,145 +0,0 @@
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::path::PathBuf;
use walkdir::WalkDir;
/// Gets directories that should contain `.desktop` files
/// and exist on the filesystem.
fn find_application_dirs() -> Vec<PathBuf> {
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
let user_dir = dirs::data_local_dir();
if let Some(mut user_dir) = user_dir {
user_dir.push("applications");
dirs.push(user_dir);
}
dirs.into_iter().filter(|dir| dir.exists()).collect()
}
/// Attempts to locate a `.desktop` file for an app id
/// (or app class).
///
/// A simple case-insensitive check is performed on filename == `app_id`.
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
let dirs = find_application_dirs();
for dir in dirs {
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
let entry = walker.find(|entry| match entry {
Ok(entry) => {
let file_name = entry.file_name().to_string_lossy().to_lowercase();
let test_name = format!("{}.desktop", app_id.to_lowercase());
file_name == test_name
}
_ => false,
});
if let Some(Ok(entry)) = entry {
let path = entry.path().to_owned();
return Some(path);
}
}
None
}
/// Parses a desktop file into a flat hashmap of keys/values.
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
let file = File::open(path)?;
let lines = io::BufReader::new(file).lines();
let mut map = HashMap::new();
for line in lines.flatten() {
if let Some((key, value)) = line.split_once('=') {
map.insert(key.to_string(), value.to_string());
}
}
Ok(map)
}
/// Attempts to get the icon name from the app's `.desktop` file.
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
match find_desktop_file(app_id) {
Some(file) => {
let map = parse_desktop_file(file);
match map {
Ok(map) => map.get("Icon").map(std::string::ToString::to_string),
Err(_) => None,
}
}
None => None,
}
}
enum IconLocation {
Theme(String),
File(PathBuf),
}
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_{}.png",
steam_id
));
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);
match icon {
Ok(icon) => icon,
Err(_) => None,
}
}
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
None => None,
}
}

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

@@ -0,0 +1,55 @@
use super::ImageProvider;
use crate::gtk_helpers::IronbarGtkExt;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
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();
image.add_class("image");
image.add_class("icon");
match ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image.clone()))
{
Some(_) => {
button.set_image(Some(&image));
button.set_always_show_image(true);
}
None => {
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();
image.add_class("icon");
image.add_class("image");
container.add(&image);
ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image));
} else {
let label = Label::new(Some(input));
label.add_class("icon");
label.add_class("text-icon");
container.add(&label);
}
container
}

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

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

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

@@ -0,0 +1,284 @@
use crate::desktop_file::get_desktop_icon_name;
use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result};
use gtk::cairo::Surface;
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf};
use tracing::warn;
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, use_fallback: bool, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
Some(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).
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://")
|| input.starts_with('/')
}
fn get_location(
input: &str,
theme: &'a IconTheme,
size: i32,
use_fallback: bool,
recurse_depth: usize,
) -> Option<ImageLocation<'a>> {
macro_rules! fallback {
() => {
if use_fallback {
Some(Self::get_fallback_icon(theme))
} else {
None
}
};
}
const MAX_RECURSE_DEPTH: usize = 2;
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
let (input_type, input_name) = input
.split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n));
match input_type {
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
}),
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
input_name[2..].to_string(),
))),
#[cfg(feature = "http")]
Some(input_type) if input_type == "http" || input_type == "https" => {
input.parse().ok().map(ImageLocation::Remote)
}
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
input_name.chars().skip("steam_app_".len()).collect(),
)),
None if theme
.lookup_icon(input, size, IconLookupFlags::empty())
.is_some() =>
{
Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
})
}
Some(input_type) => {
warn!(
"{:?}",
Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available")
);
fallback!()
}
None if PathBuf::from(input_name).is_file() => {
Some(ImageLocation::Local(PathBuf::from(input_name)))
}
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
None if should_parse_desktop_file => {
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
}) {
location
} else {
warn!("Failed to find image: {input}");
fallback!()
}
}
None => {
warn!("Failed to find image: {input}");
fallback!()
}
}
}
/// 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 scale = image.scale_factor();
let scaled_size = size * scale;
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
scaled_size,
scaled_size,
true,
Some(&Cancellable::new()),
);
// Different error types makes this a bit awkward
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
{
Ok(Err(err)) => error!("{err:?}"),
Err(err) => error!("{err:?}"),
_ => {}
}
Continue(false)
});
}
} else {
self.load_into_image_sync(&image)?;
};
#[cfg(not(feature = "http"))]
self.load_into_image_sync(&image)?;
Ok(())
}
/// Attempts to synchronously fetch an image from location
/// and load into into the image.
fn load_into_image_sync(&self, image: &gtk::Image) -> Result<()> {
let scale = image.scale_factor();
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
ImageLocation::Local(path) => self.get_from_file(path, scale),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
#[cfg(feature = "http")]
_ => unreachable!(), // handled above
}?;
Self::create_and_load_surface(&pixbuf, image, scale)
}
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
/// using the provided scaling factor.
/// The surface is then loaded into the provided image.
///
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image, scale: i32) -> Result<()> {
let surface = unsafe {
let ptr =
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
Surface::from_raw_full(ptr)
}?;
image.set_from_surface(Some(&surface));
Ok(())
}
/// Attempts to get a `Pixbuf` from the GTK icon theme.
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
let pixbuf =
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
Some(_) => theme.load_icon(name, self.size * scale, 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, scale: i32) -> Result<Pixbuf> {
let scaled_size = self.size * scale;
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_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, scale: i32) -> 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, scale)
}
/// 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 res = reqwest::get(url).await?;
let status = res.status();
if status.is_success() {
let bytes = res.bytes().await?;
Ok(glib::Bytes::from_owned(bytes))
} else {
Err(Report::msg(format!(
"Received non-success HTTP code ({status})"
)))
}
}
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
ImageLocation::Icon {
name: "dialog-question-symbolic".to_string(),
theme,
}
}
}

28
src/ipc/client.rs Normal file
View File

@@ -0,0 +1,28 @@
use super::Ipc;
use crate::ipc::{Command, Response};
use color_eyre::Result;
use color_eyre::{Help, Report};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
impl Ipc {
/// Sends a command to the IPC server.
/// The server response is returned.
pub async fn send(&self, command: Command) -> Result<Response> {
let mut stream = match UnixStream::connect(&self.path).await {
Ok(stream) => Ok(stream),
Err(err) => Err(Report::new(err)
.wrap_err("Failed to connect to Ironbar IPC server")
.suggestion("Is Ironbar running?")),
}?;
let write_buffer = serde_json::to_vec(&command)?;
stream.write_all(&write_buffer).await?;
let mut read_buffer = vec![0; 1024];
let bytes = stream.read(&mut read_buffer).await?;
let response = serde_json::from_slice(&read_buffer[..bytes])?;
Ok(response)
}
}

79
src/ipc/commands.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::path::PathBuf;
use clap::Subcommand;
use serde::{Deserialize, Serialize};
#[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Command {
/// Return "ok"
Ping,
/// Open the GTK inspector
Inspect,
/// Reload the config
Reload,
/// Set an `ironvar` value.
/// This creates it if it does not already exist, and updates it if it does.
/// Any references to this variable are automatically and immediately updated.
/// Keys and values can be any valid UTF-8 string.
Set {
/// Variable key. Can be any alphanumeric ASCII string.
key: Box<str>,
/// Variable value. Can be any valid UTF-8 string.
value: String,
},
/// Get the current value of an `ironvar`.
Get {
/// Variable key.
key: Box<str>,
},
/// Load an additional CSS stylesheet.
/// The sheet is automatically hot-reloaded.
LoadCss {
/// The path to the sheet.
path: PathBuf,
},
/// Set the visibility of the bar with the given name.
SetVisible {
///Bar name to target.
bar_name: String,
/// The visibility status.
#[arg(short, long)]
visible: bool,
},
/// Get the visibility of the bar with the given name.
GetVisible {
/// Bar name to target.
bar_name: String,
},
/// Toggle a popup open/closed.
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
TogglePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Open a popup, regardless of current state.
OpenPopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Close a popup, regardless of current state.
ClosePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
},
}

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