33 Commits

Author SHA1 Message Date
Jake Stanger
1d7c3772e4 chore(release): v0.4.0 2022-08-22 23:09:01 +01:00
Jake Stanger
f2ee2dfe7a fix: error when using with swaybar_command 2022-08-22 23:08:41 +01:00
Jake Stanger
ab8f7ecfc8 feat: logging support and proper error handling 2022-08-21 23:36:51 +01:00
Jake Stanger
917838c98c ci(deploy): fix reading wrong branch for changelog 2022-08-15 21:26:13 +01:00
Jake Stanger
5ec46b2a2a ci(build): change job name 2022-08-15 21:26:01 +01:00
Jake Stanger
789d54e2ea ci: add deploy job 2022-08-15 21:23:54 +01:00
Jake Stanger
be726ddde8 ci(build): always use latest toolchain 2022-08-15 21:23:47 +01:00
Jake Stanger
96ff974b82 ci(build): fix clippy, add fmt check 2022-08-15 21:16:18 +01:00
Jake Stanger
226bca632e chore(release): v0.3.0 2022-08-15 21:12:15 +01:00
Jake Stanger
446720c578 chore(intellij): config update 2022-08-15 21:12:04 +01:00
Jake Stanger
7625635050 refactor: fix a couple of clippy warnings 2022-08-15 21:11:17 +01:00
Jake Stanger
8576ac5c44 fix: popup placement issues 2022-08-15 21:11:00 +01:00
Jake Stanger
8518262053 chore(intellij): add run config 2022-08-15 21:05:13 +01:00
Jake Stanger
a29df37e77 docs: include example files
These are also present in the wiki
2022-08-15 17:06:55 +01:00
Jake Stanger
2d755d37e5 feat: support for multiple bars per monitor, loading config from env var
Restructures `monitor` config key into object of monitor names -> configs.
2022-08-15 17:06:42 +01:00
Jake Stanger
001dd5473a chore(intellij): load config from environment variable 2022-08-15 17:05:53 +01:00
Jake Stanger
120580994c docs: write better configuration/styling guides 2022-08-15 17:05:13 +01:00
Jake Stanger
18e088d593 ci: add libgtk-layer-shell-dev 2022-08-15 00:15:36 +01:00
Jake Stanger
2c6338f82f ci: add missing sudo call to apt 2022-08-15 00:11:35 +01:00
Jake Stanger
882fa30b66 ci: install libgtk3 for build 2022-08-15 00:09:47 +01:00
Jake Stanger
3c564a7774 ci: add build job 2022-08-15 00:04:06 +01:00
Jake Stanger
73f0e7e48e docs(readme): tidy 2022-08-15 00:03:04 +01:00
Jake Stanger
5e72a7ba32 docs(readme): add install links, aur package 2022-08-15 00:01:38 +01:00
Jake Stanger
de80b99e64 refactor: use macro for module config matching 2022-08-14 23:32:20 +01:00
Jake Stanger
ebf7a97a85 chore(release): v0.2.0 2022-08-14 20:42:42 +01:00
Jake Stanger
7a515359ac chore(release): v0.2.0 2022-08-14 20:42:25 +01:00
Jake Stanger
2e0f033bed feat: config option for bar height 2022-08-14 20:41:38 +01:00
Jake Stanger
dc14cb003f feat: new focused window module 2022-08-14 20:40:11 +01:00
Jake Stanger
e416e03b0a chore(intellij): update run configs 2022-08-14 20:38:57 +01:00
Jake Stanger
65c5d391d9 fix(launcher): some icons displaying too large 2022-08-14 19:45:10 +01:00
Jake Stanger
53adaa846c feat(workspaces): support for toggling showing workspaces for all monitors 2022-08-14 16:23:41 +01:00
Jake Stanger
a358037d3e feat: add support for showing bar at top of screen 2022-08-14 15:56:21 +01:00
Jake Stanger
19d009fe5b feat(script): enable markup support 2022-08-14 15:55:20 +01:00
38 changed files with 1782 additions and 581 deletions

37
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install build deps
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
- name: Build
run: cargo build --verbose
- name: Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Check formatting
run: cargo fmt --check

45
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Deploy
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Update CHANGELOG
id: changelog
uses: Requarks/changelog-action@v1
with:
token: ${{ github.token }}
tag: ${{ github.ref_name }}
- name: Create release
uses: ncipollo/release-action@v1
with:
allowUpdates: true
draft: false
name: ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.changes }}
token: ${{ github.token }}
- name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: master
commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]'
file_pattern: CHANGELOG.md
- uses: katyo/publish-crates@v1
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

1
.idea/ironbar.iml generated
View File

@@ -2,6 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>

View File

@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used" />
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />

View File

@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -10,6 +10,7 @@
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs>
<env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
</envs>
<option name="isRedirectInput" value="false" />

View File

@@ -11,6 +11,7 @@
<option name="backtrace" value="SHORT" />
<envs>
<env name="GTK_DEBUG" value="interactive" />
<env name="IRONBAR_CONFIG" value="examples/config.json" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />

View File

@@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run (Live Config)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs>
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

275
Cargo.lock generated
View File

@@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
@@ -22,12 +37,27 @@ dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anyhow"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-broadcast"
version = "0.4.1"
@@ -142,6 +172,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -244,7 +289,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"time 0.1.44",
"winapi 0.3.9",
]
@@ -287,6 +332,33 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "color-eyre"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "colored"
version = "2.0.0"
@@ -500,6 +572,16 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "fastrand"
version = "1.8.0"
@@ -737,6 +819,12 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "gio"
version = "0.15.12"
@@ -983,6 +1071,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "1.9.1"
@@ -1033,10 +1127,12 @@ dependencies = [
[[package]]
name = "ironbar"
version = "0.1.0"
version = "0.4.0"
dependencies = [
"chrono",
"color-eyre",
"cornfig",
"crossbeam-channel 0.3.9",
"dirs",
"futures-util",
"glib",
@@ -1050,9 +1146,14 @@ dependencies = [
"serde_json",
"serde_yaml 0.9.4",
"stray",
"strip-ansi-escapes",
"sysinfo",
"tokio",
"toml",
"tracing",
"tracing-appender",
"tracing-error",
"tracing-subscriber",
"walkdir",
]
@@ -1141,6 +1242,15 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.5.0"
@@ -1162,6 +1272,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.6.23"
@@ -1345,6 +1464,24 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.13.0"
@@ -1367,6 +1504,12 @@ version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "owo-colors"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]]
name = "pango"
version = "0.15.10"
@@ -1644,6 +1787,15 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.27"
@@ -1659,6 +1811,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustc_version"
version = "0.2.3"
@@ -1824,6 +1982,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "slab"
version = "0.4.7"
@@ -1871,6 +2038,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
dependencies = [
"vte",
]
[[package]]
name = "strsim"
version = "0.10.0"
@@ -1996,6 +2172,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
dependencies = [
"itoa",
"libc",
"num_threads",
]
[[package]]
name = "tokio"
version = "1.20.1"
@@ -2058,6 +2245,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
dependencies = [
"crossbeam-channel 0.5.6",
"time 0.3.13",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.22"
@@ -2076,6 +2274,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b"
dependencies = [
"ansi_term",
"matchers",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -2118,6 +2356,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-compare"
version = "0.1.0"
@@ -2130,6 +2380,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vte"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2 1.0.42",
"quote 1.0.20",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ironbar"
version = "0.1.0"
version = "0.4.0"
edition = "2021"
license = "MIT"
description = "Customisable wlroots/sway bar"
@@ -11,8 +11,13 @@ description = "Customisable wlroots/sway bar"
gtk = "0.15.5"
gtk-layer-shell = "0.4.1"
glib = "0.15.12"
stray = "0.1.1"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", 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"] }
@@ -20,10 +25,13 @@ serde_json = "1.0.82"
serde_yaml = "0.9.4"
toml = "0.5.9"
cornfig = "0.2.0"
mpd_client = "0.7.5"
regex = "1.6.0"
ksway = "0.1.0"
sysinfo = "0.25.1"
stray = "0.1.1"
dirs = "4.0.0"
walkdir = "2.3.2"
notify = "4.0.17"
notify = "4.0.17"
mpd_client = "0.7.5"
ksway = "0.1.0"
sysinfo = "0.25.1"
# required for wrapping ksway
crossbeam-channel = "0.3.9"

114
README.md
View File

@@ -8,108 +8,68 @@ 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
Install with cargo:
Run using `ironbar`.
### Cargo
```sh
cargo install ironbar
```
Then just run with `ironbar`.
[crate](https://crates.io/crates/ironbar)
### Arch Linux
```sh
yay -S ironbar-git
```
### Source
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
cargo build --release
# change path to wherever you want to install
install target/release/ironbar ~/.local/bin/ironbar
```
[aur package](https://aur.archlinux.org/packages/ironbar-git)
## Configuration
By default, running will get you a blank bar. To start, you will need a configuration file in `.config/ironbar`.
Ironbar supports a range of file formats so pick your favourite:
Ironbar gives a lot of flexibility when configuring, including multiple file formats
and options for scaling complexity: you can use a single config across all monitors,
or configure different/multiple bars per monitor.
- JSON
- TOML
- YAML
- [Corn](https://github.com/jakestanger/corn) (Experimental. JSON/Nix like config lang. Supports variables.)
For a full list of modules and their configuration options, please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
There are two different approaches to configuring the bar:
### Same configuration across all monitors
> If you have a single monitor, or want the same bar to appear across each of your monitors, choose this option.
The top-level object takes any combination of `left`, `center`, and `right`. These each take a list of modules and determine where they are positioned.
```json
{
"left": [],
"center": [],
"right": []
}
```
### Different configuration across monitors
> If you have multiple monitors and want them to differ in configuration, choose this option.
The top-level object takes a single key called `monitors`. This takes an array where each entry is an object with a configuration for each monitor.
The monitor's config object takes any combination of `left`, `center`, and `right`. These each take a list of modules and determine where they are positioned.
```json
{
"monitors": [
{
"left": [],
"center": [],
"right": []
},
{
"left": [],
"center": [],
"right": []
}
]
}
```
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
## Styling
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the file.
An example stylesheet and information about each module's styling information can be found on the [wiki](https://github.com/JakeStanger/ironbar/wiki).
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
## Project Status
This project is in very early stages:
This project is in alpha, but should be usable.
Everything that is implemented works and should be documented.
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
- Error handling is barely implemented - expect crashes
- There will be bugs!
- Lots of modules need more configuration options
- There's room for lots of modules
- The code is messy and quite prototypal in places
- Config options aren't set in stone - expect breaking changes
- Documentation is probably missing in lots of places
There is currently room for lots more modules, and lots more configuration options for the existing modules.
The current configuration schema is not set in stone and breaking changes could come along at any point;
until the project matures I am more interested in ease of use than backwards compatibility.
That said, it will be *actively developed* as I am using it on my daily driver.
A few bugs do exist, and I am sure there are plenty more to be found.
The project will be *actively developed* as I am using it on my daily driver.
Bugs will be fixed, features will be added, code will be refactored.
## Contribution Guidelines
I welcome contributions of any kind with open arms. That said, please do stick to some basics:
- For code contributions:
- Fix any `cargo clippy` warnings, using at least the default configuration.
- Make sure your code is formatted using `cargo fmt`.
- Keep any documentation up to date.
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
- For PRs:
- Please open an issue or discussion beforehand.
I'll accept most contributions, but it's best to make sure you're not working on something that won't get accepted :)
- For issues:
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
## Acknowledgements

43
examples/config.corn Normal file
View File

@@ -0,0 +1,43 @@
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"
}
$left = [ $workspaces $launcher ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
}
in {
left = $left right = $right
}

18
examples/config.json Normal file
View File

@@ -0,0 +1,18 @@
{
"monitors": {
"DP-1": [
{
"left": [{"type": "clock"}]
},
{
"position": "top",
"left": []
}
],
"DP-2": {
"position": "bottom",
"height": 30,
"left": []
}
}
}

148
examples/style.css Normal file
View File

@@ -0,0 +1,148 @@
* {
/* `otf-font-awesome` is required to be installed for icons */
font-family: Noto Sans Nerd Font, sans-serif;
/* font-family: 'Jetbrains Mono', monospace;*/
font-size: 16px;
/*color: white;*/
/*background-color: #2d2d2d;*/
/*background-color: red;*/
border: none;
/*opacity: 0.4;*/
}
#bar {
border-top: 1px solid #424242;
}
.container {
background-color: #2d2d2d;
}
/* test 34543*/
#right > * + * {
margin-left: 20px;
}
#workspaces .item {
color: white;
background-color: #2d2d2d;
border-radius: 0;
}
#workspaces .item.focused {
box-shadow: inset 0 -3px;
background-color: #1c1c1c;
}
#workspaces *:not(.focused):hover {
box-shadow: inset 0 -3px;
}
#launcher .item {
border-radius: 0;
background-color: #2d2d2d;
margin-right: 4px;
}
#launcher .item:not(.focused):hover {
background-color: #1c1c1c;
}
#launcher .open {
border-bottom: 2px solid #6699cc;
}
#launcher .focused {
color: white;
background-color: black;
border-bottom: 4px solid #6699cc;
}
#launcher .urgent {
color: white;
background-color: #8f0a0a;
}
#clock {
color: white;
background-color: #2d2d2d;
font-weight: bold;
}
#script {
color: white;
}
#sysinfo {
color: white;
}
#tray .item {
background-color: #2d2d2d;
}
#mpd {
background-color: #2d2d2d;
color: white;
}
.popup {
background-color: #2d2d2d;
border: 1px solid #424242;
}
#popup-clock {
padding: 1em;
}
#calendar-clock {
color: white;
font-size: 2.5em;
padding-bottom: 0.1em;
}
#calendar {
background-color: #2d2d2d;
color: white;
}
#calendar .header {
padding-top: 1em;
border-top: 1px solid #424242;
font-size: 1.5em;
}
#calendar:selected {
background-color: #6699cc;
}
#popup-mpd {
color: white;
padding: 1em;
}
#popup-mpd #album-art {
/*border: 1px solid #424242;*/
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;
}
#focused {
color: white;
}

View File

@@ -1,20 +1,26 @@
use crate::config::ModuleConfig;
use crate::config::{BarPosition, ModuleConfig};
use crate::modules::{Module, ModuleInfo, ModuleLocation};
use crate::Config;
use color_eyre::Result;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Orientation};
pub fn create_bar(app: &Application, monitor: &Monitor, config: Config) {
pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: &str,
config: Config,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
setup_layer_shell(&win, monitor);
setup_layer_shell(&win, monitor, &config.position);
let content = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(0)
.hexpand(false)
.height_request(42)
.height_request(config.height)
.name("bar")
.build();
@@ -31,7 +37,7 @@ pub fn create_bar(app: &Application, monitor: &Monitor, config: Config) {
content.set_center_widget(Some(&center));
content.pack_end(&right, false, false, 0);
load_modules(&left, &center, &right, app, config);
load_modules(&left, &center, &right, app, config, monitor, monitor_name)?;
win.add(&content);
win.connect_destroy_event(|_, _| {
@@ -40,6 +46,8 @@ pub fn create_bar(app: &Application, monitor: &Monitor, config: Config) {
});
win.show_all();
Ok(())
}
fn load_modules(
@@ -48,78 +56,74 @@ fn load_modules(
right: &gtk::Box,
app: &Application,
config: Config,
) {
monitor: &Monitor,
output_name: &str,
) -> Result<()> {
if let Some(modules) = config.left {
let info = ModuleInfo {
app,
location: ModuleLocation::Left,
bar_position: &config.position,
monitor,
output_name,
};
add_modules(left, modules, info);
add_modules(left, modules, &info)?;
}
if let Some(modules) = config.center {
let info = ModuleInfo {
app,
location: ModuleLocation::Center,
bar_position: &config.position,
monitor,
output_name,
};
add_modules(center, modules, info);
add_modules(center, modules, &info)?;
}
if let Some(modules) = config.right {
let info = ModuleInfo {
app,
location: ModuleLocation::Right,
bar_position: &config.position,
monitor,
output_name,
};
add_modules(right, modules, info);
add_modules(right, modules, &info)?;
}
Ok(())
}
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: ModuleInfo) {
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
macro_rules! add_module {
($module:expr, $name:literal) => {{
let widget = $module.into_widget(&info)?;
widget.set_widget_name($name);
content.add(&widget);
}};
}
for config in modules {
match config {
ModuleConfig::Clock(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("clock");
content.add(&widget);
}
ModuleConfig::Mpd(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("mpd");
content.add(&widget);
}
ModuleConfig::Tray(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("tray");
content.add(&widget);
}
ModuleConfig::Workspaces(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("workspaces");
content.add(&widget);
}
ModuleConfig::SysInfo(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("sysinfo");
content.add(&widget);
}
ModuleConfig::Launcher(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("launcher");
content.add(&widget);
}
ModuleConfig::Script(module) => {
let widget = module.into_widget(&info);
widget.set_widget_name("script");
content.add(&widget);
}
ModuleConfig::Clock(module) => add_module!(module, "clock"),
ModuleConfig::Mpd(module) => add_module!(module, "mpd"),
ModuleConfig::Tray(module) => add_module!(module, "tray"),
ModuleConfig::Workspaces(module) => add_module!(module, "workspaces"),
ModuleConfig::SysInfo(module) => add_module!(module, "sysinfo"),
ModuleConfig::Launcher(module) => add_module!(module, "launcher"),
ModuleConfig::Script(module) => add_module!(module, "script"),
ModuleConfig::Focused(module) => add_module!(module, "focused"),
}
}
Ok(())
}
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor) {
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor);
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
@@ -130,8 +134,16 @@ fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor) {
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Top, false);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Bottom, true);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Top,
position == &BarPosition::Top,
);
gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Bottom,
position == &BarPosition::Bottom,
);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Left, true);
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Right, true);
}

View File

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

View File

@@ -58,9 +58,7 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
let mut map = HashMap::new();
for line in lines.flatten() {
let is_pair = line.contains('=');
if is_pair {
let (key, value) = line.split_once('=').unwrap();
if let Some((key, value)) = line.split_once('=') {
map.insert(key.to_string(), value.to_string());
}
}
@@ -100,13 +98,18 @@ fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconL
let is_steam_game = app_id.starts_with("steam_app_");
if is_steam_game {
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
let home_dir = dirs::data_dir().unwrap();
let path = home_dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{}.png",
steam_id
));
return Some(IconLocation::File(path));
return match dirs::data_dir() {
Some(dir) => {
let path = dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{}.png",
steam_id
));
return Some(IconLocation::File(path));
}
None => None,
};
}
let icon_name = get_desktop_icon_name(app_id);
@@ -129,7 +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::empty());
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
match icon {
Ok(icon) => icon,

53
src/logging.rs Normal file
View File

@@ -0,0 +1,53 @@
use color_eyre::Result;
use dirs::data_dir;
use std::env;
use strip_ansi_escapes::Writer;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_error::ErrorLayer;
use tracing_subscriber::fmt::{Layer, MakeWriter};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
struct MakeFileWriter {
file_writer: NonBlocking,
}
impl MakeFileWriter {
const fn new(file_writer: NonBlocking) -> Self {
Self { file_writer }
}
}
impl<'a> MakeWriter<'a> for MakeFileWriter {
type Writer = Writer<NonBlocking>;
fn make_writer(&'a self) -> Self::Writer {
Writer::new(self.file_writer.clone())
}
}
pub fn install_tracing() -> Result<WorkerGuard> {
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 log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");
let appender = tracing_appender::rolling::never(log_path, "error.log");
let (file_writer, guard) = tracing_appender::non_blocking(appender);
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.with(
Layer::default()
.with_writer(MakeFileWriter::new(file_writer))
.with_ansi(false)
.with_filter(file_filter_layer),
)
.init();
Ok(guard)
}

View File

@@ -1,52 +1,140 @@
mod bar;
mod collection;
mod config;
mod icon;
mod logging;
mod modules;
mod popup;
mod style;
mod sway;
use crate::bar::create_bar;
use crate::config::Config;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use crate::sway::{get_client_error, SwayOutput};
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::{gdk, Application};
use gtk::Application;
use ksway::client::Client;
use ksway::IpcCommand;
use std::env;
use std::process::exit;
use crate::logging::install_tracing;
use tracing::{debug, error, info};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main]
async fn main() {
async fn main() -> Result<()> {
// Disable backtraces by default
if env::var("RUST_LIB_BACKTRACE").is_err() {
env::set_var("RUST_LIB_BACKTRACE", "0");
}
// keep guard in scope
// otherwise file logging drops
let _guard = install_tracing()?;
color_eyre::install()?;
info!("Ironbar version {}", VERSION);
info!("Starting application");
let app = Application::builder()
.application_id("dev.jstanger.waylandbar")
.application_id("dev.jstanger.ironbar")
.build();
app.connect_activate(|app| {
let config = Config::load().unwrap_or_default();
app.connect_activate(move |app| {
let display = match Display::default() {
Some(display) => display,
None => {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(1)
}
};
// TODO: Better logging (https://crates.io/crates/tracing)
// TODO: error handling (https://crates.io/crates/color-eyre)
let config = match Config::load() {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
Config::default()
}
};
debug!("Loaded config file");
// TODO: Embedded Deno/lua - build custom modules via script???
let display = gdk::Display::default().expect("Failed to get default GDK display");
let num_monitors = display.n_monitors();
for i in 0..num_monitors {
let monitor = display.monitor(i).unwrap();
let config = config.monitors.as_ref().map_or(&config, |monitor_config| {
monitor_config.get(i as usize).unwrap_or(&config)
});
create_bar(app, &monitor, config.clone());
if let Err(err) = create_bars(app, &display, &config) {
error!("{:?}", err);
exit(2);
}
let style_path = config_dir()
.expect("Failed to locate user config dir")
.join("ironbar")
.join("style.css");
debug!("Created bars");
let style_path = match config_dir() {
Some(dir) => dir.join("ironbar").join("style.css"),
None => {
let report = Report::msg("Failed to locate user config dir");
error!("{:?}", report);
exit(3);
}
};
if style_path.exists() {
load_css(style_path);
debug!("Loaded CSS watcher file");
}
});
app.run();
// Ignore CLI args
// Some are provided by swaybar_config but not currently supported
app.run_with_args(&Vec::<&str>::new());
Ok(())
}
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
let mut sway_client = match Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}?;
let outputs = match sway_client.ipc(IpcCommand::GetOutputs) {
Ok(outputs) => Ok(outputs),
Err(err) => Err(get_client_error(err)),
}?;
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
let num_monitors = display.n_monitors();
for i in 0..num_monitors {
let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?;
let monitor_name = &outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?.name;
config.monitors.as_ref().map_or_else(
|| create_bar(app, &monitor, monitor_name, config.clone()),
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => {
create_bar(app, &monitor, monitor_name, config.clone())
}
Some(MonitorConfig::Multiple(configs)) => {
for config in configs {
create_bar(app, &monitor, monitor_name, config.clone())?;
}
Ok(())
}
_ => Ok(()),
}
},
)?;
}
Ok(())
}

View File

@@ -2,8 +2,8 @@ mod popup;
use self::popup::Popup;
use crate::modules::{Module, ModuleInfo};
use crate::popup::PopupAlignment;
use chrono::Local;
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, Orientation};
@@ -27,23 +27,22 @@ fn default_format() -> String {
}
impl Module<Button> for ClockModule {
fn into_widget(self, info: &ModuleInfo) -> Button {
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let button = Button::new();
let popup = Popup::new("popup-clock", info.app, Orientation::Vertical);
let popup = Popup::new(
"popup-clock",
info.app,
info.monitor,
Orientation::Vertical,
info.bar_position,
);
popup.add_clock_widgets();
button.show_all();
button.connect_clicked(move |button| {
let button_w = button.allocation().width();
let (button_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
.unwrap();
popup.show();
popup.set_pos(f64::from(button_x + button_w), PopupAlignment::Right);
popup.show(button);
});
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@@ -53,7 +52,8 @@ impl Module<Button> for ClockModule {
let date = Local::now();
let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap();
tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await;
}
});
@@ -66,6 +66,6 @@ impl Module<Button> for ClockModule {
});
}
button
Ok(button)
}
}

View File

@@ -24,7 +24,8 @@ impl Popup {
let date = Local::now();
let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap();
tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await;
}
});

103
src/modules/focused.rs Normal file
View File

@@ -0,0 +1,103 @@
use crate::icon;
use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, WindowEvent};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{IconTheme, Image, Label, Orientation};
use ksway::IpcEvent;
use serde::Deserialize;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
#[serde(default = "crate::config::default_true")]
show_icon: bool,
#[serde(default = "crate::config::default_true")]
show_title: bool,
#[serde(default = "default_icon_size")]
icon_size: i32,
icon_theme: Option<String>,
}
const fn default_icon_size() -> i32 {
32
}
impl Module<gtk::Box> for FocusedModule {
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme));
}
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = Image::builder().name("icon").build();
let label = Label::builder().name("label").build();
container.add(&icon);
container.add(&label);
let mut sway = SwayClient::connect()?;
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let focused = sway
.get_open_windows()?
.into_iter()
.find(|node| node.focused);
if let Some(focused) = focused {
tx.send(focused)?;
}
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
match serde_json::from_slice::<WindowEvent>(&payload) {
Ok(payload) => {
let update = match payload.change.as_str() {
"focus" => true,
"title" => payload.container.focused,
_ => false,
};
if update {
tx.send(payload.container)
.expect("Failed to sendf focus update");
}
}
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
});
{
rx.attach(None, move |node| {
let value = node.name.as_deref().unwrap_or_else(|| node.get_id());
let pixbuf = icon::get_icon(&icon_theme, node.get_id(), self.icon_size);
if self.show_icon {
icon.set_pixbuf(pixbuf.as_ref());
}
if self.show_title {
label.set_label(value);
}
Continue(true)
});
}
Ok(container)
}
}

View File

@@ -1,9 +1,10 @@
use crate::collection::Collection;
use crate::modules::launcher::icon::{find_desktop_file, get_icon};
use crate::modules::launcher::node::SwayNode;
use crate::icon::{find_desktop_file, get_icon};
use crate::modules::launcher::popup::Popup;
use crate::modules::launcher::FocusEvent;
use crate::popup::PopupAlignment;
use crate::sway::SwayNode;
use crate::Report;
use color_eyre::Help;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image};
use std::process::{Command, Stdio};
@@ -11,6 +12,7 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex, RwLock};
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::error;
#[derive(Debug, Clone)]
pub struct LauncherItem {
@@ -27,12 +29,42 @@ pub struct LauncherWindow {
pub name: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum OpenState {
Closed,
Open,
Focused,
Urgent,
}
impl OpenState {
pub const fn from_node(node: &SwayNode) -> Self {
if node.focused {
Self::Urgent
} else if node.urgent {
Self::Focused
} else {
Self::Open
}
}
pub fn highest_of(a: &Self, b: &Self) -> Self {
if a == &Self::Urgent || b == &Self::Urgent {
Self::Urgent
} else if a == &Self::Focused || b == &Self::Focused {
Self::Focused
} else if a == &Self::Open || b == &Self::Open {
Self::Open
} else {
Self::Closed
}
}
}
#[derive(Debug, Clone)]
pub struct State {
pub is_xwayland: bool,
pub open: bool,
pub focused: bool,
pub urgent: bool,
pub open_state: OpenState,
}
#[derive(Debug, Clone)]
@@ -50,9 +82,7 @@ impl LauncherItem {
button.style_context().add_class("item");
let state = State {
open: false,
focused: false,
urgent: false,
open_state: OpenState::Closed,
is_xwayland: false,
};
@@ -81,9 +111,7 @@ impl LauncherItem {
));
let state = State {
open: true,
focused: node.focused,
urgent: node.urgent,
open_state: OpenState::from_node(node),
is_xwayland: node.is_xwayland(),
};
@@ -102,10 +130,14 @@ impl LauncherItem {
fn configure_button(&self, config: &ButtonConfig) {
let button = &self.button;
let windows = self.windows.lock().unwrap();
let windows = self.windows.lock().expect("Failed to get lock on windows");
let name = if windows.len() == 1 {
windows.first().unwrap().name.as_ref()
windows
.first()
.expect("Failed to get first window")
.name
.as_ref()
} else {
Some(&self.app_id)
};
@@ -130,21 +162,31 @@ impl LauncherItem {
let (focus_tx, mut focus_rx) = mpsc::channel(32);
button.connect_clicked(move |_| {
let state = state.read().unwrap();
if state.open {
focus_tx.try_send(()).unwrap();
let state = state.read().expect("Failed to get read lock on state");
if state.open_state == OpenState::Open {
focus_tx.try_send(()).expect("Failed to send focus event");
} else {
// attempt to find desktop file and launch
match find_desktop_file(&app_id) {
Some(file) => {
Command::new("gtk-launch")
.arg(file.file_name().unwrap())
if let Err(err) = Command::new("gtk-launch")
.arg(
file.file_name()
.expect("File segment missing from path to desktop file"),
)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
{
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run gtk-launch command.")
.suggestion("Perhaps the desktop file is invalid?")
);
}
}
None => (),
None => error!("Could not find desktop file for {}", app_id),
}
}
});
@@ -154,15 +196,15 @@ impl LauncherItem {
spawn(async move {
while focus_rx.recv().await == Some(()) {
let state = state.read().unwrap();
let state = state.read().expect("Failed to get read lock on state");
if state.is_xwayland {
tx_click
.try_send(FocusEvent::Class(app_id.clone()))
.unwrap();
.expect("Failed to send focus event");
} else {
tx_click
.try_send(FocusEvent::AppId(app_id.clone()))
.unwrap();
.expect("Failed to send focus event");
}
}
});
@@ -173,21 +215,10 @@ impl LauncherItem {
let tx_hover = config.tx.clone();
button.connect_enter_notify_event(move |button, _| {
let windows = windows.lock().unwrap();
let windows = windows.lock().expect("Failed to get lock on windows");
if windows.len() > 1 {
let button_w = button.allocation().width();
let (button_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
.unwrap();
let button_center = f64::from(button_x) + f64::from(button_w) / 2.0;
popup.set_windows(windows.as_slice(), &tx_hover);
popup.show();
// TODO: Pass through module location
popup.set_pos(button_center, PopupAlignment::Center);
popup.show(button);
}
Inhibit(false)
@@ -208,7 +239,7 @@ impl LauncherItem {
let style = button.style_context();
style.add_class("launcher-item");
self.update_button_classes(&self.state.read().unwrap());
self.update_button_classes(&self.state.read().expect("Failed to get read lock on state"));
button.show_all();
}
@@ -235,19 +266,19 @@ impl LauncherItem {
style.remove_class("favorite");
}
if state.open {
if state.open_state == OpenState::Open {
style.add_class("open");
} else {
style.remove_class("open");
}
if state.focused {
if state.open_state == OpenState::Focused {
style.add_class("focused");
} else {
style.remove_class("focused");
}
if state.urgent {
if state.open_state == OpenState::Urgent {
style.add_class("urgent");
} else {
style.remove_class("urgent");

View File

@@ -1,47 +1,33 @@
mod icon;
mod item;
mod node;
mod popup;
use crate::collection::Collection;
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
use crate::modules::launcher::node::{get_open_windows, SwayNode};
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow, OpenState};
use crate::modules::launcher::popup::Popup;
use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, SwayNode, WindowEvent};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use ksway::{Client, IpcEvent};
use ksway::IpcEvent;
use serde::Deserialize;
use std::rc::Rc;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct LauncherModule {
favorites: Option<Vec<String>>,
#[serde(default = "default_false")]
#[serde(default = "crate::config::default_false")]
show_names: bool,
#[serde(default = "default_true")]
#[serde(default = "crate::config::default_true")]
show_icons: bool,
icon_theme: Option<String>,
}
const fn default_false() -> bool {
false
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Deserialize)]
struct WindowEvent {
change: String,
container: SwayNode,
}
#[derive(Debug)]
pub enum FocusEvent {
AppId(String),
@@ -87,15 +73,17 @@ impl Launcher {
let id = window.get_id().to_string();
if let Some(item) = self.items.get_mut(&id) {
let mut state = item.state.write().unwrap();
state.open = true;
state.focused = window.focused || state.focused;
state.urgent = window.urgent || state.urgent;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
let new_open_state = OpenState::from_node(&window);
state.open_state = OpenState::highest_of(&state.open_state, &new_open_state);
state.is_xwayland = window.is_xwayland();
item.update_button_classes(&state);
let mut windows = item.windows.lock().unwrap();
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
windows.insert(
window.id,
@@ -122,13 +110,13 @@ impl Launcher {
let remove = if let Some(item) = item {
let windows = Rc::clone(&item.windows);
let mut windows = windows.lock().unwrap();
let mut windows = windows.lock().expect("Failed to get lock on windows");
windows.remove(&window.id);
if windows.is_empty() {
let mut state = item.state.write().unwrap();
state.open = false;
let mut state = item.state.write().expect("Failed to get lock on windows");
state.open_state = OpenState::Closed;
item.update_button_classes(&state);
if item.favorite {
@@ -152,20 +140,30 @@ impl Launcher {
fn set_window_focused(&mut self, window: &SwayNode) {
let id = window.get_id().to_string();
let currently_focused = self
.items
.iter_mut()
.find(|item| item.state.read().unwrap().focused);
let currently_focused = self.items.iter_mut().find(|item| {
item.state
.read()
.expect("Failed to get read lock on state")
.open_state
== OpenState::Focused
});
if let Some(currently_focused) = currently_focused {
let mut state = currently_focused.state.write().unwrap();
state.focused = false;
let mut state = currently_focused
.state
.write()
.expect("Failed to get write lock on state");
state.open_state = OpenState::Open;
currently_focused.update_button_classes(&state);
}
let item = self.items.get_mut(&id);
if let Some(item) = item {
let mut state = item.state.write().unwrap();
state.focused = true;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
state.open_state = OpenState::Focused;
item.update_button_classes(&state);
}
}
@@ -175,13 +173,16 @@ impl Launcher {
let item = self.items.get_mut(&id);
if let (Some(item), Some(name)) = (item, window.name) {
let mut windows = item.windows.lock().unwrap();
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
if windows.len() == 1 {
item.set_title(&name, &self.button_config);
} else if let Some(window) = windows.get_mut(&window.id) {
window.name = Some(name);
} else {
windows.get_mut(&window.id).unwrap().name = Some(name);
// This should never happen
// But makes more sense to wipe title than keep old one in case of error
item.set_title("", &self.button_config);
}
}
}
@@ -190,24 +191,34 @@ impl Launcher {
let item = self.items.get_mut(&id);
if let Some(item) = item {
let mut state = item.state.write().unwrap();
state.urgent = window.urgent;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
state.open_state =
OpenState::highest_of(&state.open_state, &OpenState::from_node(window));
item.update_button_classes(&state);
}
}
}
impl Module<gtk::Box> for LauncherModule {
fn into_widget(self, info: &ModuleInfo) -> gtk::Box {
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme));
}
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
let popup = Popup::new("popup-launcher", info.app, Orientation::Vertical);
let popup = Popup::new(
"popup-launcher",
info.app,
info.monitor,
Orientation::Vertical,
info.bar_position,
);
let container = gtk::Box::new(Orientation::Horizontal, 0);
let (ui_tx, mut ui_rx) = mpsc::channel(32);
@@ -226,22 +237,29 @@ impl Module<gtk::Box> for LauncherModule {
button_config,
);
let open_windows = get_open_windows(&mut sway);
let open_windows = sway.get_open_windows()?;
for window in open_windows {
launcher.add_window(window);
}
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap();
tx.send(payload).unwrap();
match serde_json::from_slice::<WindowEvent>(&payload) {
Ok(payload) => {
tx.send(payload)
.expect("Failed to send window event payload");
}
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
sway.poll().unwrap();
});
{
@@ -260,7 +278,7 @@ impl Module<gtk::Box> for LauncherModule {
}
spawn(async move {
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
while let Some(event) = ui_rx.recv().await {
let selector = match event {
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
@@ -268,10 +286,12 @@ impl Module<gtk::Box> for LauncherModule {
FocusEvent::ConId(id) => format!("[con_id={}]", id),
};
sway.run(format!("{} focus", selector)).unwrap();
sway.run(format!("{} focus", selector))?;
}
Ok::<(), Report>(())
});
container
Ok(container)
}
}

View File

@@ -1,65 +0,0 @@
use ksway::{Client, IpcCommand};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct SwayNode {
#[serde(rename = "type")]
pub node_type: String,
pub id: i32,
pub name: Option<String>,
pub app_id: Option<String>,
pub focused: bool,
pub urgent: bool,
pub nodes: Vec<SwayNode>,
pub floating_nodes: Vec<SwayNode>,
pub shell: Option<String>,
pub window_properties: Option<WindowProperties>,
}
#[derive(Debug, Deserialize)]
pub struct WindowProperties {
pub class: String,
}
impl SwayNode {
pub fn get_id(&self) -> &str {
self.app_id.as_ref().map_or_else(
|| {
&self
.window_properties
.as_ref()
.expect("cannot find node name")
.class
},
|app_id| app_id,
)
}
pub fn is_xwayland(&self) -> bool {
self.shell == Some(String::from("xwayland"))
}
}
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
window_nodes.push(node);
} else {
node.nodes.into_iter().for_each(|node| {
check_node(node, window_nodes);
});
node.floating_nodes.into_iter().for_each(|node| {
check_node(node, window_nodes);
});
}
}
pub fn get_open_windows(sway: &mut Client) -> Vec<SwayNode> {
let raw = sway.ipc(IpcCommand::GetTree).unwrap();
let root_node = serde_json::from_slice::<SwayNode>(&raw).unwrap();
let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes);
window_nodes
}

View File

@@ -25,7 +25,8 @@ impl Popup {
let window = self.window.clone();
let tx = tx.clone();
button.connect_clicked(move |_| {
tx.try_send(FocusEvent::ConId(con_id)).unwrap();
tx.try_send(FocusEvent::ConId(con_id))
.expect("Failed to send focus event");
window.hide();
});

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 focused;
pub mod launcher;
pub mod mpd;
pub mod script;
@@ -12,12 +13,13 @@ pub mod sysinfo;
pub mod tray;
pub mod workspaces;
use crate::config::BarPosition;
use color_eyre::Result;
/// Shamelessly stolen from here:
/// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs>
use glib::IsA;
use gtk::gdk::Monitor;
use gtk::{Application, Widget};
use serde::de::DeserializeOwned;
use serde_json::Value;
#[derive(Clone)]
pub enum ModuleLocation {
@@ -29,6 +31,9 @@ pub enum ModuleLocation {
pub struct ModuleInfo<'a> {
pub app: &'a Application,
pub location: ModuleLocation,
pub bar_position: &'a BarPosition,
pub monitor: &'a Monitor,
pub output_name: &'a str,
}
pub trait Module<W>
@@ -37,12 +42,5 @@ where
{
/// Consumes the module config
/// and produces a GTK widget of type `W`
fn into_widget(self, info: &ModuleInfo) -> W;
fn from_value(v: &Value) -> Box<Self>
where
Self: DeserializeOwned,
{
serde_json::from_value(v.clone()).unwrap()
}
fn into_widget(self, info: &ModuleInfo) -> Result<W>;
}

View File

@@ -2,57 +2,76 @@ use mpd_client::commands::responses::Status;
use mpd_client::raw::MpdProtocolError;
use mpd_client::{Client, Connection};
use std::path::PathBuf;
use std::time::Duration;
use tokio::net::{TcpStream, UnixStream};
use tokio::spawn;
use tokio::time::sleep;
fn is_unix_socket(host: &String) -> bool {
pub async fn wait_for_connection(
hosts: Vec<String>,
interval: Duration,
max_retries: Option<usize>,
) -> Option<Client> {
let mut retries = 0;
spawn(async move {
let max_retries = max_retries.unwrap_or(usize::MAX);
loop {
if retries == max_retries {
break None;
}
if let Some(conn) = try_get_mpd_conn(&hosts).await {
break Some(conn.0);
}
retries += 1;
sleep(interval).await;
}
})
.await
.expect("Error occurred while handling tasks")
}
/// Cycles through each MPD host and
/// returns the first one which connects,
/// or none if there are none
async fn try_get_mpd_conn(hosts: &[String]) -> Option<Connection> {
for host in hosts {
let connection = if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
};
if let Ok(connection) = connection {
return Some(connection);
}
}
None
}
fn is_unix_socket(host: &str) -> bool {
PathBuf::from(host).is_file()
}
pub async fn get_connection(host: &String) -> Result<Connection, MpdProtocolError> {
if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
}
}
async fn connect_unix(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host).await?;
Client::connect(connection).await
}
// /// Gets MPD server status.
// /// Panics on error.
// pub async fn get_status(client: &Client) -> Status {
// client
// .command(commands::Status)
// .await
// .expect("Failed to get MPD server status")
// }
/// Gets the duration of the current song
pub fn get_duration(status: &Status) -> u64 {
status
.duration
.expect("Failed to get duration from MPD status")
.as_secs()
pub fn get_duration(status: &Status) -> Option<u64> {
status.duration.map(|duration| duration.as_secs())
}
/// Gets the elapsed time of the current song
pub fn get_elapsed(status: &Status) -> u64 {
status
.elapsed
.expect("Failed to get elapsed time from MPD status")
.as_secs()
pub fn get_elapsed(status: &Status) -> Option<u64> {
status.elapsed.map(|duration| duration.as_secs())
}

View File

@@ -2,11 +2,11 @@ mod client;
mod popup;
use self::popup::Popup;
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
use crate::modules::mpd::client::{get_duration, get_elapsed, wait_for_connection};
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
use crate::modules::{Module, ModuleInfo};
use crate::popup::PopupAlignment;
use dirs::home_dir;
use color_eyre::Result;
use dirs::{audio_dir, home_dir};
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, Orientation};
@@ -15,9 +15,11 @@ use mpd_client::{commands, Tag};
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct MpdModule {
@@ -42,16 +44,18 @@ fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_play() -> Option<String> {
Some(String::from(""))
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_pause() -> Option<String> {
Some(String::from(""))
}
fn default_music_dir() -> PathBuf {
home_dir().unwrap().join("Music")
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
}
/// Attempts to read the first value for a tag
@@ -80,20 +84,26 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
}
enum Event {
Open(f64),
Open,
Update(Box<Option<(Song, Status, String)>>),
}
impl Module<Button> for MpdModule {
fn into_widget(self, info: &ModuleInfo) -> Button {
let re = Regex::new(r"\{([\w-]+)}").unwrap();
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
let button = Button::new();
let (ui_tx, mut ui_rx) = mpsc::channel(32);
let popup = Popup::new("popup-mpd", info.app, Orientation::Horizontal);
let popup = Popup::new(
"popup-mpd",
info.app,
info.monitor,
Orientation::Horizontal,
info.bar_position,
);
let mpd_popup = MpdPopup::new(popup, ui_tx);
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@@ -101,22 +111,18 @@ impl Module<Button> for MpdModule {
let music_dir = self.music_dir.clone();
button.connect_clicked(move |button| {
let button_w = button.allocation().width();
let (button_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
.unwrap();
button.connect_clicked(move |_| {
click_tx
.send(Event::Open(f64::from(button_x + button_w)))
.unwrap();
.send(Event::Open)
.expect("Failed to send popup open event");
});
let host = self.host.clone();
let host2 = self.host.clone();
spawn(async move {
let (client, _) = get_connection(&host).await.unwrap(); // TODO: Handle connecting properly
let client = wait_for_connection(vec![host], Duration::from_secs(1), None)
.await
.expect("Unexpected error when trying to connect to MPD server");
loop {
let current_song = client.command(commands::CurrentSong).await;
@@ -128,31 +134,38 @@ impl Module<Button> for MpdModule {
.await;
tx.send(Event::Update(Box::new(Some((song.song, status, string)))))
.unwrap();
.expect("Failed to send update event");
} else {
tx.send(Event::Update(Box::new(None))).unwrap();
tx.send(Event::Update(Box::new(None)))
.expect("Failed to send update event");
}
sleep(tokio::time::Duration::from_secs(1)).await;
sleep(Duration::from_secs(1)).await;
}
});
spawn(async move {
let (client, _) = get_connection(&host2).await.unwrap(); // TODO: Handle connecting properly
let client = wait_for_connection(vec![host2], Duration::from_secs(1), None)
.await
.expect("Unexpected error when trying to connect to MPD server");
while let Some(event) = ui_rx.recv().await {
match event {
let res = match event {
PopupEvent::Previous => client.command(commands::Previous).await,
PopupEvent::Toggle => {
let status = client.command(commands::Status).await.unwrap();
match status.state {
PopupEvent::Toggle => match client.command(commands::Status).await {
Ok(status) => match status.state {
PlayState::Playing => client.command(commands::SetPause(true)).await,
PlayState::Paused => client.command(commands::SetPause(false)).await,
PlayState::Stopped => Ok(())
}
}
PopupEvent::Next => client.command(commands::Next).await
}.unwrap();
PlayState::Stopped => Ok(()),
},
Err(err) => Err(err),
},
PopupEvent::Next => client.command(commands::Next).await,
};
if let Err(err) = res {
error!("Failed to send command to MPD server: {:?}", err);
}
}
});
@@ -161,9 +174,8 @@ impl Module<Button> for MpdModule {
rx.attach(None, move |event| {
match event {
Event::Open(pos) => {
mpd_popup.popup.show();
mpd_popup.popup.set_pos(pos, PopupAlignment::Right);
Event::Open => {
mpd_popup.popup.show(&button);
}
Event::Update(mut msg) => {
if let Some((song, status, string)) = msg.take() {
@@ -181,7 +193,7 @@ impl Module<Button> for MpdModule {
});
};
button
Ok(button)
}
}
@@ -214,7 +226,7 @@ impl MpdModule {
PlayState::Playing => self.icon_play.as_ref(),
PlayState::Paused => self.icon_pause.as_ref(),
};
icon.map(|i| i.as_str())
icon.map(String::as_str)
}
"title" => song.title(),
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
@@ -223,10 +235,11 @@ impl MpdModule {
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
"duration" => return format_time(get_duration(status)),
"elapsed" => return format_time(get_elapsed(status)),
_ => return token.to_string(),
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
_ => Some(token),
};
s.unwrap_or_default().to_string()
}
}
}

View File

@@ -90,17 +90,23 @@ impl MpdPopup {
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
tx_prev.try_send(PopupEvent::Previous).unwrap();
tx_prev
.try_send(PopupEvent::Previous)
.expect("Failed to send prev track message");
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |_| {
tx_toggle.try_send(PopupEvent::Toggle).unwrap();
tx_toggle
.try_send(PopupEvent::Toggle)
.expect("Failed to send play/pause track message");
});
let tx_next = tx;
btn_next.connect_clicked(move |_| {
tx_next.try_send(PopupEvent::Next).unwrap();
tx_next
.try_send(PopupEvent::Next)
.expect("Failed to send next track message");
});
Self {
@@ -121,7 +127,12 @@ impl MpdPopup {
// only update art when album changes
if prev_album != curr_album {
let cover_path = path.join(song.file_path().parent().unwrap().join("cover.jpg"));
let cover_path = path.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
self.cover.set_from_pixbuf(Some(&pixbuf));

View File

@@ -1,10 +1,12 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use std::process::Command;
use tokio::spawn;
use tokio::time::sleep;
use tracing::{error, instrument};
#[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule {
@@ -19,19 +21,15 @@ const fn default_interval() -> u64 {
}
impl Module<Label> for ScriptModule {
fn into_widget(self, _info: &ModuleInfo) -> Label {
let label = Label::new(None);
fn into_widget(self, _info: &ModuleInfo) -> Result<Label> {
let label = Label::builder().use_markup(true).build();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
loop {
let output = Command::new("sh").arg("-c").arg(&self.path).output();
if let Ok(output) = output {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.expect("Script output not valid UTF-8");
tx.send(stdout).unwrap();
match self.run_script() {
Ok(stdout) => tx.send(stdout).expect("Failed to send stdout"),
Err(err) => error!("{:?}", err),
}
sleep(tokio::time::Duration::from_millis(self.interval)).await;
@@ -46,6 +44,34 @@ impl Module<Label> for ScriptModule {
});
}
label
Ok(label)
}
}
impl ScriptModule {
#[instrument]
fn run_script(&self) -> Result<String> {
let output = Command::new("sh")
.arg("-c")
.arg(&self.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,4 +1,5 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Label, Orientation};
use regex::{Captures, Regex};
@@ -14,8 +15,8 @@ pub struct SysInfoModule {
}
impl Module<gtk::Box> for SysInfoModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
let re = Regex::new(r"\{([\w-]+)}").unwrap();
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let re = Regex::new(r"\{([\w-]+)}")?;
let container = gtk::Box::new(Orientation::Horizontal, 10);
@@ -46,7 +47,8 @@ impl Module<gtk::Box> for SysInfoModule {
format_info.insert("memory-percent", format!("{:0>2.0}", memory_percent));
format_info.insert("cpu-percent", format!("{:0>2.0}", cpu_percent));
tx.send(format_info).unwrap();
tx.send(format_info)
.expect("Failed to send system info map");
sleep(tokio::time::Duration::from_secs(1)).await;
}
@@ -69,6 +71,6 @@ impl Module<gtk::Box> for SysInfoModule {
});
}
container
Ok(container)
}
}

View File

@@ -1,4 +1,5 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use futures_util::StreamExt;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
@@ -26,10 +27,11 @@ 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);
let icon_name = item.icon_name.as_ref().unwrap();
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
})
})
}
@@ -38,8 +40,8 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
fn get_menu_items(
menu: &[MenuItemInfo],
tx: &mpsc::Sender<NotifierItemCommand>,
id: String,
path: String,
id: &str,
path: &str,
) -> Vec<MenuItem> {
menu.iter()
.map(|item_info| {
@@ -53,7 +55,7 @@ fn get_menu_items(
if !item_info.submenu.is_empty() {
let menu = Menu::new();
get_menu_items(&item_info.submenu, &tx.clone(), id.clone(), path.clone())
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
.iter()
.for_each(|item| menu.add(item));
@@ -63,8 +65,8 @@ fn get_menu_items(
let item = builder.build();
let info = item_info.clone();
let id = id.clone();
let path = path.clone();
let id = id.to_string();
let path = path.to_string();
{
let tx = tx.clone();
@@ -74,7 +76,7 @@ fn get_menu_items(
menu_path: path.clone(),
notifier_address: id.clone(),
})
.unwrap();
.expect("Failed to send menu item clicked event");
});
}
@@ -88,7 +90,7 @@ fn get_menu_items(
}
impl Module<MenuBar> for TrayModule {
fn into_widget(self, _info: &ModuleInfo) -> MenuBar {
fn into_widget(self, _info: &ModuleInfo) -> Result<MenuBar> {
let container = MenuBar::new();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@@ -107,10 +109,11 @@ impl Module<MenuBar> for TrayModule {
menu,
} => {
tx.send(TrayUpdate::Update(id, Box::new(item), menu))
.unwrap();
.expect("Failed to send tray update event");
}
NotifierItemMessage::Remove { address: id } => {
tx.send(TrayUpdate::Remove(id)).unwrap();
tx.send(TrayUpdate::Remove(id))
.expect("Failed to send tray remove event");
}
}
}
@@ -138,13 +141,11 @@ impl Module<MenuBar> for TrayModule {
menu_item
});
if let Some(menu_opts) = menu {
let menu_path = item.menu.as_ref().unwrap().to_string();
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
let submenus = menu_opts.submenus;
if !submenus.is_empty() {
let menu = Menu::new();
get_menu_items(&submenus, &ui_tx.clone(), id.clone(), menu_path)
get_menu_items(&submenus, &ui_tx.clone(), &id, &menu_path)
.iter()
.for_each(|item| menu.add(item));
menu_item.set_submenu(Some(&menu));
@@ -154,8 +155,9 @@ impl Module<MenuBar> for TrayModule {
widgets.insert(id, menu_item);
}
TrayUpdate::Remove(id) => {
let widget = widgets.get(&id).unwrap();
container.remove(widget);
if let Some(widget) = widgets.get(&id) {
container.remove(widget);
}
}
};
@@ -163,6 +165,6 @@ impl Module<MenuBar> for TrayModule {
});
};
container
Ok(container)
}
}

View File

@@ -1,25 +1,22 @@
use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, Workspace, WorkspaceEvent};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, Orientation};
use ksway::client::Client;
use ksway::{IpcCommand, IpcEvent};
use serde::Deserialize;
use std::collections::HashMap;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule {
pub(crate) name_map: Option<HashMap<String, String>>,
}
name_map: Option<HashMap<String, String>>,
#[derive(Deserialize, Debug)]
struct Workspace {
name: String,
focused: bool,
// num: i32,
// output: String,
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
}
impl Workspace {
@@ -38,29 +35,34 @@ impl Workspace {
{
let tx = tx.clone();
let name = self.name.clone();
button.connect_clicked(move |_item| tx.try_send(name.clone()).unwrap());
button.connect_clicked(move |_item| {
tx.try_send(name.clone())
.expect("Failed to send workspace click event");
});
}
button
}
}
#[derive(Deserialize, Debug)]
struct WorkspaceEvent {
change: String,
old: Option<Workspace>,
current: Option<Workspace>,
}
impl Module<gtk::Box> for WorkspacesModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
let mut sway = Client::connect().unwrap();
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let mut sway = SwayClient::connect()?;
let container = gtk::Box::new(Orientation::Horizontal, 0);
let workspaces = {
let raw = sway.ipc(IpcCommand::GetWorkspaces).unwrap();
serde_json::from_slice::<Vec<Workspace>>(&raw).unwrap()
let raw = sway.ipc(IpcCommand::GetWorkspaces)?;
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?;
if self.all_monitors {
workspaces
} else {
workspaces
.into_iter()
.filter(|workspace| workspace.output == info.output_name)
.collect()
}
};
let name_map = self.name_map.unwrap_or_default();
@@ -75,42 +77,55 @@ impl Module<gtk::Box> for WorkspacesModule {
button_map.insert(workspace.name, item);
}
let srx = sway.subscribe(vec![IpcEvent::Workspace]).unwrap();
let srx = sway.subscribe(vec![IpcEvent::Workspace])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
let payload: WorkspaceEvent = serde_json::from_slice(&payload).unwrap();
tx.send(payload).unwrap();
match serde_json::from_slice::<WorkspaceEvent>(&payload) {
Ok(payload) => tx.send(payload).expect("Failed to send workspace event"),
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
sway.poll().unwrap();
});
{
let menubar = container.clone();
let output_name = info.output_name.to_string();
rx.attach(None, move |event| {
match event.change.as_str() {
"focus" => {
let old = event.old.unwrap();
let old_button = button_map.get(&old.name).unwrap();
old_button.style_context().remove_class("focused");
let old = event.old.and_then(|old| button_map.get(&old.name));
if let Some(old) = old {
old.style_context().remove_class("focused");
}
let new = event.current.unwrap();
let new_button = button_map.get(&new.name).unwrap();
new_button.style_context().add_class("focused");
let new = event.current.and_then(|new| button_map.get(&new.name));
if let Some(new) = new {
new.style_context().add_class("focused");
}
}
"init" => {
let workspace = event.current.unwrap();
let item = workspace.as_button(&name_map, &ui_tx);
if let Some(workspace) = event.current {
if self.all_monitors || workspace.output == output_name {
let item = workspace.as_button(&name_map, &ui_tx);
item.show();
menubar.add(&item);
button_map.insert(workspace.name, item);
item.show();
menubar.add(&item);
button_map.insert(workspace.name, item);
}
}
}
"empty" => {
let current = event.current.unwrap();
let item = button_map.get(&current.name).unwrap();
menubar.remove(item);
if let Some(workspace) = event.current {
if let Some(item) = button_map.get(&workspace.name) {
menubar.remove(item);
}
}
}
_ => {}
}
@@ -120,12 +135,14 @@ impl Module<gtk::Box> for WorkspacesModule {
}
spawn(async move {
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
while let Some(name) = ui_rx.recv().await {
sway.run(format!("workspace {}", name)).unwrap();
sway.run(format!("workspace {}", name))?;
}
Ok::<(), Report>(())
});
container
Ok(container)
}
}

View File

@@ -1,32 +1,59 @@
use crate::config::BarPosition;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Orientation};
use gtk::{Application, ApplicationWindow, Button, Orientation};
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub container: gtk::Box,
}
pub enum PopupAlignment {
Left,
Center,
Right,
monitor: Monitor,
}
impl Popup {
pub fn new(name: &str, app: &Application, orientation: Orientation) -> Self {
pub fn new(
name: &str,
app: &Application,
monitor: &Monitor,
orientation: Orientation,
bar_position: &BarPosition,
) -> Self {
let win = ApplicationWindow::builder().application(app).build();
gtk_layer_shell::init_for_window(&win);
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Top, 0);
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Bottom, 5);
gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Top,
if bar_position == &BarPosition::Top {
5
} else {
0
},
);
gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Bottom,
if bar_position == &BarPosition::Bottom {
5
} else {
0
},
);
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Left, 0);
gtk_layer_shell::set_margin(&win, gtk_layer_shell::Edge::Right, 0);
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Top, false);
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Bottom, true);
gtk_layer_shell::set_anchor(
&win,
gtk_layer_shell::Edge::Top,
bar_position == &BarPosition::Top,
);
gtk_layer_shell::set_anchor(
&win,
gtk_layer_shell::Edge::Bottom,
bar_position == &BarPosition::Bottom,
);
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Left, true);
gtk_layer_shell::set_anchor(&win, gtk_layer_shell::Edge::Right, false);
@@ -42,11 +69,11 @@ impl Popup {
win.add(&content);
win.connect_leave_notify_event(|win, ev| {
const THRESHOLD: f64 = 3.0;
let (w, _h) = win.size();
let (x, y) = ev.position();
const THRESHOLD: f64 = 3.0;
// some child widgets trigger this event
// so check we're actually outside the window
if x < THRESHOLD || y < THRESHOLD || x > f64::from(w) - THRESHOLD {
@@ -59,29 +86,42 @@ impl Popup {
Self {
window: win,
container: content,
monitor: monitor.clone(),
}
}
/// Sets the popover's X position relative to the left border of the screen
pub fn set_pos(&self, pos: f64, alignment: PopupAlignment) {
let width = self.window.allocated_width();
let offset = match alignment {
PopupAlignment::Left => pos,
PopupAlignment::Center => (pos - (f64::from(width) / 2.0)).round(),
PopupAlignment::Right => pos - f64::from(width),
};
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
}
/// Shows the popover
pub fn show(&self) {
pub fn show(&self, button: &Button) {
self.window.show_all();
self.set_pos(button);
}
/// Hides the popover
pub fn hide(&self) {
self.window.hide();
}
/// Sets the popover's X position relative to the left border of the screen
fn set_pos(&self, button: &Button) {
let widget_width = button.allocation().width();
let screen_width = self.monitor.workarea().width();
let popup_width = self.window.allocated_width();
let top_level = button.toplevel().expect("Failed to get top-level widget");
let (widget_x, _) = button
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0;
let mut offset = (widget_center - (f64::from(popup_width) / 2.0)).round();
if offset < 5.0 {
offset = 5.0;
} else if offset > f64::from(screen_width - popup_width) - 5.0 {
offset = f64::from(screen_width - popup_width) - 5.0;
}
gtk_layer_shell::set_margin(&self.window, gtk_layer_shell::Edge::Left, offset as i32);
}
}

View File

@@ -1,3 +1,4 @@
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext};
@@ -6,40 +7,56 @@ use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
use tokio::spawn;
use tracing::{error, info};
pub fn load_css(style_path: PathBuf) {
let provider = CssProvider::new();
provider
.load_from_file(&gio::File::for_path(&style_path))
.expect("Couldn't load custom style");
StyleContext::add_provider_for_screen(
&gdk::Screen::default().expect("Couldn't get default GDK screen"),
&provider,
800,
);
if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
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);
let (watcher_tx, watcher_rx) = mpsc::channel::<DebouncedEvent>();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
let mut watcher = notify::watcher(watcher_tx, Duration::from_millis(500)).unwrap();
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.unwrap();
match notify::watcher(watcher_tx, Duration::from_millis(500)) {
Ok(mut watcher) => {
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.expect("Unexpected error when attempting to watch CSS");
loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).unwrap();
loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).expect("Failed to send style changed message");
}
}
}
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err("Failed to start CSS watcher")
),
}
});
{
rx.attach(None, move |path| {
println!("Reloading CSS");
provider
.load_from_file(&gio::File::for_path(path))
.expect("Couldn't load custom style");
info!("Reloading CSS");
if let Err(err) = provider
.load_from_file(&gio::File::for_path(path)) {
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.")
);
}
Continue(true)
});

109
src/sway/mod.rs Normal file
View File

@@ -0,0 +1,109 @@
use color_eyre::{Report, Result};
use ksway::{Error, IpcCommand, IpcEvent};
use serde::Deserialize;
pub mod node;
#[derive(Deserialize, Debug)]
pub struct WorkspaceEvent {
pub change: String,
pub old: Option<Workspace>,
pub current: Option<Workspace>,
}
#[derive(Deserialize, Debug)]
pub struct Workspace {
pub name: String,
pub focused: bool,
// pub num: i32,
pub output: String,
}
#[derive(Debug, Deserialize)]
pub struct WindowEvent {
pub change: String,
pub container: SwayNode,
}
#[derive(Debug, Deserialize)]
pub struct SwayNode {
#[serde(rename = "type")]
pub node_type: String,
pub id: i32,
pub name: Option<String>,
pub app_id: Option<String>,
pub focused: bool,
pub urgent: bool,
pub nodes: Vec<SwayNode>,
pub floating_nodes: Vec<SwayNode>,
pub shell: Option<String>,
pub window_properties: Option<WindowProperties>,
}
#[derive(Debug, Deserialize)]
pub struct WindowProperties {
pub class: Option<String>,
}
#[derive(Deserialize)]
pub struct SwayOutput {
pub name: String,
}
pub struct SwayClient {
client: ksway::Client,
}
impl SwayClient {
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
match self.client.run(cmd) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
}
impl SwayClient {
pub fn connect() -> Result<Self> {
let client = match ksway::Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}?;
Ok(Self { client })
}
pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> {
match self.client.ipc(command) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub fn subscribe(
&mut self,
event_types: Vec<IpcEvent>,
) -> Result<crossbeam_channel::Receiver<(IpcEvent, Vec<u8>)>> {
match self.client.subscribe(event_types) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub fn poll(&mut self) -> Result<()> {
match self.client.poll() {
Ok(()) => Ok(()),
Err(err) => Err(get_client_error(err)),
}
}
}
/// Gets an error report from a `ksway` error enum variant
pub fn get_client_error(error: Error) -> Report {
match error {
Error::SockPathNotFound => Report::msg("Sway socket path not found"),
Error::SubscriptionError => Report::msg("Sway IPC subscription error"),
Error::AlreadySubscribed => Report::msg("Already subscribed to Sway IPC server"),
Error::Io(err) => Report::new(err),
}
}

49
src/sway/node.rs Normal file
View File

@@ -0,0 +1,49 @@
use crate::sway::{SwayClient, SwayNode};
use color_eyre::Result;
use ksway::IpcCommand;
impl SwayNode {
pub fn get_id(&self) -> &str {
self.app_id.as_ref().map_or_else(
|| {
self.window_properties
.as_ref()
.expect("Cannot find node window properties")
.class
.as_ref()
.expect("Cannot find node name")
},
|app_id| app_id,
)
}
pub fn is_xwayland(&self) -> bool {
self.shell == Some(String::from("xwayland"))
}
}
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
window_nodes.push(node);
} else {
node.nodes.into_iter().for_each(|node| {
check_node(node, window_nodes);
});
node.floating_nodes.into_iter().for_each(|node| {
check_node(node, window_nodes);
});
}
}
impl SwayClient {
pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> {
let root_node = self.ipc(IpcCommand::GetTree)?;
let root_node = serde_json::from_slice(&root_node)?;
let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes);
Ok(window_nodes)
}
}