270 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
31a57ae637 fix: scripts don't work while running ironbar under a systemd service
Fixes #134
2023-05-01 20:57:08 +01:00
100 changed files with 4828 additions and 1941 deletions

1
.envrc Normal file
View File

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

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"

View File

@@ -22,6 +22,9 @@ jobs:
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
name: Cache dependencies
- name: Install build deps
run: |
sudo apt-get update
@@ -70,4 +73,6 @@ jobs:
name: jakestanger
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build --print-build-logs

View File

@@ -4,6 +4,157 @@ 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))*:
@@ -271,4 +422,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.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.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.
- Make sure your code is formatted using `cargo fmt`.
- 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:

856
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,17 @@
[package]
name = "ironbar"
version = "0.12.0"
version = "0.14.0-pre"
edition = "2021"
license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar"
repository = "https://github.com/jakestanger/ironbar"
categories = ["gui"]
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features]
default = [
"cli",
"ipc",
"http",
"config+all",
"clipboard",
@@ -17,10 +22,19 @@ default = [
"upower",
"workspaces+all"
]
http = ["dep:reqwest"]
upower = ["upower_dbus", "zbus", "futures-lite"]
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
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"]
@@ -38,7 +52,9 @@ music = ["regex"]
sys_info = ["sysinfo", "regex"]
tray = ["stray"]
tray = ["system-tray"]
upower = ["upower_dbus", "zbus", "futures-lite"]
workspaces = ["futures-util"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
@@ -49,57 +65,76 @@ workspaces = ["futures-util"]
# core
gtk = "0.17.0"
gtk-layer-shell = "0.6.0"
glib = "0.17.5"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
glib = "0.17.10"
tokio = { version = "1.32.0", features = [
"macros",
"rt-multi-thread",
"time",
"process",
"sync",
"io-util",
"net",
] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1"
strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.2"
serde = { version = "1.0.141", features = ["derive"] }
indexmap = "1.9.1"
dirs = "5.0.0"
walkdir = "2.3.2"
notify = { version = "5.0.0", default-features = false }
wayland-client = "0.30.0"
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
serde = { version = "1.0.188", features = ["derive"] }
indexmap = "2.0.0"
dirs = "5.0.1"
walkdir = "2.4.0"
notify = { version = "6.1.1", default-features = false }
wayland-client = "0.30.2"
wayland-protocols = { version = "0.30.1", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] }
universal-config = { version = "0.4.0", default_features = false }
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
"calloop",
] }
universal-config = { version = "0.4.3", default_features = false }
ctrlc = "3.4.1"
lazy_static = "1.4.0"
async_once = "0.2.6"
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.14", optional = true }
reqwest = { version = "0.11.20", optional = true }
# clipboard
nix = { version = "0.26.2", optional = true, features = ["event"] }
nix = { version = "0.27.1", optional = true, features = ["event"] }
# clock
chrono = { version = "0.4.19", optional = true }
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
# music
mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.0", optional = true }
mpd_client = { version = "1.2.0", optional = true }
mpris = { version = "2.0.1", optional = true }
# sys_info
sysinfo = { version = "0.28.4", optional = true }
sysinfo = { version = "0.29.10", optional = true }
# tray
stray = { version = "0.1.3", optional = true }
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.11.0", optional = true }
zbus = { version = "3.14.1", optional = true }
# workspaces
swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.1", optional = true }
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
futures-util = { version = "0.3.21", optional = true }
# shared
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
regex = { version = "1.9.5", default-features = false, features = [
"std",
], optional = true } # music, sys_info

133
README.md
View File

@@ -1,49 +1,94 @@
# Ironbar
<h1 align="center" >--- Ironbar ---</h1>
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
It uses GTK3 and gtk-layer-shell.
<div align="center">
<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).
---
<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, but should (mostly) work on any wlroots compositor.
- Fully themeable with CSS and hot-loaded styles.
- Support for multiple configuration languages.
- Popups used by widgets to show rich content and controls on click.
- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
- 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
### Cargo
[crate](https://crates.io/crates/ironbar)
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh
cargo install ironbar
```
[crate](https://crates.io/crates/ironbar)
### Arch Linux
[aur package](https://aur.archlinux.org/packages/ironbar-git)
```sh
yay -S ironbar-git
```
[aur package](https://aur.archlinux.org/packages/ironbar-git)
### Nix
### Nix Flake
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
A flake is included with the repo which can be used with home-manager.
```sh
nix-shell -p ironbar
```
#### Example
#### Flake
Here is an example nix flake that uses Ironbar.
A flake is included with the repo which can be used with Home Manager.
<details>
<summary>Example usage</summary>
```nix
{
@@ -80,13 +125,14 @@ Here is an example nix flake that uses Ironbar.
}
```
#### Binary Caching
</details>
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`
in case you don't want to compile Ironbar.
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
### Source
[repo](https://github.com/jakestanger/ironbar)
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
```sh
@@ -100,53 +146,36 @@ install target/release/ironbar ~/.local/bin/ironbar
By default, all features are enabled.
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
[repo](https://github.com/jakestanger/ironbar)
## Running
All of the above installation methods provide a binary called `ironbar`.
Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
Ironbar can be launched using the `ironbar` binary.
Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
You can set the `IRONBAR_LOG` or `IRONBAR_FILE_LOG` environment variables to
`error`, `warn`, `info`, `debug` or `trace` to configure the log output level.
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
File output can be found at `~/.local/share/ironbar/error.log`.
## Configuration
## Status
Ironbar gives a lot of flexibility when configuring, including multiple file formats
and options for scaling complexity: you can use a single config across all monitors,
or configure different/multiple bars per monitor.
Ironbar is an **alpha** project.
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
## Styling
I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the
file.
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
## Project Status
This project is in alpha, but should be usable.
Everything that is implemented works and should be documented.
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
There is currently room for lots more modules, and lots more configuration options for the existing modules.
The current configuration schema is not set in stone and breaking changes could come along at any point;
until the project matures I am more interested in ease of use than backwards compatibility.
A few bugs do exist, and I am sure there are plenty more to be found.
The project will be *actively developed* as I am using it on my daily driver.
Bugs will be fixed, features will be added, code will be refactored.
## Contribution Guidelines
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
- [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
- [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

View File

@@ -58,6 +58,8 @@ cargo build --release --no-default-features \
|---------------------|-----------------------------------------------------------------------------------|
| **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. |

View File

@@ -267,20 +267,22 @@ Check [here](config) for an example config file for a fully configured bar in ea
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
| `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. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
| 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
@@ -306,9 +308,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her
| Name | Type | Default | Description |
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
| `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. |
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
#### Appearance

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
```

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.

View File

@@ -10,15 +10,17 @@ which only includes a subset of the full web spec (plus a few non-standard prope
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. |
| `.popup` | Any popup box. |
| 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.

View File

@@ -2,10 +2,16 @@
- [Compiling from source](compiling)
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
# Dynamic content
- [Controlling Ironbar](controlling-ironbar)
- [Dynamic values](dynamic-values)
- [Scripts](scripts)
- [Ironvars](ironvars)
# Examples
- [Config](config)
@@ -28,4 +34,4 @@
- [Sys_Info](sys-info)
- [Tray](tray)
- [Upower](upower)
- [Workspaces](workspaces)
- [Workspaces](workspaces)

View File

@@ -9,17 +9,15 @@ Supports plain text and images.
> Type: `clipboard`
| Name | Type | Default | Description |
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string/image` | `󰨸` | 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. |
See [here](images) for information on images.
| 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>
@@ -86,6 +84,9 @@ end:
|--------------------------------------|------------------------------------------------------|
| `.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. |

View File

@@ -8,9 +8,13 @@ Clicking on the widget opens a popup with the time and a calendar.
> Type: `clock`
| Name | Type | Default | Description |
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
| 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>

View File

@@ -1,6 +1,9 @@
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
@@ -18,11 +21,11 @@ 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. |
| 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
@@ -30,20 +33,20 @@ 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. |
| 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 and embedded scripts are supported.
A text label. Pango markup is supported.
> Type `label`
| Name | Type | Default | Description |
|---------|----------|--------------|---------------------------------------------------------------------|
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
#### Button
@@ -51,10 +54,10 @@ A clickable button, which can run a command when clicked.
> Type `button`
| Name | Type | Default | Description |
|------------|--------------------|--------------|---------------------------------------------------------------------|
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
| 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
@@ -62,10 +65,10 @@ An image or icon from disk or http.
> Type `image`
| Name | Type | Default | Description |
|--------|-----------|---------|---------------------------------------------------------------------------------------------|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
| 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
@@ -76,18 +79,16 @@ A draggable 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 |
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
| `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. |
| 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.
@@ -115,14 +116,12 @@ A progress bar.
Note that `value` expects a numeric value **between 0-`max`** as output.
| Name | Type | Default | Description |
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
| `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. |
| 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:

View File

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

View File

@@ -1,12 +1,15 @@
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
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` | `string` | `null` | Text, optionally with embedded scripts. |
| Name | Type | Default | Description |
|---------|-------------------------------------------------|---------|------------------------|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
<details>
<summary>JSON</summary>

View File

@@ -11,27 +11,27 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
> Type: `music`
| | Type | Default | Description |
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string/image` | `` | Icon to show when playing. |
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
| `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/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
| | 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.
@@ -128,33 +128,46 @@ and will be replaced with values from the currently playing track:
| `{track}` | Track number |
| `{disc}` | Disc number |
| `{genre}` | Genre |
| `{duration}` | Duration in `mm:ss` |
| `{elapsed}` | Time elapsed in `mm:ss` |
## Styling
| Selector | Description |
|-------------------------------------|------------------------------------------|
| `.music` | Tray widget button |
| `.music .contents` | Tray widget button contents box |
| `.popup-music` | Popup box |
| `.popup-music .album-art` | Album art image inside popup box |
| `.popup-music .title` | Track title container inside popup box |
| `.popup-music .title .icon` | Track title icon label inside popup box |
| `.popup-music .title .label` | Track title label inside popup box |
| `.popup-music .album` | Track album container inside popup box |
| `.popup-music .album .icon` | Track album icon label inside popup box |
| `.popup-music .album .label` | Track album label inside popup box |
| `.popup-music .artist` | Track artist container inside popup box |
| `.popup-music .artist .icon` | Track artist icon label inside popup box |
| `.popup-music .artist .label` | Track artist label inside popup box |
| `.popup-music .controls` | Controls container inside popup box |
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
| `.popup-music .controls .btn-play` | Play button inside popup box |
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
| `.popup-music .controls .btn-next` | Next button inside popup box |
| `.popup-music .volume` | Volume container inside popup box |
| `.popup-music .volume .slider` | Volume slider popup box |
| `.popup-music .volume .icon` | Volume icon label inside popup box |
| 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).

View File

@@ -1,6 +1,9 @@
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`

View File

@@ -168,6 +168,8 @@ The following tokens can be used in the `format` configuration option:
| `{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 |
@@ -175,4 +177,4 @@ The following tokens can be used in the `format` configuration option:
| `.sysinfo` | Sysinfo widget box |
| `.sysinfo .item` | Individual information label |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -9,9 +9,10 @@ Displays system power information such as the battery percentage, and estimated
> Type: `upower`
| Name | Type | Default | Description |
|----------|----------|-----------------|---------------------------------------------------|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
| 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>
@@ -70,13 +71,14 @@ end:
## Styling
| Selector | Description |
|---------------------------------|-----------------------------|
| `.upower` | Upower widget container. |
| `.upower .icon` | Upower widget battery icon. |
| `.upower .button` | Upower widget button. |
| `.upower .button .label` | Upower widget button label. |
| `.popup-upower` | Upower popup box. |
| `.popup-upower .upower-details` | Label inside the popup. |
| 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).
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -8,12 +8,14 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
| Name | Type | Default | Description |
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
<details>
<summary>JSON</summary>
@@ -28,6 +30,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
"2": "",
"3": ""
},
"favorites": ["1", "2", "3"],
"all_monitors": false
}
]
@@ -43,6 +46,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
[[end]]
type = "workspaces"
all_monitors = false
favorites = ["1", "2", "3"]
[[end.name_map]]
1 = ""
@@ -63,6 +67,10 @@ end:
1: ""
2: ""
3: ""
favorites:
- "1"
- "2"
- "3"
all_monitors: false
```
@@ -79,6 +87,7 @@ end:
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
favorites = [ "1" "2" "3" ]
all_monitors = false
}
]
@@ -89,10 +98,15 @@ end:
## Styling
| Selector | Description |
|-----------------------------|--------------------------------------|
| `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) |
| Selector | Description |
|--------------------------------|--------------------------------------|
| `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) |
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
| `.workspaces .item .icon` | Workspace button icon (any type) |
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) |
For more information on styling, please see the [styling guide](styling-guide).
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -3,7 +3,7 @@ let {
type = "workspaces"
all_monitors = false
name_map = {
1 = ""
1 = "󰙯"
2 = "icon:firefox"
3 = ""
Games = "icon:steam"
@@ -15,7 +15,7 @@ let {
$launcher = {
type = "launcher"
favorites = ["firefox" "discord" "Steam"]
favorites = ["firefox" "discord" "steam"]
show_names = false
show_icons = true
}
@@ -67,7 +67,7 @@ let {
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
$label = { type = "label" label = "random num: {{500:echo $RANDOM}}" }
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
// -- begin custom --
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }

View File

@@ -109,7 +109,7 @@
{
"all_monitors": false,
"name_map": {
"1": "",
"1": "󰙯",
"2": "icon:firefox",
"3": "",
"Code": "",
@@ -121,14 +121,14 @@
"favorites": [
"firefox",
"discord",
"Steam"
"steam"
],
"show_icons": true,
"show_names": false,
"type": "launcher"
},
{
"label": "random num: {{500:echo $RANDOM}}",
"label": "random num: {{500:echo FIXME}}",
"type": "label"
}
]

View File

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

View File

@@ -1,87 +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
- 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-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
- 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 $RANDOM}}'
type: label
- 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

@@ -16,6 +16,7 @@
box, menubar, button {
background-color: @color_bg;
background-image: none;
}
button, label {
@@ -119,7 +120,11 @@ button:hover {
margin-right: 1em;
}
.popup-music .title .icon *, .popup-music .title .label {
.popup-music .icon-box {
margin-right: 0.4em;
}
.popup-music .title .icon, .popup-music .title .label {
font-size: 1.7em;
}
@@ -127,15 +132,17 @@ button:hover {
color: @color_border;
}
.popup-music .volume scale slider {
.popup-music .volume .slider slider {
border-radius: 100%;
}
/* volume icon */
.popup-music .volume > box:last-child label {
margin-left: 6px;
.popup-music .volume .icon {
margin-left: 4px;
}
.popup-music .progress .slider slider {
border-radius: 100%;
}
/* -- script -- */

149
flake.lock generated
View File

@@ -1,9 +1,66 @@
{
"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=",
@@ -18,13 +75,45 @@
"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": 1682786779,
"narHash": "sha256-m7QFzPS/CE8hbkbIVK4UStihAQMtczr0vSpOgETOM1g=",
"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": "08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b",
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
"type": "github"
},
"original": {
@@ -36,23 +125,50 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
"crane": "crane",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay_2"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"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": 1682821181,
"narHash": "sha256-7MYRqO9Ge46sULbQwJbcH/IMDNBdxCGUO9w7bEOc3CI=",
"lastModified": 1693447852,
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "1be440e9119e69b68151cd9c84876ff3063a2e45",
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
"type": "github"
},
"original": {
@@ -75,6 +191,21 @@
"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",

View File

@@ -6,11 +6,18 @@
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;
@@ -27,10 +34,18 @@
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.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;
@@ -42,11 +57,33 @@
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
builder = "naersk";
in {
ironbar = prev.callPackage ./nix/default.nix {
ironbar = let
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
inherit rustPlatform;
};
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
@@ -82,6 +119,14 @@
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";

View File

@@ -19,46 +19,77 @@
lib,
version ? "git",
features ? [],
}:
rustPlatform.buildRustPackage rec {
inherit version;
pname = "ironbar";
src = builtins.path {
name = "ironbar";
path = lib.cleanSource ../.;
};
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
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}";
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";
};
};
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=";
})

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

@@ -3,12 +3,14 @@ use crate::modules::{
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::popup::Popup;
use crate::Config;
use crate::unique_id::get_unique_usize;
use crate::{Config, GlobalState};
use color_eyre::Result;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::sync::{Arc, RwLock};
use std::cell::RefCell;
use std::rc::Rc;
use tracing::{debug, info};
/// Creates a new window for a bar,
@@ -18,8 +20,16 @@ pub fn create_bar(
monitor: &Monitor,
monitor_name: &str,
config: Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
let bar_name = config
.name
.clone()
.unwrap_or_else(|| format!("bar-{}", get_unique_usize()));
win.set_widget_name(&bar_name);
info!("Creating bar {}", bar_name);
setup_layer_shell(
&win,
@@ -54,7 +64,12 @@ pub fn create_bar(
content.set_center_widget(Some(&center));
content.pack_end(&end, false, false, 0);
load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
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);
win.add(&content);
win.connect_destroy_event(|_, _| {
@@ -135,6 +150,11 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
container
}
#[derive(Debug)]
struct BarLoadResult {
popup: Rc<RefCell<Popup>>,
}
/// Loads the configured modules onto a bar.
fn load_modules(
left: &gtk::Box,
@@ -144,7 +164,7 @@ fn load_modules(
config: Config,
monitor: &Monitor,
output_name: &str,
) -> Result<()> {
) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
@@ -163,22 +183,28 @@ fn load_modules(
};
}
// 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, config.popup_gap)?;
add_modules(left, modules, &info, &popup)?;
}
if let Some(modules) = config.center {
let info = info!(ModuleLocation::Center);
add_modules(center, modules, &info, config.popup_gap)?;
add_modules(center, modules, &info, &popup)?;
}
if let Some(modules) = config.end {
let info = info!(ModuleLocation::Right);
add_modules(right, modules, &info, config.popup_gap)?;
add_modules(right, modules, &info, &popup)?;
}
Ok(())
let result = BarLoadResult { popup };
Ok(result)
}
/// Adds modules into a provided GTK box,
@@ -187,17 +213,20 @@ fn add_modules(
content: &gtk::Box,
modules: Vec<ModuleConfig>,
info: &ModuleInfo,
popup_gap: i32,
popup: &Rc<RefCell<Popup>>,
) -> Result<()> {
let popup = Popup::new(info, popup_gap);
let popup = Arc::new(RwLock::new(popup));
let orientation = info.bar_position.get_orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist");
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
let common = $module.common.take().expect("common config to exist");
let widget_parts = create_module(
*$module,
$id,
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);
@@ -205,7 +234,8 @@ fn add_modules(
}};
}
for (id, config) in modules.into_iter().enumerate() {
for config in modules {
let id = get_unique_usize();
match config {
#[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id),

View File

@@ -2,7 +2,7 @@ use crate::send;
use tokio::spawn;
use tokio::sync::mpsc;
/// MPSC async -> sync channel.
/// MPSC async -> GTK sync channel.
/// The sender uses `tokio::sync::mpsc`
/// while the receiver uses `glib::MainContext::channel`.
///

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()),
}
}

View File

@@ -1,5 +1,5 @@
use super::wayland::{self, ClipboardItem};
use crate::{lock, try_send};
use crate::{arc_mut, lock, try_send};
use indexmap::map::Iter;
use indexmap::IndexMap;
use lazy_static::lazy_static;
@@ -28,9 +28,9 @@ impl ClipboardClient {
fn new() -> Self {
trace!("Initializing clipboard client");
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
let cache = arc_mut!(ClipboardCache::new());
{
let senders = senders.clone();
@@ -38,7 +38,8 @@ impl ClipboardClient {
spawn(async move {
let (mut rx, item) = {
let wl = wayland::get_client().await;
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_clipboard()
};
@@ -111,7 +112,7 @@ impl ClipboardClient {
rx
}
pub async fn copy(&self, id: usize) {
pub fn copy(&self, id: usize) {
debug!("Copying item with id {id}");
let item = {
@@ -120,7 +121,8 @@ impl ClipboardClient {
};
if let Some(item) = item {
let wl = wayland::get_client().await;
let wl = wayland::get_client();
let wl = lock!(wl);
wl.copy_to_clipboard(item);
}

View File

@@ -1,13 +1,12 @@
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{lock, send};
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::EventListenerMutable as EventListener;
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
use hyprland::shared::WorkspaceType;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::task::spawn_blocking;
use tracing::{debug, error, info};
@@ -36,28 +35,25 @@ impl EventClient {
let mut event_listener = EventListener::new();
// we need a lock to ensure events don't run at the same time
let lock = Arc::new(Mutex::new(()));
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::new(Mutex::new(Some(active)));
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, _state| {
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 focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Add(workspace));
@@ -70,7 +66,7 @@ impl EventClient {
let lock = lock.clone();
let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type, _state| {
event_listener.add_workspace_change_handler(move |workspace_type| {
let _lock = lock!(lock);
let mut prev_workspace = lock!(active);
@@ -81,10 +77,7 @@ impl EventClient {
);
let workspace_name = get_workspace_name(workspace_type);
let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
workspace.map_or_else(
|| {
@@ -93,7 +86,7 @@ impl EventClient {
|workspace| {
// there may be another type of update so dispatch that regardless of focus change
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
if !focused {
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
},
@@ -106,9 +99,9 @@ impl EventClient {
let lock = lock.clone();
let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data, _state| {
event_listener.add_active_monitor_change_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.1;
let workspace_type = event_data.workspace;
let mut prev_workspace = lock!(active);
@@ -118,12 +111,11 @@ impl EventClient {
);
let workspace_name = get_workspace_name(workspace_type);
let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let (Some(workspace), false) = (workspace, focused) {
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");
@@ -135,23 +127,20 @@ impl EventClient {
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_workspace_moved_handler(move |event_data, _state| {
event_listener.add_workspace_moved_handler(move |event_data| {
let _lock = lock!(lock);
let workspace_type = event_data.1;
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 focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
if !focused {
if !workspace.visibility.is_focused() {
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
}
}
@@ -159,7 +148,7 @@ impl EventClient {
}
{
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
event_listener.add_workspace_destroy_handler(move |workspace_type| {
let _lock = lock!(lock);
debug!("Received workspace destroy: {workspace_type:?}");
@@ -181,32 +170,28 @@ impl EventClient {
workspace: Workspace,
tx: &Sender<WorkspaceUpdate>,
) {
let old = prev_workspace
.as_ref()
.map(|w| w.name.clone())
.unwrap_or_default();
send!(
tx,
WorkspaceUpdate::Focus {
old,
new: workspace.name.clone(),
old: prev_workspace.take(),
new: workspace.clone(),
}
);
prev_workspace.replace(workspace);
}
/// Gets a workspace by name from the server.
///
/// Use `focused` to manually mark the workspace as focused,
/// as this is not automatically checked.
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
/// 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 {
Some(Workspace::from((focused, w)))
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
create_is_visible()(w)
}));
Some(Workspace::from((vis, w)))
} else {
None
}
@@ -215,16 +200,19 @@ impl EventClient {
/// Gets the active workspace from the server.
fn get_active_workspace() -> Result<Workspace> {
let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?;
Ok(w)
}
}
impl WorkspaceClient for EventClient {
fn focus(&self, id: String) -> Result<()> {
Dispatch::call(DispatchType::Workspace(
WorkspaceIdentifierWithSpecial::Name(&id),
))?;
let identifier = match id.parse::<i32>() {
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
};
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(())
}
@@ -234,13 +222,16 @@ impl WorkspaceClient for EventClient {
{
let tx = self.workspace_tx.clone();
let active_name = HWorkspace::get_active()
.map(|active| active.name)
.unwrap_or_default();
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| Workspace::from((w.name == active_name, w)))
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect();
send!(tx, WorkspaceUpdate::Init(workspaces));
@@ -269,13 +260,36 @@ fn get_workspace_name(name: WorkspaceType) -> String {
}
}
impl From<(bool, hyprland::data::Workspace)> for Workspace {
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
/// 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,
focused,
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

@@ -75,8 +75,38 @@ pub struct Workspace {
pub name: String,
/// Name of the monitor (output) the workspace is located on
pub monitor: String,
/// Whether the workspace is in focus
pub focused: bool,
/// 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)]
@@ -90,8 +120,8 @@ pub enum WorkspaceUpdate {
Move(Workspace),
/// Declares focus moved from the old workspace to the new.
Focus {
old: String,
new: String,
old: Option<Workspace>,
new: Workspace,
},
}

View File

@@ -1,4 +1,4 @@
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send};
use async_once::AsyncOnce;
use color_eyre::Report;
@@ -105,22 +105,50 @@ pub fn get_sub_client() -> &'static SwayEventClient {
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(),
focused: node.focused,
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,
focused: workspace.focused,
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
}
}
}
@@ -139,16 +167,8 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
.unwrap_or_default(),
),
WorkspaceChange::Focus => Self::Focus {
old: event
.old
.expect("Missing old workspace")
.name
.unwrap_or_default(),
new: event
.current
.expect("Missing current workspace")
.name
.unwrap_or_default(),
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())

View File

@@ -9,9 +9,17 @@ 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,
}
@@ -27,23 +35,27 @@ pub struct Track {
pub cover_path: Option<String>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub enum PlayerState {
Playing,
Paused,
Stopped,
}
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct Status {
pub state: PlayerState,
pub volume_percent: u8,
pub duration: Option<Duration>,
pub elapsed: Option<Duration>,
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<()>;
@@ -51,6 +63,7 @@ pub trait MusicClient {
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>;
}

View File

@@ -1,9 +1,11 @@
use super::{MusicClient, Status, Track};
use crate::await_sync;
use crate::clients::music::{PlayerState, PlayerUpdate};
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;
@@ -16,7 +18,7 @@ use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpStream, UnixStream};
use tokio::spawn;
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
use tokio::sync::broadcast;
use tokio::sync::Mutex;
use tokio::time::sleep;
use tracing::{debug, error, info};
@@ -29,8 +31,8 @@ lazy_static! {
pub struct MpdClient {
client: Client,
music_dir: PathBuf,
tx: Sender<PlayerUpdate>,
_rx: Receiver<PlayerUpdate>,
tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>,
}
#[derive(Debug)]
@@ -57,7 +59,7 @@ impl MpdClient {
let (client, mut state_changes) =
wait_for_connection(host, Duration::from_secs(5), None).await?;
let (tx, rx) = channel(16);
let (tx, rx) = broadcast::channel(16);
{
let music_dir = music_dir.clone();
@@ -78,7 +80,19 @@ impl MpdClient {
}
}
Ok::<(), SendError<(Option<Track>, Status)>>(())
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;
}
});
}
@@ -92,9 +106,9 @@ impl MpdClient {
async fn send_update(
client: &Client,
tx: &Sender<PlayerUpdate>,
tx: &broadcast::Sender<PlayerUpdate>,
music_dir: &Path,
) -> Result<(), SendError<PlayerUpdate>> {
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
let current_song = client.command(commands::CurrentSong).await;
let status = client.command(commands::Status).await;
@@ -102,17 +116,33 @@ impl MpdClient {
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
let status = Status::from(status);
tx.send(PlayerUpdate::Update(Box::new(track), 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<(), SendError<PlayerUpdate>> {
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
info!("Connection to MPD server lost");
self.tx.send(PlayerUpdate::Disconnect)?;
Ok(())
@@ -182,7 +212,12 @@ impl MusicClient for MpdClient {
Ok(())
}
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
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)
@@ -291,9 +326,7 @@ impl From<mpd_client::responses::Status> for Status {
fn from(status: mpd_client::responses::Status) -> Self {
Self {
state: PlayerState::from(status.state),
volume_percent: status.volume,
duration: status.duration,
elapsed: status.elapsed,
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,
}

View File

@@ -1,15 +1,15 @@
use super::{MusicClient, PlayerUpdate, Status, Track};
use crate::clients::music::PlayerState;
use crate::{lock, send};
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::string;
use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
use tokio::sync::broadcast::{channel, Receiver, Sender};
use std::{cmp, string};
use tokio::sync::broadcast;
use tokio::task::spawn_blocking;
use tracing::{debug, error, trace};
@@ -19,18 +19,18 @@ lazy_static! {
pub struct Client {
current_player: Arc<Mutex<Option<String>>>,
tx: Sender<PlayerUpdate>,
_rx: Receiver<PlayerUpdate>,
tx: broadcast::Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>,
}
impl Client {
fn new() -> Self {
let (tx, rx) = channel(32);
let (tx, rx) = broadcast::channel(32);
let current_player = Arc::new(Mutex::new(None));
let current_player = arc_mut!(None);
{
let players_list = Arc::new(Mutex::new(HashSet::new()));
let players_list = arc_mut!(HashSet::new());
let current_player = current_player.clone();
let tx = tx.clone();
@@ -84,6 +84,20 @@ impl Client {
});
}
{
let current_player = current_player.clone();
let tx = tx.clone();
spawn_blocking(move || {
let player_finder = PlayerFinder::new().expect("to get new player finder");
loop {
Self::send_tick_update(&player_finder, &current_player, &tx);
sleep(Duration::from_millis(TICK_INTERVAL_MS));
}
});
}
Self {
current_player,
tx,
@@ -95,7 +109,7 @@ impl Client {
player_id: String,
players: Arc<Mutex<HashSet<String>>>,
current_player: Arc<Mutex<Option<String>>>,
tx: Sender<PlayerUpdate>,
tx: broadcast::Sender<PlayerUpdate>,
) {
spawn_blocking(move || {
let player_finder = PlayerFinder::new()?;
@@ -138,7 +152,7 @@ impl Client {
});
}
fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
debug!("Sending update using '{}'", player.identity());
let metadata = player.get_metadata()?;
@@ -148,10 +162,7 @@ impl Client {
let track_list = player.get_track_list();
let volume_percent = player
.get_volume()
.map(|vol| (vol * 100.0) as u8)
.unwrap_or(0);
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,
@@ -159,8 +170,6 @@ impl Client {
playlist_position: 1,
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
state: PlayerState::from(playback_status),
elapsed: player.get_position().ok(),
duration: metadata.length(),
volume_percent,
};
@@ -181,6 +190,26 @@ impl Client {
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 {
@@ -223,7 +252,23 @@ impl MusicClient for Client {
Ok(())
}
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
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();
@@ -231,6 +276,14 @@ impl MusicClient for Client {
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
@@ -247,9 +300,18 @@ impl From<Metadata> for Track {
const KEY_GENRE: &str = "xesam:genre";
Self {
title: value.title().map(std::string::ToString::to_string),
album: value.album_name().map(std::string::ToString::to_string),
artist: value.artists().map(|artists| artists.join(", ")),
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)
@@ -274,3 +336,11 @@ impl From<PlaybackStatus> for PlayerState {
}
}
}
fn replace_empty_none(string: String) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
}
}

View File

@@ -1,13 +1,14 @@
use crate::{lock, send};
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 stray::message::menu::TrayMenu;
use stray::message::tray::StatusNotifierItem;
use stray::message::{NotifierItemCommand, NotifierItemMessage};
use stray::StatusNotifierWatcher;
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};
@@ -23,14 +24,16 @@ pub struct TrayEventReceiver {
}
impl TrayEventReceiver {
async fn new() -> stray::error::Result<Self> {
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 = tray.create_notifier_host("ironbar").await?;
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
let tray = Arc::new(Mutex::new(BTreeMap::new()));
let tray = arc_mut!(BTreeMap::new());
{
let b_tx = b_tx.clone();
@@ -103,7 +106,7 @@ lazy_static! {
let value = loop {
retries += 1;
let tray = TrayEventReceiver::new().await;
let tray = Box::pin(TrayEventReceiver::new()).await;
match tray {
Ok(tray) => break Some(tray),

View File

@@ -6,7 +6,7 @@ use zbus::fdo::PropertiesProxy;
lazy_static! {
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
let dbus = zbus::Connection::system()
let dbus = Box::pin(zbus::Connection::system())
.await
.expect("failed to create connection to system bus");

View File

@@ -2,8 +2,9 @@ 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::send;
use crate::{cached_broadcast, send};
use cfg_if::cfg_if;
use color_eyre::Report;
use smithay_client_toolkit::output::{OutputInfo, OutputState};
@@ -25,17 +26,14 @@ cfg_if! {
use super::ClipboardItem;
use super::wlr_data_control::manager::DataControlDeviceManagerState;
use crate::lock;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
}
}
#[derive(Debug)]
pub enum Request {
/// Sends a request for all the outputs.
/// These are then sent on the `output` channel.
Outputs,
/// Sends a request for all the seats.
/// These are then sent ont the `seat` channel.
/// 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.
@@ -53,8 +51,10 @@ pub enum Request {
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")]
@@ -62,7 +62,6 @@ pub struct WaylandClient {
// Internal channels
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
#[cfg(feature = "clipboard")]
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
@@ -71,13 +70,17 @@ pub struct WaylandClient {
}
impl WaylandClient {
pub(super) async fn new() -> Self {
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 (output_tx, output_rx) = mpsc::channel();
let (seat_tx, seat_rx) = mpsc::channel();
let toplevel_tx2 = toplevel_tx.clone();
@@ -99,6 +102,7 @@ impl WaylandClient {
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");
@@ -138,7 +142,8 @@ impl WaylandClient {
seats: vec![],
handles: HashMap::new(),
#[cfg(feature = "clipboard")]
clipboard: Arc::new(Mutex::new(None)),
clipboard: crate::arc_mut!(None),
output_tx,
toplevel_tx,
#[cfg(feature = "clipboard")]
clipboard_tx,
@@ -156,11 +161,6 @@ impl WaylandClient {
trace!("{event:?}");
match event {
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
Event::Msg(Request::Outputs) => {
trace!("Received get outputs request");
send!(output_tx, env.output_info());
}
Event::Msg(Request::Seats) => {
trace!("Receive get seats request");
send!(seat_tx, env.seats.clone());
@@ -196,12 +196,12 @@ impl WaylandClient {
});
Self {
output_channel,
toplevel_tx,
_toplevel_rx: toplevel_rx,
toplevel_init_rx,
#[cfg(feature = "clipboard")]
clipboard_init_rx,
output_rx,
seat_rx,
#[cfg(feature = "clipboard")]
clipboard_tx,
@@ -242,6 +242,10 @@ impl WaylandClient {
(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) {
@@ -249,11 +253,13 @@ impl WaylandClient {
send!(self.request_tx, Request::Roundtrip);
}
pub fn get_outputs(&self) -> Vec<OutputInfo> {
trace!("Sending get outputs request");
send!(self.request_tx, Request::Outputs);
self.output_rx.recv().expect(ERR_CHANNEL_RECV)
/// 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> {

View File

@@ -1,6 +1,6 @@
/// 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
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
// --- Data Control Device --- \\

View File

@@ -6,11 +6,10 @@ mod wl_seat;
mod wlr_foreign_toplevel;
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
use async_once::AsyncOnce;
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::OutputState;
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;
@@ -18,6 +17,7 @@ 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;
@@ -33,7 +33,6 @@ cfg_if! {
use self::wlr_data_control::manager::DataControlDeviceManagerState;
use self::wlr_data_control::source::CopyPasteSource;
use self::wlr_data_control::SelectionOfferItem;
use std::sync::{Arc, Mutex};
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
@@ -66,6 +65,7 @@ pub struct Environment {
#[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>>,
@@ -106,10 +106,9 @@ impl ProvidesRegistryState for Environment {
}
lazy_static! {
static ref CLIENT: AsyncOnce<WaylandClient> =
AsyncOnce::new(async { WaylandClient::new().await });
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
}
pub async fn get_client() -> &'static WaylandClient {
CLIENT.get().await
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
CLIENT.clone()
}

View File

@@ -1,4 +1,6 @@
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;
@@ -31,9 +33,12 @@ impl OutputHandler for Environment {
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
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(
@@ -42,14 +47,26 @@ impl OutputHandler for Environment {
_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,
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

@@ -7,11 +7,12 @@ 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_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp};
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;
@@ -19,21 +20,14 @@ 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::atomic::{AtomicUsize, Ordering};
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;
static COUNTER: AtomicUsize = AtomicUsize::new(1);
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub struct SelectionOfferItem {
offer: SelectionOffer,
token: Option<RegistrationToken>,
@@ -151,7 +145,7 @@ impl Environment {
};
Ok(ClipboardItem {
id: get_id(),
id: get_unique_usize(),
value,
mime_type: mime_type.value.clone(),
})
@@ -245,7 +239,7 @@ impl DataControlOfferHandler for Environment {
_offer: &mut DataControlDeviceOffer,
_mime_type: String,
) {
debug!("Handler received offer");
trace!("Handler received offer");
}
}
@@ -295,23 +289,22 @@ impl DataControlSourceHandler for Environment {
trace!("Num bytes: {}", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll_fd = epoll_create().unwrap();
epoll_ctl(
epoll_fd,
EpollOp::EpollCtlAdd,
fd.as_raw_fd(),
&mut epoll_event,
)
.unwrap();
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_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
epoll_fd
.wait(&mut events, 100)
.expect("Failed to wait to epoll");
match file.write(chunk) {
Ok(_) => bytes = &bytes[chunk.len()..],
@@ -321,22 +314,6 @@ impl DataControlSourceHandler for Environment {
}
}
}
// for chunk in bytes.chunks(pipe_size as usize) {
// trace!("Writing chunk");
// file.write(chunk).expect("Failed to write chunk to buffer");
// file.flush().expect("Failed to flush to file");
// }
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
// match file.write_all(bytes) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
} else {
error!("Failed to find source");
}
@@ -381,11 +358,14 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
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

View File

@@ -7,7 +7,7 @@ use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut;
use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex};
use tracing::{debug, warn};
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,
@@ -149,7 +149,7 @@ where
let data = data.data_control_offer_data();
if let Event::Offer { mime_type } = event {
debug!("Adding new offer with type '{mime_type}'");
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);
}

View File

@@ -1,7 +1,7 @@
use super::manager::ToplevelManagerState;
use crate::lock;
use crate::unique_id::get_unique_usize;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use tracing::trace;
use wayland_client::protocol::wl_output::WlOutput;
@@ -11,12 +11,6 @@ use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_h
Event, ZwlrForeignToplevelHandleV1,
};
static COUNTER: AtomicUsize = AtomicUsize::new(1);
fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone)]
pub struct ToplevelHandle {
pub handle: ZwlrForeignToplevelHandleV1,
@@ -74,7 +68,7 @@ pub struct ToplevelInfo {
impl Default for ToplevelInfo {
fn default() -> Self {
Self {
id: get_id(),
id: get_unique_usize(),
app_id: String::new(),
title: String::new(),
fullscreen: false,
@@ -157,9 +151,8 @@ where
lock!(data.inner).current_info = Some(pending_info);
}
if !lock!(data.inner).initial_done {
lock!(data.inner).initial_done = true;
state.new_handle(
if lock!(data.inner).initial_done {
state.update_handle(
conn,
qh,
ToplevelHandle {
@@ -167,7 +160,8 @@ where
},
);
} else {
state.update_handle(
lock!(data.inner).initial_done = true;
state.new_handle(
conn,
qh,
ToplevelHandle {

View File

@@ -30,7 +30,7 @@ impl ToplevelManagerHandler for Environment {
impl ToplevelHandleHandler for Environment {
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
debug!("Handler received new handle");
trace!("Handler received new handle");
match handle.info() {
Some(info) => {
@@ -50,7 +50,7 @@ impl ToplevelHandleHandler for Environment {
_qh: &QueueHandle<Self>,
handle: ToplevelHandle,
) {
debug!("Handler received handle update");
trace!("Handler received handle update");
match handle.info() {
Some(info) => {

View File

@@ -1,11 +1,9 @@
use crate::dynamic_string::DynamicString;
use crate::dynamic_value::{dynamic_string, DynamicBool};
use crate::script::{Script, ScriptInput};
use crate::send;
use gtk::gdk::ScrollDirection;
use gtk::prelude::*;
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
use serde::Deserialize;
use tokio::spawn;
use tracing::trace;
/// Common configuration options
@@ -15,7 +13,7 @@ pub struct CommonConfig {
pub class: Option<String>,
pub name: Option<String>,
pub show_if: Option<ScriptInput>,
pub show_if: Option<DynamicBool>,
pub transition_type: Option<TransitionType>,
pub transition_duration: Option<u32>,
@@ -114,7 +112,7 @@ impl CommonConfig {
if let Some(tooltip) = self.tooltip {
let container = container.clone();
DynamicString::new(&tooltip, move |string| {
dynamic_string(&tooltip, move |string| {
container.set_tooltip_text(Some(&string));
Continue(true)
});
@@ -127,23 +125,13 @@ impl CommonConfig {
container.show_all();
},
|show_if| {
let script = Script::new_polling(show_if);
let container = container.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
script
.run(None, |_, success| {
send!(tx, success);
})
.await;
});
{
let revealer = revealer.clone();
let container = container.clone();
rx.attach(None, move |success| {
show_if.subscribe(move |success| {
if success {
container.show_all();
}

View File

@@ -21,6 +21,7 @@ use crate::modules::tray::TrayModule;
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;
@@ -30,7 +31,7 @@ pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "clock")]
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
Clock(Box<ClockModule>),
@@ -96,10 +97,13 @@ pub struct Config {
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>>,
@@ -107,6 +111,36 @@ pub struct Config {
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
}

View File

@@ -1,16 +1,42 @@
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::path::PathBuf;
use walkdir::WalkDir;
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};
/// Gets directories that should contain `.desktop` files
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")];
let user_dir = dirs::data_local_dir();
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);
@@ -19,55 +45,164 @@ fn find_application_dirs() -> Vec<PathBuf> {
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> {
/// 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()
}
for dir in dirs {
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
/// 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();
let entry = walker.find(|entry| {
entry.as_ref().map_or(false, |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
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))
})
});
if let Some(Ok(entry)) = entry {
let path = entry.path().to_owned();
return Some(path);
}
}
None
file.map(|(path, _)| path).cloned()
}
/// 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();
/// 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 map = HashMap::new();
let mut desktop_file: DesktopFile = DesktopFile::new();
for line in lines.flatten() {
if let Some((key, value)) = line.split_once('=') {
map.insert(key.to_string(), value.to_string());
}
}
file.lines()
.filter_map(|line| {
let Some((key, value)) = line.split_once('=') else {
return None;
};
Ok(map)
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> {
find_desktop_file(app_id).and_then(|file| {
let map = parse_desktop_file(file);
map.map_or(None, |map| {
map.get("Icon").map(std::string::ToString::to_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

@@ -1,160 +0,0 @@
use crate::script::{OutputStream, Script};
use crate::{lock, send};
use gtk::prelude::*;
use std::sync::{Arc, Mutex};
use tokio::spawn;
/// A segment of a dynamic string,
/// containing either a static string
/// or a script.
#[derive(Debug)]
enum DynamicStringSegment {
Static(String),
Dynamic(Script),
}
/// A string with embedded scripts for dynamic content.
pub struct DynamicString;
impl DynamicString {
/// Creates a new dynamic string, based off the input template.
/// Runs `f` with the compiled string each time one of the scripts updates.
///
/// # Example
///
/// ```rs
/// DynamicString::new(&text, move |string| {
/// label.set_markup(&string);
/// Continue(true)
/// });
/// ```
pub fn new<F>(input: &str, f: F) -> Self
where
F: FnMut(String) -> Continue + 'static,
{
let segments = Self::parse_input(input);
let label_parts = Arc::new(Mutex::new(Vec::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in segments.into_iter().enumerate() {
match segment {
DynamicStringSegment::Static(str) => {
lock!(label_parts).push(str);
}
DynamicStringSegment::Dynamic(script) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
// insert blank value to preserve segment order
lock!(label_parts).push(String::new());
spawn(async move {
script
.run(None, |out, _| {
if let OutputStream::Stdout(out) = out {
let mut label_parts = lock!(label_parts);
let _: String = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join("");
send!(tx, string);
}
})
.await;
});
}
}
}
// initialize
{
let label_parts = lock!(label_parts).join("");
send!(tx, label_parts);
}
rx.attach(None, f);
Self
}
/// Parses the input string into static and dynamic segments
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
if !input.contains("{{") {
return vec![DynamicStringSegment::Static(input.to_string())];
}
let mut segments = vec![];
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char_pair = if chars.len() > 1 {
Some(&chars[..=1])
} else {
None
};
let (token, skip) = if let Some(['{', '{']) = char_pair {
const SKIP_BRACKETS: usize = 4; // two braces either side
let str = chars
.windows(2)
.skip(2)
.take_while(|win| win != &['}', '}'])
.map(|w| w[0])
.collect::<String>();
let len = str.len();
(
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
len + SKIP_BRACKETS,
)
} else {
let mut str = chars
.windows(2)
.take_while(|win| win != &['{', '{'])
.map(|w| w[0])
.collect::<String>();
// if segment is at end of string, last char gets missed above due to uneven window.
if chars.len() == str.len() + 1 {
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
str.push(remaining_char);
}
let len = str.len();
(DynamicStringSegment::Static(str), len)
};
// quick runtime check to make sure the parser is working as expected
assert_ne!(skip, 0);
segments.push(token);
chars.drain(..skip);
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test() {
// TODO: see if we can run gtk tests in ci
if gtk::init().is_ok() {
let label = gtk::Label::new(None);
DynamicString::new(
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
move |string| {
label.set_label(&string);
Continue(true)
},
);
}
}
}

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;

View File

@@ -2,7 +2,6 @@
pub enum ExitCode {
GtkDisplay = 1,
CreateBars = 2,
Config = 3,
}
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";

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
}
}
}

View File

@@ -1,8 +1,77 @@
use glib::IsA;
use gtk::prelude::*;
use gtk::Widget;
use gtk::{Orientation, Widget};
/// Adds a new CSS class to a widget.
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
widget.style_context().add_class(class);
/// 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,8 +1,7 @@
use super::ImageProvider;
use crate::gtk_helpers::add_class;
use crate::gtk_helpers::IronbarGtkExt;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error;
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
@@ -10,17 +9,17 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
add_class(&image, "image");
image.add_class("image");
image.add_class("icon");
match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone()))
match ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image.clone()))
{
Ok(_) => {
Some(_) => {
button.set_image(Some(&image));
button.set_always_show_image(true);
}
Err(err) => {
error!("{err:?}");
None => {
button.set_label(input);
}
}
@@ -37,18 +36,17 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
add_class(&image, "image");
image.add_class("icon");
image.add_class("image");
container.add(&image);
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image))
{
error!("{err:?}");
}
ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(image));
} else {
let label = Label::new(Some(input));
add_class(&label, "label");
label.add_class("icon");
label.add_class("text-icon");
container.add(&label);
}

View File

@@ -1,10 +1,13 @@
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")] {
@@ -38,60 +41,98 @@ impl<'a> ImageProvider<'a> {
///
/// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
let location = Self::get_location(input, theme, size)?;
Ok(Self { location, size })
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).
#[cfg(any(feature = "music", feature = "workspaces"))]
pub fn is_definitely_image_input(input: &str) -> bool {
input.starts_with("icon:")
|| input.starts_with("file://")
|| input.starts_with("http://")
|| input.starts_with("https://")
|| input.starts_with('/')
}
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
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" => Ok(ImageLocation::Icon {
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
}),
Some(input_type) if input_type == "file" => Ok(ImageLocation::Local(PathBuf::from(
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" => {
Ok(ImageLocation::Remote(input.parse()?))
input.parse().ok().map(ImageLocation::Remote)
}
None if input.starts_with("steam_app_") => Ok(ImageLocation::Steam(
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() =>
{
Ok(ImageLocation::Icon {
Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
})
}
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available")),
None if PathBuf::from(input_name).is_file() => {
Ok(ImageLocation::Local(PathBuf::from(input_name)))
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!()
}
None => get_desktop_icon_name(input_name).map_or_else(
|| Err(Report::msg(format!("Unknown image type: '{input}'"))),
|input| Self::get_location(&input, theme, size),
),
}
}
@@ -115,17 +156,24 @@ impl<'a> ImageProvider<'a> {
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,
size,
size,
scaled_size,
scaled_size,
true,
Some(&Cancellable::new()),
);
match pixbuf {
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
// 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)
@@ -141,18 +189,35 @@ impl<'a> ImageProvider<'a> {
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, image.scale_factor())
}
ImageLocation::Local(path) => self.get_from_file(path),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
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
}?;
image.set_pixbuf(Some(&pixbuf));
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(())
}
@@ -161,7 +226,7 @@ impl<'a> ImageProvider<'a> {
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, IconLookupFlags::FORCE_SIZE),
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
None => Ok(None),
}?;
@@ -172,14 +237,15 @@ impl<'a> ImageProvider<'a> {
}
/// Attempts to get a `Pixbuf` from a local file.
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
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) -> Result<Pixbuf> {
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")),
@@ -190,7 +256,7 @@ impl<'a> ImageProvider<'a> {
},
)?;
self.get_from_file(&path)
self.get_from_file(&path, scale)
}
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
@@ -208,4 +274,11 @@ impl<'a> ImageProvider<'a> {
)))
}
}
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,
},
}

42
src/ipc/mod.rs Normal file
View File

@@ -0,0 +1,42 @@
mod client;
pub mod commands;
pub mod responses;
mod server;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use tracing::warn;
use crate::GlobalState;
pub use commands::Command;
pub use responses::Response;
#[derive(Debug)]
pub struct Ipc {
path: PathBuf,
global_state: Rc<RefCell<GlobalState>>,
}
impl Ipc {
/// Creates a new IPC instance.
/// This can be used as both a server and client.
pub fn new(global_state: Rc<RefCell<GlobalState>>) -> Self {
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
.join("ironbar-ipc.sock");
if format!("{}", ipc_socket_file.display()).len() > 100 {
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
}
Self {
path: ipc_socket_file,
global_state,
}
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
}

18
src/ipc/responses.rs Normal file
View File

@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Response {
Ok,
OkValue { value: String },
Err { message: Option<String> },
}
impl Response {
/// Creates a new `Response::Error`.
pub fn error(message: &str) -> Self {
Self::Err {
message: Some(message.to_string()),
}
}
}

256
src/ipc/server.rs Normal file
View File

@@ -0,0 +1,256 @@
use std::cell::RefCell;
use std::fs;
use std::path::Path;
use std::rc::Rc;
use color_eyre::{Report, Result};
use glib::Continue;
use gtk::prelude::*;
use gtk::Application;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::spawn;
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{debug, error, info, warn};
use crate::bridge_channel::BridgeChannel;
use crate::ipc::{Command, Response};
use crate::ironvar::get_variable_manager;
use crate::modules::PopupButton;
use crate::style::load_css;
use crate::{await_sync, read_lock, send_async, try_send, write_lock, GlobalState};
use super::Ipc;
impl Ipc {
/// Starts the IPC server on its socket.
///
/// Once started, the server will begin accepting connections.
pub fn start(&self, application: &Application) {
let bridge = BridgeChannel::<Command>::new();
let cmd_tx = bridge.create_sender();
let (res_tx, mut res_rx) = mpsc::channel(32);
let path = self.path.clone();
if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address");
Self::shutdown(&path);
}
spawn(async move {
info!("Starting IPC on {}", path.display());
let listener = match UnixListener::bind(&path) {
Ok(listener) => listener,
Err(err) => {
error!(
"{:?}",
Report::new(err).wrap_err("Unable to start IPC server")
);
return;
}
};
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
if let Err(err) =
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
{
error!("{err:?}");
}
}
Err(err) => {
error!("{err:?}");
}
}
}
});
let application = application.clone();
let global_state = self.global_state.clone();
bridge.recv(move |command| {
let res = Self::handle_command(command, &application, &global_state);
try_send!(res_tx, res);
Continue(true)
});
}
/// Takes an incoming connections,
/// reads the command message, and sends the response.
///
/// The connection is closed once the response has been written.
async fn handle_connection(
mut stream: UnixStream,
cmd_tx: &Sender<Command>,
res_rx: &mut Receiver<Response>,
) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split();
let mut read_buffer = vec![0; 1024];
let bytes = stream_read.read(&mut read_buffer).await?;
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
debug!("Received command: {command:?}");
send_async!(cmd_tx, command);
let res = res_rx
.recv()
.await
.unwrap_or(Response::Err { message: None });
let res = serde_json::to_vec(&res)?;
stream_write.write_all(&res).await?;
stream_write.shutdown().await?;
Ok(())
}
/// Takes an input command, runs it and returns with the appropriate response.
///
/// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(
command: Command,
application: &Application,
global_state: &Rc<RefCell<GlobalState>>,
) -> Response {
match command {
Command::Inspect => {
gtk::Window::set_interactive_debugging(true);
Response::Ok
}
Command::Reload => {
await_sync(async move { crate::reload(application, global_state).await }).unwrap();
Response::Ok
}
Command::Set { key, value } => {
let variable_manager = get_variable_manager();
let mut variable_manager = write_lock!(variable_manager);
match variable_manager.set(key, value) {
Ok(_) => Response::Ok,
Err(err) => Response::error(&format!("{err}")),
}
}
Command::Get { key } => {
let variable_manager = get_variable_manager();
let value = read_lock!(variable_manager).get(&key);
match value {
Some(value) => Response::OkValue { value },
None => Response::error("Variable not found"),
}
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
Response::Ok
} else {
Response::error("File not found")
}
}
Command::TogglePopup { bar_name, name } => {
let global_state = global_state.borrow();
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
let current_widget = popup.current_widget();
popup.hide();
let data = popup
.cache
.iter()
.find(|(_, (module_name, _))| module_name == &name)
.map(|module| (module, module.1 .1.buttons.first()));
match data {
Some(((&id, _), Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
Some(_) => Response::Ok,
None => Response::error("Invalid module name"),
}
});
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
}
Command::OpenPopup { bar_name, name } => {
let global_state = global_state.borrow();
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
// only one popup per bar, so hide if open for another widget
popup.hide();
let data = popup
.cache
.iter()
.find(|(_, (module_name, _))| module_name == &name)
.map(|module| (module, module.1 .1.buttons.first()));
match data {
Some(((&id, _), Some(button))) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
None => Response::error("Invalid module name"),
}
});
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
}
Command::ClosePopup { bar_name } => {
let global_state = global_state.borrow();
let popup_found = global_state
.with_popup_mut(&bar_name, |mut popup| popup.hide())
.is_some();
if popup_found {
Response::Ok
} else {
Response::error("Invalid monitor name")
}
}
Command::Ping => Response::Ok,
Command::SetVisible { bar_name, visible } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
window.set_visible(visible);
Response::Ok
} else {
Response::error("Bar not found")
}
}
Command::GetVisible { bar_name } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
Response::OkValue {
value: window.is_visible().to_string(),
}
} else {
Response::error("Bar not found")
}
}
}
}
/// Shuts down the IPC server,
/// removing the socket file in the process.
///
/// Note this is static as the `Ipc` struct is not `Send`.
pub fn shutdown<P: AsRef<Path>>(path: P) {
fs::remove_file(&path).ok();
}
}

107
src/ironvar.rs Normal file
View File

@@ -0,0 +1,107 @@
#![doc = include_str!("../docs/Ironvars.md")]
use crate::{arc_rw, send};
use color_eyre::{Report, Result};
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio::sync::broadcast;
lazy_static! {
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
}
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
VARIABLE_MANAGER.clone()
}
/// Global singleton manager for `IronVar` variables.
pub struct VariableManager {
variables: HashMap<Box<str>, IronVar>,
}
impl VariableManager {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
}
}
/// Sets the value for a variable,
/// creating it if it does not exist.
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
if Self::key_is_valid(&key) {
if let Some(var) = self.variables.get_mut(&key) {
var.set(Some(value));
} else {
let var = IronVar::new(Some(value));
self.variables.insert(key, var);
}
Ok(())
} else {
Err(Report::msg("Invalid key"))
}
}
/// Gets the current value of an `ironvar`.
/// Prefer to use `subscribe` where possible.
pub fn get(&self, key: &str) -> Option<String> {
self.variables.get(key).and_then(IronVar::get)
}
/// Subscribes to an `ironvar`, creating it if it does not exist.
/// Any time the var is set, its value is sent on the channel.
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
self.variables
.entry(key)
.or_insert_with(|| IronVar::new(None))
.subscribe()
}
fn key_is_valid(key: &str) -> bool {
!key.is_empty()
&& key
.chars()
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
}
}
/// Ironbar dynamic variable representation.
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
#[derive(Debug)]
struct IronVar {
value: Option<String>,
tx: broadcast::Sender<Option<String>>,
_rx: broadcast::Receiver<Option<String>>,
}
impl IronVar {
/// Creates a new variable.
fn new(value: Option<String>) -> Self {
let (tx, rx) = broadcast::channel(32);
Self { value, tx, _rx: rx }
}
/// Gets the current variable value.
/// Prefer to subscribe to changes where possible.
fn get(&self) -> Option<String> {
self.value.clone()
}
/// Sets the current variable value.
/// The change is broadcast to all receivers.
fn set(&mut self, value: Option<String>) {
self.value = value.clone();
send!(self.tx, value);
}
/// Subscribes to the variable.
/// The latest value is immediately sent to all receivers.
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
let rx = self.tx.subscribe();
send!(self.tx, self.value.clone());
rx
}
}

View File

@@ -1,7 +1,7 @@
/// Sends a message on an asynchronous `Sender` using `send()`
/// Panics if the message cannot be sent.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// send_async!(tx, "my message");
@@ -16,7 +16,7 @@ macro_rules! send_async {
/// Sends a message on an synchronous `Sender` using `send()`
/// Panics if the message cannot be sent.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// send!(tx, "my message");
@@ -31,7 +31,7 @@ macro_rules! send {
/// Sends a message on an synchronous `Sender` using `try_send()`
/// Panics if the message cannot be sent.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// try_send!(tx, "my message");
@@ -46,7 +46,7 @@ macro_rules! try_send {
/// Locks a `Mutex`.
/// Panics if the `Mutex` cannot be locked.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// let mut val = lock!(my_mutex);
@@ -62,7 +62,7 @@ macro_rules! lock {
/// Gets a read lock on a `RwLock`.
/// Panics if the `RwLock` cannot be locked.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// let val = read_lock!(my_rwlock);
@@ -77,7 +77,7 @@ macro_rules! read_lock {
/// Gets a write lock on a `RwLock`.
/// Panics if the `RwLock` cannot be locked.
///
/// Usage:
/// # Usage:
///
/// ```rs
/// let mut val = write_lock!(my_rwlock);
@@ -88,3 +88,33 @@ macro_rules! write_lock {
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
};
}
/// Wraps `val` in a new `Arc<Mutex<T>>`.
///
/// # Usage:
///
/// ```rs
/// let val = arc_mut!(MyService::new());
/// ```
///
#[macro_export]
macro_rules! arc_mut {
($val:expr) => {
std::sync::Arc::new(std::sync::Mutex::new($val))
};
}
/// Wraps `val` in a new `Arc<RwLock<T>>`.
///
/// # Usage:
///
/// ```rs
/// let val = arc_rw!(MyService::new());
/// ```
///
#[macro_export]
macro_rules! arc_rw {
($val:expr) => {
std::sync::Arc::new(std::sync::RwLock::new($val))
};
}

View File

@@ -1,43 +1,63 @@
#![doc = include_str!("../README.md")]
use std::cell::{Cell, RefCell};
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::{Display, Monitor};
use gtk::prelude::*;
use gtk::Application;
use smithay_client_toolkit::output::OutputInfo;
use tokio::runtime::Handle;
use tokio::spawn;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader;
use clients::wayland;
use crate::bar::create_bar;
use crate::bridge_channel::BridgeChannel;
use crate::cached_broadcast::Event;
use crate::config::{Config, MonitorConfig};
use crate::error::ExitCode;
use crate::global_state::GlobalState;
use crate::style::load_css;
mod bar;
mod bridge_channel;
mod cached_broadcast;
#[cfg(feature = "cli")]
mod cli;
mod clients;
mod config;
mod desktop_file;
mod dynamic_string;
mod dynamic_value;
mod error;
mod global_state;
mod gtk_helpers;
mod image;
#[cfg(feature = "ipc")]
mod ipc;
#[cfg(feature = "ipc")]
mod ironvar;
mod logging;
mod macros;
mod modules;
mod popup;
mod script;
mod style;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use std::cell::Cell;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use tokio::runtime::Handle;
use tokio::task::block_in_place;
use crate::error::ExitCode;
use clients::wayland::{self, WaylandClient};
use tracing::{debug, error, info};
use universal_config::ConfigLoader;
mod unique_id;
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -46,15 +66,45 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
async fn main() {
let _guard = logging::install_logging();
let global_state = Rc::new(RefCell::new(GlobalState::new()));
cfg_if! {
if #[cfg(feature = "cli")] {
run_with_args(global_state).await;
} else {
start_ironbar(global_state);
}
}
}
#[cfg(feature = "cli")]
async fn run_with_args(global_state: Rc<RefCell<GlobalState>>) {
let args = cli::Args::parse();
match args.command {
Some(command) => {
let ipc = ipc::Ipc::new(global_state);
match ipc.send(command).await {
Ok(res) => cli::handle_response(res),
Err(err) => error!("{err:?}"),
};
}
None => start_ironbar(global_state).await,
}
}
async fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
info!("Ironbar version {}", VERSION);
info!("Starting application");
let wayland_client = wayland::get_client().await;
let app = Application::builder().application_id(GTK_APP_ID).build();
let output_bridge = BridgeChannel::<Event<OutputInfo>>::new();
let output_tx = output_bridge.create_sender();
let running = Rc::new(Cell::new(false));
let global_state2 = global_state.clone();
app.connect_activate(move |app| {
if running.get() {
info!("Ironbar already running, returning");
@@ -63,37 +113,13 @@ async fn main() {
running.set(true);
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
);
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
);
let config = match config_res {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
exit(ExitCode::Config as i32)
cfg_if! {
if #[cfg(feature = "ipc")] {
let ipc = ipc::Ipc::new(global_state2.clone());
ipc.start(app);
}
};
debug!("Loaded config file");
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
debug!("Created bars");
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| {
config_dir().map_or_else(
@@ -111,68 +137,163 @@ async fn main() {
if style_path.exists() {
load_css(style_path);
}
let (tx, rx) = mpsc::channel();
#[cfg(feature = "ipc")]
let ipc_path = ipc.path().to_path_buf();
spawn_blocking(move || {
rx.recv().expect("to receive from channel");
info!("Shutting down");
#[cfg(feature = "ipc")]
ipc::Ipc::shutdown(ipc_path);
exit(0);
});
let wc = wayland::get_client();
let output_tx = output_tx.clone();
let mut output_rx = lock!(wc).subscribe_outputs();
spawn(async move {
while let Some(event) = output_rx.recv().await {
try_send!(output_tx.clone(), event);
}
});
ctrlc::set_handler(move || send!(tx, ())).expect("Error setting Ctrl-C handler");
});
let config = load_config();
{
let app = app.clone();
let global_state = global_state.clone();
output_bridge.recv(move |event: cached_broadcast::Event<_>| {
let display = get_display();
match event {
Event::Add(output) => {
debug!("Adding bar(s) for monitor {:?}", &output.name);
create_bars_for_monitor(&app, &display, &output, config.clone(), &global_state)
.unwrap();
}
// TODO: Implement
Event::Remove(_) => {}
Event::Replace(_, _) => {}
}
Continue(true)
});
}
// Ignore CLI args
// Some are provided by swaybar_config but not currently supported
app.run_with_args(&Vec::<&str>::new());
info!("Shutting down");
exit(0);
}
/// Creates each of the bars across each of the (configured) outputs.
fn create_bars(
app: &Application,
display: &Display,
wl: &WaylandClient,
config: &Config,
) -> Result<()> {
/// Closes all current bars and entirely reloads Ironbar.
/// This re-reads the config file.
pub async fn reload(app: &Application, global_state: &Rc<RefCell<GlobalState>>) -> Result<()> {
info!("Closing existing bars");
let windows = app.windows();
for window in windows {
window.close();
}
let config = load_config();
let wl = wayland::get_client();
let wl = lock!(wl);
let outputs = wl.get_outputs();
debug!("Received {} outputs from Wayland", outputs.len());
debug!("Outputs: {:?}", outputs);
let num_monitors = display.n_monitors();
for i in 0..num_monitors {
let monitor = display
.monitor(i)
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
let output = outputs
.get(i as usize)
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
let Some(monitor_name) = &output.name else { continue };
config.monitors.as_ref().map_or_else(
|| {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())
},
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())
}
Some(MonitorConfig::Multiple(configs)) => {
for config in configs {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())?;
}
Ok(())
}
_ => Ok(()),
}
},
)?;
let display = get_display();
for output in outputs.iter() {
create_bars_for_monitor(app, &display, output, config.clone(), global_state)?;
}
Ok(())
}
fn create_bars_for_monitor(
app: &Application,
display: &Display,
output: &OutputInfo,
config: Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> {
let Some(monitor_name) = &output.name else {
return Ok(());
};
let monitor = match get_monitor(&monitor_name, display) {
Ok(monitor) => monitor,
Err(err) => return Err(err),
};
let Some(monitor_config) = config.get_monitor_config(&monitor_name) else {
return Ok(());
};
match monitor_config {
MonitorConfig::Single(config) => {
create_bar(&app, &monitor, &monitor_name, config, global_state)
}
MonitorConfig::Multiple(configs) => configs
.into_iter()
.map(|config| create_bar(&app, &monitor, &monitor_name, config, global_state))
.collect(),
}
}
fn get_display() -> Display {
Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
)
}
fn get_monitor(name: &str, display: &Display) -> Result<Monitor> {
let wl = wayland::get_client();
let wl = lock!(wl);
let outputs = wl.get_outputs();
let monitor = (0..display.n_monitors()).into_iter().find_map(|i| {
let monitor = display.monitor(i)?;
let output = outputs.get(i as usize)?;
let is_match = output.name.as_ref().map(|n| n == name).unwrap_or_default();
if is_match {
Some(monitor)
} else {
None
}
});
monitor.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))
}
fn load_config() -> Config {
let config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
debug!("Loaded config file");
config
}
/// Blocks on a `Future` until it resolves.
///

View File

@@ -2,8 +2,9 @@ use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
@@ -111,7 +112,7 @@ impl Module<Button> for ClipboardModule {
while let Some(event) = rx.recv().await {
let client = clipboard::get_client();
match event {
UIEvent::Copy(id) => client.copy(id).await,
UIEvent::Copy(id) => client.copy(id),
UIEvent::Remove(id) => client.remove(id),
}
}
@@ -124,25 +125,26 @@ impl Module<Button> for ClipboardModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
) -> color_eyre::Result<ModuleParts<Button>> {
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
button.style_context().add_class("btn");
button.connect_clicked(move |button| {
let pos = Popup::widget_geometry(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
// we need to bind to the receiver as the channel does not open
// until the popup is first opened.
context.widget_rx.attach(None, |_| Continue(true));
Ok(ModuleWidget {
widget: button,
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
}
fn into_popup(

View File

@@ -1,9 +1,6 @@
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use chrono::{DateTime, Local};
use std::env;
use chrono::{DateTime, Local, Locale};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
@@ -13,6 +10,13 @@ use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{send_async, try_send};
#[derive(Debug, Deserialize, Clone)]
pub struct ClockModule {
/// Date/time format string.
@@ -23,14 +27,48 @@ pub struct ClockModule {
#[serde(default = "default_format")]
format: String,
#[serde(default = "default_popup_format")]
format_popup: String,
#[serde(default = "default_locale")]
locale: String,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
impl Default for ClockModule {
fn default() -> Self {
ClockModule {
format: default_format(),
format_popup: default_popup_format(),
locale: default_locale(),
common: Some(CommonConfig::default()),
}
}
}
fn default_format() -> String {
String::from("%d/%m/%Y %H:%M")
}
fn default_popup_format() -> String {
String::from("%H:%M:%S")
}
fn default_locale() -> String {
env::var("LC_TIME")
.or_else(|_| env::var("LANG"))
.map_or_else(|_| "POSIX".to_string(), strip_tail)
}
fn strip_tail(string: String) -> String {
string
.split_once('.')
.map(|(head, _)| head.to_string())
.unwrap_or(string)
}
impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>;
type ReceiveMessage = ();
@@ -60,35 +98,33 @@ impl Module<Button> for ClockModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let label = Label::new(None);
label.set_angle(info.bar_position.get_angle());
button.add(&label);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
let format = self.format.clone();
{
context.widget_rx.attach(None, move |date| {
let date_string = format!("{}", date.format(&format));
label.set_label(&date_string);
Continue(true)
});
}
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
context.widget_rx.attach(None, move |date| {
let date_string = format!("{}", date.format_localized(&format, locale));
label.set_label(&date_string);
Continue(true)
});
Ok(ModuleWidget {
widget: button,
popup,
})
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@@ -100,22 +136,22 @@ impl Module<Button> for ClockModule {
let container = gtk::Box::new(Orientation::Vertical, 0);
let clock = Label::builder().halign(Align::Center).build();
add_class(&clock, "calendar-clock");
let format = "%H:%M:%S";
clock.add_class("calendar-clock");
container.add(&clock);
let calendar = Calendar::new();
add_class(&calendar, "calendar");
calendar.add_class("calendar");
container.add(&calendar);
{
rx.attach(None, move |date| {
let date_string = format!("{}", date.format(format));
clock.set_label(&date_string);
Continue(true)
});
}
let format = self.format_popup;
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
rx.attach(None, move |date| {
let date_string = format!("{}", date.format_localized(&format, locale));
clock.set_label(&date_string);
Continue(true)
});
container.show_all();

View File

@@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
if let Some(widgets) = self.widgets {
for widget in widgets {
widget.widget.add_to(&container, context, widget.common);
widget.widget.add_to(&container, &context, widget.common);
}
}

View File

@@ -1,11 +1,13 @@
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use crate::dynamic_string::DynamicString;
use crate::popup::Popup;
use crate::{build, try_send};
use gtk::prelude::*;
use gtk::{Button, Label};
use serde::Deserialize;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
name: Option<String>,
@@ -19,20 +21,20 @@ impl CustomWidget for ButtonWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
button.add(&label);
DynamicString::new(&text, move |string| {
dynamic_string(&text, move |string| {
label.set_markup(&string);
Continue(true)
});
}
if let Some(exec) = self.on_click {
let bar_orientation = context.bar_orientation;
let tx = context.tx.clone();
button.connect_clicked(move |button| {
@@ -41,7 +43,7 @@ impl CustomWidget for ButtonWidget {
ExecEvent {
cmd: exec.clone(),
args: None,
geometry: Popup::widget_geometry(button, bar_orientation),
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
}
);
});

View File

@@ -1,11 +1,12 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_string::DynamicString;
use crate::image::ImageProvider;
use gtk::prelude::*;
use gtk::Image;
use serde::Deserialize;
use tracing::error;
use crate::build;
use crate::dynamic_value::dynamic_string;
use crate::image::ImageProvider;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ImageWidget {
@@ -30,13 +31,9 @@ impl CustomWidget for ImageWidget {
let gtk_image = gtk_image.clone();
let icon_theme = context.icon_theme.clone();
DynamicString::new(&self.src, move |src| {
let res = ImageProvider::parse(&src, &icon_theme, self.size)
.and_then(|image| image.load_into_image(gtk_image.clone()));
if let Err(err) = res {
error!("{err:?}");
}
dynamic_string(&self.src, move |src| {
ImageProvider::parse(&src, &icon_theme, false, self.size)
.map(|image| image.load_into_image(gtk_image.clone()));
Continue(true)
});

View File

@@ -1,10 +1,12 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_string::DynamicString;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget {
name: Option<String>,
@@ -22,7 +24,7 @@ impl CustomWidget for LabelWidget {
{
let label = label.clone();
DynamicString::new(&self.label, move |string| {
dynamic_string(&self.label, move |string| {
label.set_markup(&string);
Continue(true)
});

View File

@@ -13,15 +13,16 @@ use crate::config::CommonConfig;
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::popup::WidgetGeometry;
use crate::script::Script;
use crate::send_async;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use gtk::{Button, IconTheme, Orientation};
use serde::Deserialize;
use std::cell::RefCell;
use std::rc::Rc;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
@@ -56,11 +57,12 @@ pub enum Widget {
Progress(ProgressWidget),
}
#[derive(Clone, Copy)]
#[derive(Clone)]
struct CustomWidgetContext<'a> {
tx: &'a Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
}
trait CustomWidget {
@@ -115,11 +117,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, context: CustomWidgetContext, common: CommonConfig) {
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) {
macro_rules! create {
($widget:expr) => {
wrap_widget(
&$widget.into_widget(context),
&$widget.into_widget(context.clone()),
common,
context.bar_orientation,
)
@@ -143,7 +145,7 @@ impl Widget {
pub struct ExecEvent {
cmd: String,
args: Option<Vec<String>>,
geometry: WidgetGeometry,
id: usize,
}
impl Module<gtk::Box> for CustomModule {
@@ -173,9 +175,9 @@ impl Module<gtk::Box> for CustomModule {
error!("{err:?}");
}
} else if event.cmd == "popup:toggle" {
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
} else if event.cmd == "popup:open" {
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
} else if event.cmd == "popup:close" {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
} else {
@@ -191,25 +193,30 @@ impl Module<gtk::Box> for CustomModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build();
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext {
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
};
self.bar.clone().into_iter().for_each(|widget| {
widget
.widget
.add_to(&container, custom_context, widget.common);
.add_to(&container, &custom_context, widget.common);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup,
})
@@ -231,12 +238,13 @@ impl Module<gtk::Box> for CustomModule {
tx: &tx,
bar_orientation: info.bar_position.get_orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
};
for widget in popup {
widget
.widget
.add_to(&container, custom_context, widget.common);
.add_to(&container, &custom_context, widget.common);
}
}

View File

@@ -1,14 +1,16 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use crate::dynamic_string::DynamicString;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use gtk::prelude::*;
use gtk::ProgressBar;
use serde::Deserialize;
use tokio::spawn;
use tracing::error;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
name: Option<String>,
@@ -69,7 +71,7 @@ impl CustomWidget for ProgressWidget {
let progress = progress.clone();
progress.set_show_text(true);
DynamicString::new(&text, move |string| {
dynamic_string(&text, move |string| {
progress.set_text(Some(&string));
Continue(true)
});

View File

@@ -1,16 +1,18 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use crate::modules::custom::set_length;
use crate::popup::Popup;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use std::cell::Cell;
use std::ops::Neg;
use gtk::prelude::*;
use gtk::Scale;
use serde::Deserialize;
use std::cell::Cell;
use std::ops::Neg;
use tokio::spawn;
use tracing::error;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
name: Option<String>,
@@ -78,7 +80,7 @@ impl CustomWidget for SliderWidget {
Inhibit(false)
});
scale.connect_change_value(move |scale, _, val| {
scale.connect_change_value(move |_, _, val| {
// GTK will send values outside min/max range
let val = val.clamp(min, max);
@@ -88,7 +90,7 @@ impl CustomWidget for SliderWidget {
ExecEvent {
cmd: on_change.clone(),
args: Some(vec![val.to_string()]),
geometry: Popup::widget_geometry(scale, context.bar_orientation),
id: usize::MAX // ignored
}
);

View File

@@ -1,9 +1,9 @@
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::add_class;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{lock, send_async, try_send};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
@@ -11,7 +11,7 @@ use gtk::Label;
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
use tracing::debug;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
@@ -32,6 +32,18 @@ pub struct FocusedModule {
pub common: Option<CommonConfig>,
}
impl Default for FocusedModule {
fn default() -> Self {
Self {
show_icon: crate::config::default_true(),
show_title: crate::config::default_true(),
icon_size: default_icon_size(),
truncate: None,
common: Some(CommonConfig::default()),
}
}
}
const fn default_icon_size() -> i32 {
32
}
@@ -52,7 +64,8 @@ impl Module<gtk::Box> for FocusedModule {
) -> Result<()> {
spawn(async move {
let (mut wlrx, handles) = {
let wl = wayland::get_client().await;
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_toplevels()
};
@@ -91,32 +104,35 @@ impl Module<gtk::Box> for FocusedModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::new();
add_class(&icon, "icon");
if self.show_icon {
icon.add_class("icon");
container.add(&icon);
}
let label = Label::new(None);
add_class(&label, "label");
label.add_class("label");
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
container.add(&icon);
container.add(&label);
{
let icon_theme = icon_theme.clone();
context.widget_rx.attach(None, move |(name, id)| {
if self.show_icon {
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
.and_then(|image| image.load_into_image(icon.clone()))
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
.map(|image| image.load_into_image(icon.clone()))
{
error!("{err:?}");
Some(Ok(_)) => icon.show(),
_ => icon.hide(),
}
}
@@ -128,7 +144,7 @@ impl Module<gtk::Box> for FocusedModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@@ -1,6 +1,6 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::try_send;
use color_eyre::Result;
use glib::Continue;
@@ -17,6 +17,15 @@ pub struct LabelModule {
pub common: Option<CommonConfig>,
}
impl LabelModule {
pub(crate) fn new(label: String) -> Self {
Self {
label,
common: Some(CommonConfig::default()),
}
}
}
impl Module<Label> for LabelModule {
type SendMessage = String;
type ReceiveMessage = ();
@@ -31,7 +40,7 @@ impl Module<Label> for LabelModule {
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
DynamicString::new(&self.label, move |string| {
dynamic_string(&self.label, move |string| {
try_send!(tx, ModuleUpdateEvent::Update(string));
Continue(true)
});
@@ -43,18 +52,19 @@ impl Module<Label> for LabelModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleWidget<Label>> {
) -> Result<ModuleParts<Label>> {
let label = Label::new(None);
label.set_use_markup(true);
{
let label = label.clone();
context.widget_rx.attach(None, move |string| {
label.set_label(&string);
label.set_markup(&string);
Continue(true)
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: label,
popup: None,
})

View File

@@ -1,13 +1,14 @@
use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle;
use crate::config::BarPosition;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use gtk::{Button, IconTheme};
use indexmap::IndexMap;
use std::rc::Rc;
use std::sync::RwLock;
@@ -176,7 +177,7 @@ impl ItemButton {
item: &Item,
appearance: AppearanceOptions,
icon_theme: &IconTheme,
orientation: Orientation,
bar_position: BarPosition,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>,
) -> Self {
@@ -191,17 +192,14 @@ impl ItemButton {
if appearance.show_icons {
let gtk_image = gtk::Image::new();
let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
match image {
Ok(image) => {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
if let Some(image) = image {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
if let Err(err) = image.load_into_image(gtk_image) {
error!("{err:?}");
}
if let Err(err) = image.load_into_image(gtk_image) {
error!("{err:?}");
}
Err(err) => error!("{err:?}"),
};
}
@@ -252,7 +250,9 @@ impl ItemButton {
try_send!(
tx,
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::OpenPopupAt(
button.geometry(bar_position.get_orientation())
)
);
} else {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
@@ -262,6 +262,31 @@ impl ItemButton {
});
}
{
let tx = tx.clone();
button.connect_leave_notify_event(move |button, ev| {
const THRESHOLD: f64 = 5.0;
let alloc = button.allocation();
let (x, y) = ev.position();
let close = match bar_position {
BarPosition::Top => y + THRESHOLD < alloc.height() as f64,
BarPosition::Bottom => y > THRESHOLD,
BarPosition::Left => x + THRESHOLD < alloc.width() as f64,
BarPosition::Right => x > THRESHOLD,
};
if close {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Inhibit(false)
});
}
button.show_all();
Self {

View File

@@ -7,8 +7,10 @@ use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, send_async, try_send, write_lock};
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{arc_mut, lock, send_async, try_send, write_lock};
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::*;
@@ -16,7 +18,7 @@ use gtk::{Button, Orientation};
use indexmap::IndexMap;
use serde::Deserialize;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, trace};
@@ -108,7 +110,7 @@ impl Module<gtk::Box> for LauncherModule {
.collect::<IndexMap<_, _>>()
});
let items = Arc::new(Mutex::new(items));
let items = arc_mut!(items);
let items2 = Arc::clone(&items);
let tx2 = tx.clone();
@@ -117,7 +119,8 @@ impl Module<gtk::Box> for LauncherModule {
let tx = tx2;
let (mut wlrx, handles) = {
let wl = wayland::get_client().await;
let wl = wayland::get_client();
let wl = lock!(wl);
wl.subscribe_toplevels()
};
@@ -162,6 +165,7 @@ impl Module<gtk::Box> for LauncherModule {
match item {
None => {
let item: Item = handle.try_into()?;
items.insert(info.app_id.clone(), item.clone());
ItemOrWindow::Item(item)
@@ -270,14 +274,18 @@ impl Module<gtk::Box> for LauncherModule {
} else {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
let wl = wayland::get_client().await;
let wl = wayland::get_client();
let items = lock!(items);
let id = match event {
ItemEvent::FocusItem(app_id) => items
.get(&app_id)
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
ItemEvent::FocusWindow(id) => Some(id), // FIXME: Broken on wlroots-git
ItemEvent::FocusItem(app_id) => items.get(&app_id).and_then(|item| {
item.windows
.iter()
.find(|(_, win)| !win.open_state.is_focused())
.or_else(|| item.windows.first())
.map(|(_, win)| win.id)
}),
ItemEvent::FocusWindow(id) => Some(id),
ItemEvent::OpenItem(_) => unreachable!(),
};
@@ -285,13 +293,18 @@ impl Module<gtk::Box> for LauncherModule {
if let Some(window) =
items.iter().find_map(|(_, item)| item.windows.get(&id))
{
let seat = wl.get_seats().pop().expect("Failed to get Wayland seat");
debug!("Focusing window {id}: {}", window.name);
let seat = lock!(wl)
.get_seats()
.pop()
.expect("Failed to get Wayland seat");
window.focus(&seat);
}
}
// roundtrip to immediately send activate event
wl.roundtrip();
lock!(wl).roundtrip();
}
}
});
@@ -303,7 +316,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> crate::Result<ModuleWidget<gtk::Box>> {
) -> crate::Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
@@ -321,7 +334,7 @@ impl Module<gtk::Box> for LauncherModule {
};
let show_names = self.show_names;
let orientation = info.bar_position.get_orientation();
let bar_position = info.bar_position;
let mut buttons = IndexMap::<String, ItemButton>::new();
@@ -337,7 +350,7 @@ impl Module<gtk::Box> for LauncherModule {
&item,
appearance_options,
&icon_theme,
orientation,
bar_position,
&context.tx,
&controller_tx,
);
@@ -346,9 +359,10 @@ impl Module<gtk::Box> for LauncherModule {
buttons.insert(item.app_id, button);
}
}
LauncherUpdate::AddWindow(app_id, _) => {
LauncherUpdate::AddWindow(app_id, win) => {
if let Some(button) = buttons.get(&app_id) {
button.set_open(true);
button.set_focused(win.open_state.is_focused());
let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows += 1;
@@ -369,8 +383,12 @@ impl Module<gtk::Box> for LauncherModule {
}
}
}
LauncherUpdate::RemoveWindow(app_id, _) => {
LauncherUpdate::RemoveWindow(app_id, win_id) => {
debug!("Removing window {win_id} with id {app_id}");
if let Some(button) = buttons.get(&app_id) {
button.set_focused(false);
let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows -= 1;
}
@@ -398,8 +416,11 @@ impl Module<gtk::Box> for LauncherModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
widget: container,
popup,
})
@@ -466,12 +487,8 @@ impl Module<gtk::Box> for LauncherModule {
{
let tx = controller_tx.clone();
button.connect_clicked(move |button| {
button.connect_clicked(move |_button| {
try_send!(tx, ItemEvent::FocusWindow(win.id));
if let Some(win) = button.window() {
win.hide();
}
});
}

View File

@@ -1,3 +1,20 @@
use std::cell::RefCell;
use std::rc::Rc;
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
use tokio::sync::mpsc;
use tracing::debug;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::send;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
@@ -24,19 +41,6 @@ pub mod upower;
#[cfg(feature = "workspaces")]
pub mod workspaces;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::popup::{Popup, WidgetGeometry};
use crate::{read_lock, send, write_lock};
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Clone)]
pub enum ModuleLocation {
Left,
@@ -54,13 +58,15 @@ pub struct ModuleInfo<'a> {
#[derive(Debug)]
pub enum ModuleUpdateEvent<T> {
/// Sends an update to the module UI
/// Sends an update to the module UI.
Update(T),
/// Toggles the open state of the popup.
TogglePopup(WidgetGeometry),
/// Takes the button ID.
TogglePopup(usize),
/// Force sets the popup open.
/// Takes the button X position and width.
OpenPopup(WidgetGeometry),
/// Takes the button ID.
OpenPopup(usize),
OpenPopupAt(WidgetGeometry),
/// Force sets the popup closed.
ClosePopup,
}
@@ -73,9 +79,62 @@ pub struct WidgetContext<TSend, TReceive> {
pub popup_rx: glib::Receiver<TSend>,
}
pub struct ModuleWidget<W: IsA<Widget>> {
pub struct ModuleParts<W: IsA<Widget>> {
pub widget: W,
pub popup: Option<gtk::Box>,
pub popup: Option<ModulePopupParts>,
}
impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
}
#[derive(Debug, Clone)]
pub struct ModulePopupParts {
/// The popup container, with all its contents
pub container: gtk::Box,
/// An array of buttons which can be used for opening the popup.
/// For most modules, this will only be a single button.
/// For some advanced modules, such as `Launcher`, this is all item buttons.
pub buttons: Vec<Button>,
}
pub trait ModulePopup {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
}
impl ModulePopup for Option<gtk::Box> {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
}
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
self.map(|container| ModulePopupParts { container, buttons })
}
}
pub trait PopupButton {
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
self.get_tag("popup-id").copied()
}
/// Gets the popup ID associated with this button.
/// This should only be called on buttons which are known to be associated with popups.
///
/// # Panics
/// Will panic if an ID has not been set.
fn popup_id(&self) -> usize {
self.try_popup_id().expect("id to exist")
}
}
pub trait Module<W>
@@ -98,7 +157,7 @@ where
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<W>>;
) -> Result<ModuleParts<W>>;
fn into_popup(
self,
@@ -118,9 +177,10 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
name: Option<String>,
info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleWidget<TWidget>>
popup: &Rc<RefCell<Popup>>,
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
@@ -142,29 +202,46 @@ where
controller_tx: ui_tx,
};
let name = TModule::name();
let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
let module_parts = module.into_widget(context, info)?;
module_parts.widget.style_context().add_class(name);
module_parts.widget.add_class("widget");
module_parts.widget.add_class(module_name);
let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() {
let has_popup = if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{name}"));
.add_class(&format!("popup-{module_name}"));
register_popup_content(popup, id, popup_content);
has_popup = true;
}
register_popup_content(popup, id, instance_name, popup_content);
true
} else {
false
};
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
setup_receiver(
channel,
w_tx,
p_tx,
popup.clone(),
module_name,
id,
has_popup,
);
Ok(module_parts)
}
/// Registers the popup content with the popup.
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
write_lock!(popup).register_content(id, popup_content);
fn register_popup_content(
popup: &Rc<RefCell<Popup>>,
id: usize,
name: String,
popup_content: ModulePopupParts,
) {
popup.borrow_mut().register_content(id, name, popup_content);
}
/// Sets up the bridge channel receiver
@@ -176,7 +253,7 @@ fn setup_receiver<TSend>(
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
w_tx: glib::Sender<TSend>,
p_tx: glib::Sender<TSend>,
popup: Arc<RwLock<Popup>>,
popup: Rc<RefCell<Popup>>,
name: &'static str,
id: usize,
has_popup: bool,
@@ -196,40 +273,51 @@ fn setup_receiver<TSend>(
send!(w_tx, update);
}
ModuleUpdateEvent::TogglePopup(geometry) => {
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
} else {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = popup.borrow_mut();
popup.hide();
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
has_popup_opened = true;
}
}
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_at(id, geometry);
has_popup_opened = true;
}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = popup.borrow_mut();
popup.hide();
}
}
@@ -239,14 +327,14 @@ fn setup_receiver<TSend>(
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleWidget<TWidget>,
widget_parts: &ModuleParts<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup {
popup.set_widget_name(&format!("popup-{name}"));
popup.container.set_widget_name(&format!("popup-{name}"));
}
}
@@ -258,7 +346,10 @@ pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup.style_context().add_class(&format!("popup-{part}"));
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
}
}
@@ -286,6 +377,8 @@ pub fn wrap_widget<W: IsA<Widget>>(
revealer.set_reveal_child(true);
let container = EventBox::new();
container.add_class("widget-container");
container.add_events(EventMask::SCROLL_MASK);
container.add(&revealer);

View File

@@ -121,27 +121,27 @@ fn default_icon_pause() -> String {
}
fn default_icon_prev() -> String {
String::from("\u{f9ad}")
String::from("󰒮")
}
fn default_icon_next() -> String {
String::from("\u{f9ac}")
String::from("󰒭")
}
fn default_icon_volume() -> String {
String::from("")
String::from("󰕾")
}
fn default_icon_track() -> String {
String::from("\u{f886}")
String::from("󰎈")
}
fn default_icon_album() -> String {
String::from("\u{f524}")
String::from("󰀥")
}
fn default_icon_artist() -> String {
String::from("\u{fd01}")
String::from("󰠃")
}
fn default_music_dir() -> PathBuf {

View File

@@ -1,26 +1,33 @@
mod config;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::gtk_helpers::add_class;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use color_eyre::Result;
use glib::Continue;
use glib::{Continue, PropertySet};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Label, Orientation, Scale};
use regex::Regex;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{send_async, try_send};
pub use self::config::MusicModule;
use self::config::PlayerType;
mod config;
#[derive(Debug)]
pub enum PlayerCommand {
Previous,
@@ -28,6 +35,7 @@ pub enum PlayerCommand {
Pause,
Next,
Volume(u8),
Seek(Duration),
}
/// Formats a duration given in seconds
@@ -47,6 +55,12 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
.collect::<Vec<_>>()
}
#[derive(Clone, Debug)]
pub enum ControllerEvent {
Update(Option<SongUpdate>),
UpdateProgress(ProgressTick),
}
#[derive(Clone, Debug)]
pub struct SongUpdate {
song: Track,
@@ -67,7 +81,7 @@ async fn get_client(
}
impl Module<Button> for MusicModule {
type SendMessage = Option<SongUpdate>;
type SendMessage = ControllerEvent;
type ReceiveMessage = PlayerCommand;
fn name() -> &'static str {
@@ -103,7 +117,7 @@ impl Module<Button> for MusicModule {
PlayerUpdate::Update(track, status) => match *track {
Some(track) => {
let display_string =
replace_tokens(format.as_str(), &tokens, &track, &status);
replace_tokens(format.as_str(), &tokens, &track);
let update = SongUpdate {
song: track,
@@ -111,10 +125,24 @@ impl Module<Button> for MusicModule {
display_string,
};
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
send_async!(
tx,
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
update
)))
);
}
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
None => send_async!(
tx,
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
),
},
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
tx,
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
progress_tick
))
),
PlayerUpdate::Disconnect => break,
}
}
@@ -137,6 +165,7 @@ impl Module<Button> for MusicModule {
PlayerCommand::Pause => client.pause(),
PlayerCommand::Next => client.next(),
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
PlayerCommand::Seek(duration) => client.seek(duration),
};
if let Err(err) = res {
@@ -153,10 +182,10 @@ impl Module<Button> for MusicModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&button_contents, "contents");
button_contents.add_class("contents");
button.add(&button_contents);
@@ -174,16 +203,11 @@ impl Module<Button> for MusicModule {
button_contents.add(&icon_play);
button_contents.add(&label);
let orientation = info.bar_position.get_orientation();
{
let tx = context.tx.clone();
button.connect_clicked(move |button| {
try_send!(
tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
);
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
}
@@ -191,7 +215,11 @@ impl Module<Button> for MusicModule {
let button = button.clone();
let tx = context.tx.clone();
context.widget_rx.attach(None, move |mut event| {
context.widget_rx.attach(None, move |event| {
let ControllerEvent::Update(mut event) = event else {
return Continue(true);
};
if let Some(event) = event.take() {
label.set_label(&event.display_string);
@@ -225,12 +253,11 @@ impl Module<Button> for MusicModule {
});
};
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleWidget {
widget: button,
popup,
})
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@@ -241,13 +268,14 @@ impl Module<Button> for MusicModule {
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(Orientation::Horizontal, 10);
let container = gtk::Box::new(Orientation::Vertical, 10);
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
let album_image = gtk::Image::builder()
.width_request(128)
.height_request(128)
.build();
add_class(&album_image, "album-art");
album_image.add_class("album-art");
let icons = self.icons;
@@ -256,28 +284,28 @@ impl Module<Button> for MusicModule {
let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
add_class(&title_label.container, "title");
add_class(&album_label.container, "album");
add_class(&artist_label.container, "artist");
title_label.container.add_class("title");
album_label.container.add_class("album");
artist_label.container.add_class("artist");
info_box.add(&title_label.container);
info_box.add(&album_label.container);
info_box.add(&artist_label.container);
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
add_class(&controls_box, "controls");
controls_box.add_class("controls");
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
add_class(&btn_prev, "btn-prev");
btn_prev.add_class("btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
add_class(&btn_play, "btn-play");
btn_play.add_class("btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
add_class(&btn_pause, "btn-pause");
btn_pause.add_class("btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
add_class(&btn_next, "btn-next");
btn_next.add_class("btn-next");
controls_box.add(&btn_prev);
controls_box.add(&btn_play);
@@ -287,21 +315,22 @@ impl Module<Button> for MusicModule {
info_box.add(&controls_box);
let volume_box = gtk::Box::new(Orientation::Vertical, 5);
add_class(&volume_box, "volume");
volume_box.add_class("volume");
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true);
add_class(&volume_slider, "slider");
volume_slider.add_class("slider");
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
add_class(&volume_icon, "icon");
volume_icon.add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0);
container.add(&album_image);
container.add(&info_box);
container.add(&volume_box);
main_container.add(&album_image);
main_container.add(&info_box);
main_container.add(&volume_box);
container.add(&main_container);
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
@@ -323,12 +352,49 @@ impl Module<Button> for MusicModule {
try_send!(tx_next, PlayerCommand::Next);
});
let tx_vol = tx;
let tx_vol = tx.clone();
volume_slider.connect_change_value(move |_, _, val| {
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
Inhibit(false)
});
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
progress_box.add_class("progress");
let progress_label = Label::new(None);
progress_label.add_class("label");
let progress = Scale::builder()
.orientation(Orientation::Horizontal)
.draw_value(false)
.hexpand(true)
.build();
progress.add_class("slider");
progress_box.add(&progress);
progress_box.add(&progress_label);
container.add(&progress_box);
let drag_lock = Arc::new(AtomicBool::new(false));
{
let drag_lock = drag_lock.clone();
progress.connect_button_press_event(move |_, _| {
drag_lock.set(true);
Inhibit(false)
});
}
{
let drag_lock = drag_lock.clone();
progress.connect_button_release_event(move |scale, _| {
let value = scale.value();
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
drag_lock.set(false);
Inhibit(false)
});
}
container.show_all();
{
@@ -336,72 +402,91 @@ impl Module<Button> for MusicModule {
let image_size = self.cover_image_size;
let mut prev_cover = None;
rx.attach(None, move |update| {
if let Some(update) = update {
// only update art when album changes
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
let res = match new_cover.map(|cover_path| {
ImageProvider::parse(&cover_path, &icon_theme, image_size)
}) {
Some(Ok(image)) => image.load_into_image(album_image.clone()),
Some(Err(err)) => {
album_image.set_from_pixbuf(None);
Err(err)
}
None => {
rx.attach(None, move |event| {
match event {
ControllerEvent::Update(Some(update)) => {
// only update art when album changes
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
let res = if let Some(image) = new_cover.and_then(|cover_path| {
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
}) {
album_image.show();
image.load_into_image(album_image.clone())
} else {
album_image.set_from_pixbuf(None);
album_image.hide();
Ok(())
};
if let Err(err) = res {
error!("{err:?}");
}
};
if let Err(err) = res {
error!("{err:?}");
}
update_popup_metadata_label(update.song.title, &title_label);
update_popup_metadata_label(update.song.album, &album_label);
update_popup_metadata_label(update.song.artist, &artist_label);
match update.status.state {
PlayerState::Stopped => {
btn_pause.hide();
btn_play.show();
btn_play.set_sensitive(false);
}
PlayerState::Playing => {
btn_play.set_sensitive(false);
btn_play.hide();
btn_pause.set_sensitive(true);
btn_pause.show();
}
PlayerState::Paused => {
btn_pause.set_sensitive(false);
btn_pause.hide();
btn_play.set_sensitive(true);
btn_play.show();
}
}
let enable_prev = update.status.playlist_position > 0;
let enable_next =
update.status.playlist_position < update.status.playlist_length;
btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next);
if let Some(volume) = update.status.volume_percent {
volume_slider.set_value(volume as f64);
volume_box.show();
} else {
volume_box.hide();
}
}
ControllerEvent::UpdateProgress(progress_tick)
if !drag_lock.load(Ordering::Relaxed) =>
{
if let (Some(elapsed), Some(duration)) =
(progress_tick.elapsed, progress_tick.duration)
{
progress_label.set_label(&format!(
"{}/{}",
format_time(elapsed),
format_time(duration)
));
title_label
.label
.set_text(&update.song.title.unwrap_or_default());
album_label
.label
.set_text(&update.song.album.unwrap_or_default());
artist_label
.label
.set_text(&update.song.artist.unwrap_or_default());
match update.status.state {
PlayerState::Stopped => {
btn_pause.hide();
btn_play.show();
btn_play.set_sensitive(false);
}
PlayerState::Playing => {
btn_play.set_sensitive(false);
btn_play.hide();
btn_pause.set_sensitive(true);
btn_pause.show();
}
PlayerState::Paused => {
btn_pause.set_sensitive(false);
btn_pause.hide();
btn_play.set_sensitive(true);
btn_play.show();
progress.set_value(elapsed.as_secs_f64());
progress.set_range(0.0, duration.as_secs_f64());
progress_box.show_all();
} else {
progress_box.hide();
}
}
let enable_prev = update.status.playlist_position > 0;
let enable_next =
update.status.playlist_position < update.status.playlist_length;
btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next);
volume_slider.set_value(update.status.volume_percent as f64);
}
_ => {}
};
Continue(true)
});
@@ -411,17 +496,24 @@ impl Module<Button> for MusicModule {
}
}
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
match text {
Some(value) => {
label.label.set_text(&value);
label.container.show_all();
}
None => {
label.container.hide();
}
}
}
/// Replaces each of the formatting tokens in the formatting string
/// with actual data pulled from the music player
fn replace_tokens(
format_string: &str,
tokens: &Vec<String>,
song: &Track,
status: &Status,
) -> String {
fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> String {
let mut compiled_string = format_string.to_string();
for token in tokens {
let value = get_token_value(song, status, token);
let value = get_token_value(song, token);
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
}
compiled_string
@@ -429,7 +521,7 @@ fn replace_tokens(
/// Converts a string format token value
/// into its respective value.
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
fn get_token_value(song: &Track, token: &str) -> String {
match token {
"title" => song.title.clone(),
"album" => song.album.clone(),
@@ -438,8 +530,6 @@ fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
"disc" => song.disc.map(|x| x.to_string()),
"genre" => song.genre.clone(),
"track" => song.track.map(|x| x.to_string()),
"duration" => status.duration.map(format_time),
"elapsed" => status.elapsed.map(format_time),
_ => Some(token.to_string()),
}
.unwrap_or_default()
@@ -458,8 +548,8 @@ impl IconLabel {
let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label);
add_class(&icon, "icon");
add_class(&label, "label");
icon.add_class("icon-box");
label.add_class("label");
container.add(&icon);
container.add(&label);

View File

@@ -1,5 +1,5 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::try_send;
use color_eyre::{Help, Report, Result};
@@ -83,7 +83,7 @@ impl Module<Label> for ScriptModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Label>> {
) -> Result<ModuleParts<Label>> {
let label = Label::builder().use_markup(true).build();
label.set_angle(info.bar_position.get_angle());
@@ -95,7 +95,7 @@ impl Module<Label> for ScriptModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: label,
popup: None,
})

View File

@@ -1,6 +1,6 @@
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::send_async;
use color_eyre::Result;
use gtk::prelude::*;
@@ -186,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
@@ -196,7 +196,7 @@ impl Module<gtk::Box> for SysInfoModule {
for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build();
add_class(&label, "item");
label.add_class("item");
label.set_angle(info.bar_position.get_angle());
container.add(&label);
@@ -220,7 +220,7 @@ impl Module<gtk::Box> for SysInfoModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@@ -1,6 +1,6 @@
use crate::clients::system_tray::get_tray_event_client;
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{await_sync, try_send};
use color_eyre::Result;
use gtk::gdk_pixbuf::{Colorspace, InterpType};
@@ -11,9 +11,9 @@ use gtk::{
};
use serde::Deserialize;
use std::collections::HashMap;
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
use stray::message::tray::StatusNotifierItem;
use stray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType};
use system_tray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
@@ -172,7 +172,7 @@ impl Module<MenuBar> for TrayModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleWidget<MenuBar>> {
) -> Result<ModuleParts<MenuBar>> {
let container = MenuBar::new();
{
@@ -238,7 +238,7 @@ impl Module<MenuBar> for TrayModule {
});
};
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@@ -1,10 +1,3 @@
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{await_sync, error, send_async, try_send};
use color_eyre::Result;
use futures_lite::stream::StreamExt;
use gtk::{prelude::*, Button};
@@ -15,6 +8,16 @@ use tokio::sync::mpsc::{Receiver, Sender};
use upower_dbus::BatteryState;
use zbus;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{await_sync, error, send_async, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
const MINUTE: i64 = 60;
@@ -24,6 +27,9 @@ pub struct UpowerModule {
#[serde(default = "default_format")]
format: String,
#[serde(default = "default_icon_size")]
icon_size: i32,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@@ -32,6 +38,10 @@ fn default_format() -> String {
String::from("{percentage}%")
}
const fn default_icon_size() -> i32 {
24
}
#[derive(Clone, Debug)]
pub struct UpowerProperties {
percentage: f64,
@@ -41,7 +51,7 @@ pub struct UpowerProperties {
time_to_empty: i64,
}
impl Module<gtk::Box> for UpowerModule {
impl Module<gtk::Button> for UpowerModule {
type SendMessage = UpowerProperties;
type ReceiveMessage = ();
@@ -143,32 +153,31 @@ impl Module<gtk::Box> for UpowerModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<Button>> {
let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new();
add_class(&icon, "icon");
icon.add_class("icon");
let label = Label::builder()
.label(&self.format)
.use_markup(true)
.build();
add_class(&label, "label");
label.add_class("label");
let container = gtk::Box::new(Orientation::Horizontal, 0);
add_class(&container, "upower");
let container = gtk::Box::new(Orientation::Horizontal, 5);
container.add_class("contents");
let button = Button::new();
add_class(&button, "button");
button.add_class("button");
button.add(&label);
container.add(&button);
container.add(&icon);
container.add(&label);
button.add(&container);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
@@ -180,21 +189,17 @@ impl Module<gtk::Box> for UpowerModule {
.attach(None, move |properties: UpowerProperties| {
let format = format.replace("{percentage}", &properties.percentage.to_string());
let icon_name = String::from("icon:") + &properties.icon_name;
if let Err(err) = ImageProvider::parse(&icon_name, &icon_theme, 32)
.and_then(|provider| provider.load_into_image(icon.clone()))
{
error!("{err:?}");
}
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone()));
label.set_markup(format.as_ref());
Continue(true)
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleWidget {
widget: container,
popup,
})
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@@ -211,29 +216,36 @@ impl Module<gtk::Box> for UpowerModule {
.build();
let label = Label::new(None);
add_class(&label, "upower-details");
label.add_class("upower-details");
container.add(&label);
rx.attach(None, move |properties| {
let mut format = String::new();
let state = u32_to_battery_state(properties.state);
match state {
let format = match state {
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
let ttf = properties.time_to_full;
if ttf > 0 {
format = format!("Full in {}", seconds_to_string(ttf));
format!("Full in {}", seconds_to_string(ttf))
} else {
String::new()
}
}
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
let tte = properties.time_to_empty;
if tte > 0 {
format = format!("Empty in {}", seconds_to_string(tte));
format!("Empty in {}", seconds_to_string(tte))
} else {
String::new()
}
}
Err(state) => error!("Invalid battery state: {state}"),
_ => {}
}
label.set_markup(&format);
Err(state) => {
error!("Invalid battery state: {state}");
String::new()
}
_ => String::new(),
};
label.set_markup(&format);
Continue(true)
});

View File

@@ -1,14 +1,14 @@
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme};
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::trace;
@@ -29,11 +29,32 @@ impl Default for SortOrder {
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum Favorites {
ByMonitor(HashMap<String, Vec<String>>),
Global(Vec<String>),
}
impl Default for Favorites {
fn default() -> Self {
Self::Global(vec![])
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule {
/// Map of actual workspace names to custom names.
name_map: Option<HashMap<String, String>>,
/// Array of always shown workspaces, and what monitor to show on
#[serde(default)]
favorites: Favorites,
/// List of workspace names to never show
#[serde(default)]
hidden: Vec<String>,
/// Whether to display buttons for all monitors.
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
@@ -55,7 +76,7 @@ const fn default_icon_size() -> i32 {
/// Creates a button from a workspace
fn create_button(
name: &str,
focused: bool,
visibility: Visibility,
name_map: &HashMap<String, String>,
icon_theme: &IconTheme,
icon_size: i32,
@@ -69,7 +90,11 @@ fn create_button(
let style_context = button.style_context();
style_context.add_class("item");
if focused {
if visibility.is_visible() {
style_context.add_class("visible");
}
if visibility.is_focused() {
style_context.add_class("focused");
}
@@ -105,6 +130,13 @@ fn reorder_workspaces(container: &gtk::Box) {
}
}
impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
&& (self.all_monitors || output == &work.monitor)
}
}
impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
@@ -154,10 +186,12 @@ impl Module<gtk::Box> for WorkspacesModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let name_map = self.name_map.unwrap_or_default();
let name_map = self.name_map.clone().unwrap_or_default();
let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![];
let mut button_map: HashMap<String, Button> = HashMap::new();
@@ -176,19 +210,48 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Init(workspaces) => {
if !has_initialized {
trace!("Creating workspace buttons");
for workspace in workspaces {
if self.all_monitors || workspace.monitor == output_name {
let item = create_button(
&workspace.name,
workspace.focused,
&name_map,
&icon_theme,
icon_size,
&context.controller_tx,
);
container.add(&item);
button_map.insert(workspace.name, item);
let mut added = HashSet::new();
let mut add_workspace = |name: &str, visibility: Visibility| {
let item = create_button(
name,
visibility,
&name_map,
&icon_theme,
icon_size,
&context.controller_tx,
);
container.add(&item);
button_map.insert(name.to_string(), item);
};
// add workspaces from client
for workspace in &workspaces {
if self.show_workspace_check(&output_name, workspace) {
add_workspace(&workspace.name, workspace.visibility);
added.insert(workspace.name.to_string());
}
}
let mut add_favourites = |names: &Vec<String>| {
for name in names {
if !added.contains(name) {
add_workspace(name, Visibility::Hidden);
added.insert(name.to_string());
fav_names.push(name.to_string());
}
}
};
// add workspaces from favourites
match &favs {
Favorites::Global(names) => add_favourites(names),
Favorites::ByMonitor(map) => {
if let Some(to_add) = map.get(&output_name) {
add_favourites(to_add);
}
}
}
@@ -201,22 +264,33 @@ impl Module<gtk::Box> for WorkspacesModule {
}
}
WorkspaceUpdate::Focus { old, new } => {
let old = button_map.get(&old);
if let Some(old) = old {
old.style_context().remove_class("focused");
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) {
if Some(new.monitor) == old.map(|w| w.monitor) {
btn.style_context().remove_class("visible");
}
btn.style_context().remove_class("focused");
}
let new = button_map.get(&new);
if let Some(new) = new {
new.style_context().add_class("focused");
let new = button_map.get(&new.name);
if let Some(btn) = new {
let style = btn.style_context();
style.add_class("visible");
style.add_class("focused");
}
}
WorkspaceUpdate::Add(workspace) => {
if self.all_monitors || workspace.monitor == output_name {
if fav_names.contains(&workspace.name) {
let btn = button_map.get(&workspace.name);
if let Some(btn) = btn {
btn.style_context().remove_class("inactive");
}
} else if self.show_workspace_check(&output_name, &workspace) {
let name = workspace.name;
let item = create_button(
&name,
workspace.focused,
workspace.visibility,
&name_map,
&icon_theme,
icon_size,
@@ -236,12 +310,12 @@ impl Module<gtk::Box> for WorkspacesModule {
}
}
WorkspaceUpdate::Move(workspace) => {
if !self.all_monitors {
if !self.hidden.contains(&workspace.name) && !self.all_monitors {
if workspace.monitor == output_name {
let name = workspace.name;
let item = create_button(
&name,
workspace.focused,
workspace.visibility,
&name_map,
&icon_theme,
icon_size,
@@ -267,7 +341,11 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace);
if let Some(item) = button {
container.remove(item);
if fav_names.contains(&workspace) {
item.style_context().add_class("inactive");
} else {
container.remove(item);
}
}
}
WorkspaceUpdate::Update(_) => {}
@@ -277,7 +355,7 @@ impl Module<gtk::Box> for WorkspacesModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@@ -1,18 +1,22 @@
use std::collections::HashMap;
use crate::config::BarPosition;
use crate::modules::ModuleInfo;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation};
use tracing::debug;
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::unique_id::get_unique_usize;
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: HashMap<usize, gtk::Box>,
pub cache: HashMap<usize, (String, ModulePopupParts)>,
monitor: Monitor,
pos: BarPosition,
current_widget: Option<usize>,
}
impl Popup {
@@ -28,6 +32,7 @@ impl Popup {
.build();
gtk_layer_shell::init_for_window(&win);
gtk_layer_shell::set_monitor(&win, module_info.monitor);
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
@@ -108,20 +113,54 @@ impl Popup {
cache: HashMap::new(),
monitor: module_info.monitor.clone(),
pos,
current_widget: None,
}
}
pub fn register_content(&mut self, key: usize, content: gtk::Box) {
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
debug!("Registered popup content for #{}", key);
self.cache.insert(key, content);
for button in &content.buttons {
let id = get_unique_usize();
button.set_tag("popup-id", id);
}
self.cache.insert(key, (name, content));
}
pub fn show_content(&self, key: usize) {
pub fn show(&mut self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(content) = self.cache.get(&key) {
content.style_context().add_class("popup");
self.window.add(content);
if let Some((_name, content)) = self.cache.get(&widget_id) {
self.current_widget = Some(widget_id);
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some((_name, content)) = self.cache.get(&widget_id) {
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
self.set_pos(geometry);
}
}
@@ -132,14 +171,9 @@ impl Popup {
}
}
/// Shows the popup
pub fn show(&self, geometry: WidgetGeometry) {
self.window.show();
self.set_pos(geometry);
}
/// Hides the popover
pub fn hide(&self) {
pub fn hide(&mut self) {
self.current_widget = None;
self.window.hide();
}
@@ -148,6 +182,10 @@ impl Popup {
self.window.is_visible()
}
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
}
/// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation).
fn set_pos(&self, geometry: WidgetGeometry) {
@@ -187,48 +225,4 @@ impl Popup {
gtk_layer_shell::set_margin(&self.window, edge, offset as i32);
}
/// Gets the absolute X position of the button
/// and its width / height (depending on orientation).
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
where
W: IsA<gtk::Widget>,
{
let widget_size = if orientation == Orientation::Horizontal {
widget.allocation().width()
} else {
widget.allocation().height()
};
let top_level = widget.toplevel().expect("Failed to get top-level widget");
let bar_size = if orientation == Orientation::Horizontal {
top_level.allocation().width()
} else {
top_level.allocation().height()
};
let (widget_x, widget_y) = widget
.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,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct WidgetGeometry {
position: i32,
size: i32,
bar_size: i32,
}

View File

@@ -234,7 +234,7 @@ impl Script {
debug!("Running sh with args: {args_list:?}");
let output = Command::new("sh")
let output = Command::new("/bin/sh")
.args(&args_list)
.output()
.await
@@ -265,7 +265,7 @@ impl Script {
/// Returns a `mpsc::Receiver` that sends a message
/// every time a new line is written to `stdout` or `stderr`.
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
let mut handle = Command::new("sh")
let mut handle = Command::new("/bin/sh")
.args(["-c", &self.cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())

View File

@@ -1,6 +1,7 @@
use crate::send;
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext};
use notify::event::{DataChange, ModifyKind};
@@ -29,7 +30,11 @@ pub fn load_css(style_path: PathBuf) {
};
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(&screen, &provider, 800);
StyleContext::add_provider_for_screen(
&screen,
&provider,
GTK_STYLE_PROVIDER_PRIORITY_USER as u32,
);
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);

9
src/unique_id.rs Normal file
View File

@@ -0,0 +1,9 @@
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(1);
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
/// This is just an `AtomicUsize` that increments every time this function is called.
pub fn get_unique_usize() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}