63 Commits

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

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

* Fix readme syntax issue

* Format nix flake

* ci(build): add nix flake support

* ci(build): fix workflow_dispatch casing

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

* ci: add nix flake lock update timer job

old method didn't work

* improve example and add cachix info

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

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

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

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

View File

@@ -1,6 +1,7 @@
name: Build
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
@@ -24,8 +25,8 @@ jobs:
- name: Install build deps
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
- name: Build
run: cargo build --verbose
- name: Check formatting
run: cargo fmt --check
- name: Clippy
uses: actions-rs/clippy-check@v1
@@ -33,5 +34,32 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Check formatting
run: cargo fmt --check
- name: Build
run: cargo build --verbose
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
build-nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v17
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v11
with:
name: jakestanger
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
- run: nix build --print-build-logs

View File

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

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

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

View File

@@ -4,6 +4,69 @@ 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.7.0] - 2022-11-05
### :sparkles: New Features
- [`fad90fd`](https://github.com/JakeStanger/ironbar/commit/fad90fdad683a612497ac7822a66a90f43fce0a2) - **sys-info**: add loads more formatting tokens *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`dec402e`](https://github.com/JakeStanger/ironbar/commit/dec402edd9d6c5b8677ff337699ad99ebc69b776) - **sys-info**: config options for refresh intervals *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`91c57ed`](https://github.com/JakeStanger/ironbar/commit/91c57edc73f15397ea0de70c4a6a6532c35caf2a) - **sys-info**: pango markup support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ec1d596`](https://github.com/JakeStanger/ironbar/commit/ec1d59677b13c9654a98d78f909ba2d0fcfbb72d) - **logging**: `IRONBAR_LOG` and `IRONBAR_FILE_LOG` env vars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`493df6b`](https://github.com/JakeStanger/ironbar/commit/493df6bb49fec8c465706d3f9b395728ba73a621) - **mpd**: add volume slider to popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3750124`](https://github.com/JakeStanger/ironbar/commit/3750124d8cfb4783932a6b3359384f245fcd2394) - new custom module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b7792a4`](https://github.com/JakeStanger/ironbar/commit/b7792a415e09fc535750ea5af530f91aa791c4bc) - env var to set custom css location *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ad77dc4`](https://github.com/JakeStanger/ironbar/commit/ad77dc4e4c2f80fcb4c9604c796be0f981e895ee) - improved logging & error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`9e6dbbd`](https://github.com/JakeStanger/ironbar/commit/9e6dbbd131a09f101b0d490265fe7d4ec564e38c) - **sys-info**: tokens not replaced if more than one in string *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f17ae7a`](https://github.com/JakeStanger/ironbar/commit/f17ae7a415b931c64942de085e8889f37b3f9b11) - **script**: not parsing pango markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b66bd78`](https://github.com/JakeStanger/ironbar/commit/b66bd788b23256a2127a1352693fdd3f929d9c4b) - logging for creating bar incorrect still *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3c43c20`](https://github.com/JakeStanger/ironbar/commit/3c43c20c6ae53a9aa6b67770b0c489806784f4ac) - weird behaviour when config does not exist *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`70e1b52`](https://github.com/JakeStanger/ironbar/commit/70e1b526a9681b16545d7f05d77470d76bd8819e) - **logging**: file log not capturing panics *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cbd0c49`](https://github.com/JakeStanger/ironbar/commit/cbd0c49e251b5c8e0289ca6200a393d89994992d) - css watcher not working *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`e693c1c`](https://github.com/JakeStanger/ironbar/commit/e693c1c166eef0b5edcdcd033bb12d572e4e5f04) - **mpd**: volume slider causing mpd server errors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3a83bd3`](https://github.com/JakeStanger/ironbar/commit/3a83bd31ab165869f7f274b054b2f16485261fd1) - able to insert duplicate keys into collection *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`5ebc84c`](https://github.com/JakeStanger/ironbar/commit/5ebc84c7b98cc648a659ca37fdc0f041057f0ea4) - **logging**: consts for default log levels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bc625b9`](https://github.com/JakeStanger/ironbar/commit/bc625b929b8644ce92f275b5d98cdf74b93fe067) - clippy & fmt *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`9d9c275`](https://github.com/JakeStanger/ironbar/commit/9d9c2753137331ae85ac8ab7d75a6de9a9c82042) - update CHANGELOG.md for v0.6.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27d0479`](https://github.com/JakeStanger/ironbar/commit/27d04795af1c25fe5f765c7480d5dd5d096a8ab7) - **readme**: add warning about crate being outdated *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a06c4bc`](https://github.com/JakeStanger/ironbar/commit/a06c4bccca6cb51935605ac9239e63024fb7c663) - **examples**: add full system info config *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`0a331f3`](https://github.com/JakeStanger/ironbar/commit/0a331f31381f0d967793c0d8b7a14e2a43bf666f) - **readme**: remove warning about outdated cargo package *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.6.0] - 2022-10-15
### :sparkles: New Features
- [`b188bc7`](https://github.com/JakeStanger/ironbar/commit/b188bc714614406935d8bb88a719adab2dfce32f) - initial support for running outside sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`324f00c`](https://github.com/JakeStanger/ironbar/commit/324f00cdf9200e3e3ecedfa68ab4c99b170242e2) - wlroots-agnostic support for `focused` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b1c66b9`](https://github.com/JakeStanger/ironbar/commit/b1c66b9117cf8a10350cdb857a5267a1a72ad914) - wlroots-agnostic support for `launcher` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1dd0a9e`](https://github.com/JakeStanger/ironbar/commit/1dd0a9e52f69e672d9ac313c1da0e201c911e6c2) - **launcher**: add popup css selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`5523e9a`](https://github.com/JakeStanger/ironbar/commit/5523e9af46e457f9d45902debaaacf26b586e457) - **popup**: often opening in wrong place *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`8536ad7`](https://github.com/JakeStanger/ironbar/commit/8536ad719a92aec4166e35b75cb029075ad3ae34) - **mpd**: incorrectly checking for unix sockets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bd5bdf5`](https://github.com/JakeStanger/ironbar/commit/bd5bdf5af548304958663d593fccb454afa6c8ff) - logging for creating bar incorrect *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`75339f0`](https://github.com/JakeStanger/ironbar/commit/75339f07ed164fa94838036a604a1dcb6d53564c) - vertical bars ignoring height config option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b7b6488`](https://github.com/JakeStanger/ironbar/commit/b7b64886e3c48ace3faffbb1e277275aeeac3adf) - sometimes panicking on startup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`5ce50b0`](https://github.com/JakeStanger/ironbar/commit/5ce50b0987812a1ade2d1262e8d7df6916cfc39a) - tidy and format *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`daafa09`](https://github.com/JakeStanger/ironbar/commit/daafa0943e5b9886b09fd18d6fff04558fb02335) - update CHANGELOG.md for v0.5.2 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b352181`](https://github.com/JakeStanger/ironbar/commit/b352181b3d232ccc79ffc1d9e22a633729d01a47) - update json example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bb4fe7f`](https://github.com/JakeStanger/ironbar/commit/bb4fe7f7f58fa2a6d0a2259bd9442700d2c884f7) - **readme**: credit smithay client toolkit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :boom: BREAKING CHANGES
- due to [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*:
The `left` and `right` config options have been renamed to `start` and `end`
## [v0.5.2] - 2022-09-07
### :wrench: Chores
- [`b801751`](https://github.com/JakeStanger/ironbar/commit/b801751bdabd8416084f46e6b6d803ea28a259ec) - **release**: v0.5.2 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
@@ -45,4 +108,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0

View File

@@ -14,3 +14,4 @@ I welcome contributions of any kind with open arms. That said, please do stick t
- For issues:
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
- If reporting an error, please ensure you use `IRONBAR_LOG` or `IRONBAR_FILE_LOG` set to `debug`.

648
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,40 @@
[package]
name = "ironbar"
version = "0.6.0"
version = "0.8.0"
edition = "2021"
license = "MIT"
description = "Customisable wlroots/sway bar"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Customisable GTK Layer Shell wlroots/sway bar"
[dependencies]
derive_builder = "0.11.2"
gtk = "0.15.5"
gtk-layer-shell = "0.4.1"
glib = "0.15.12"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
gtk = "0.16.0"
gtk-layer-shell = "0.5.0"
glib = "0.16.2"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2"
futures-util = "0.3.21"
chrono = "0.4.19"
serde = { version = "1.0.141", features = ["derive"] }
serde_json = "1.0.82"
serde_yaml = "0.9.4"
toml = "0.5.9"
cornfig = "0.3.0"
libcorn = "0.6.0"
lazy_static = "1.4.0"
async_once = "0.2.6"
regex = "1.6.0"
stray = { git = "https://github.com/JakeStanger/stray.git", branch = "fix/tracing" }
indexmap = "1.9.1"
futures-util = "0.3.21"
chrono = "0.4.19"
regex = { version = "1.6.0", default-features = false, features = ["std"] }
stray = { version = "0.1.2" }
dirs = "4.0.0"
walkdir = "2.3.2"
notify = "5.0.0"
notify = { version = "5.0.0", default-features = false }
mpd_client = "1.0.0"
swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" }
sysinfo = "0.26.2"
swayipc-async = { version = "2.0.1" }
sysinfo = "0.26.4"
wayland-client = "0.29.5"
wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] }
smithay-client-toolkit = "0.16.0"
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }

View File

@@ -1,6 +1,6 @@
# Ironbar
Ironbar is a customisable and feature-rich bar targeting wlroots compositors, written in Rust.
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
It uses GTK3 and gtk-layer-shell.
The bar can be styled to your liking using CSS and hot-loads style changes.
@@ -8,9 +8,8 @@ For information and examples on styling please see the [wiki](https://github.com
![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png)
## Installation
Run using `ironbar`.
## Installation
### Cargo
@@ -28,6 +27,49 @@ yay -S ironbar-git
[aur package](https://aur.archlinux.org/packages/ironbar-git)
### Nix Flake
#### Example
Here is an example nix flake that uses ironbar, this is just a
proof of concept, please adapt it to your config
```nix
{
# Add the ironbar flake input
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
inputs.ironbar = {
url = "github:JakeStanger/ironbar";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.hm = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs: {
homeManagerConfigurations."USER@HOSTNAME" = inputs.hm.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [
# And add the home-manager module
inputs.ironbar.homeManagerModules.default
{
# And configure
programs.ironbar = {
enable = true;
config = {};
style = "";
};
}
];
};
};
}
```
#### Binary Caching
There is also a cachix cache at `https://app.cachix.org/cache/jakestanger`
incase you don't want to compile ironbar!
### Source
```sh
@@ -40,6 +82,15 @@ install target/release/ironbar ~/.local/bin/ironbar
[repo](https://github.com/jakestanger/ironbar)
## Running
All of the above installation methods provide a binary called `ironbar`.
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
Ironbar gives a lot of flexibility when configuring, including multiple file formats
@@ -78,4 +129,4 @@ Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUT
- [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
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland

288
docs/Configuration guide.md Normal file
View File

@@ -0,0 +1,288 @@
By default, you get a single bar at the bottom of all your screens.
To change that, you'll unsurprisingly need a config file.
This page details putting together the skeleton for your config to get you to a stage where you can start configuring modules.
It may look long and overwhelming, but that is just because the bar supports a lot of scenarios!
If you want to see some ready-to-go config files check the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples)
and the example pages in the sidebar.
## 1. Create config file
The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`.
Ironbar supports a range of configuration formats, so you can pick your favourite:
- `config.json`
- `config.toml`
- `config.yaml`
- `config.corn` (Experimental, includes variable support for re-using blocks.
See [here](https://github.com/jakestanger/corn) for info)
You can also override the default config path using the `IRONBAR_CONFIG` environment variable.
## 2. Pick your use-case
Ironbar gives you a few ways to configure the bar to suit your needs.
This allows you to keep your config simple and relatively flat if your use-case is simple,
and make it more complex if required.
### a) I want the same bar across all monitors
Place the bar config inside the top-level object. This is automatically applied to each of your monitors.
<details>
<summary>JSON</summary>
```json
{
"position": "bottom",
"height": 42,
"start": [],
"center": [],
"end": []
}
```
</details>
<details>
<summary>TOML</summary>
```toml
position = "bottom"
height = 42
start = []
center = []
end = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
position: "bottom"
height: 42
start: [ ]
center: [ ]
end: [ ]
```
</details>
<details>
<summary>Corn</summary>
```
{
position = "bottom"
height = 42
start = []
center = []
end = []
}
```
</details>
### b) I want my config to differ across one or more monitors
Create a map/object called `monitors` inside the top-level object.
Each of the map's keys should be an output name,
and each value should be an object containing the bar config.
To find your output names, run `wayland-info | grep wl_output -A1`.
<details>
<summary>JSON</summary>
```json
{
"monitors": {
"DP-1": {
"start": []
},
"DP-2": {
"position": "bottom",
"height": 30,
"start": []
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[monitors]
[monitors.DP-1]
start = []
[monitors.DP-2]
position = "bottom"
height = 30
start = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
monitors:
DP-1:
start: [ ]
DP-2:
position: "bottom"
height: 30
start: [ ]
```
</details>
<details>
<summary>Corn</summary>
```
{
monitors.DP-1.start = []
monitors.DP-2 = {
position = "bottom"
height = 30
start = []
}
}
```
</details>
### c) I want one or more monitors to have multiple bars
Create a map/object called `monitors` inside the top-level object.
Each of the map's keys should be an output name.
If you want the screen to have multiple bars, use an array of bar config objects.
If you want the screen to have a single bar, use an object.
To find your output names, run `wayland-info | grep wl_output -A1`.
<details>
<summary>JSON</summary>
```json
{
"monitors": {
"DP-1": [
{
"start": []
},
{
"position": "top",
"start": []
}
],
"DP-2": {
"position": "bottom",
"height": 30,
"start": []
}
}
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[monitors]
[[monitors.DP-1]]
start = []
[[monitors.DP-2]]
position = "top"
start = []
[monitors.DP-2]
position = "bottom"
height = 30
start = []
```
</details>
<details>
<summary>YAML</summary>
```yaml
monitors:
DP-1:
- start: [ ]
- position: "top"
start: [ ]
DP-2:
position: "bottom"
height: 30
start: [ ]
```
</details>
<details>
<summary>Corn</summary>
```
{
monitors.DP-1 = [
{ start = [] }
{ position = "top" start = [] }
]
monitors.DP-2 = {
position = "bottom"
height = 30
start = []
}
}
```
</details>
## 3. Write your bar config(s)
Once you have the basic config structure set up, it's time to actually configure your bar(s).
Check [here](config) for an example config file for a fully configured bar in each format.
### 3.1 Top-level options
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
| `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. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
### 3.2 Module-level options
The following table lists each of the module-level options that are present on **all** modules.
For details on available modules and each of their config options, check the sidebar.
For information on the `Script` type, and embedding scripts in strings, see [here](script).
| 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. |
| `on_click` | `Script [polling]` | `null` | Runs the script when the module is clicked. |
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |

4
docs/Home.md Normal file
View File

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

102
docs/Scripts.md Normal file
View File

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

20
docs/Styling guide.md Normal file
View File

@@ -0,0 +1,20 @@
Ironbar ships with no styles by default, so will fall back to the default GTK styles.
To style the bar, create a file at `~/.config/ironbar/style.css`.
Style changes are hot-loaded so there is no need to reload the bar.
A reminder: since the bar is GTK-based, it uses GTK's implementation of CSS,
which only includes a subset of the full web spec (plus a few non-standard properties).
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 |

26
docs/_Sidebar.md Normal file
View File

@@ -0,0 +1,26 @@
# Guides
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Styling guide](styling-guide)
# Examples
- [Config](config)
- [Stylesheet](stylesheet)
## Custom
- [Power Menu](power-menu)
# Modules
- [Clock](clock)
- [Custom](custom)
- [Focused](focused)
- [Launcher](launcher)
- [MPD](mpd)
- [Script](script)
- [Sys_Info](sys-info)
- [Tray](tray)
- [Workspaces](workspaces)

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

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

142
docs/examples/Stylesheet.md Normal file
View File

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

View File

@@ -0,0 +1,230 @@
Creates a button on the bar, which opens a popup. The popup contains a header, shutdown button, restart button, and uptime.
![A screenshot of the custom power menu module open, with some other modules present on the bar](../../_imgs/custom-power-menu.png)
## Configuration
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock"
},
{
"bar": [
{
"on_click": "popup:toggle",
"label": "",
"name": "power-btn",
"type": "button"
}
],
"class": "power-menu",
"popup": [
{
"orientation": "vertical",
"type": "box",
"widgets": [
{
"label": "Power menu",
"name": "header",
"type": "label"
},
{
"type": "box",
"widgets": [
{
"class": "power-btn",
"on_click": "!shutdown now",
"label": "<span font-size='40pt'></span>",
"type": "button"
},
{
"class": "power-btn",
"on_click": "!reboot",
"label": "<span font-size='40pt'></span>",
"type": "button"
}
]
},
{
"label": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
}
]
}
],
"type": "custom"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = 'clock'
[[end]]
class = 'power-menu'
type = 'custom'
[[end.bar]]
on_click = 'popup:toggle'
label = ''
name = 'power-btn'
type = 'button'
[[end.popup]]
orientation = 'vertical'
type = 'box'
[[end.popup.widgets]]
label = 'Power menu'
name = 'header'
type = 'label'
[[end.popup.widgets]]
type = 'box'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!shutdown now'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!reboot'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets]]
label = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: clock
- bar:
- on_click: popup:toggle
label:
name: power-btn
type: button
class: power-menu
popup:
- orientation: vertical
type: box
widgets:
- label: Power menu
name: header
type: label
- type: box
widgets:
- class: power-btn
on_click: '!shutdown now'
label: <span font-size='40pt'></span>
type: button
- class: power-btn
on_click: '!reboot'
label: <span font-size='40pt'></span>
type: button
- label: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
type: custom
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
]
}
{ type = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
}
} in {
end = [ $power_menu ]
}
```
</details>
## Styling
```css
.power-menu {
margin-left: 10px;
}
.power-menu #power-btn {
color: white;
background-color: #2d2d2d;
}
.power-menu #power-btn:hover {
background-color: #1c1c1c;
}
.popup-power-menu {
padding: 1em;
}
.popup-power-menu #header {
color: white;
font-size: 1.4em;
border-bottom: 1px solid white;
padding-bottom: 0.4em;
margin-bottom: 0.8em;
}
.popup-power-menu .power-btn {
color: white;
background-color: #2d2d2d;
border: 1px solid white;
padding: 0.6em 1em;
}
.popup-power-menu .power-btn + .power-btn {
margin-left: 1em;
}
.popup-power-menu .power-btn:hover {
background-color: #1c1c1c;
}
```

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

@@ -0,0 +1,77 @@
Displays the current date and time.
Clicking on the widget opens a popup with the time and a calendar.
![Screenshot of clock widget with popup open](https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png)
## Configuration
> Type: `clock`
| Name | Type | Default | Description |
|----------|--------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock",
"format": "%d/%m/%Y %H:%M"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "clock"
format = "%d/%m/%Y %H:%M"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "clock"
format: "%d/%m/%Y %H:%M"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "clock"
format = "%d/%m/%Y %H:%M"
}
]
}
```
</details>
## Styling
| Selector | Description |
|-------------------------------|------------------------------------------------------------------------------------|
| `#clock` | Clock widget button |
| `#popup-clock` | Clock popup box |
| `#popup-clock #calendar-clock` | Clock inside the popup |
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |

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

@@ -0,0 +1,272 @@
Allows you to compose custom modules consisting of multiple widgets, including popups.
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://user-images.githubusercontent.com/5057870/196058785-042ef171-7e77-4d5c-921a-eca03c6424bd.png)
## Configuration
> Type: `custom`
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
It is well worth looking at the examples.
| Name | Type | Default | Description |
|---------|------------|---------|--------------------------------------|
| `class` | `string` | `null` | Container class name. |
| `bar` | `Widget[]` | `null` | List of widgets to add to the bar. |
| `popup` | `Widget[]` | `[]` | List of widgets to add to the popup. |
### `Widget`
| Name | Type | Default | Description |
|---------------|------------------------------|--------------|---------------------------------------------------------------------------|
| `widget_type` | `box` or `label` or `button` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
### Labels
Labels can interpolate text from scripts to dynamically show content.
This can be done by including scripts in `{{double braces}}` using the shorthand script syntax.
For example, the following label would output your system uptime, updated every 30 seconds.
```
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
```
Both polling and watching mode are supported. For more information on script syntax, see [here](script).
### Commands
Buttons can execute commands that interact with the bar,
as well as any arbitrary shell command.
To execute shell commands, prefix them with an `!`.
For example, if you want to run `~/.local/bin/my-script.sh` on click,
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
The following bar commands are supported:
- `popup:toggle`
- `popup:open`
- `popup:close`
XML is arguably better-suited and easier to read for this sort of markup,
but currently is not supported.
Nonetheless, it may be worth comparing the examples to the below equivalent
to help get your head around what's going on:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<custom class="power-menu">
<bar>
<button name="power-btn" label="" on_click="popup:toggle"/>
</bar>
<popup>
<box orientation="vertical">
<label name="header" label="Power menu" />
<box>
<button class="power-btn" label="" on_click="!shutdown now" />
<button class="power-btn" label="" on_click="!reboot" />
</box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
</box>
</popup>
</custom>
```
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "clock"
},
{
"bar": [
{
"on_click": "popup:toggle",
"label": "",
"name": "power-btn",
"type": "button"
}
],
"class": "power-menu",
"popup": [
{
"orientation": "vertical",
"type": "box",
"widgets": [
{
"label": "Power menu",
"name": "header",
"type": "label"
},
{
"type": "box",
"widgets": [
{
"class": "power-btn",
"on_click": "!shutdown now",
"label": "<span font-size='40pt'></span>",
"type": "button"
},
{
"class": "power-btn",
"on_click": "!reboot",
"label": "<span font-size='40pt'></span>",
"type": "button"
}
]
},
{
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
}
]
}
],
"type": "custom"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = 'clock'
[[end]]
class = 'power-menu'
type = 'custom'
[[end.bar]]
on_click = 'popup:toggle'
label = ''
name = 'power-btn'
type = 'button'
[[end.popup]]
orientation = 'vertical'
type = 'box'
[[end.popup.widgets]]
label = 'Power menu'
name = 'header'
type = 'label'
[[end.popup.widgets]]
type = 'box'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!shutdown now'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
on_click = '!reboot'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets]]
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: clock
- bar:
- on_click: popup:toggle
label:
name: power-btn
type: button
class: power-menu
popup:
- orientation: vertical
type: box
widgets:
- label: Power menu
name: header
type: label
- type: box
widgets:
- class: power-btn
on_click: '!shutdown now'
label: <span font-size='40pt'></span>
type: button
- class: power-btn
on_click: '!reboot'
label: <span font-size='40pt'></span>
type: button
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
type: custom
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
]
}
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
}
} in {
end = [ $power_menu ]
}
```
</details>
## Styling
Since the widgets are all custom, you can target them using `#name` and `.class`.
| Selector | Description |
|-----------|-------------------------|
| `#custom` | Custom widget container |

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

@@ -0,0 +1,90 @@
Displays the title and/or icon of the currently focused window.
![Screenshot of focused widget, showing this page open on firefox](https://user-images.githubusercontent.com/5057870/184714118-c1fb1c67-cd8c-4cc0-b5cd-6faccff818ac.png)
## Configuration
> Type: `focused`
| Name | Type | Default | Description |
|--------------|-----------|---------|---------------------------------|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `icon_theme` | `string` | `null` | GTK icon theme to use |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "focused",
"show_icon": true,
"show_title": true,
"icon_size": 32,
"icon_theme": "Paper"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "focused"
show_icon = true
show_title = true
icon_size = 32
icon_theme = "Paper"
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "focused"
show_icon: true
show_title: true
icon_size: 32
icon_theme: "Paper"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "focused"
show_icon = true
show_title = true
icon_size = 32
icon_theme = "Paper"
}
]
}
```
</details>
## Styling
| Selector | Description |
|--------------------------|--------------------|
| `#focused` | Focused widget box |
| `#focused #icon` | App icon |
| `#focused #label` | App name |

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

@@ -0,0 +1,103 @@
Windows-style taskbar that displays running windows, grouped by program.
Hovering over a program with multiple windows open shows a popup with each window.
Clicking an icon/popup item focuses or launches the program.
Optionally displays a launchable set of favourites.
![Screenshot showing several open applications, including a focused terminal.](https://user-images.githubusercontent.com/5057870/184540058-120e190e-2a45-4167-99c7-ed76482d1f16.png)
## Configuration
> Type: `launcher`
| | Type | Default | Description |
|--------------|------------|---------|-----------------------------------------------------------------------------------------------------|
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_theme` | `string` | `null` | GTK icon theme to use. |
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "launcher",
"favourites": [
"firefox",
"discord"
],
"show_names": false,
"show_icons": true,
"icon_theme": "Paper"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "launcher"
favorites = ["firefox", "discord"]
show_names = false
show_icons = true
icon_theme = "Paper"
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "launcher"
favorites:
- firefox
- discord
show_names: false
show_icons: true
icon_theme: "Paper"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "launcher"
favorites = ["firefox" "discord"]
show_names = false
show_icons = true
icon_theme = "Paper"
}
]
}
```
</details>
## Styling
| Selector | Description |
|-------------------------------|--------------------------|
| `#launcher` | Launcher widget box |
| `#launcher .item` | App button |
| `#launcher .item.open` | App button (open app) |
| `#launcher .item.focused` | App button (focused app) |
| `#launcher .item.urgent` | App button (urgent app) |
| `#launcher-popup` | Popup container |
| `#launcher-popup .popup-item` | Window button in popup |

131
docs/modules/MPD.md Normal file
View File

@@ -0,0 +1,131 @@
Displays currently playing song from MPD.
Clicking on the widget opens a popout displaying info about the current song, album art
and playback controls.
![Screenshot showing MPD widget with track playing with popout open](https://user-images.githubusercontent.com/5057870/184539664-a8f3ad5b-69c0-492d-a27d-82303c09a347.png)
## Configuration
> Type: `mpd`
| | Type | Default | Description |
|----------------|----------|-----------------------------|-----------------------------------------------------------------------|
| `host` | `string` | `localhost:6600` | TCP or Unix socket for the MPD server. |
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
| `icons.play` | `string` | `` | Icon to show when playing. |
| `icons.pause` | `string` | `` | Icon to show when paused. |
| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. |
| `music_dir` | `string` | `$HOME/Music` | Path to MPD server's music directory on disc. Required for album art. |
<details>
<summary>JSON</summary>
```json
{
"start": [
{
"type": "mpd",
"format": "{icon} {title} / {artist}",
"icons": {
"play": "",
"pause": ""
},
"music_dir": "/home/jake/Music"
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[start]]
type = "mpd"
format = "{icon} {title} / {artist}"
music_dir = "/home/jake/Music"
[[start.icons]]
play = ""
pause = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
start:
- type: "mpd"
format: "{icon} {title} / {artist}"
icons:
play: ""
pause: ""
music_dir: "/home/jake/Music"
```
</details>
<details>
<summary>Corn</summary>
```corn
{
start = [
{
type = "mpd"
format = "{icon} {title} / {artist}"
icons.play = ""
icons.pause = ""
music_dir = "/home/jake/Music"
}
]
}
```
</details>
### Formatting Tokens
The following tokens can be used in the `format` config option,
and will be replaced with values from the currently playing track:
| Token | Description |
|--------------|--------------------------------------|
| `{icon}` | Either `icons.play` or `icons.pause` |
| `{title}` | Title |
| `{album}` | Album name |
| `{artist}` | Artist name |
| `{date}` | Release date |
| `{track}` | Track number |
| `{disc}` | Disc number |
| `{genre}` | Genre |
| `{duration}` | Duration in `mm:ss` |
| `{elapsed}` | Time elapsed in `mm:ss` |
## Styling
| Selector | Description |
|----------------------------------------|------------------------------------------|
| `#mpd` | Tray widget button |
| `#popup-mpd` | Popup box |
| `#popup-mpd #album-art` | Album art image inside popup box |
| `#popup-mpd #title` | Track title container inside popup box |
| `#popup-mpd #title .icon` | Track title icon label inside popup box |
| `#popup-mpd #title .label` | Track title label inside popup box |
| `#popup-mpd #album` | Track album container inside popup box |
| `#popup-mpd #album .icon` | Track album icon label inside popup box |
| `#popup-mpd #album .label` | Track album label inside popup box |
| `#popup-mpd #artist` | Track artist container inside popup box |
| `#popup-mpd #artist .icon` | Track artist icon label inside popup box |
| `#popup-mpd #artist .label` | Track artist label inside popup box |
| `#popup-mpd #controls` | Controls container inside popup box |
| `#popup-mpd #controls #btn-prev` | Previous button inside popup box |
| `#popup-mpd #controls #btn-play-pause` | Play/pause button inside popup box |
| `#popup-mpd #controls #btn-next` | Next button inside popup box |
| `#popup-mpd #volume` | Volume container inside popup box |
| `#popup-mpd #volume #slider` | Volume slider popup box |
| `#popup-mpd #volume .icon` | Volume icon label inside popup box |

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

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

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

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

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

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

View File

@@ -0,0 +1,94 @@
> ⚠ **This module is currently only supported on Sway**
Shows all current Sway workspaces. Clicking a workspace changes focus to it.
![Screenshot showing workspaces widget using custom icons with browser workspace focused](https://user-images.githubusercontent.com/5057870/184540156-26cfe4ec-ab8d-4e0f-a883-8b641025366b.png)
## Configuration
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|-----------------------|---------|----------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string>` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
<details>
<summary>JSON</summary>
```json
{
"end": [
{
"type": "workspaces",
"name_map": {
"1": "",
"2": "",
"3": ""
},
"all_monitors": false
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[end]]
type = "workspaces"
all_monitors = false
[[end.name_map]]
1 = ""
2 = ""
3 = ""
```
</details>
<details>
<summary>YAML</summary>
```yaml
end:
- type: "workspaces"
name_map:
1: ""
2: ""
3: ""
all_monitors: false
```
</details>
<details>
<summary>Corn</summary>
```corn
{
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
all_monitors = false
}
]
}
```
</details>
## Styling
| Selector | Description |
|-----------------------------|--------------------------------------|
| `#workspaces` | Workspaces widget box |
| `#workspaces .item` | Workspace button |
| `#workspaces .item.focused` | Workspace button (workspace focused) |

View File

@@ -23,20 +23,56 @@ let {
$mpd_server = { type = "mpd" host = "chloe:6600" }
$sys_info = {
type = "sys-info"
format = ["{cpu-percent}% " "{memory-percent}% "]
type = "sys_info"
format = ["{cpu_percent}% " "{memory_percent}% "]
}
$tray = { type = "tray" }
$clock = { type = "clock" }
$clock = {
type = "clock"
// show-if = "500:[ $(($(date +%s) % 2)) -eq 0 ]"
show_if.cmd = "exit 0"
show_if.interval = 500
}
$phone_battery = {
type = "script"
path = "/home/jake/bin/phone-battery"
cmd = "/home/jake/bin/phone-battery"
}
$log_tail = {
type = "script"
path = "tail -f /home/jake/.local/share/ironbar/error.log"
mode = "watch"
}
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
]
}
{ type = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
}
$left = [ $workspaces $launcher ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $power_menu $clock ]
}
in {
anchor_to_edges = true

View File

@@ -28,10 +28,10 @@
},
{
"format": [
"{cpu-percent}% ",
"{memory-percent}% "
"{cpu_percent}% ",
"{memory_percent}% "
],
"type": "sys-info"
"type": "sys_info"
},
{
"type": "tray"

29
examples/custom.corn Normal file
View File

@@ -0,0 +1,29 @@
let {
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
$popup = {
type = "box"
orientation = "vertical"
widgets = [
{ type = "label" name = "header" label = "Power menu" }
{
type = "box"
widgets = [
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
]
}
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
}
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ $button ]
popup = [ $popup ]
}
} in {
end = [ $power_menu { type = "clock" } ]
}

23
examples/sys-info.corn Normal file
View File

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

64
flake.lock generated Normal file
View File

@@ -0,0 +1,64 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1668994630,
"narHash": "sha256-1lqx6HLyw6fMNX/hXrrETG1vMvZRGm2XVC9O/Jt0T6c=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "af50806f7c6ab40df3e6b239099e8f8385f6c78b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1669084742,
"narHash": "sha256-aLYwYVnrmEE1LVqd17v99CuqVmAZQrlgi2DVTAs4wFg=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "9652ef34c7439eca9f86cee11e94dbef5c9adb09",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

142
flake.nix Normal file
View File

@@ -0,0 +1,142 @@
{
description = "Nix Flake for ironbar";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
#nci.url = "github:yusdacra/nix-cargo-integration";
#nci.inputs.nixpkgs.follows = "nixpkgs";
#nci.inputs.rust-overlay.follows = "rust-overlay";
};
outputs = {
self,
nixpkgs,
rust-overlay,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-linux"
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
self.overlays.default
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
in {
overlays.default = final: prev: let
rust = mkRustToolchain final;
rustPlatform = prev.makeRustPlatform {
cargo = rust;
rustc = rust;
};
in {
ironbar = rustPlatform.buildRustPackage {
pname = "ironbar";
version = self.rev or "dirty";
src = builtins.path {
name = "ironbar";
path = prev.lib.cleanSource ./.;
};
cargoDeps = rustPlatform.importCargoLock {lockFile = ./Cargo.lock;};
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with prev; [pkg-config];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon];
};
};
packages = genSystems (
system: let
pkgs = pkgsFor system;
in
(self.overlays.default pkgs pkgs)
// {
default = self.packages.${system}.ironbar;
}
);
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
gcc
gtk3
gtk-layer-shell
pkg-config
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
homeManagerModules.default = {
config,
lib,
pkgs,
...
}: let
cfg = config.programs.ironbar;
defaultIronbarPackage = self.packages.${pkgs.system}.default;
jsonFormat = pkgs.formats.json {};
in {
options.programs.ironbar = {
enable = lib.mkEnableOption "ironbar status bar";
package = lib.mkOption {
type = with lib.types; package;
default = defaultIronbarPackage;
description = "The package for ironbar to use";
};
systemd = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for ironbar";
};
style = lib.mkOption {
type = lib.types.lines;
default = "";
description = "The stylesheet to apply to ironbar";
};
config = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The config to pass to ironbar";
};
};
config = lib.mkIf cfg.enable {
home.packages = [cfg.package];
xdg.configFile = {
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
source = jsonFormat.generate "ironbar-config" cfg.config;
};
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
text = cfg.style;
};
};
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Ironbar";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/ironbar";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
];
};
};
};
};
}

View File

@@ -1,11 +1,14 @@
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, ModuleConfig};
use crate::dynamic_string::DynamicString;
use crate::modules::custom::ExecEvent;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::mpd::{PlayerCommand, SongUpdate};
use crate::modules::workspaces::WorkspaceUpdate;
use crate::modules::{Module, ModuleInfoBuilder, ModuleLocation, ModuleUpdateEvent, WidgetContext};
use crate::popup::Popup;
use crate::Config;
use crate::script::{OutputStream, Script};
use crate::{await_sync, Config};
use chrono::{DateTime, Local};
use color_eyre::Result;
use gtk::gdk::Monitor;
@@ -15,8 +18,9 @@ use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use stray::message::NotifierItemCommand;
use stray::NotifierItemMessage;
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::{debug, info};
use tracing::{debug, error, info, trace};
/// Creates a new window for a bar,
/// sets it up and adds its widgets.
@@ -80,7 +84,11 @@ pub fn create_bar(
});
debug!("Showing bar");
win.show_all();
start.show();
center.show();
end.show();
content.show();
win.show();
Ok(())
}
@@ -154,11 +162,63 @@ fn add_modules(
controller_tx: ui_tx,
};
let common = $module.common.clone();
let widget = $module.into_widget(context, &info)?;
content.add(&widget.widget);
let container = gtk::EventBox::new();
container.add(&widget.widget);
content.add(&container);
widget.widget.set_widget_name(info.module_name);
if let Some(show_if) = common.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(|(_, success)| {
tx.send(success)
.expect("Failed to send widget visibility toggle message");
})
.await;
});
rx.attach(None, move |success| {
if success {
container.show_all()
} else {
container.hide()
};
Continue(true)
});
} else {
container.show_all();
}
if let Some(on_click) = common.on_click {
let script = Script::new_polling(on_click);
container.connect_button_press_event(move |_, _| {
trace!("Running on-click script");
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
Inhibit(false)
});
}
if let Some(tooltip) = common.tooltip {
DynamicString::new(&tooltip, move |string| {
container.set_tooltip_text(Some(&string));
Continue(true)
});
}
let has_popup = widget.popup.is_some();
if let Some(popup_content) = widget.popup {
popup
@@ -236,6 +296,9 @@ fn add_modules(
ModuleConfig::Launcher(module) => {
add_module!(module, id, "launcher", LauncherUpdate, ItemEvent);
}
ModuleConfig::Custom(module) => {
add_module!(module, id, "custom", (), ExecEvent);
}
}
}

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

@@ -0,0 +1,4 @@
pub mod mpd;
pub mod sway;
pub mod system_tray;
pub mod wayland;

View File

@@ -59,8 +59,9 @@ impl MpdClient {
while let Some(change) = state_changes.next().await {
debug!("Received state change: {:?}", change);
if let ConnectionEvent::SubsystemChange(Subsystem::Player | Subsystem::Queue) =
change
if let ConnectionEvent::SubsystemChange(
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
) = change
{
tx2.send(())?;
}
@@ -140,10 +141,9 @@ async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
fn is_unix_socket(host: &str) -> bool {
let path = PathBuf::from(host);
path.exists()
&& match path.metadata() {
Ok(metadata) => metadata.file_type().is_socket(),
Err(_) => false,
}
&& path
.metadata()
.map_or(false, |metadata| metadata.file_type().is_socket())
}
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {

View File

@@ -1,8 +1,9 @@
use super::toplevel::{ToplevelEvent, ToplevelInfo};
use super::toplevel_manager::listen_for_toplevels;
use super::ToplevelChange;
use super::{Env, ToplevelHandler};
use crate::collection::Collection;
use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo};
use crate::wayland::toplevel_manager::listen_for_toplevels;
use crate::wayland::ToplevelChange;
use color_eyre::Report;
use indexmap::IndexMap;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
use smithay_client_toolkit::reexports::calloop;
@@ -11,7 +12,7 @@ use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::sync::{broadcast, oneshot};
use tokio::task::spawn_blocking;
use tracing::trace;
use tracing::{error, trace};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
@@ -21,7 +22,7 @@ use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
pub struct WaylandClient {
pub outputs: Vec<OutputInfo>,
pub seats: Vec<WlSeat>,
pub toplevels: Arc<RwLock<Collection<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
}
@@ -35,7 +36,7 @@ impl WaylandClient {
let toplevel_tx2 = toplevel_tx.clone();
let toplevels = Arc::new(RwLock::new(Collection::new()));
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
let toplevels2 = toplevels.clone();
// `queue` is not send so we need to handle everything inside the task
@@ -89,9 +90,12 @@ impl WaylandClient {
loop {
// TODO: Avoid need for duration here - can we force some event when sending requests?
event_loop
.dispatch(Duration::from_millis(50), &mut ())
.expect("Failed to dispatch pending wayland events");
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
error!(
"{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
);
}
}
});

View File

@@ -4,11 +4,10 @@ mod toplevel_manager;
extern crate smithay_client_toolkit as sctk;
use self::toplevel_manager::ToplevelHandler;
pub use crate::wayland::toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
use crate::wayland::toplevel_manager::{ToplevelHandling, ToplevelStatusListener};
use async_once::AsyncOnce;
use lazy_static::lazy_static;
pub use toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
use toplevel_manager::{ToplevelHandler, ToplevelHandling, ToplevelStatusListener};
use wayland_client::{Attached, DispatchData, Interface};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,

View File

@@ -1,5 +1,5 @@
use crate::wayland::toplevel::{Toplevel, ToplevelEvent};
use crate::wayland::LazyGlobal;
use super::toplevel::{Toplevel, ToplevelEvent};
use super::LazyGlobal;
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use std::cell::RefCell;
use std::rc;

View File

@@ -1,161 +0,0 @@
use serde::Serialize;
use std::slice::{Iter, IterMut};
use std::vec;
/// An ordered map.
/// Internally this is just two vectors -
/// one for keys and one for values.
#[derive(Debug, Clone, Serialize)]
pub struct Collection<TKey, TData> {
keys: Vec<TKey>,
values: Vec<TData>,
}
impl<TKey: PartialEq, TData> Collection<TKey, TData> {
/// Creates a new empty collection.
pub const fn new() -> Self {
Self {
keys: vec![],
values: vec![],
}
}
/// Inserts a new key/value pair at the end of the collection.
pub fn insert(&mut self, key: TKey, value: TData) {
self.keys.push(key);
self.values.push(value);
assert_eq!(self.keys.len(), self.values.len());
}
/// Gets a reference of the value for the specified key
/// if it exists in the collection.
pub fn get(&self, key: &TKey) -> Option<&TData> {
let index = self.keys.iter().position(|k| k == key);
match index {
Some(index) => self.values.get(index),
None => None,
}
}
/// Gets a mutable reference for the value with the specified key
/// if it exists in the collection.
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
let index = self.keys.iter().position(|k| k == key);
match index {
Some(index) => self.values.get_mut(index),
None => None,
}
}
/// Checks if a value for the given key exists inside the collection
pub fn contains(&self, key: &TKey) -> bool {
self.keys.contains(key)
}
/// Removes the key/value from the collection
/// if it exists
/// and returns the removed value.
pub fn remove(&mut self, key: &TKey) -> Option<TData> {
assert_eq!(self.keys.len(), self.values.len());
let index = self.keys.iter().position(|k| k == key);
if let Some(index) = index {
self.keys.remove(index);
Some(self.values.remove(index))
} else {
None
}
}
/// Gets the length of the collection.
pub fn len(&self) -> usize {
self.keys.len()
}
/// Gets a reference to the first value in the collection.
pub fn first(&self) -> Option<&TData> {
self.values.first()
}
/// Gets the values as a slice.
pub fn as_slice(&self) -> &[TData] {
self.values.as_slice()
}
/// Checks whether the collection is empty.
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
/// Gets an iterator for the collection.
pub fn iter(&self) -> Iter<'_, TData> {
self.values.iter()
}
/// Gets a mutable iterator for the collection
pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
self.values.iter_mut()
}
}
impl<TKey: PartialEq, TData> From<(TKey, TData)> for Collection<TKey, TData> {
fn from((key, value): (TKey, TData)) -> Self {
let mut collection = Self::new();
collection.insert(key, value);
collection
}
}
impl<TKey: PartialEq, TData> FromIterator<(TKey, TData)> for Collection<TKey, TData> {
fn from_iter<T: IntoIterator<Item = (TKey, TData)>>(iter: T) -> Self {
let mut collection = Self::new();
for (key, value) in iter {
collection.insert(key, value);
}
collection
}
}
impl<'a, TKey: PartialEq, TData> IntoIterator for &'a Collection<TKey, TData> {
type Item = &'a TData;
type IntoIter = CollectionIntoIterator<'a, TKey, TData>;
fn into_iter(self) -> Self::IntoIter {
CollectionIntoIterator {
collection: self,
index: 0,
}
}
}
pub struct CollectionIntoIterator<'a, TKey, TData> {
collection: &'a Collection<TKey, TData>,
index: usize,
}
impl<'a, TKey: PartialEq, TData> Iterator for CollectionIntoIterator<'a, TKey, TData> {
type Item = &'a TData;
fn next(&mut self) -> Option<Self::Item> {
let res = self.collection.values.get(self.index);
self.index += 1;
res
}
}
impl<TKey: PartialEq, TData> Default for Collection<TKey, TData> {
fn default() -> Self {
Self::new()
}
}
impl<TKey: PartialEq, TData> IntoIterator for Collection<TKey, TData> {
type Item = TData;
type IntoIter = vec::IntoIter<TData>;
fn into_iter(self) -> Self::IntoIter {
self.values.into_iter()
}
}

View File

@@ -1,4 +1,5 @@
use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule;
use crate::modules::focused::FocusedModule;
use crate::modules::launcher::LauncherModule;
use crate::modules::mpd::MpdModule;
@@ -6,6 +7,7 @@ use crate::modules::script::ScriptModule;
use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule;
use crate::modules::workspaces::WorkspacesModule;
use crate::script::ScriptInput;
use color_eyre::eyre::{Context, ContextCompat};
use color_eyre::{eyre, Help, Report};
use dirs::config_dir;
@@ -15,9 +17,17 @@ use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::{env, fs};
use tracing::instrument;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub struct CommonConfig {
pub show_if: Option<ScriptInput>,
pub on_click: Option<ScriptInput>,
pub tooltip: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
Clock(ClockModule),
Mpd(MpdModule),
@@ -27,6 +37,7 @@ pub enum ModuleConfig {
Launcher(LauncherModule),
Script(ScriptModule),
Focused(FocusedModule),
Custom(CustomModule),
}
#[derive(Debug, Deserialize, Clone)]
@@ -37,7 +48,7 @@ pub enum MonitorConfig {
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "snake_case")]
pub enum BarPosition {
Top,
Bottom,
@@ -69,7 +80,7 @@ impl BarPosition {
}
}
#[derive(Debug, Deserialize, Clone, Default)]
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default_bar_position")]
pub position: BarPosition,
@@ -96,18 +107,23 @@ const fn default_bar_height() -> i32 {
impl Config {
/// Attempts to load the config file from file,
/// parse it and return a new instance of `Self`.
#[instrument]
pub fn load() -> Result<Self> {
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
let path = PathBuf::from(config_path);
if path.exists() {
Ok(path)
} else {
Err(Report::msg("Specified config file does not exist")
let config_path = env::var("IRONBAR_CONFIG").map_or_else(
|_| Self::try_find_config(),
|config_path| {
let path = PathBuf::from(config_path);
if path.exists() {
Ok(path)
} else {
Err(Report::msg(format!(
"Specified config file does not exist: {}",
path.display()
))
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
}
} else {
Self::try_find_config()
}?;
}
},
)?;
Self::load_file(&config_path)
}
@@ -116,6 +132,7 @@ impl Config {
/// by checking each valid format's extension.
///
/// Returns the path of the first valid match, if any.
#[instrument]
fn try_find_config() -> Result<PathBuf> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
@@ -133,10 +150,15 @@ impl Config {
}
});
match file {
Some(file) => Ok(file),
None => Err(Report::msg("Could not find config file")),
}
file.map_or_else(
|| {
Err(Report::msg("Could not find config file")
.suggestion("Ironbar does not include a configuration out of the box")
.suggestion("A guide on writing a config can be found on the wiki:")
.suggestion("https://github.com/JakeStanger/ironbar/wiki/configuration-guide"))
},
Ok,
)
}
/// Loads the config file at the specified path
@@ -153,14 +175,7 @@ impl Config {
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
"corn" => {
// corn doesn't support deserialization yet
// so serialize the interpreted result then deserialize that
let file =
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
}
"corn" => libcorn::from_slice(&file).wrap_err("Invalid Corn config"),
_ => unreachable!(),
}
}

143
src/dynamic_string.rs Normal file
View File

@@ -0,0 +1,143 @@
use crate::script::{OutputStream, Script};
use gtk::prelude::*;
use indexmap::IndexMap;
use std::sync::{Arc, Mutex};
use tokio::spawn;
#[derive(Debug)]
enum DynamicStringSegment {
Static(String),
Dynamic(Script),
}
pub struct DynamicString {
// pub label: gtk::Label,
}
impl DynamicString {
pub fn new<F>(input: &str, f: F) -> Self
where
F: FnMut(String) -> Continue + 'static,
{
let mut segments = vec![];
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char = &chars[..=1];
let (token, skip) = if let ['{', '{'] = char {
const SKIP_BRACKETS: usize = 4;
let str = chars
.iter()
.skip(2)
.enumerate()
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
len + SKIP_BRACKETS,
)
} else {
let str = chars
.iter()
.enumerate()
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(DynamicStringSegment::Static(str), len)
};
assert_ne!(skip, 0);
segments.push(token);
chars.drain(..skip);
}
let label_parts = Arc::new(Mutex::new(IndexMap::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in segments.into_iter().enumerate() {
match segment {
DynamicStringSegment::Static(str) => {
label_parts
.lock()
.expect("Failed to get lock on label parts")
.insert(i, str);
}
DynamicStringSegment::Dynamic(script) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
spawn(async move {
script
.run(|(out, _)| {
if let OutputStream::Stdout(out) = out {
let mut label_parts = label_parts
.lock()
.expect("Failed to get lock on label parts");
label_parts
// .lock()
// .expect("Failed to get lock on label parts")
.insert(i, out);
let string = label_parts
.iter()
.map(|(_, part)| part.as_str())
.collect::<String>();
tx.send(string).expect("Failed to send update");
}
})
.await;
});
}
}
}
// initialize
{
let label_parts = label_parts
.lock()
.expect("Failed to get lock on label parts")
.iter()
.map(|(_, part)| part.as_str())
.collect::<String>();
tx.send(label_parts).expect("Failed to send update");
}
rx.attach(None, f);
// Self { label }
Self {}
}
}
#[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

@@ -68,17 +68,12 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
/// Attempts to get the icon name from the app's `.desktop` file.
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
match find_desktop_file(app_id) {
Some(file) => {
let map = parse_desktop_file(file);
match map {
Ok(map) => map.get("Icon").map(std::string::ToString::to_string),
Err(_) => None,
}
}
None => None,
}
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)
})
})
}
enum IconLocation {
@@ -137,11 +132,7 @@ pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
match icon_location {
Some(IconLocation::Theme(icon_name)) => {
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
match icon {
Ok(icon) => icon,
Err(_) => None,
}
icon.map_or(None, |icon| icon)
}
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
None => None,

View File

@@ -31,10 +31,15 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
/// The returned `WorkerGuard` must remain in scope
/// for the lifetime of the application for logging to file to work.
pub fn install_tracing() -> Result<WorkerGuard> {
const DEFAULT_LOG: &str = "info";
const DEFAULT_FILE_LOG: &str = "warn";
let fmt_layer = fmt::layer().with_target(true);
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
let file_filter_layer =
EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn"))?;
let filter_layer =
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
let file_filter_layer = EnvFilter::try_from_env("IRONBAR_FILE_LOG")
.or_else(|_| EnvFilter::try_new(DEFAULT_FILE_LOG))?;
let log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");

View File

@@ -1,14 +1,14 @@
mod bar;
mod bridge_channel;
mod collection;
mod clients;
mod config;
mod dynamic_string;
mod icon;
mod logging;
mod modules;
mod popup;
mod script;
mod style;
mod sway;
mod wayland;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
@@ -19,18 +19,26 @@ use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::{env, panic};
use tokio::runtime::Handle;
use tokio::task::block_in_place;
use crate::logging::install_tracing;
use clients::wayland::{self, WaylandClient};
use tracing::{debug, error, info};
use wayland::WaylandClient;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[repr(i32)]
enum ErrorCode {
GtkDisplay = 1,
CreateBars = 2,
Config = 3,
}
#[tokio::main]
async fn main() -> Result<()> {
// Disable backtraces by default
@@ -42,7 +50,15 @@ async fn main() -> Result<()> {
// otherwise file logging drops
let _guard = install_tracing()?;
color_eyre::install()?;
let hook_builder = color_eyre::config::HookBuilder::default();
let (panic_hook, eyre_hook) = hook_builder.into_hooks();
eyre_hook.install()?;
// custom hook allows tracing_appender to capture panics
panic::set_hook(Box::new(move |panic_info| {
error!("{}", panic_hook.panic_report(panic_info));
}));
info!("Ironbar version {}", VERSION);
info!("Starting application");
@@ -58,7 +74,7 @@ async fn main() -> Result<()> {
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(1)
exit(ErrorCode::GtkDisplay as i32)
},
|display| display,
);
@@ -67,30 +83,34 @@ async fn main() -> Result<()> {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
Config::default()
exit(ErrorCode::Config as i32)
}
};
debug!("Loaded config file");
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
error!("{:?}", err);
exit(2);
exit(ErrorCode::CreateBars as i32);
}
debug!("Created bars");
let style_path = config_dir().map_or_else(
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| {
let report = Report::msg("Failed to locate user config dir");
error!("{:?}", report);
exit(3);
config_dir().map_or_else(
|| {
let report = Report::msg("Failed to locate user config dir");
error!("{:?}", report);
exit(ErrorCode::CreateBars as i32);
},
|dir| dir.join("ironbar").join("style.css"),
)
},
|dir| dir.join("ironbar").join("style.css"),
PathBuf::from,
);
if style_path.exists() {
load_css(style_path);
debug!("Loaded CSS watcher file");
}
});
@@ -111,7 +131,7 @@ fn create_bars(
let outputs = wl.outputs.as_slice();
debug!("Received {} outputs from Wayland", outputs.len());
debug!("Output names: {:?}", outputs);
debug!("Outputs: {:?}", outputs);
let num_monitors = display.n_monitors();
@@ -122,7 +142,10 @@ fn create_bars(
// TODO: Could we use an Arc<Config> or `Cow<Config>` here to avoid cloning?
config.monitors.as_ref().map_or_else(
|| create_bar(app, &monitor, monitor_name, config.clone()),
|| {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())
},
|config| {
let config = config.get(monitor_name);
match &config {

View File

@@ -1,3 +1,4 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use chrono::{DateTime, Local};
@@ -18,7 +19,10 @@ pub struct ClockModule {
/// Detail on available tokens can be found here:
/// <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
#[serde(default = "default_format")]
pub(crate) format: String,
format: String,
#[serde(flatten)]
pub common: CommonConfig,
}
fn default_format() -> String {

256
src/modules/custom.rs Normal file
View File

@@ -0,0 +1,256 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::{ButtonGeometry, Popup};
use crate::script::Script;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct CustomModule {
/// Container class name
class: Option<String>,
/// Widgets to add to the bar container
bar: Vec<Widget>,
/// Widgets to add to the popup container
popup: Option<Vec<Widget>>,
#[serde(flatten)]
pub common: CommonConfig,
}
/// Attempts to parse an `Orientation` from `String`
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
}
}
/// Widget attributes
#[derive(Debug, Deserialize, Clone)]
pub struct Widget {
/// Type of GTK widget to add
#[serde(rename = "type")]
widget_type: WidgetType,
widgets: Option<Vec<Widget>>,
label: Option<String>,
name: Option<String>,
class: Option<String>,
on_click: Option<String>,
orientation: Option<String>,
}
/// Supported GTK widget types
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum WidgetType {
Box,
Label,
Button,
}
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
match self.widget_type {
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
WidgetType::Label => parent.add(&self.into_label()),
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
}
}
/// Creates a `gtk::Box` from this widget
fn into_box(self, tx: &Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
let mut builder = gtk::Box::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
if let Some(orientation) = self.orientation {
builder = builder
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
}
let container = builder.build();
if let Some(class) = self.class {
container.style_context().add_class(&class);
}
if let Some(widgets) = self.widgets {
widgets
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation));
}
container
}
/// Creates a `gtk::Label` from this widget
fn into_label(self) -> Label {
let mut builder = Label::builder().use_markup(true);
if let Some(name) = self.name {
builder = builder.name(&name);
}
let label = builder.build();
if let Some(class) = self.class {
label.style_context().add_class(&class);
}
let text = self.label.map_or_else(String::new, |text| text);
{
let label = label.clone();
DynamicString::new(&text, move |string| {
label.set_label(&string);
Continue(true)
});
}
label
// DynamicString::new(label, &text)
}
/// Creates a `gtk::Button` from this widget
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
let mut builder = Button::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
let button = builder.build();
if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
label.set_markup(&text);
button.add(&label);
}
if let Some(class) = self.class {
button.style_context().add_class(&class);
}
if let Some(exec) = self.on_click {
button.connect_clicked(move |button| {
tx.try_send(ExecEvent {
cmd: exec.clone(),
geometry: Popup::button_pos(button, bar_orientation),
})
.expect("Failed to send exec message");
});
}
button
}
}
#[derive(Debug)]
pub struct ExecEvent {
cmd: String,
geometry: ButtonGeometry,
}
impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
spawn(async move {
while let Some(event) = rx.recv().await {
if event.cmd.starts_with('!') {
let script = Script::from(&event.cmd[1..]);
debug!("executing command: '{}'", script.cmd);
// TODO: Migrate to use script.run
if let Err(err) = script.get_output().await {
error!("{err:?}");
}
} else if event.cmd == "popup:toggle" {
tx.send(ModuleUpdateEvent::TogglePopup(event.geometry))
.await
.expect("Failed to send open popup event");
} else if event.cmd == "popup:open" {
tx.send(ModuleUpdateEvent::OpenPopup(event.geometry))
.await
.expect("Failed to send open popup event");
} else if event.cmd == "popup:close" {
tx.send(ModuleUpdateEvent::ClosePopup)
.await
.expect("Failed to send open popup event");
} else {
error!("Received invalid command: '{}'", event.cmd);
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build();
if let Some(ref class) = self.class {
container.style_context().add_class(class);
}
self.bar.clone().into_iter().for_each(|widget| {
widget.add_to(&container, context.controller_tx.clone(), orientation);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx);
Ok(ModuleWidget {
widget: container,
popup,
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::builder().name("popup-custom").build();
if let Some(class) = self.class {
container
.style_context()
.add_class(format!("popup-{class}").as_str());
}
if let Some(popup) = self.popup {
popup
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
}
Some(container)
}
}

View File

@@ -1,6 +1,7 @@
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::wayland::ToplevelChange;
use crate::{await_sync, icon, wayland};
use crate::{await_sync, icon};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
@@ -23,6 +24,9 @@ pub struct FocusedModule {
icon_size: i32,
/// GTK icon theme to use.
icon_theme: Option<String>,
#[serde(flatten)]
pub common: CommonConfig,
}
const fn default_icon_size() -> i32 {
@@ -47,10 +51,10 @@ impl Module<gtk::Box> for FocusedModule {
.expect("Failed to get read lock on toplevels")
.clone();
toplevels.into_iter().find(|(top, _)| top.active)
toplevels.into_iter().find(|(_, (top, _))| top.active)
});
if let Some((top, _)) = focused {
if let Some((_, (top, _))) = focused {
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
}

View File

@@ -1,12 +1,12 @@
use super::open_state::OpenState;
use crate::collection::Collection;
use crate::clients::wayland::ToplevelInfo;
use crate::icon::get_icon;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::wayland::ToplevelInfo;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Orientation};
use indexmap::IndexMap;
use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
@@ -16,17 +16,17 @@ pub struct Item {
pub app_id: String,
pub favorite: bool,
pub open_state: OpenState,
pub windows: Collection<usize, Window>,
pub windows: IndexMap<usize, Window>,
pub name: String,
}
impl Item {
pub const fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
Self {
app_id,
favorite,
open_state,
windows: Collection::new(),
windows: IndexMap::new(),
name: String::new(),
}
}
@@ -78,7 +78,7 @@ impl Item {
&self
.windows
.iter()
.map(|win| &win.open_state)
.map(|(_, win)| &win.open_state)
.collect::<Vec<_>>(),
);
self.open_state = new_state;
@@ -91,7 +91,7 @@ impl From<ToplevelInfo> for Item {
let name = toplevel.title.clone();
let app_id = toplevel.app_id.clone();
let mut windows = Collection::new();
let mut windows = IndexMap::new();
windows.insert(toplevel.id, toplevel.into());
Self {

View File

@@ -3,15 +3,15 @@ mod open_state;
use self::item::{Item, ItemButton, Window};
use self::open_state::OpenState;
use crate::collection::Collection;
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig;
use crate::icon::find_desktop_file;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::wayland;
use crate::wayland::ToplevelChange;
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap;
use serde::Deserialize;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
@@ -34,6 +34,9 @@ pub struct LauncherModule {
/// Name of the GTK icon theme to use.
icon_theme: Option<String>,
#[serde(flatten)]
pub common: CommonConfig,
}
#[derive(Debug, Clone)]
@@ -81,18 +84,20 @@ impl Module<gtk::Box> for LauncherModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> crate::Result<()> {
let items = match &self.favorites {
Some(favorites) => favorites
.iter()
.map(|app_id| {
(
app_id.to_string(),
Item::new(app_id.to_string(), OpenState::Closed, true),
)
})
.collect::<Collection<_, _>>(),
None => Collection::new(),
};
let items = self
.favorites
.as_ref()
.map_or_else(IndexMap::new, |favorites| {
favorites
.iter()
.map(|app_id| {
(
app_id.to_string(),
Item::new(app_id.to_string(), OpenState::Closed, true),
)
})
.collect::<IndexMap<_, _>>()
});
let items = Arc::new(Mutex::new(items));
@@ -108,7 +113,7 @@ impl Module<gtk::Box> for LauncherModule {
let mut items = items.lock().expect("Failed to get lock on items");
for (window, _) in open_windows.clone() {
for (_, (window, _)) in open_windows.clone() {
let item = items.get_mut(&window.app_id);
match item {
Some(item) => {
@@ -121,7 +126,7 @@ impl Module<gtk::Box> for LauncherModule {
}
let items = items.iter();
for item in items {
for (_, item) in items {
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
item.clone(),
)))?;
@@ -282,7 +287,7 @@ impl Module<gtk::Box> for LauncherModule {
let id = match event {
ItemEvent::FocusItem(app_id) => items
.get(&app_id)
.and_then(|item| item.windows.first().map(|win| win.id)),
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
ItemEvent::FocusWindow(id) => Some(id),
ItemEvent::OpenItem(_) => unreachable!(),
};
@@ -325,7 +330,7 @@ impl Module<gtk::Box> for LauncherModule {
let show_icons = self.show_icons;
let orientation = info.bar_position.get_orientation();
let mut buttons = Collection::<String, ItemButton>::new();
let mut buttons = IndexMap::<String, ItemButton>::new();
let controller_tx2 = context.controller_tx.clone();
context.widget_rx.attach(None, move |event| {
@@ -427,11 +432,12 @@ impl Module<gtk::Box> for LauncherModule {
.name("popup-launcher")
.build();
// we need some content to force the container to have a size
let placeholder = Button::with_label("PLACEHOLDER");
placeholder.set_width_request(MAX_WIDTH);
container.add(&placeholder);
let mut buttons = Collection::<String, Collection<usize, Button>>::new();
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
{
let container = container.clone();
@@ -439,11 +445,12 @@ impl Module<gtk::Box> for LauncherModule {
match event {
LauncherUpdate::AddItem(item) => {
let app_id = item.app_id.clone();
trace!("Adding item with id '{app_id}' to the popup: {item:?}");
let window_buttons = item
.windows
.into_iter()
.map(|win| {
.map(|(_, win)| {
let button = Button::builder()
.label(&clamp(&win.name))
.height_request(40)
@@ -468,6 +475,11 @@ impl Module<gtk::Box> for LauncherModule {
buttons.insert(app_id, window_buttons);
}
LauncherUpdate::AddWindow(app_id, win) => {
debug!(
"Adding new window to popup for '{app_id}': '{}' ({})",
win.name, win.id
);
if let Some(buttons) = buttons.get_mut(&app_id) {
let button = Button::builder()
.height_request(40)
@@ -490,11 +502,17 @@ impl Module<gtk::Box> for LauncherModule {
}
}
LauncherUpdate::RemoveWindow(app_id, win_id) => {
debug!("Removing window from popup for '{app_id}': {win_id}");
if let Some(buttons) = buttons.get_mut(&app_id) {
buttons.remove(&win_id);
}
}
LauncherUpdate::Title(app_id, win_id, title) => {
debug!(
"Updating window title on popup for '{app_id}'/{win_id} to '{title}'"
);
if let Some(buttons) = buttons.get_mut(&app_id) {
if let Some(button) = buttons.get(&win_id) {
button.set_label(&title);
@@ -509,7 +527,7 @@ impl Module<gtk::Box> for LauncherModule {
// add app's buttons
if let Some(buttons) = buttons.get(&app_id) {
for button in buttons {
for (_, button) in buttons {
button.style_context().add_class("popup-item");
container.add(button);
}

View File

@@ -1,4 +1,4 @@
use crate::wayland::ToplevelInfo;
use crate::clients::wayland::ToplevelInfo;
/// Open state for a launcher item, or item window.
#[derive(Debug, Clone, Eq, PartialEq, Copy)]

View File

@@ -5,6 +5,7 @@
/// Clicking the widget opens a popup containing the current time
/// with second-level precision and a calendar.
pub mod clock;
pub mod custom;
pub mod focused;
pub mod launcher;
pub mod mpd;

View File

@@ -1,7 +1,5 @@
mod client;
use crate::modules::mpd::client::MpdConnectionError;
use crate::modules::mpd::client::{get_client, get_duration, get_elapsed};
use crate::clients::mpd::{get_client, get_duration, get_elapsed, MpdConnectionError};
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use color_eyre::Result;
@@ -9,7 +7,7 @@ use dirs::{audio_dir, home_dir};
use glib::Continue;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{Button, Image, Label, Orientation};
use gtk::{Button, Image, Label, Orientation, Scale};
use mpd_client::commands;
use mpd_client::responses::{PlayState, Song, Status};
use mpd_client::tag::Tag;
@@ -26,16 +24,20 @@ pub enum PlayerCommand {
Previous,
Toggle,
Next,
Volume(u8),
}
#[derive(Debug, Deserialize, Clone)]
pub struct Icons {
/// Icon to display when playing.
#[serde(default = "default_icon_play")]
play: Option<String>,
play: String,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")]
pause: Option<String>,
pause: String,
/// Icon to display under volume slider
#[serde(default = "default_icon_volume")]
volume: String,
}
impl Default for Icons {
@@ -43,6 +45,7 @@ impl Default for Icons {
Self {
pause: default_icon_pause(),
play: default_icon_play(),
volume: default_icon_volume(),
}
}
}
@@ -63,6 +66,9 @@ pub struct MpdModule {
/// Path to root of music directory.
#[serde(default = "default_music_dir")]
music_dir: PathBuf,
#[serde(flatten)]
pub common: CommonConfig,
}
fn default_socket() -> String {
@@ -73,14 +79,16 @@ fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_play() -> Option<String> {
Some(String::from(""))
fn default_icon_play() -> String {
String::from("")
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_pause() -> Option<String> {
Some(String::from(""))
fn default_icon_pause() -> String {
String::from("")
}
fn default_icon_volume() -> String {
String::from("")
}
fn default_music_dir() -> PathBuf {
@@ -90,10 +98,7 @@ fn default_music_dir() -> PathBuf {
/// Attempts to read the first value for a tag
/// (since the MPD client returns a vector of tags, or None)
pub fn try_get_first_tag(vec: Option<&Vec<String>>) -> Option<&str> {
match vec {
Some(vec) => vec.first().map(String::as_str),
None => None,
}
vec.and_then(|vec| vec.first().map(String::as_str))
}
/// Formats a duration given in seconds
@@ -186,6 +191,7 @@ impl Module<Button> for MpdModule {
Err(err) => Err(err),
},
PlayerCommand::Next => client.command(commands::Next).await,
PlayerCommand::Volume(vol) => client.command(commands::SetVolume(vol)).await,
};
if let Err(err) = res {
@@ -211,18 +217,21 @@ impl Module<Button> for MpdModule {
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
context
.tx
.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
{
let tx = context.tx.clone();
button.connect_clicked(move |button| {
tx.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
button,
orientation,
)))
.expect("Failed to send MPD popup open event");
});
});
}
{
let button = button.clone();
let tx = context.tx.clone();
context.widget_rx.attach(None, move |mut event| {
if let Some(event) = event.take() {
@@ -230,6 +239,8 @@ impl Module<Button> for MpdModule {
button.show();
} else {
button.hide();
tx.try_send(ModuleUpdateEvent::ClosePopup)
.expect("Failed to send close popup message");
}
Continue(true)
@@ -285,8 +296,25 @@ impl Module<Button> for MpdModule {
info_box.add(&controls_box);
let volume_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.spacing(5)
.name("volume")
.build();
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true);
volume_slider.set_widget_name("scale");
let volume_icon = Label::new(Some(&self.icons.volume));
volume_icon.style_context().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);
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
@@ -302,13 +330,22 @@ impl Module<Button> for MpdModule {
.expect("Failed to send play/pause track message");
});
let tx_next = tx;
let tx_next = tx.clone();
btn_next.connect_clicked(move |_| {
tx_next
.try_send(PlayerCommand::Next)
.expect("Failed to send next track message");
});
let tx_vol = tx;
volume_slider.connect_change_value(move |_, _, val| {
tx_vol
.try_send(PlayerCommand::Volume(val as u8))
.expect("Failed to send volume message");
Inhibit(false)
});
container.show_all();
{
@@ -330,11 +367,14 @@ impl Module<Button> for MpdModule {
.join("cover.jpg"),
);
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
album_image.set_from_pixbuf(Some(&pixbuf));
} else {
album_image.set_from_pixbuf(None);
}
Pixbuf::from_file_at_scale(cover_path, 128, 128, true).map_or_else(
|_| {
album_image.set_from_pixbuf(None);
},
|pixbuf| {
album_image.set_from_pixbuf(Some(&pixbuf));
},
);
}
title_label
@@ -371,6 +411,8 @@ impl Module<Button> for MpdModule {
btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next);
volume_slider.set_value(update.status.volume as f64);
}
Continue(true)
@@ -406,8 +448,8 @@ fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) ->
"icon" => {
let icon = match status.state {
PlayState::Stopped => None,
PlayState::Playing => icons.play.as_ref(),
PlayState::Paused => icons.pause.as_ref(),
PlayState::Playing => Some(&icons.play),
PlayState::Paused => Some(&icons.pause),
};
icon.map(String::as_str)
}

View File

@@ -1,21 +1,32 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
use crate::script::{OutputStream, Script, ScriptMode};
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use std::process::Command;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::sleep;
use tracing::{error, instrument};
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule {
/// Path to script to execute.
path: String,
cmd: String,
/// Script execution mode
#[serde(default = "default_mode")]
mode: ScriptMode,
/// Time in milliseconds between executions.
#[serde(default = "default_interval")]
interval: u64,
#[serde(flatten)]
pub common: CommonConfig,
}
/// `Mode::Poll`
const fn default_mode() -> ScriptMode {
ScriptMode::Poll
}
/// 5000ms
@@ -23,6 +34,16 @@ const fn default_interval() -> u64 {
5000
}
impl From<&ScriptModule> for Script {
fn from(module: &ScriptModule) -> Self {
Self {
mode: module.mode,
cmd: module.cmd.clone(),
interval: module.interval,
}
}
}
impl Module<Label> for ScriptModule {
type SendMessage = String;
type ReceiveMessage = ();
@@ -33,20 +54,21 @@ impl Module<Label> for ScriptModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let interval = self.interval;
let path = self.path.clone();
spawn(async move {
loop {
match run_script(&path) {
Ok(stdout) => tx
.send(ModuleUpdateEvent::Update(stdout))
.await
.expect("Failed to send stdout"),
Err(err) => error!("{:?}", err),
}
let script: Script = self.into();
sleep(tokio::time::Duration::from_millis(interval)).await;
}
spawn(async move {
script.run(move |(out, _)| match out {
OutputStream::Stdout(stdout) => {
tx.try_send(ModuleUpdateEvent::Update(stdout))
.expect("Failed to send stdout"); }
OutputStream::Stderr(stderr) => {
error!("{:?}", Report::msg(stderr)
.wrap_err("Watched script error:")
.suggestion("Check the path to your script")
.suggestion("Check the script for errors")
.suggestion("If you expect the script to write to stderr, consider redirecting its output to /dev/null to suppress these messages"));
}
}).await;
});
Ok(())
@@ -63,7 +85,7 @@ impl Module<Label> for ScriptModule {
{
let label = label.clone();
context.widget_rx.attach(None, move |s| {
label.set_label(s.as_str());
label.set_markup(s.as_str());
Continue(true)
});
}
@@ -74,29 +96,3 @@ impl Module<Label> for ScriptModule {
})
}
}
#[instrument]
fn run_script(path: &str) -> Result<String> {
let output = Command::new("sh")
.arg("-c")
.arg(path)
.output()
.wrap_err("Failed to get script output")?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.wrap_err("Script stdout not valid UTF-8")?;
Ok(stdout)
} else {
let stderr = String::from_utf8(output.stderr)
.map(|output| output.trim().to_string())
.wrap_err("Script stderr not valid UTF-8")?;
Err(Report::msg(stderr)
.wrap_err("Script returned non-zero error code")
.suggestion("Check the path to your script")
.suggestion("Check the script for errors"))
}
}

View File

@@ -1,3 +1,4 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use color_eyre::Result;
use gtk::prelude::*;
@@ -5,8 +6,10 @@ use gtk::Label;
use regex::{Captures, Regex};
use serde::Deserialize;
use std::collections::HashMap;
use sysinfo::{CpuExt, System, SystemExt};
use std::time::Duration;
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::sleep;
@@ -14,6 +17,99 @@ use tokio::time::sleep;
pub struct SysInfoModule {
/// List of formatting strings.
format: Vec<String>,
/// Number of seconds between refresh
#[serde(default = "Interval::default")]
interval: Interval,
#[serde(flatten)]
pub common: CommonConfig,
}
#[derive(Debug, Deserialize, Copy, Clone)]
pub struct Intervals {
#[serde(default = "default_interval")]
memory: u64,
#[serde(default = "default_interval")]
cpu: u64,
#[serde(default = "default_interval")]
temps: u64,
#[serde(default = "default_interval")]
disks: u64,
#[serde(default = "default_interval")]
networks: u64,
#[serde(default = "default_interval")]
system: u64,
}
#[derive(Debug, Deserialize, Copy, Clone)]
#[serde(untagged)]
pub enum Interval {
All(u64),
Individual(Intervals),
}
impl Default for Interval {
fn default() -> Self {
Self::All(default_interval())
}
}
impl Interval {
const fn memory(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.memory,
}
}
const fn cpu(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.cpu,
}
}
const fn temps(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.temps,
}
}
const fn disks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.disks,
}
}
const fn networks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.networks,
}
}
const fn system(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.system,
}
}
}
const fn default_interval() -> u64 {
5
}
#[derive(Debug)]
enum RefreshType {
Memory,
Cpu,
Temps,
Disks,
Network,
System,
}
impl Module<gtk::Box> for SysInfoModule {
@@ -26,35 +122,115 @@ impl Module<gtk::Box> for SysInfoModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let interval = self.interval;
let refresh_kind = RefreshKind::everything()
.without_processes()
.without_users_list();
let mut sys = System::new_with_specifics(refresh_kind);
sys.refresh_components_list();
sys.refresh_disks_list();
sys.refresh_networks_list();
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
// memory refresh
{
let tx = refresh_tx.clone();
spawn(async move {
loop {
tx.send(RefreshType::Memory)
.await
.expect("Failed to send memory refresh");
sleep(Duration::from_secs(interval.memory())).await;
}
});
}
// cpu refresh
{
let tx = refresh_tx.clone();
spawn(async move {
loop {
tx.send(RefreshType::Cpu)
.await
.expect("Failed to send cpu refresh");
sleep(Duration::from_secs(interval.cpu())).await;
}
});
}
// temp refresh
{
let tx = refresh_tx.clone();
spawn(async move {
loop {
tx.send(RefreshType::Temps)
.await
.expect("Failed to send temperature refresh");
sleep(Duration::from_secs(interval.temps())).await;
}
});
}
// disk refresh
{
let tx = refresh_tx.clone();
spawn(async move {
loop {
tx.send(RefreshType::Disks)
.await
.expect("Failed to send disk refresh");
sleep(Duration::from_secs(interval.disks())).await;
}
});
}
// network refresh
{
let tx = refresh_tx.clone();
spawn(async move {
loop {
tx.send(RefreshType::Network)
.await
.expect("Failed to send network refresh");
sleep(Duration::from_secs(interval.networks())).await;
}
});
}
// system refresh
{
let tx = refresh_tx;
spawn(async move {
loop {
tx.send(RefreshType::System)
.await
.expect("Failed to send system refresh");
sleep(Duration::from_secs(interval.system())).await;
}
});
}
spawn(async move {
let mut sys = System::new_all();
let mut format_info = HashMap::new();
loop {
sys.refresh_all();
while let Some(refresh) = refresh_rx.recv().await {
match refresh {
RefreshType::Memory => refresh_memory_tokens(&mut format_info, &mut sys),
RefreshType::Cpu => refresh_cpu_tokens(&mut format_info, &mut sys),
RefreshType::Temps => refresh_temp_tokens(&mut format_info, &mut sys),
RefreshType::Disks => refresh_disk_tokens(&mut format_info, &mut sys),
RefreshType::Network => {
refresh_network_tokens(&mut format_info, &mut sys, interval.networks());
}
RefreshType::System => refresh_system_tokens(&mut format_info, &sys),
};
let mut format_info = HashMap::new();
let actual_used_memory = sys.total_memory() - sys.available_memory();
let memory_percent = actual_used_memory as f64 / sys.total_memory() as f64 * 100.0;
let cpu_percent = sys.global_cpu_info().cpu_usage();
// TODO: Add remaining format info
format_info.insert(
String::from("memory-percent"),
format!("{:0>2.0}", memory_percent),
);
format_info.insert(
String::from("cpu-percent"),
format!("{:0>2.0}", cpu_percent),
);
tx.send(ModuleUpdateEvent::Update(format_info))
tx.send(ModuleUpdateEvent::Update(format_info.clone()))
.await
.expect("Failed to send system info map");
sleep(tokio::time::Duration::from_secs(1)).await;
}
});
@@ -66,14 +242,18 @@ impl Module<gtk::Box> for SysInfoModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let re = Regex::new(r"\{([\w-]+)}")?;
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
let mut labels = Vec::new();
for format in &self.format {
let label = Label::builder().label(format).name("item").build();
let label = Label::builder()
.label(format)
.use_markup(true)
.name("item")
.build();
label.set_angle(info.bar_position.get_angle());
container.add(&label);
labels.push(label);
@@ -83,13 +263,13 @@ impl Module<gtk::Box> for SysInfoModule {
let formats = self.format;
context.widget_rx.attach(None, move |info| {
for (format, label) in formats.iter().zip(labels.clone()) {
let format_compiled = re.replace(format, |caps: &Captures| {
let format_compiled = re.replace_all(format, |caps: &Captures| {
info.get(&caps[1])
.unwrap_or(&caps[0].to_string())
.to_string()
});
label.set_text(format_compiled.as_ref());
label.set_markup(format_compiled.as_ref());
}
Continue(true)
@@ -102,3 +282,175 @@ impl Module<gtk::Box> for SysInfoModule {
})
}
}
fn refresh_memory_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_memory();
let total_memory = sys.total_memory();
let available_memory = sys.available_memory();
let actual_used_memory = total_memory - available_memory;
let memory_percent = actual_used_memory as f64 / total_memory as f64 * 100.0;
format_info.insert(
String::from("memory_free"),
(bytes_to_gigabytes(available_memory)).to_string(),
);
format_info.insert(
String::from("memory_used"),
(bytes_to_gigabytes(actual_used_memory)).to_string(),
);
format_info.insert(
String::from("memory_total"),
(bytes_to_gigabytes(total_memory)).to_string(),
);
format_info.insert(
String::from("memory_percent"),
format!("{:0>2.0}", memory_percent),
);
let used_swap = sys.used_swap();
let total_swap = sys.total_swap();
format_info.insert(
String::from("swap_free"),
(bytes_to_gigabytes(sys.free_swap())).to_string(),
);
format_info.insert(
String::from("swap_used"),
(bytes_to_gigabytes(used_swap)).to_string(),
);
format_info.insert(
String::from("swap_total"),
(bytes_to_gigabytes(total_swap)).to_string(),
);
format_info.insert(
String::from("swap_percent"),
format!("{:0>2.0}", used_swap as f64 / total_swap as f64 * 100.0),
);
}
fn refresh_cpu_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_cpu();
let cpu_info = sys.global_cpu_info();
let cpu_percent = cpu_info.cpu_usage();
format_info.insert(
String::from("cpu_percent"),
format!("{:0>2.0}", cpu_percent),
);
}
fn refresh_temp_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_components();
let components = sys.components();
for component in components {
let key = component.label().replace(' ', "-");
let temp = component.temperature();
format_info.insert(format!("temp_c:{key}"), format!("{temp:.0}"));
format_info.insert(format!("temp_f:{key}"), format!("{:.0}", c_to_f(temp)));
}
}
fn refresh_disk_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_disks();
for disk in sys.disks() {
// replace braces to avoid conflict with regex
let key = disk
.mount_point()
.to_str()
.map(|s| s.replace(['{', '}'], ""));
if let Some(key) = key {
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
format_info.insert(
format!("disk_free:{key}"),
bytes_to_gigabytes(available).to_string(),
);
format_info.insert(
format!("disk_used:{key}"),
bytes_to_gigabytes(used).to_string(),
);
format_info.insert(
format!("disk_total:{key}"),
bytes_to_gigabytes(total).to_string(),
);
format_info.insert(
format!("disk_percent:{key}"),
format!("{:0>2.0}", used as f64 / total as f64 * 100.0),
);
}
}
}
fn refresh_network_tokens(
format_info: &mut HashMap<String, String>,
sys: &mut System,
interval: u64,
) {
sys.refresh_networks();
for (iface, network) in sys.networks() {
format_info.insert(
format!("net_down:{iface}"),
format!("{:0>2.0}", bytes_to_megabits(network.received()) / interval),
);
format_info.insert(
format!("net_up:{iface}"),
format!(
"{:0>2.0}",
bytes_to_megabits(network.transmitted()) / interval
),
);
}
}
fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System) {
// no refresh required for these tokens
let load_average = sys.load_average();
format_info.insert(String::from("load_average:1"), load_average.one.to_string());
format_info.insert(
String::from("load_average:5"),
load_average.five.to_string(),
);
format_info.insert(
String::from("load_average:15"),
load_average.fifteen.to_string(),
);
let uptime = Duration::from_secs(sys.uptime()).as_secs();
let hours = uptime / 3600;
format_info.insert(
String::from("uptime"),
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60),
);
}
/// Converts celsius to fahrenheit.
fn c_to_f(c: f32) -> f32 {
c * 9.0 / 5.0 + 32.0
}
const fn bytes_to_gigabytes(b: u64) -> u64 {
const BYTES_IN_GIGABYTE: u64 = 1_000_000_000;
b / BYTES_IN_GIGABYTE
}
const fn bytes_to_megabits(b: u64) -> u64 {
const BYTES_IN_MEGABIT: u64 = 125_000;
b / BYTES_IN_MEGABIT
}

View File

@@ -1,7 +1,6 @@
mod client;
use crate::await_sync;
use crate::modules::tray::client::get_tray_event_client;
use crate::clients::system_tray::get_tray_event_client;
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use color_eyre::Result;
use gtk::prelude::*;
@@ -16,14 +15,17 @@ use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
#[derive(Debug, Deserialize, Clone)]
pub struct TrayModule;
pub struct TrayModule {
#[serde(flatten)]
pub common: CommonConfig,
}
/// Gets a GTK `Image` component
/// for the status notifier item's icon.
fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
item.icon_theme_path.as_ref().and_then(|path| {
let theme = IconTheme::new();
theme.append_search_path(&path);
theme.append_search_path(path);
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());

View File

@@ -1,6 +1,7 @@
use crate::await_sync;
use crate::clients::sway::{get_client, get_sub_client};
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::sway::{get_client, get_sub_client};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Button;
@@ -19,6 +20,9 @@ pub struct WorkspacesModule {
/// Whether to display buttons for all monitors.
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
#[serde(flatten)]
pub common: CommonConfig,
}
#[derive(Clone, Debug)]

354
src/script.rs Normal file
View File

@@ -0,0 +1,354 @@
use color_eyre::eyre::WrapErr;
use color_eyre::{Report, Result};
use serde::Deserialize;
use std::fmt::{Display, Formatter};
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tokio::{select, spawn};
use tracing::{error, warn};
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum ScriptInput {
String(String),
Struct(Script),
}
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ScriptMode {
Poll,
Watch,
}
#[derive(Debug, Clone)]
pub enum OutputStream {
Stdout(String),
Stderr(String),
}
impl From<&str> for ScriptMode {
fn from(str: &str) -> Self {
match str {
"poll" | "p" => Self::Poll,
"watch" | "w" => Self::Watch,
_ => {
warn!("Invalid script mode: '{str}', falling back to polling");
ScriptMode::Poll
}
}
}
}
impl Default for ScriptMode {
fn default() -> Self {
Self::Poll
}
}
impl Display for ScriptMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
ScriptMode::Poll => "poll",
ScriptMode::Watch => "watch",
}
)
}
}
impl ScriptMode {
fn try_parse(str: &str) -> Result<Self> {
match str {
"poll" | "p" => Ok(Self::Poll),
"watch" | "w" => Ok(Self::Watch),
_ => Err(Report::msg(format!("Invalid script mode: {str}"))),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Script {
#[serde(default = "ScriptMode::default")]
pub(crate) mode: ScriptMode,
pub cmd: String,
#[serde(default = "default_interval")]
pub(crate) interval: u64,
}
const fn default_interval() -> u64 {
5000
}
impl Default for Script {
fn default() -> Self {
Self {
mode: ScriptMode::default(),
interval: default_interval(),
cmd: String::new(),
}
}
}
impl From<ScriptInput> for Script {
fn from(input: ScriptInput) -> Self {
match input {
ScriptInput::String(string) => Self::from(string.as_str()),
ScriptInput::Struct(script) => script,
}
}
}
#[derive(Debug)]
enum ScriptInputToken {
Mode(ScriptMode),
Interval(u64),
Cmd(String),
Colon,
}
impl From<&str> for Script {
fn from(str: &str) -> Self {
let mut script = Self::default();
let mut tokens = vec![];
let mut chars = str.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char = chars[0];
let (token, skip) = match char {
':' => (ScriptInputToken::Colon, 1),
// interval
'0'..='9' => {
let interval_str = chars
.iter()
.take_while(|c| c.is_ascii_digit())
.collect::<String>();
(
ScriptInputToken::Interval(
interval_str.parse::<u64>().expect("Invalid interval"),
),
interval_str.len(),
)
}
// watching or polling
'w' | 'p' => {
let mode_str = chars.iter().take_while(|&c| c != &':').collect::<String>();
let len = mode_str.len();
let token = ScriptMode::try_parse(&mode_str)
.map_or(ScriptInputToken::Cmd(mode_str), |mode| {
ScriptInputToken::Mode(mode)
});
(token, len)
}
_ => {
let cmd_str = chars.iter().take_while(|_| true).collect::<String>();
let len = cmd_str.len();
(ScriptInputToken::Cmd(cmd_str), len)
}
};
tokens.push(token);
chars.drain(..skip);
}
for token in tokens {
match token {
ScriptInputToken::Mode(mode) => script.mode = mode,
ScriptInputToken::Interval(interval) => script.interval = interval,
ScriptInputToken::Cmd(cmd) => script.cmd = cmd,
ScriptInputToken::Colon => {}
}
}
script
}
}
impl Script {
pub fn new_polling(input: ScriptInput) -> Self {
let mut script = Self::from(input);
script.mode = ScriptMode::Poll;
script
}
pub async fn run<F>(&self, callback: F)
where
F: Fn((OutputStream, bool)),
{
loop {
match self.mode {
ScriptMode::Poll => match self.get_output().await {
Ok(output) => callback(output),
Err(err) => error!("{err:?}"),
},
ScriptMode::Watch => match self.spawn().await {
Ok(mut rx) => {
while let Some(msg) = rx.recv().await {
callback((msg, true));
}
}
Err(err) => error!("{err:?}"),
},
};
sleep(tokio::time::Duration::from_millis(self.interval)).await;
}
}
/// Attempts to execute a given command,
/// waiting for it to finish.
/// If the command returns status 0,
/// the `stdout` is returned.
/// Otherwise, an `Err` variant
/// containing the `stderr` is returned.
pub async fn get_output(&self) -> Result<(OutputStream, bool)> {
let output = Command::new("sh")
.args(["-c", &self.cmd])
.output()
.await
.wrap_err("Failed to get script output")?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.wrap_err("Script stdout not valid UTF-8")?;
Ok((OutputStream::Stdout(stdout), true))
} else {
let stderr = String::from_utf8(output.stderr)
.map(|output| output.trim().to_string())
.wrap_err("Script stderr not valid UTF-8")?;
Ok((OutputStream::Stderr(stderr), false))
}
}
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
let mut handle = Command::new("sh")
.args(["-c", &self.cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.spawn()?;
let mut stdout_lines = BufReader::new(
handle
.stdout
.take()
.expect("Failed to take script handle stdout"),
)
.lines();
let mut stderr_lines = BufReader::new(
handle
.stderr
.take()
.expect("Failed to take script handle stderr"),
)
.lines();
let (tx, rx) = mpsc::channel(32);
spawn(async move {
loop {
select! {
_ = handle.wait() => break,
Ok(Some(line)) = stdout_lines.next_line() => {
tx.send(OutputStream::Stdout(line)).await.expect("Failed to send stdout");
}
Ok(Some(line)) = stderr_lines.next_line() => {
tx.send(OutputStream::Stderr(line)).await.expect("Failed to send stderr");
}
}
}
});
Ok(rx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_basic() {
let cmd = "echo 'hello'";
let script = Script::from(cmd);
assert_eq!(script.cmd, cmd);
assert_eq!(script.interval, default_interval());
assert_eq!(script.mode, ScriptMode::default());
}
#[test]
fn test_parse_full() {
let cmd = "echo 'hello'";
let mode = ScriptMode::Watch;
let interval = 300;
let full_cmd = format!("{mode}:{interval}:{cmd}");
let script = Script::from(full_cmd.as_str());
assert_eq!(script.cmd, cmd);
assert_eq!(script.mode, mode);
assert_eq!(script.interval, interval);
}
#[test]
fn test_parse_interval_and_cmd() {
let cmd = "echo 'hello'";
let interval = 300;
let full_cmd = format!("{interval}:{cmd}");
let script = Script::from(full_cmd.as_str());
assert_eq!(script.cmd, cmd);
assert_eq!(script.interval, interval);
assert_eq!(script.mode, ScriptMode::default());
}
#[test]
fn test_parse_mode_and_cmd() {
let cmd = "echo 'hello'";
let mode = ScriptMode::Watch;
let full_cmd = format!("{mode}:{cmd}");
let script = Script::from(full_cmd.as_str());
assert_eq!(script.cmd, cmd);
assert_eq!(script.interval, default_interval());
assert_eq!(script.mode, mode);
}
#[test]
fn test_parse_cmd_with_colon() {
let cmd = "uptime | awk '{print \"Uptime: \" $1}'";
let script = Script::from(cmd);
assert_eq!(script.cmd, cmd);
assert_eq!(script.interval, default_interval());
assert_eq!(script.mode, ScriptMode::default());
}
#[test]
fn test_no_cmd() {
let mode = ScriptMode::Watch;
let interval = 300;
let full_cmd = format!("{mode}:{interval}");
let script = Script::from(full_cmd.as_str());
assert_eq!(script.cmd, ""); // TODO: Probably better handle this case
assert_eq!(script.interval, interval);
assert_eq!(script.mode, mode);
}
}

View File

@@ -2,10 +2,13 @@ use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext};
use notify::{Event, RecursiveMode, Result, Watcher};
use notify::event::{DataChange, ModifyKind};
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher};
use std::path::PathBuf;
use std::time::Duration;
use tokio::spawn;
use tracing::{error, info};
use tokio::time::sleep;
use tracing::{debug, error, info};
/// Attempts to load CSS file at the given path
/// and attach if to the current GTK application.
@@ -15,13 +18,14 @@ use tracing::{error, info};
pub fn load_css(style_path: PathBuf) {
let provider = CssProvider::new();
if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
error!("{:?}", Report::new(err)
match provider.load_from_file(&gio::File::for_path(&style_path)) {
Ok(()) => debug!("Loaded css from '{}'", style_path.display()),
Err(err) => error!("{:?}", Report::new(err)
.wrap_err("Failed to load CSS")
.suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
);
}
)
};
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(&screen, &provider, 800);
@@ -29,24 +33,27 @@ pub fn load_css(style_path: PathBuf) {
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
match notify::recommended_watcher(move |res: Result<Event>| match res {
Ok(event) => {
let mut watcher = recommended_watcher(move |res: Result<Event>| match res {
Ok(event) if event.kind == EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
debug!("{event:?}");
if let Some(path) = event.paths.first() {
tx.send(path.clone())
.expect("Failed to send style changed message");
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
}) {
Ok(mut watcher) => {
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.expect("Unexpected error when attempting to watch CSS");
}
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err("Failed to start CSS watcher")
),
_ => {}
})
.expect("Failed to create CSS file watcher");
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.expect("Failed to start CSS file watcher");
debug!("Installed CSS file watcher on '{}'", style_path.display());
// avoid watcher from dropping
loop {
sleep(Duration::from_secs(1)).await;
}
});