Compare commits
567 Commits
v0.4.0
...
feat/works
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fad8c6a16 | ||
|
|
0c0163cfa1 | ||
|
|
e1b1d6f465 | ||
|
|
1f824edd70 | ||
|
|
088ac971c1 | ||
|
|
59f66df079 | ||
|
|
2d5d1b450c | ||
|
|
7a2edcdffc | ||
|
|
8f4ad02c75 | ||
|
|
c5a1694b10 | ||
|
|
f8cc634cc9 | ||
|
|
ccc6ff2d94 | ||
|
|
71ea185a65 | ||
|
|
c793cd3c1a | ||
|
|
1f4b349423 | ||
|
|
4755e2c827 | ||
|
|
b548e3ec01 | ||
|
|
7033d29c6a | ||
|
|
0335aa4470 | ||
|
|
dce245ef5f | ||
|
|
bd002e278b | ||
|
|
ae1bc2ef84 | ||
|
|
4e67b73a83 | ||
|
|
60bb69feec | ||
|
|
5bc7b7f317 | ||
|
|
f88bec93ff | ||
|
|
0d0447bbd2 | ||
|
|
49a79bb011 | ||
|
|
1dcda6c90e | ||
|
|
4e9c05761e | ||
|
|
af674bb769 | ||
|
|
a31a07e451 | ||
|
|
0df3d4c725 | ||
|
|
50ab2e4742 | ||
|
|
abd1f80548 | ||
|
|
3ddf799739 | ||
|
|
6fbe0d5e71 | ||
|
|
bc553b4918 | ||
|
|
4ee2ce4d67 | ||
|
|
cf3dc17ad3 | ||
|
|
8b8ccf7be7 | ||
|
|
1035ce670f | ||
|
|
43b446d266 | ||
|
|
93eb4c6472 | ||
|
|
40432c8704 | ||
|
|
9ced8597e4 | ||
|
|
10f8bbae3f | ||
|
|
af1f9e39b0 | ||
|
|
25c490b8b4 | ||
|
|
6c38ff29c4 | ||
|
|
fea1f18524 | ||
|
|
b9c41af0f7 | ||
|
|
2a8a62eea6 | ||
|
|
d010fd6398 | ||
|
|
57cca121bf | ||
|
|
7f6ba90bfd | ||
|
|
9431d09de7 | ||
|
|
69bd650810 | ||
|
|
5f9ac64892 | ||
|
|
3f7904fefb | ||
|
|
84e0065c0c | ||
|
|
e5281e9619 | ||
|
|
1b476eb9f9 | ||
|
|
50741941fb | ||
|
|
19c684e49f | ||
|
|
d1be6100d6 | ||
|
|
9f65cf293d | ||
|
|
7e877f6631 | ||
|
|
6203447ec5 | ||
|
|
3621414843 | ||
|
|
e75616c547 | ||
|
|
71002c4250 | ||
|
|
eb58d9caf8 | ||
|
|
6f9b61865d | ||
|
|
fec07c6407 | ||
|
|
8ec0237bc5 | ||
|
|
9e2ac0f43d | ||
|
|
baeb4eae74 | ||
|
|
b6e4ed6608 | ||
|
|
54f0f232f2 | ||
|
|
5255ddffbb | ||
|
|
9fe6d49195 | ||
|
|
b649525a2c | ||
|
|
fb2e30c839 | ||
|
|
82030c7d51 | ||
|
|
4679d8b842 | ||
|
|
901a86caa4 | ||
|
|
1da8c90415 | ||
|
|
9fc3c5302f | ||
|
|
22ab33d194 | ||
|
|
78ba508fb3 | ||
|
|
b6895b9312 | ||
|
|
c59d4267c8 | ||
|
|
2902331af0 | ||
|
|
a47ec69a07 | ||
|
|
c094179fca | ||
|
|
311284590f | ||
|
|
ff04be70cc | ||
|
|
89ec06fc7b | ||
|
|
7f6fef6338 | ||
|
|
36f3db7411 | ||
|
|
2367faab04 | ||
|
|
1c68a97d33 | ||
|
|
00d5606f06 | ||
|
|
ef443e6978 | ||
|
|
4a8c823590 | ||
|
|
1114f67b5d | ||
|
|
0d388d91f9 | ||
|
|
bf682d84ff | ||
|
|
2cd8be72a3 | ||
|
|
eb1347f20b | ||
|
|
ef446f084a | ||
|
|
4bb75282b4 | ||
|
|
5a6b14e5f8 | ||
|
|
806cfc61aa | ||
|
|
782fbf102c | ||
|
|
c823bcc83b | ||
|
|
0654f146a3 | ||
|
|
caecee6875 | ||
|
|
3853c953a6 | ||
|
|
f21d473b5a | ||
|
|
3d949874de | ||
|
|
c09bec2d2b | ||
|
|
6f57ad47ac | ||
|
|
87dd7646fc | ||
|
|
06251e293e | ||
|
|
36abe4073e | ||
|
|
b7ee794bfc | ||
|
|
c582bc3390 | ||
|
|
2a8313a9b4 | ||
|
|
2ccb2633c6 | ||
|
|
2f8443f349 | ||
|
|
fbb41f33a3 | ||
|
|
cc2669a3a6 | ||
|
|
bf470cdadd | ||
|
|
ab67a0a414 | ||
|
|
4305f3e3bc | ||
|
|
eee2182ab9 | ||
|
|
abf490b04e | ||
|
|
4ca17d1337 | ||
|
|
fc820746a4 | ||
|
|
738b9e3da7 | ||
|
|
1a272e00fb | ||
|
|
f8d8c06300 | ||
|
|
c711dd8585 | ||
|
|
68432b066b | ||
|
|
b310ea7636 | ||
|
|
7c8d4668bc | ||
|
|
f1231384c1 | ||
|
|
c2cf41a8b0 | ||
|
|
aa635291c3 | ||
|
|
5b2a1787ef | ||
|
|
95786a1c90 | ||
|
|
d8121de9c5 | ||
|
|
697aac1161 | ||
|
|
b695eeb415 | ||
|
|
f41afce91d | ||
|
|
c99c5de0d6 | ||
|
|
7f4a816379 | ||
|
|
b785b4d4ae | ||
|
|
3502c23817 | ||
|
|
7d3bb02b46 | ||
|
|
4620f29d38 | ||
|
|
a9ac29d885 | ||
|
|
6ae15f44bd | ||
|
|
1759945912 | ||
|
|
12053f111a | ||
|
|
bd90167f4e | ||
|
|
7016f7f79e | ||
|
|
ac04cc27ce | ||
|
|
f78c7f9b98 | ||
|
|
6db7742e06 | ||
|
|
4b88079561 | ||
|
|
9a68dc99bd | ||
|
|
cc181a8b6d | ||
|
|
27f920d012 | ||
|
|
4a9410abac | ||
|
|
607c7285d7 | ||
|
|
c6319b78fd | ||
|
|
ded50cca6f | ||
|
|
f5bdc5a027 | ||
|
|
44313bfc75 | ||
|
|
9e5f72087f | ||
|
|
449795b4e9 | ||
|
|
a67bf38faa | ||
|
|
e539eadd8d | ||
|
|
592213d8af | ||
|
|
d121dc3d1e | ||
|
|
0e8c8a1770 | ||
|
|
93baf8f568 | ||
|
|
1ef32059da | ||
|
|
5be0750792 | ||
|
|
aea8de2552 | ||
|
|
c8c84446d6 | ||
|
|
103a224355 | ||
|
|
18b36423e2 | ||
|
|
09a26d04bc | ||
|
|
96323801d9 | ||
|
|
de98cf3dae | ||
|
|
b3b96673b0 | ||
|
|
de3aa5d7b1 | ||
|
|
ac34c05d2e | ||
|
|
6f7af07cdd | ||
|
|
96d36c43d4 | ||
|
|
e11177fea3 | ||
|
|
cbba2bc614 | ||
|
|
658d040607 | ||
|
|
81c69f644e | ||
|
|
55327c6f98 | ||
|
|
fc2e93491f | ||
|
|
a10cbd6287 | ||
|
|
1991662bc4 | ||
|
|
b4f1c7ac2d | ||
|
|
1e799f7635 | ||
|
|
a9fe75b2f7 | ||
|
|
8ce7f8cb80 | ||
|
|
e709200242 | ||
|
|
f8a2c0f002 | ||
|
|
3b18e1d8a1 | ||
|
|
f37bc80292 | ||
|
|
e0bc05acb5 | ||
|
|
5a675153b4 | ||
|
|
090a6669b8 | ||
|
|
80655883ef | ||
|
|
ce015f8d19 | ||
|
|
5a09e46b28 | ||
|
|
0fce762eef | ||
|
|
0a6da15bb2 | ||
|
|
19d1414daa | ||
|
|
4456bb5d20 | ||
|
|
942d401472 | ||
|
|
952fef270e | ||
|
|
a5ecb363fd | ||
|
|
e036ff03c1 | ||
|
|
6c48d40e5b | ||
|
|
a0881fc909 | ||
|
|
d938298e7b | ||
|
|
e18fb0661d | ||
|
|
6950c79906 | ||
|
|
c4af6a8069 | ||
|
|
7ede33c9c1 | ||
|
|
7f0fdf2391 | ||
|
|
807a31bf92 | ||
|
|
e1b0c9b43d | ||
|
|
c3e9654cd3 | ||
|
|
e6a70f7663 | ||
|
|
b4d7344200 | ||
|
|
a6b686624b | ||
|
|
b9740cba8f | ||
|
|
1f980ca783 | ||
|
|
48d6af0281 | ||
|
|
22b630a10b | ||
|
|
5877f773aa | ||
|
|
87ca399220 | ||
|
|
960da55a05 | ||
|
|
0e65f93a23 | ||
|
|
91ed1ee384 | ||
|
|
9012feee4f | ||
|
|
3b54d527b2 | ||
|
|
242b70ed39 | ||
|
|
bd144e87a8 | ||
|
|
3ccb54b49c | ||
|
|
ff315ff5db | ||
|
|
cdeafbdc72 | ||
|
|
13d39235ad | ||
|
|
327e345630 | ||
|
|
f82f897982 | ||
|
|
fe12251af8 | ||
|
|
d116a51083 | ||
|
|
6836abefd1 | ||
|
|
91e766d6ab | ||
|
|
dea66415c2 | ||
|
|
528a8d6dd6 | ||
|
|
e1abadcf39 | ||
|
|
cf32870f8a | ||
|
|
139bc5d23f | ||
|
|
735f5cc9f1 | ||
|
|
aed04c1ccf | ||
|
|
c1ea5fad7e | ||
|
|
38da59cd41 | ||
|
|
7f46cb4976 | ||
|
|
5c18ec8ba0 | ||
|
|
81acc176ed | ||
|
|
618b7ef552 | ||
|
|
2a155b9aa8 | ||
|
|
31a57ae637 | ||
|
|
bc87c7f0d4 | ||
|
|
bde469816a | ||
|
|
98aaaa0d14 | ||
|
|
51e95d9e01 | ||
|
|
ea9f7caaf7 | ||
|
|
338829e275 | ||
|
|
610c3528af | ||
|
|
f95e1e8f74 | ||
|
|
35dfbbf91d | ||
|
|
14b6c1a69f | ||
|
|
0e3102de8c | ||
|
|
ad3c171eca | ||
|
|
e5bc44168f | ||
|
|
cc62927f15 | ||
|
|
76e2b7ba3e | ||
|
|
033d0f7e6e | ||
|
|
dc16b1e15a | ||
|
|
03cd263095 | ||
|
|
db0868a3fc | ||
|
|
0382b50cf4 | ||
|
|
338f5a0e1b | ||
|
|
20949a7744 | ||
|
|
2da28b9bf5 | ||
|
|
618e97f1e8 | ||
|
|
dd7c9f30db | ||
|
|
1fa0c0e977 | ||
|
|
74d18aedfb | ||
|
|
2c88c99cb6 | ||
|
|
236bb09170 | ||
|
|
83f44fd92f | ||
|
|
1855416db4 | ||
|
|
e63509a3a7 | ||
|
|
4a09b70854 | ||
|
|
9d09855fce | ||
|
|
e9d0273176 | ||
|
|
7926bb07eb | ||
|
|
6fd69d657c | ||
|
|
27d11de661 | ||
|
|
07df51c249 | ||
|
|
b038e7671a | ||
|
|
e5ab9f33b5 | ||
|
|
68bc8230dd | ||
|
|
246313136f | ||
|
|
15a9d8d42c | ||
|
|
a87d8d5c30 | ||
|
|
8e99fd4d0f | ||
|
|
1e1d65ae49 | ||
|
|
2815cef440 | ||
|
|
138b5b3903 | ||
|
|
7355db74ec | ||
|
|
c214f65ecb | ||
|
|
3d308ab572 | ||
|
|
b770ae716c | ||
|
|
3613aef5c5 | ||
|
|
a9d1233909 | ||
|
|
72b14b6c4e | ||
|
|
910945306c | ||
|
|
dfe1964abf | ||
|
|
e928b30f99 | ||
|
|
2ab06f044e | ||
|
|
4b4f1ffc21 | ||
|
|
0691db3b87 | ||
|
|
cac064f479 | ||
|
|
6c622864b3 | ||
|
|
55c06c4766 | ||
|
|
1b0287becc | ||
|
|
7bf44ca75d | ||
|
|
fb04ceab7d | ||
|
|
102d2478a9 | ||
|
|
80a414ab67 | ||
|
|
72ba17add3 | ||
|
|
2b07620847 | ||
|
|
ba488ad38f | ||
|
|
d0b7bdbafc | ||
|
|
0f5ec1fe34 | ||
|
|
6221f7454a | ||
|
|
ecdd71a43d | ||
|
|
01a36a9476 | ||
|
|
d4dd8c41ea | ||
|
|
83c5dceaa7 | ||
|
|
711644e190 | ||
|
|
8cbb73b75e | ||
|
|
7212bbcf61 | ||
|
|
0125ce5916 | ||
|
|
2b26eaf410 | ||
|
|
33676fc4dc | ||
|
|
7978c48d5c | ||
|
|
1d37e010c8 | ||
|
|
54b9b28c75 | ||
|
|
3a44d74cf3 | ||
|
|
b1475a1aff | ||
|
|
b2749fee92 | ||
|
|
9984b638b5 | ||
|
|
207b60db7e | ||
|
|
7779c33e0c | ||
|
|
575d6cc30f | ||
|
|
5bbe64bb86 | ||
|
|
83a49165c4 | ||
|
|
d84139a914 | ||
|
|
ca4fe422f2 | ||
|
|
1ad1961396 | ||
|
|
d253c4bd7f | ||
|
|
fbee6e8bd4 | ||
|
|
7c36f5cb0c | ||
|
|
7dff3e6f8b | ||
|
|
2ac507144b | ||
|
|
82875cde68 | ||
|
|
d40b3b7d80 | ||
|
|
181561fe2a | ||
|
|
7b23e61e7d | ||
|
|
6a39905b43 | ||
|
|
2780d98ee0 | ||
|
|
51d2c2279f | ||
|
|
c347b6c944 | ||
|
|
e83618b1d6 | ||
|
|
90f57d61b9 | ||
|
|
0b9af6bb26 | ||
|
|
11a65d4fbc | ||
|
|
054262365e | ||
|
|
058c8f4228 | ||
|
|
d78d851858 | ||
|
|
db72bc09b4 | ||
|
|
5fb412572f | ||
|
|
400ac00d23 | ||
|
|
80a4b1d177 | ||
|
|
96141d4990 | ||
|
|
b054c17d14 | ||
|
|
3cf9be89fd | ||
|
|
393800aaa2 | ||
|
|
5772711192 | ||
|
|
15f0857859 | ||
|
|
8ba9826cd9 | ||
|
|
07dbf78010 | ||
|
|
97502559b3 | ||
|
|
2b0eb6506a | ||
|
|
012762e102 | ||
|
|
8691824db1 | ||
|
|
ad97550583 | ||
|
|
1ed3220733 | ||
|
|
c906dd40fb | ||
|
|
eb30105fc2 | ||
|
|
90cd078973 | ||
|
|
1cdfebf8db | ||
|
|
0cefcbd02b | ||
|
|
08cfbbc2ea | ||
|
|
e1f523cf2a | ||
|
|
c223892a57 | ||
|
|
9ba28fe7fa | ||
|
|
0d7ab54160 | ||
|
|
6e5d0c1e8c | ||
|
|
a79900d842 | ||
|
|
6d8e647f12 | ||
|
|
1949d07721 | ||
|
|
f779520545 | ||
|
|
df7c447e9c | ||
|
|
90b9d70941 | ||
|
|
da806d38c6 | ||
|
|
8076412bfc | ||
|
|
fa67d077b1 | ||
|
|
b2afe78c07 | ||
|
|
1dd5863431 | ||
|
|
0a341f6673 | ||
|
|
bb81f8e583 | ||
|
|
a45ebfc1f5 | ||
|
|
ea2c84d1bd | ||
|
|
5e21cbcca6 | ||
|
|
9d5049dde0 | ||
|
|
fd2d7e5c7a | ||
|
|
2c1b2924d4 | ||
|
|
490f3f3f65 | ||
|
|
843e40ef45 | ||
|
|
d8c60d9d47 | ||
|
|
b97f018e81 | ||
|
|
c1e1743b5e | ||
|
|
37458642df | ||
|
|
862c46c7ec | ||
|
|
afedf0214d | ||
|
|
64f54040ef | ||
|
|
d20972cb32 | ||
|
|
1320639d4e | ||
|
|
907a565f3d | ||
|
|
ec69649a04 | ||
|
|
c4cdf4be8b | ||
|
|
00f973c3a4 | ||
|
|
5d153a02fc | ||
|
|
e274ba39cd | ||
|
|
8c75bc46ac | ||
|
|
df77020c52 | ||
|
|
0fb5fa8c2a | ||
|
|
cf87bb4e8d | ||
|
|
badfcc0c2d | ||
|
|
c9e66d4664 | ||
|
|
a3f90adaf1 | ||
|
|
47420d83bf | ||
|
|
4662f60ac5 | ||
|
|
94693c92e3 | ||
|
|
8c774100f1 | ||
|
|
b4db0226cd | ||
|
|
ff17ec1996 | ||
|
|
c48029664d | ||
|
|
58d55db660 | ||
|
|
73158c2fce | ||
|
|
1c032ae8e3 | ||
|
|
3b04642148 | ||
|
|
0a331f3138 | ||
|
|
bc625b929b | ||
|
|
ad77dc4e4c | ||
|
|
3a83bd31ab | ||
|
|
5ebc84c7b9 | ||
|
|
51d1cd4a16 | ||
|
|
b7792a415e | ||
|
|
9f82ba58cd | ||
|
|
a93700e8fd | ||
|
|
2a3fe33446 | ||
|
|
3750124d8c | ||
|
|
e693c1c166 | ||
|
|
cbd0c49e25 | ||
|
|
e23e691bc6 | ||
|
|
be0f4c6366 | ||
|
|
493df6bb49 | ||
|
|
b4ac1c9850 | ||
|
|
27f6abad67 | ||
|
|
ec1d59677b | ||
|
|
70e1b526a9 | ||
|
|
3c43c20c6a | ||
|
|
b66bd788b2 | ||
|
|
f17ae7a415 | ||
|
|
a06c4bccca | ||
|
|
e4e72d8008 | ||
|
|
9e6dbbd131 | ||
|
|
91c57edc73 | ||
|
|
dec402edd9 | ||
|
|
fad90fdad6 | ||
|
|
35ce3b4d45 | ||
|
|
27d04795af | ||
|
|
9d9c275313 | ||
|
|
0669504519 | ||
|
|
eb5170ff6a | ||
|
|
b7b64886e3 | ||
|
|
75339f07ed | ||
|
|
06cfad62e2 | ||
|
|
1b853bcb71 | ||
|
|
bd5bdf5af5 | ||
|
|
8536ad719a | ||
|
|
006c242f49 | ||
|
|
2cd59ef5ff | ||
|
|
f411b7c451 | ||
|
|
1dd0a9e52f | ||
|
|
5523e9af46 | ||
|
|
9e31107251 | ||
|
|
668fe4a308 | ||
|
|
994d0f580b | ||
|
|
5ce50b0987 | ||
|
|
b1c66b9117 | ||
|
|
bb4fe7f7f5 | ||
|
|
324f00cdf9 | ||
|
|
b188bc7146 | ||
|
|
d22d954e83 | ||
|
|
45e44d7913 | ||
|
|
b352181b3d | ||
|
|
720ba7bfb0 | ||
|
|
daafa0943e | ||
|
|
b801751bda | ||
|
|
ee67b3be28 | ||
|
|
6442667961 | ||
|
|
68574d4327 | ||
|
|
6871126bd8 | ||
|
|
481adfcaa4 | ||
|
|
64650fbf3a | ||
|
|
a35d25520c | ||
|
|
78e30b39fe | ||
|
|
b81927e3a5 | ||
|
|
5d319e91f2 | ||
|
|
015dcd3204 | ||
|
|
1e38719996 | ||
|
|
6dcae66570 | ||
|
|
649b0efb19 | ||
|
|
023c2fb118 | ||
|
|
ea57f5e18d | ||
|
|
53142d1bea | ||
|
|
7e0f2cad1c |
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report an issue with the bar not working as expected
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
> A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
> A clear and concise description of what you expected to happen.
|
||||
|
||||
**System information:**
|
||||
- Distro: [e.g. Arch Linux, Ubuntu 22.10]
|
||||
- Compositor: [e.g. Sway]
|
||||
- Ironbar version: [e.g. 0.8.0]
|
||||
|
||||
**Configuration**
|
||||
> Share your bar configuration and stylesheet as applicable:
|
||||
|
||||
<details><summary>Config</summary>
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary>Styles</summary>
|
||||
|
||||
```css
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
**Additional context**
|
||||
> Add any other context about the problem here.
|
||||
|
||||
**Screenshots**
|
||||
> If applicable, add screenshots to help explain your problem.
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
**Describe the solution you'd like**
|
||||
> A clear and concise description of what you want to happen.
|
||||
> The more info here about what you are trying to achieve, the better - there's likely more than one way to go about implementing a solution.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
> A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
> Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Other
|
||||
about: Any other issue type
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
43
.github/scripts/sync-wiki.sh
vendored
Executable file
43
.github/scripts/sync-wiki.sh
vendored
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
TEMP_REPO_DIR="wiki_action_$GITHUB_REPOSITORY$GITHUB_SHA"
|
||||
TEMP_WIKI_DIR="temp_wiki_$GITHUB_SHA"
|
||||
|
||||
WIKI_DIR='docs'
|
||||
|
||||
if [ -z "$GH_TOKEN" ]; then
|
||||
echo "Token is not specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Clone repo
|
||||
echo "Cloning repo https://github.com/$GITHUB_REPOSITORY"
|
||||
git clone "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY" "$TEMP_REPO_DIR"
|
||||
|
||||
#Clone wiki repo
|
||||
echo "Cloning wiki repo https://github.com/$GITHUB_REPOSITORY.wiki.git"
|
||||
cd "$TEMP_REPO_DIR"
|
||||
git clone "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.wiki.git" "$TEMP_WIKI_DIR"
|
||||
|
||||
#Get commit details
|
||||
author='Jake Stanger'
|
||||
email='mail@jstanger.dev'
|
||||
message='action: sync wiki'
|
||||
|
||||
echo "Copying edited wiki"
|
||||
cp -R "$TEMP_WIKI_DIR/.git" "$WIKI_DIR/"
|
||||
|
||||
echo "Checking if wiki has changes"
|
||||
cd "$WIKI_DIR"
|
||||
git config --local user.email "$email"
|
||||
git config --local user.name "$author"
|
||||
git add .
|
||||
|
||||
if git diff-index --quiet HEAD; then
|
||||
echo "Nothing changed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Pushing changes to wiki"
|
||||
git commit -m "$message" && git push "https://$GITHUB_ACTOR:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.wiki.git"
|
||||
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
@@ -21,17 +22,57 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
name: Cache dependencies
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
- name: Clippy (base features)
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --no-default-features --features config+json
|
||||
|
||||
- name: Clippy (all features)
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
|
||||
|
||||
build-nix:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
install_url: https://nixos.org/nix/install
|
||||
extra_nix_config: |
|
||||
auto-optimise-store = true
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: jakestanger
|
||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- run: nix build --print-build-logs
|
||||
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -17,12 +17,18 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install build deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Update CHANGELOG
|
||||
id: changelog
|
||||
uses: Requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
tag: ${{ github.ref_name }}
|
||||
excludeTypes: 'build,chore,style'
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
|
||||
27
.github/workflows/update-nix-flake-lock.yml
vendored
Normal file
27
.github/workflows/update-nix-flake-lock.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: update-nix-flake-lock
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
schedule:
|
||||
- cron: '0 0 1 * *' # first day of every month
|
||||
|
||||
jobs:
|
||||
update-nix-flake-lock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update flake.lock
|
||||
uses: DeterminateSystems/update-flake-lock@v15
|
||||
with:
|
||||
pr-title: "Update flake.lock" # Title of PR to be created
|
||||
pr-labels: | # Labels to be set on the PR
|
||||
dependencies
|
||||
automated
|
||||
17
.github/workflows/wiki.yml
vendored
Normal file
17
.github/workflows/wiki.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Sync Wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Sync Wiki
|
||||
run: ./.github/scripts/sync-wiki.sh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
17
.idea/runConfigurations/Format.xml
generated
Normal file
17
.idea/runConfigurations/Format.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="fmt" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="emulateTerminal" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
1
.idea/runConfigurations/Run.xml
generated
1
.idea/runConfigurations/Run.xml
generated
@@ -12,6 +12,7 @@
|
||||
<envs>
|
||||
<env name="IRONBAR_CONFIG" value="examples/config.json" />
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
<env name="RUST_LOG" value="debug" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run (Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<configuration default="false" name="Run (GTK Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="run --package ironbar --bin ironbar" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
@@ -12,6 +12,8 @@
|
||||
<envs>
|
||||
<env name="GTK_DEBUG" value="interactive" />
|
||||
<env name="IRONBAR_CONFIG" value="examples/config.json" />
|
||||
<env name="RUST_LOG" value="debug" />
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
1
.idea/runConfigurations/Run__Live_Config_.xml
generated
1
.idea/runConfigurations/Run__Live_Config_.xml
generated
@@ -11,6 +11,7 @@
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs>
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
<env name="RUST_LOG" value="debug" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
|
||||
428
CHANGELOG.md
Normal file
428
CHANGELOG.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v0.13.0] - 2023-08-16
|
||||
### :sparkles: New Features
|
||||
- [`c3e9654`](https://github.com/JakeStanger/ironbar/commit/c3e9654cd3dfcea4276f5b114112b7541dd847fd) - **upower**: icon size option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f5bdc5a`](https://github.com/JakeStanger/ironbar/commit/f5bdc5a0272fefca4c91336699ea63913cdf3c08) - ipc server and cli *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ded50cc`](https://github.com/JakeStanger/ironbar/commit/ded50cca6f01f08a8e44257394fdde634d421e8e) - support for 'ironvar' dynamic variables *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c6319b7`](https://github.com/JakeStanger/ironbar/commit/c6319b78fd3992ad43327e90b6326ab653238f2e) - **ipc**: support for injecting additional stylesheets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27f920d`](https://github.com/JakeStanger/ironbar/commit/27f920d01217bedba279003291ad48c1aaa56bb0) - **launcher**: slightly improve focus logic when clicking item with multiple windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd90167`](https://github.com/JakeStanger/ironbar/commit/bd90167f4ea90cb97984b9f3b5e6f65b375d0c4a) - **clock**: format option for popup header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`12053f1`](https://github.com/JakeStanger/ironbar/commit/12053f111a6f05a59e33396b9042821b98b9bc5c) - **music**: progress/seek bar in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7d3bb02`](https://github.com/JakeStanger/ironbar/commit/7d3bb02b4612f278bcc8a268a48c61b239c63e82) - **ipc**: reload config command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b310ea7`](https://github.com/JakeStanger/ironbar/commit/b310ea76362bcdf10e187d6b00cd2b8ed2870c41) - **clock**: localization support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`738b9e3`](https://github.com/JakeStanger/ironbar/commit/738b9e3da714c9b998030e9f60b9b6f50c62ec76) - **config**: use default fallback with config instructions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2ccb263`](https://github.com/JakeStanger/ironbar/commit/2ccb2633c6c4d7f6eb264a6c49951853b728c9f3) - IPC for get_visible, set_visible, new bar `name` config option *(commit by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja))*
|
||||
- [`b7ee794`](https://github.com/JakeStanger/ironbar/commit/b7ee794bfc86730e7921c8a930cf8d8bb44474ad) - **ipc**: commands for opening/closing popups *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ef443e6`](https://github.com/JakeStanger/ironbar/commit/ef443e6978949479388129760dabc3f8930c0b0f) - **image resolver**: add fallback image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9f65cf2`](https://github.com/JakeStanger/ironbar/commit/9f65cf293d9527a2c536847f0005957421a9be33) - **workspaces**: add `favorites` and `hidden` options *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`19c684e`](https://github.com/JakeStanger/ironbar/commit/19c684e49facb57e3e2acf9cafecf177c2e1bfbf) - **nix**: automatic development environment with direnv *(commit by [@yavko](https://github.com/yavko))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`6db7742`](https://github.com/JakeStanger/ironbar/commit/6db7742e068dc03d67dbf35e0d9db27f900392fe) - crash on startup introduced by recent refactors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f78c7f9`](https://github.com/JakeStanger/ironbar/commit/f78c7f9b981c98676e855ff6a63e33a51927c709) - not resolving flatpak application icons *(commit by [@body20002](https://github.com/body20002))*
|
||||
- [`1759945`](https://github.com/JakeStanger/ironbar/commit/1759945912e376581e5fcd5ed2916f89e2090f2b) - **music**: correctly show/hide popup elements based on player capabilities *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a9ac29d`](https://github.com/JakeStanger/ironbar/commit/a9ac29d8857256d13e14104db235117e3c752972) - clipboard partially behind wrong feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c711dd8`](https://github.com/JakeStanger/ironbar/commit/c711dd858554140bcfb6df515a43b40ee2baee67) - failing to resolve icons with home_manager *(commit by [@christoph00](https://github.com/christoph00))*
|
||||
- [`1a272e0`](https://github.com/JakeStanger/ironbar/commit/1a272e00fbeca4b5e39b527ffed439bc51fd4080) - **label**: not using markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4ca17d1`](https://github.com/JakeStanger/ironbar/commit/4ca17d1337acfbbc21c04058d97f689a1cce60a6) - **launcher**: incorrectly resolving some applications *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`eee2182`](https://github.com/JakeStanger/ironbar/commit/eee2182ab90fdc22cd05da9417cbee17e4c67088) - **ipc**: command/response casing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c582bc3`](https://github.com/JakeStanger/ironbar/commit/c582bc33905702a9ebe323e6dfa9413485f48fb7) - **cli**: `set-visible` command causing panic *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`87dd764`](https://github.com/JakeStanger/ironbar/commit/87dd7646fc9223ac7e67842934f3bd104b4eea80) - **launcher**: not clearing focused state when closing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6f57ad4`](https://github.com/JakeStanger/ironbar/commit/6f57ad47ac30348c0ae2b7dba35d5ffdbf96f72d) - **launcher**: not setting focus state when opening favourite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2367faa`](https://github.com/JakeStanger/ironbar/commit/2367faab0440327620052de845c6a0d3032f2f05) - **image**: using fallback in places it shouldn't *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7f6fef6`](https://github.com/JakeStanger/ironbar/commit/7f6fef6338d7a8c909f3224b32426dfc2aacc295) - **image**: matching desktop file names too eagerly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`89ec06f`](https://github.com/JakeStanger/ironbar/commit/89ec06fc7b252052f96e45f5d0f6d6504878a13a) - **music**: hide album art widget when no image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2902331`](https://github.com/JakeStanger/ironbar/commit/2902331af00f2e52fdea06964fbd89d72fe2ebbb) - **dynamic string**: incorrectly handling strings containing multipoint utf-8 chars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`901a86c`](https://github.com/JakeStanger/ironbar/commit/901a86caa491648268f0618e17a25b978552db0c) - **custom**: crash when clicking non-popup button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`54f0f23`](https://github.com/JakeStanger/ironbar/commit/54f0f232f208602699e5021942c3d0f3947ca6de) - **launcher**: popup not closing when hover leaves widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`d121dc3`](https://github.com/JakeStanger/ironbar/commit/d121dc3d1e9468a67deb528c35fc3897c3840f77) - fix unused var warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cc181a8`](https://github.com/JakeStanger/ironbar/commit/cc181a8b6d0ac1cccd4ed2f9f420c138ed5383d2) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7016f7f`](https://github.com/JakeStanger/ironbar/commit/7016f7f79e7e29a3318b535ba224aa78bd91688a) - use new smart pointer macros throughout codebase *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`06251e2`](https://github.com/JakeStanger/ironbar/commit/06251e293e8f56e1897fed80335f114fdea57183) - fix new pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3d94987`](https://github.com/JakeStanger/ironbar/commit/3d949874de90b0e5c06cb62726629133d0ea76e3) - **ipc**: add link to luajit library *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.12.1] - 2023-06-18
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.
|
||||
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`31a57ae`](https://github.com/JakeStanger/ironbar/commit/31a57ae637fa5918f163c8b191916867395912f3) - scripts don't work while running ironbar under a systemd service *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f82f897`](https://github.com/JakeStanger/ironbar/commit/f82f897982e87906e2c9156d4115013bc8e99763) - **upower**: popup always empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9012fee`](https://github.com/JakeStanger/ironbar/commit/9012feee4f9b60b2c22a956de732847892331222) - **image**: still blurry on hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0e65f93`](https://github.com/JakeStanger/ironbar/commit/0e65f93a230cb5ab010b43962fd2e829945c291b) - excess popup windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`87ca399`](https://github.com/JakeStanger/ironbar/commit/87ca399220e5d48eefe2f295d1dba1b9452c4472) - poor error handling for missing images *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`22b630a`](https://github.com/JakeStanger/ironbar/commit/22b630a10b9836531a8b03eb904e6f9fcf839fe6) - broken nerd font icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`48d6af0`](https://github.com/JakeStanger/ironbar/commit/48d6af0281f460d3ed3745a2ffb2b61848430ecb) - **music**: showing when no mpris player found *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b9740cb`](https://github.com/JakeStanger/ironbar/commit/b9740cba8f2fa9dfa18a57345027283610f6487e) - upower icon too large *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a6b6866`](https://github.com/JakeStanger/ironbar/commit/a6b686624b750863aa1c26ca4f1688dfa8c81a61) - **upower**: icon outside button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a5ecb36`](https://github.com/JakeStanger/ironbar/commit/a5ecb363fdb2eb3ab543ad56c55c186414500469) - popups occasionally getting jumbled with multiple bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ac34c05`](https://github.com/JakeStanger/ironbar/commit/ac34c05d2ecb07fd871ed03ef6ee545dc2e6743d) - **focused**: empty icon rendered when `show_icon = false` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de3aa5d`](https://github.com/JakeStanger/ironbar/commit/de3aa5d7b10e0bf6d5ff3a39b009ff53a3316a5e) - **focused**: previous icon does not clear if new icon fails to load *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de98cf3`](https://github.com/JakeStanger/ironbar/commit/de98cf3daee816a0ff72d1f6ba6bc0e15ec53fca) - **tray**: (maybe?) sometimes bus name is taken *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ff315ff`](https://github.com/JakeStanger/ironbar/commit/ff315ff5dbd545d8b72b6aa10087c940cb8a5eee) - **music**: fix incorrect type for `host`/`music_dir` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd144e8`](https://github.com/JakeStanger/ironbar/commit/bd144e87a8f6668c877d42697ebbedbe5a374c3d) - **readme**: make prettier *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`242b70e`](https://github.com/JakeStanger/ironbar/commit/242b70ed3988b85455b0dbbcb3243b31f89d2ee1) - **contributing**: enforce conventional commits *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`96d36c4`](https://github.com/JakeStanger/ironbar/commit/96d36c43d43ba2f9e9d9441ae01c0743cc56f627) - add missing icon/image selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.12.0] - 2023-05-06
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`6c62286`](https://github.com/JakeStanger/ironbar/commit/6c622864b388548eaaa595f41993606cc151d585) - new label module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cac064f`](https://github.com/JakeStanger/ironbar/commit/cac064f4795e9f418cc0820f04944f91121c426a) - ability to configure popup gap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dfe1964`](https://github.com/JakeStanger/ironbar/commit/dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d) - **custom**: slider widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`72b14b6`](https://github.com/JakeStanger/ironbar/commit/72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2) - **custom**: progress bar widget. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a9d1233`](https://github.com/JakeStanger/ironbar/commit/a9d12339097cbe0fef1628460ef538319a048223) - **custom**: support dynamic strings on buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3d308ab`](https://github.com/JakeStanger/ironbar/commit/3d308ab572a39ada2501ddc1b822e50e1f8a8363) - **custom**: support dynamic string in image source *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4a09b70`](https://github.com/JakeStanger/ironbar/commit/4a09b70854dad33bf890a3fe766f854d9195e786) - **custom**: support common options in widgets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83f44fd`](https://github.com/JakeStanger/ironbar/commit/83f44fd92fe74b45fcdfc242fb90fc932dd2b00b) - wrap modules in a revealer to support animated show/hide *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1fa0c0e`](https://github.com/JakeStanger/ironbar/commit/1fa0c0e9774c302727d414f5aef999ab71a7acb8) - **custom**: support mouse wheel on slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2da28b9`](https://github.com/JakeStanger/ironbar/commit/2da28b9bf5790adfc46c58b6f6d5fdd13cc17195) - ability to configure image icon sizes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`033d0f7`](https://github.com/JakeStanger/ironbar/commit/033d0f7e6e450b3f2d62d9a75210d52611cf346d) - **custom**: option to toggle slider label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`76e2b7b`](https://github.com/JakeStanger/ironbar/commit/76e2b7ba3e788f273039d74635881ddb96264258) - **music**: option to hide status icon on widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ad3c171`](https://github.com/JakeStanger/ironbar/commit/ad3c171ecacaebf10408c2583ed7361ed029075e) - implement upower module *(commit by [@p00f](https://github.com/p00f))*
|
||||
- [`2a155b9`](https://github.com/JakeStanger/ironbar/commit/2a155b9aa8a3634908512d9b83680925962d478f) - **music**: add css selector for button contents *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c1ea5fa`](https://github.com/JakeStanger/ironbar/commit/c1ea5fad7ec308895f0454b6de05a3177563626c) - **logging**: include line numbers *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`9109453`](https://github.com/JakeStanger/ironbar/commit/910945306c3261190a16300da2ed28efb945a6ac) - **dynamic string**: parser issue related to incorrectly matching braces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7355db7`](https://github.com/JakeStanger/ironbar/commit/7355db74ec9118c2cb46899534a3adac8d7165d9) - **image**: http provider not handling non-success codes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a87d8d5`](https://github.com/JakeStanger/ironbar/commit/a87d8d5c3071a1d8ab149deae17d261ae97368ea) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`15a9d8d`](https://github.com/JakeStanger/ironbar/commit/15a9d8d42c9319a7062e6a90086e0c1c3323f5d8) - **script**: parser incorrectly handling colons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`68bc823`](https://github.com/JakeStanger/ironbar/commit/68bc8230ddf3352cc0de9f8cc770632744c22747) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b038e76`](https://github.com/JakeStanger/ironbar/commit/b038e7671af4bfa41060adf724deb8c6151fac1f) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7926bb0`](https://github.com/JakeStanger/ironbar/commit/7926bb07eb181edaf6da2f11a7dc00f8be2240eb) - **nix**: Fix `nix run` support *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`2c88c99`](https://github.com/JakeStanger/ironbar/commit/2c88c99cb605d312e2d76d620f502c7e7cd8866e) - **dynamic string**: crash when last segment is static and a single char *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`338f5a0`](https://github.com/JakeStanger/ironbar/commit/338f5a0e1b58dc9b52caee61d6a9748cf13153c5) - **nix**: Attempt to fix image blurriness *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`db0868a`](https://github.com/JakeStanger/ironbar/commit/db0868a3fc0734daa61067e377018c692599ebff) - **image**: not scaling icons for hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`14b6c1a`](https://github.com/JakeStanger/ironbar/commit/14b6c1a69f28836ed9e3b74eeb97a42ea60ffc27) - bars duplicate when starting second instance *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`98aaaa0`](https://github.com/JakeStanger/ironbar/commit/98aaaa0d1407681b3d790c933c4972b8122f8007) - fallback to default icon theme for notifier items *(commit by [@oknozor](https://github.com/oknozor))*
|
||||
- [`735f5cc`](https://github.com/JakeStanger/ironbar/commit/735f5cc9f1518c256785d42f3d21ed5c68b11711) - **launcher**: crash when focusing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e1abadc`](https://github.com/JakeStanger/ironbar/commit/e1abadcf39a2d39078e75179a167e9277ee5e550) - **clipboard**: copying large images filling write pipe *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`2ab06f0`](https://github.com/JakeStanger/ironbar/commit/2ab06f044ec300628d6648852d395889b6752b76) - **custom**: split into enum with separate file per widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3613aef`](https://github.com/JakeStanger/ironbar/commit/3613aef5c5a4051b5a44e33342c0eaaab3d4a690) - **custom**: reduce a lot of repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c214f65`](https://github.com/JakeStanger/ironbar/commit/c214f65ecb86a0da6559025203701661924f65bb) - fix strict clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27d11de`](https://github.com/JakeStanger/ironbar/commit/27d11de6616c410422d7abd579d09b3abc02f43a) - **config**: split common code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6fd69d6`](https://github.com/JakeStanger/ironbar/commit/6fd69d657c6224bc47c9b3cb5affcf74b63a6aa6) - move module creation code to module module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e63509a`](https://github.com/JakeStanger/ironbar/commit/e63509a3a7673ea41b4c937089a1cf6d2362fed3) - fix a few new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7f46cb4`](https://github.com/JakeStanger/ironbar/commit/7f46cb49767bd722be8d42999a9ba69887efcd40) - **wayland**: update to 0.30.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dd7c9f3`](https://github.com/JakeStanger/ironbar/commit/dd7c9f30db6e4e1ede4d57255122b359636b8f58) - add transition module-level options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`610c352`](https://github.com/JakeStanger/ironbar/commit/610c3528af98b8c6b02af7ce5c07190776522c3a) - add missing link to upower page *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ea9f7ca`](https://github.com/JakeStanger/ironbar/commit/ea9f7caaf7a35eebd603ce2854672d5af2901018) - add missing `upower` feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`618b7ef`](https://github.com/JakeStanger/ironbar/commit/618b7ef5520de6f3796b66e42422a36c5a191ab0) - improve example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`139bc5d`](https://github.com/JakeStanger/ironbar/commit/139bc5d23f7f887b7b65d50adc21fa6679ea291e) - **compiling**: improve requirements list *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cf32870`](https://github.com/JakeStanger/ironbar/commit/cf32870f8a380c305a436593950c3da524a2296f) - **compiling**: add ron feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.11.0] - 2023-04-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`d253c4b`](https://github.com/JakeStanger/ironbar/commit/d253c4bd7f306c7b8fef223d1beb7b1f6e77629b) - add configurable margins around bar *(commit by [@ttoino](https://github.com/ttoino))*
|
||||
- [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - **truncate**: ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`575d6cc`](https://github.com/JakeStanger/ironbar/commit/575d6cc30f9e28079aed8425566048abd3d9e022) - new clipboard manager module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9984b63`](https://github.com/JakeStanger/ironbar/commit/9984b638b55adea11ba90412346fbb8220f05682) - **nix**: initial nix feature flags impl *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`b1475a1`](https://github.com/JakeStanger/ironbar/commit/b1475a1affd2f101f1f707ab1a0e8e5509a1d99f) - **nix**: use cargo default features *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`102d247`](https://github.com/JakeStanger/ironbar/commit/102d2478a9d0ecc8be12c5ea6019a5a5411cc6ab) - module hover options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`2ac5071`](https://github.com/JakeStanger/ironbar/commit/2ac507144b42a80507f8d2df214889c114c069df) - not setting layer shell namespace *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7dff3e6`](https://github.com/JakeStanger/ironbar/commit/7dff3e6f8b989132ff0c4406caa72f063dd57c9f) - **image**: widgets missing names *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`54b9b28`](https://github.com/JakeStanger/ironbar/commit/54b9b28c75b2fe300e2bad1436d315da1950953e) - make readme more concise *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`8cbb73b`](https://github.com/JakeStanger/ironbar/commit/8cbb73b75e7aca1aa163406f4583273e6ff4bac2) - **dynamic string**: dynamic sections not respecting ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`d0b7bdb`](https://github.com/JakeStanger/ironbar/commit/d0b7bdbafcc34967dd5b048ea12e6267ba293566) - **nix**: home manager module, and features *(commit by [@yavko](https://github.com/yavko))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`d84139a`](https://github.com/JakeStanger/ironbar/commit/d84139a914f9b35054dc6048715e1ed7e79d7441) - general tidy up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7212bbc`](https://github.com/JakeStanger/ironbar/commit/7212bbcf61e097b35a7ab341e19e9daefd2edf95) - **dynamic string**: use vec instead of indexmap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ecdd71a`](https://github.com/JakeStanger/ironbar/commit/ecdd71a43d267161f84e3c4a3c22e9454c0f7184) - **config**: use `universal-config` crate. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`82875cd`](https://github.com/JakeStanger/ironbar/commit/82875cde687628f3ee3436343068825440128599) - update CHANGELOG.md for v0.10.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7c36f5c`](https://github.com/JakeStanger/ironbar/commit/7c36f5cb0cf03191c9b03e2455b63829a64e402e) - fix a couple of issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83a4916`](https://github.com/JakeStanger/ironbar/commit/83a49165c42fa793ef1224f93cbc147bc69de894) - **compiling**: add info about build deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5bbe64b`](https://github.com/JakeStanger/ironbar/commit/5bbe64bb86fb2db0921e284a1560db2f6c1a1920) - **clock**: format table *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2b26eaf`](https://github.com/JakeStanger/ironbar/commit/2b26eaf41036609be4dfc57689ca8d770dcb6b9b) - **clipboard**: fix incorrect setting description *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0125ce5`](https://github.com/JakeStanger/ironbar/commit/0125ce5916c003d1ea9a141fe5a0f6a54b2778ab) - **examples**: update styles example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.10.0] - 2023-02-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This removes the `icon_theme` option from `launcher` and `focused`. You will need to set this at the top of your config instead.
|
||||
|
||||
- due to [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
(Missed from #96141d4) The `{icon}` token has been removed from the `music` module due to incompatibility with the new image/icon support. The icon now always displays as a separate widget before the label and should be removed from your formatting string.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`8691824`](https://github.com/JakeStanger/ironbar/commit/8691824db1a12c3f3589ff8b5315b8dba5cb8aec) - **music**: ability to truncate button text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`07dbf78`](https://github.com/JakeStanger/ironbar/commit/07dbf780105027b533b0bb34c9ae3e4e96f29f4a) - **focused**: ability to truncate label text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`393800a`](https://github.com/JakeStanger/ironbar/commit/393800aaa2093b9257c43fde8bdb8399f26ebc74) - **custom**: image widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b054c17`](https://github.com/JakeStanger/ironbar/commit/b054c17d14628c9188bfa9aed506ea1de3051f9c) - **workspaces**: support for using images in `name_map` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`96141d4`](https://github.com/JakeStanger/ironbar/commit/96141d49907412ea26d23ef30c10ade8b32b89b9) - **music**: support for using images in `name_map`, additional icon options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c347b6c`](https://github.com/JakeStanger/ironbar/commit/c347b6c9449ce4e16e2e133d7dd35544ab9a533c) - add feature flags *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`5772711`](https://github.com/JakeStanger/ironbar/commit/57727111923a419f9b7613103283aa4cf6bd082c) - **music**: remote mpris album art not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5fb4125`](https://github.com/JakeStanger/ironbar/commit/5fb412572f3da60ac482a1960d891f70bc29287b) - **tray**: some init issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`058c8f4`](https://github.com/JakeStanger/ironbar/commit/058c8f4228f9f7faa66cda9dd1636ea32e9de68b) - **hyprland**: issues with tracking workspaces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`51d2c22`](https://github.com/JakeStanger/ironbar/commit/51d2c2279f50add992def0d58cfaa9890ea3d041) - **images**: incorrectly resolving non-files *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`012762e`](https://github.com/JakeStanger/ironbar/commit/012762e10203fb2d58160acdae4dc7ca7689b131) - swap out some code for existing macros *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9750255`](https://github.com/JakeStanger/ironbar/commit/97502559b30c51e77c1dd9a7d794a88756294c83) - **music**: split config code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`15f0857`](https://github.com/JakeStanger/ironbar/commit/15f0857859d5d4a590b60b6b1a4347b4b84a58a1) - replace icon loading with improved general image loading *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1ed3220`](https://github.com/JakeStanger/ironbar/commit/1ed3220733c2dcb7c5e5cbf377b3324d3183609e) - update CHANGELOG.md for v0.9.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - **music**: remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6a39905`](https://github.com/JakeStanger/ironbar/commit/6a39905b4333582fbcda81a66a9b91055333d698) - **compiling**: add missing full stop *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7b23e61`](https://github.com/JakeStanger/ironbar/commit/7b23e61e7dedf2736a30580b6c1aa84e002c462c) - **wiki**: update screenshots and examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.9.0] - 2023-01-28
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
`on_click` is now called `on_click_left` for consistency with new options.
|
||||
|
||||
- due to [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
The `mpd` module has been renamed to `music`. You will need to update the `type` value in your config and add `player_type` to continue using MPD. You will also need to update your styles.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`1dd5863`](https://github.com/JakeStanger/ironbar/commit/1dd586343143bfd501a44c6556719fac9d582d6b) - better surface some config error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6e5d0c1`](https://github.com/JakeStanger/ironbar/commit/6e5d0c1e8c0b5d7e330608fc835e1e9733f156de) - **workspaces**: hyprland support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9ba28fe`](https://github.com/JakeStanger/ironbar/commit/9ba28fe7faf84e06febc2ffea089442f8f5b90a2) - **workspaces**: better ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`e1f523c`](https://github.com/JakeStanger/ironbar/commit/e1f523cf2a15b74a5c570dd7440db4c1b476d782) - **music**: popup artist label using wrong name *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`08cfbbc`](https://github.com/JakeStanger/ironbar/commit/08cfbbc2eaf6e74780dd7196efcc15ea6d2e7d12) - **music**: unable to go to prev with mpris *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0cefcbd`](https://github.com/JakeStanger/ironbar/commit/0cefcbd02b0af518352e35060644f281da249d3e) - **music**: wrong widget name on vol slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`90cd078`](https://github.com/JakeStanger/ironbar/commit/90cd078973b23b2291cf156e46729842f33c1806) - **mpd**: stops working if connection lost *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`2c1b292`](https://github.com/JakeStanger/ironbar/commit/2c1b2924d4a103183d3974ac066623a80277a79a) - move most of the horrible `add_module` macro content into proper functions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`fd2d7e5`](https://github.com/JakeStanger/ironbar/commit/fd2d7e5c7ab8de50c4621b19d07d8b012a451564) - move startup logging code to logging module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9d5049d`](https://github.com/JakeStanger/ironbar/commit/9d5049dde01cdb76f4772f8ce8f61a8b5bad3a50) - standardise error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5e21cbc`](https://github.com/JakeStanger/ironbar/commit/5e21cbcca6cc30d725acdea0f6561cfd6acdcc3c) - macros to reduce repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ea2c84d`](https://github.com/JakeStanger/ironbar/commit/ea2c84d1bd15798e32496397c4a6aa42fab39d95) - general code tidy-up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0d7ab54`](https://github.com/JakeStanger/ironbar/commit/0d7ab541604691455ed39c73e039ac0635307bc8) - remove redundant clone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`b97f018`](https://github.com/JakeStanger/ironbar/commit/b97f018e81aa55a871a12aa3e1e4b07b1f8eb50f) - update CHANGELOG.md for v0.8.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c223892`](https://github.com/JakeStanger/ironbar/commit/c223892a57b29ae56431fc585b8cec503f3206c7) - **workspaces**: update for hyprland/new ordering option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.8.0] - 2022-11-30
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`df77020`](https://github.com/JakeStanger/ironbar/commit/df77020c5277ae9e379bb4fd67c221be5cb20426) - use snake_case for module tokens for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This renames the module from `sys-info` to `sys_info`, and almost every formatting token from `kebab-case` to `snake_case`. Any use of this module will need to be updated.
|
||||
|
||||
- due to [`8c75bc4`](https://github.com/JakeStanger/ironbar/commit/8c75bc46ac2885a748d31df9261d988cc797e916) - rename `path` to `cmd` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This changes the option in the `script` module. Any uses of the module must be updated to use the new option name.
|
||||
|
||||
- due to [`e274ba3`](https://github.com/JakeStanger/ironbar/commit/e274ba39cd6d8f1c73033ac1e60e5bce89205ce2) - rename `exec` to `on_click` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This changes the option on buttons in the `custom` module. Any uses of the module must be updated to use the new custom widget attribute name.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`73158c2`](https://github.com/JakeStanger/ironbar/commit/73158c2fce2880347b88d58541dea000534996c8) - **script**: new watch mode *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a3f90ad`](https://github.com/JakeStanger/ironbar/commit/a3f90adaf19aebed7020eeb44b91250af080d313) - add nix flake support *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`c9e66d4`](https://github.com/JakeStanger/ironbar/commit/c9e66d4664137c50aba4aecdc3a3ba43d3da11fe) - common module options (`show_if`, `on_click`, `tooltip`) *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5d153a0`](https://github.com/JakeStanger/ironbar/commit/5d153a02fc9b113bb77a04596b806edd182fc5d3) - **custom**: ability to embed scripts in labels for dynamic content *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`d20972c`](https://github.com/JakeStanger/ironbar/commit/d20972cb32714627d0cca947021453979c76dd03) - dynamic tooltips *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`ff17ec1`](https://github.com/JakeStanger/ironbar/commit/ff17ec1996cf344663e84e79d11b08dc84b97635) - various changes based on rust 1.65 clippy *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4662f60`](https://github.com/JakeStanger/ironbar/commit/4662f60ac54165be6fb7aea12c245309db0fe5d6) - move various clients to own folder *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0fb5fa8`](https://github.com/JakeStanger/ironbar/commit/0fb5fa8c2a166c3d46b006ceb0d53af076824ff4) - use latest `libcorn` with serde support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`df77020`](https://github.com/JakeStanger/ironbar/commit/df77020c5277ae9e379bb4fd67c221be5cb20426) - **sys_info**: use snake_case for module tokens for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`8c75bc4`](https://github.com/JakeStanger/ironbar/commit/8c75bc46ac2885a748d31df9261d988cc797e916) - **script**: rename `path` to `cmd` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e274ba3`](https://github.com/JakeStanger/ironbar/commit/e274ba39cd6d8f1c73033ac1e60e5bce89205ce2) - **custom**: rename `exec` to `on_click` for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`64f5404`](https://github.com/JakeStanger/ironbar/commit/64f54040ef626157af6b6a9ce5258507a10a23fb) - move dynamic_label.rs to dynamic_string.rs and fix failing test *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :white_check_mark: Tests
|
||||
- [`907a565`](https://github.com/JakeStanger/ironbar/commit/907a565f3d418a276dfb454e1189ddede1814291) - **dynamic label**: do not run if cannot initialise gtk *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1c032ae`](https://github.com/JakeStanger/ironbar/commit/1c032ae8e3a38b82c286bab7d102842f14b708e1) - update CHANGELOG.md for v0.7.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`58d55db`](https://github.com/JakeStanger/ironbar/commit/58d55db6600fe2f9b23ae8ec6a50a686d2acaf65) - migrate wiki into main repo *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c480296`](https://github.com/JakeStanger/ironbar/commit/c48029664d5f58bf73faa2931f34b38b8b184d25) - **script**: improve doc comment *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`8c77410`](https://github.com/JakeStanger/ironbar/commit/8c774100f1c8ea051284c6950339a2c8ed59a52a) - **script**: add information on new mode options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c4cdf4b`](https://github.com/JakeStanger/ironbar/commit/c4cdf4be8ba83f3669158a1552eab4a840085204) - update example configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ec69649`](https://github.com/JakeStanger/ironbar/commit/ec69649a04f6199953836e51c2efe1fe2a19e320) - update example configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1320639`](https://github.com/JakeStanger/ironbar/commit/1320639d4e6b7c8cd8f861b26b2b854504775ef0) - add custom power menu example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`afedf02`](https://github.com/JakeStanger/ironbar/commit/afedf0214d3a71f6283c70bd3a110d24f68d2fdf) - add link to new custom power menu example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.7.0] - 2022-11-05
|
||||
### :sparkles: New Features
|
||||
- [`fad90fd`](https://github.com/JakeStanger/ironbar/commit/fad90fdad683a612497ac7822a66a90f43fce0a2) - **sys-info**: add loads more formatting tokens *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dec402e`](https://github.com/JakeStanger/ironbar/commit/dec402edd9d6c5b8677ff337699ad99ebc69b776) - **sys-info**: config options for refresh intervals *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`91c57ed`](https://github.com/JakeStanger/ironbar/commit/91c57edc73f15397ea0de70c4a6a6532c35caf2a) - **sys-info**: pango markup support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ec1d596`](https://github.com/JakeStanger/ironbar/commit/ec1d59677b13c9654a98d78f909ba2d0fcfbb72d) - **logging**: `IRONBAR_LOG` and `IRONBAR_FILE_LOG` env vars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`493df6b`](https://github.com/JakeStanger/ironbar/commit/493df6bb49fec8c465706d3f9b395728ba73a621) - **mpd**: add volume slider to popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3750124`](https://github.com/JakeStanger/ironbar/commit/3750124d8cfb4783932a6b3359384f245fcd2394) - new custom module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b7792a4`](https://github.com/JakeStanger/ironbar/commit/b7792a415e09fc535750ea5af530f91aa791c4bc) - env var to set custom css location *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ad77dc4`](https://github.com/JakeStanger/ironbar/commit/ad77dc4e4c2f80fcb4c9604c796be0f981e895ee) - improved logging & error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`9e6dbbd`](https://github.com/JakeStanger/ironbar/commit/9e6dbbd131a09f101b0d490265fe7d4ec564e38c) - **sys-info**: tokens not replaced if more than one in string *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f17ae7a`](https://github.com/JakeStanger/ironbar/commit/f17ae7a415b931c64942de085e8889f37b3f9b11) - **script**: not parsing pango markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b66bd78`](https://github.com/JakeStanger/ironbar/commit/b66bd788b23256a2127a1352693fdd3f929d9c4b) - logging for creating bar incorrect still *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3c43c20`](https://github.com/JakeStanger/ironbar/commit/3c43c20c6ae53a9aa6b67770b0c489806784f4ac) - weird behaviour when config does not exist *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`70e1b52`](https://github.com/JakeStanger/ironbar/commit/70e1b526a9681b16545d7f05d77470d76bd8819e) - **logging**: file log not capturing panics *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cbd0c49`](https://github.com/JakeStanger/ironbar/commit/cbd0c49e251b5c8e0289ca6200a393d89994992d) - css watcher not working *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e693c1c`](https://github.com/JakeStanger/ironbar/commit/e693c1c166eef0b5edcdcd033bb12d572e4e5f04) - **mpd**: volume slider causing mpd server errors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3a83bd3`](https://github.com/JakeStanger/ironbar/commit/3a83bd31ab165869f7f274b054b2f16485261fd1) - able to insert duplicate keys into collection *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`5ebc84c`](https://github.com/JakeStanger/ironbar/commit/5ebc84c7b98cc648a659ca37fdc0f041057f0ea4) - **logging**: consts for default log levels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bc625b9`](https://github.com/JakeStanger/ironbar/commit/bc625b929b8644ce92f275b5d98cdf74b93fe067) - clippy & fmt *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`9d9c275`](https://github.com/JakeStanger/ironbar/commit/9d9c2753137331ae85ac8ab7d75a6de9a9c82042) - update CHANGELOG.md for v0.6.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27d0479`](https://github.com/JakeStanger/ironbar/commit/27d04795af1c25fe5f765c7480d5dd5d096a8ab7) - **readme**: add warning about crate being outdated *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a06c4bc`](https://github.com/JakeStanger/ironbar/commit/a06c4bccca6cb51935605ac9239e63024fb7c663) - **examples**: add full system info config *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0a331f3`](https://github.com/JakeStanger/ironbar/commit/0a331f31381f0d967793c0d8b7a14e2a43bf666f) - **readme**: remove warning about outdated cargo package *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.6.0] - 2022-10-15
|
||||
### :sparkles: New Features
|
||||
- [`b188bc7`](https://github.com/JakeStanger/ironbar/commit/b188bc714614406935d8bb88a719adab2dfce32f) - initial support for running outside sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`324f00c`](https://github.com/JakeStanger/ironbar/commit/324f00cdf9200e3e3ecedfa68ab4c99b170242e2) - wlroots-agnostic support for `focused` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b1c66b9`](https://github.com/JakeStanger/ironbar/commit/b1c66b9117cf8a10350cdb857a5267a1a72ad914) - wlroots-agnostic support for `launcher` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1dd0a9e`](https://github.com/JakeStanger/ironbar/commit/1dd0a9e52f69e672d9ac313c1da0e201c911e6c2) - **launcher**: add popup css selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`5523e9a`](https://github.com/JakeStanger/ironbar/commit/5523e9af46e457f9d45902debaaacf26b586e457) - **popup**: often opening in wrong place *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`8536ad7`](https://github.com/JakeStanger/ironbar/commit/8536ad719a92aec4166e35b75cb029075ad3ae34) - **mpd**: incorrectly checking for unix sockets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd5bdf5`](https://github.com/JakeStanger/ironbar/commit/bd5bdf5af548304958663d593fccb454afa6c8ff) - logging for creating bar incorrect *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`75339f0`](https://github.com/JakeStanger/ironbar/commit/75339f07ed164fa94838036a604a1dcb6d53564c) - vertical bars ignoring height config option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b7b6488`](https://github.com/JakeStanger/ironbar/commit/b7b64886e3c48ace3faffbb1e277275aeeac3adf) - sometimes panicking on startup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`5ce50b0`](https://github.com/JakeStanger/ironbar/commit/5ce50b0987812a1ade2d1262e8d7df6916cfc39a) - tidy and format *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`daafa09`](https://github.com/JakeStanger/ironbar/commit/daafa0943e5b9886b09fd18d6fff04558fb02335) - update CHANGELOG.md for v0.5.2 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b352181`](https://github.com/JakeStanger/ironbar/commit/b352181b3d232ccc79ffc1d9e22a633729d01a47) - update json example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bb4fe7f`](https://github.com/JakeStanger/ironbar/commit/bb4fe7f7f58fa2a6d0a2259bd9442700d2c884f7) - **readme**: credit smithay client toolkit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
The `left` and `right` config options have been renamed to `start` and `end`
|
||||
|
||||
|
||||
## [v0.5.2] - 2022-09-07
|
||||
### :wrench: Chores
|
||||
- [`b801751`](https://github.com/JakeStanger/ironbar/commit/b801751bdabd8416084f46e6b6d803ea28a259ec) - **release**: v0.5.2 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.5.1] - 2022-09-06
|
||||
### :bug: Bug Fixes
|
||||
- [`b81927e`](https://github.com/JakeStanger/ironbar/commit/b81927e3a57808188e31419695a36aa4ea3f2830) - **launcher**: opening new instances when focused/urgent *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a35d255`](https://github.com/JakeStanger/ironbar/commit/a35d25520cd3fd235cdc77ec6209d88499ca3639) - **launcher**: item state changes not handled correctly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :wrench: Chores
|
||||
- [`481adfc`](https://github.com/JakeStanger/ironbar/commit/481adfcaa41c0d3a1ba7d61edb68db49d959c78f) - **intellij**: update run configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6871126`](https://github.com/JakeStanger/ironbar/commit/6871126bd8def89ccbf2934180d615e781ec32c7) - **release**: v0.5.1 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.5.0] - 2022-08-25
|
||||
### :sparkles: New Features
|
||||
- [`1e38719`](https://github.com/JakeStanger/ironbar/commit/1e387199962b81caeb40ffbd99a956f24abdf4e3) - introduce logging in some areas *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`023c2fb`](https://github.com/JakeStanger/ironbar/commit/023c2fb118f46f3592f1dfe1a6704014c062ab3f) - **workspaces**: not listening to move event *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6dcae66`](https://github.com/JakeStanger/ironbar/commit/6dcae66570cf5434e077ec823cded33771b4239c) - avoid creating loads of sway/mpd clients *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :wrench: Chores
|
||||
- [`015dcd3`](https://github.com/JakeStanger/ironbar/commit/015dcd3204dfa6a1ebcef1b4f3b345ed733fee2f) - **release**: v0.5.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.4.0] - 2022-08-22
|
||||
### :sparkles: New Features
|
||||
- [`ab8f7ec`](https://github.com/JakeStanger/ironbar/commit/ab8f7ecfc8fa4b96fce78518af75794641950140) - logging support and proper error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`f2ee2df`](https://github.com/JakeStanger/ironbar/commit/f2ee2dfe7a0f5575d0c3ec09644ca990b088cd85) - error when using with `swaybar_command` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :wrench: Chores
|
||||
- [`1d7c377`](https://github.com/JakeStanger/ironbar/commit/1d7c3772e4b97c7198043cb55fe9c71695a211ab) - **release**: v0.4.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
|
||||
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
|
||||
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1
|
||||
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
|
||||
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
|
||||
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0
|
||||
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
|
||||
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
|
||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
||||
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
|
||||
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
|
||||
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
|
||||
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0
|
||||
@@ -4,7 +4,8 @@ I welcome contributions of any kind with open arms. That said, please do stick t
|
||||
- Fix any `cargo clippy` warnings, using at least the default configuration.
|
||||
- Make sure your code is formatted using `cargo fmt`.
|
||||
- Keep any documentation up to date.
|
||||
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
- Please use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
This ensures your contributions are automatically included in the changelog.
|
||||
|
||||
|
||||
- For PRs:
|
||||
@@ -14,3 +15,4 @@ I welcome contributions of any kind with open arms. That said, please do stick t
|
||||
|
||||
- For issues:
|
||||
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
|
||||
- If reporting an error, please ensure you use `IRONBAR_LOG` or `IRONBAR_FILE_LOG` set to `debug`.
|
||||
|
||||
3086
Cargo.lock
generated
3086
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
157
Cargo.toml
157
Cargo.toml
@@ -1,37 +1,140 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.4.0"
|
||||
version = "0.14.0-pre"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable wlroots/sway bar"
|
||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||
repository = "https://github.com/jakestanger/ironbar"
|
||||
categories = ["gui"]
|
||||
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = [
|
||||
"cli",
|
||||
"ipc",
|
||||
"http",
|
||||
"config+all",
|
||||
"clipboard",
|
||||
"clock",
|
||||
"music+all",
|
||||
"sys_info",
|
||||
"tray",
|
||||
"upower",
|
||||
"workspaces+all"
|
||||
]
|
||||
|
||||
cli = ["dep:clap", "ipc"]
|
||||
ipc = ["dep:serde_json"]
|
||||
|
||||
http = ["dep:reqwest"]
|
||||
|
||||
"config+all" = [
|
||||
"config+json",
|
||||
"config+yaml",
|
||||
"config+toml",
|
||||
"config+corn",
|
||||
"config+ron",
|
||||
]
|
||||
"config+json" = ["universal-config/json"]
|
||||
"config+yaml" = ["universal-config/yaml"]
|
||||
"config+toml" = ["universal-config/toml"]
|
||||
"config+corn" = ["universal-config/corn"]
|
||||
"config+ron" = ["universal-config/ron"]
|
||||
|
||||
clipboard = ["nix"]
|
||||
|
||||
clock = ["chrono"]
|
||||
|
||||
music = ["regex"]
|
||||
"music+all" = ["music", "music+mpris", "music+mpd"]
|
||||
"music+mpris" = ["music", "mpris"]
|
||||
"music+mpd" = ["music", "mpd_client"]
|
||||
|
||||
sys_info = ["sysinfo", "regex"]
|
||||
|
||||
tray = ["system-tray"]
|
||||
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
workspaces = ["futures-util"]
|
||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||
"workspaces+hyprland" = ["workspaces", "hyprland"]
|
||||
|
||||
[dependencies]
|
||||
gtk = "0.15.5"
|
||||
gtk-layer-shell = "0.4.1"
|
||||
glib = "0.15.12"
|
||||
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
tracing = "0.1.36"
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
||||
# core
|
||||
gtk = "0.17.0"
|
||||
gtk-layer-shell = "0.6.0"
|
||||
glib = "0.17.10"
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
"process",
|
||||
"sync",
|
||||
"io-util",
|
||||
"net",
|
||||
] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-appender = "0.2.2"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
color-eyre = "0.6.2"
|
||||
futures-util = "0.3.21"
|
||||
chrono = "0.4.19"
|
||||
serde = { version = "1.0.141", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
serde_yaml = "0.9.4"
|
||||
toml = "0.5.9"
|
||||
cornfig = "0.2.0"
|
||||
regex = "1.6.0"
|
||||
stray = "0.1.1"
|
||||
dirs = "4.0.0"
|
||||
walkdir = "2.3.2"
|
||||
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"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
indexmap = "2.0.0"
|
||||
dirs = "5.0.1"
|
||||
walkdir = "2.4.0"
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
wayland-client = "0.30.2"
|
||||
wayland-protocols = { version = "0.30.1", features = ["unstable", "client"] }
|
||||
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
|
||||
"calloop",
|
||||
] }
|
||||
universal-config = { version = "0.4.3", default_features = false }
|
||||
ctrlc = "3.4.1"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
# cli
|
||||
clap = { version = "4.4.4", optional = true, features = ["derive"] }
|
||||
|
||||
# ipc
|
||||
serde_json = { version = "1.0.107", optional = true }
|
||||
|
||||
# http
|
||||
reqwest = { version = "0.11.20", optional = true }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
|
||||
|
||||
# music
|
||||
mpd_client = { version = "1.2.0", optional = true }
|
||||
mpris = { version = "2.0.1", optional = true }
|
||||
|
||||
# sys_info
|
||||
sysinfo = { version = "0.29.10", optional = true }
|
||||
|
||||
# tray
|
||||
system-tray = { version = "0.1.4", optional = true }
|
||||
|
||||
# upower
|
||||
upower_dbus = { version = "0.3.2", optional = true }
|
||||
futures-lite = { version = "1.12.0", optional = true }
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
|
||||
futures-util = { version = "0.3.21", optional = true }
|
||||
|
||||
# shared
|
||||
regex = { version = "1.9.5", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true } # music, sys_info
|
||||
164
README.md
164
README.md
@@ -1,33 +1,140 @@
|
||||
# Ironbar
|
||||
<h1 align="center" >--- Ironbar ---</h1>
|
||||
|
||||
Ironbar is a customisable and feature-rich bar targeting the Sway compositor, written in Rust.
|
||||
It uses GTK3 and gtk-layer-shell.
|
||||
<div align="center">
|
||||
<a href="https://github.com/JakeStanger/ironbar/releases">
|
||||
<img src="https://img.shields.io/crates/v/ironbar?label=version&style=for-the-badge" alt="Current version" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/actions/workflows/build.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/jakestanger/ironbar/build.yml?style=for-the-badge" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/issues">
|
||||
<img src="https://img.shields.io/github/issues/jakestanger/ironbar?style=for-the-badge" alt="Open issues" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/jakestanger/ironbar?style=for-the-badge" alt="License" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/ironbar">
|
||||
<img src="https://img.shields.io/crates/d/ironbar?label=crates.io%20downloads&style=for-the-badge" alt="Crates.io downloads" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
The bar can be styled to your liking using CSS and hot-loads style changes.
|
||||
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||
---
|
||||
|
||||

|
||||
<div align="center">
|
||||
A customisable and feature-rich GTK bar for wlroots compositors, written in Rust.
|
||||
|
||||
Ironbar is designed to support anything from a lightweight bar to a full desktop panel with ease.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
[Wiki](https://github.com/JakeStanger/ironbar/wiki)
|
||||
|
|
||||
[Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide)
|
||||
|
|
||||
[Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide)
|
||||
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
✨ Looking for a starting point, or want to show off? Head to [Show and tell](https://github.com/JakeStanger/ironbar/discussions/categories/show-and-tell) ✨
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- First-class support for Sway and Hyprland
|
||||
- Fully themeable with hot-loaded CSS
|
||||
- Popups to show rich content
|
||||
- Ability to create custom widgets, run scripts and embed dynamic content
|
||||
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
||||
- Support for multiple config languages
|
||||
|
||||
## Installation
|
||||
|
||||
Run using `ironbar`.
|
||||
|
||||
### Cargo
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
cargo install ironbar
|
||||
```
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
### Arch Linux
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
|
||||
```sh
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
### Nix
|
||||
|
||||
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
|
||||
|
||||
```sh
|
||||
nix-shell -p ironbar
|
||||
```
|
||||
|
||||
#### Flake
|
||||
|
||||
A flake is included with the repo which can be used with Home Manager.
|
||||
|
||||
<details>
|
||||
<summary>Example usage</summary>
|
||||
|
||||
```nix
|
||||
{
|
||||
# Add the ironbar flake input
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
inputs.ironbar = {
|
||||
url = "github:JakeStanger/ironbar";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
inputs.hm = {
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = inputs: {
|
||||
homeManagerConfigurations."USER@HOSTNAME" = inputs.hm.lib.homeManagerConfiguration {
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
modules = [
|
||||
# And add the home-manager module
|
||||
inputs.ironbar.homeManagerModules.default
|
||||
{
|
||||
# And configure
|
||||
programs.ironbar = {
|
||||
enable = true;
|
||||
config = {};
|
||||
style = "";
|
||||
package = inputs.ironbar;
|
||||
features = ["feature" "another_feature"];
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
|
||||
|
||||
### Source
|
||||
|
||||
[repo](https://github.com/jakestanger/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jakestanger/ironbar.git
|
||||
cd ironbar
|
||||
@@ -36,42 +143,39 @@ cargo build --release
|
||||
install target/release/ironbar ~/.local/bin/ironbar
|
||||
```
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
By default, all features are enabled.
|
||||
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
||||
|
||||
## Configuration
|
||||
## Running
|
||||
|
||||
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||
and options for scaling complexity: you can use a single config across all monitors,
|
||||
or configure different/multiple bars per monitor.
|
||||
Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
|
||||
See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
|
||||
|
||||
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
|
||||
Ironbar can be launched using the `ironbar` binary.
|
||||
|
||||
## Styling
|
||||
Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
|
||||
|
||||
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the file.
|
||||
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||
|
||||
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
|
||||
File output can be found at `~/.local/share/ironbar/error.log`.
|
||||
|
||||
## Project Status
|
||||
## Status
|
||||
|
||||
This project is in alpha, but should be usable.
|
||||
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.
|
||||
Ironbar is an **alpha** project.
|
||||
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
|
||||
|
||||
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.
|
||||
If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
|
||||
|
||||
A few bugs do exist, and I am sure there are plenty more to be found.
|
||||
I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
|
||||
|
||||
The project will be *actively developed* as I am using it on my daily driver.
|
||||
Bugs will be fixed, features will be added, code will be refactored.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
|
||||
All are welcome, but I ask a few basic things to help make things easier. Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
||||
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
||||
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
||||
|
||||
81
docs/Compiling.md
Normal file
81
docs/Compiling.md
Normal file
@@ -0,0 +1,81 @@
|
||||
You can compile Ironbar from source using `cargo`.
|
||||
Just clone the repo and build:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jakestanger/ironbar.git
|
||||
cd ironbar
|
||||
cargo build --release
|
||||
# change path to wherever you want to install
|
||||
install target/release/ironbar ~/.local/bin/ironbar
|
||||
```
|
||||
|
||||
## Build requirements
|
||||
|
||||
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
|
||||
You also need rust; only the latest stable version is supported.
|
||||
|
||||
### Arch
|
||||
|
||||
```shell
|
||||
pacman -S gtk3 gtk-layer-shell
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
|
||||
```shell
|
||||
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
||||
# for http support
|
||||
apt install libssl-dev
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```shell
|
||||
dnf install gtk3 gtk-layer-shell
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
By default, all features are enabled for convenience. This can result in a significant compile time.
|
||||
If you know you are not going to need all the features, you can compile with only the features you need.
|
||||
|
||||
As of `v0.10.0`, compiling with no features is about 33% faster.
|
||||
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
|
||||
This difference is expected to increase as the bar develops.
|
||||
|
||||
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
|
||||
|
||||
To build using only specific features, disable default features and pass a comma separated list to `cargo build`:
|
||||
|
||||
```shell
|
||||
cargo build --release --no-default-features \
|
||||
--features http,config+json,clock
|
||||
```
|
||||
|
||||
> ⚠ Make sure you enable at least one `config` feature otherwise you will not be able to start the bar!
|
||||
|
||||
| Feature | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------------|
|
||||
| **Core** | |
|
||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||
| ipc | Enables the IPC server. |
|
||||
| cli | Enables the CLI. Will also enable `ipc`. |
|
||||
| config+all | Enables support for all configuration languages. |
|
||||
| config+json | Enables configuration support for JSON. |
|
||||
| config+yaml | Enables configuration support for YAML. |
|
||||
| config+toml | Enables configuration support for TOML. |
|
||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
|
||||
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
|
||||
| **Modules** | |
|
||||
| clipboard | Enables the `clipboard` module. |
|
||||
| clock | Enables the `clock` module. |
|
||||
| music+all | Enables the `music` module with support for all player types. |
|
||||
| music+mpris | Enables the `music` module with MPRIS support. |
|
||||
| music+mpd | Enables the `music` module with MPD support. |
|
||||
| sys_info | Enables the `sys_info` module. |
|
||||
| tray | Enables the `tray` module. |
|
||||
| upower | Enables the `upower` module. |
|
||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||
|
||||
323
docs/Configuration guide.md
Normal file
323
docs/Configuration guide.md
Normal file
@@ -0,0 +1,323 @@
|
||||
By default, you get a single bar at the bottom of all your screens.
|
||||
To change that, you'll unsurprisingly need a config file.
|
||||
|
||||
This page details putting together the skeleton for your config to get you to a stage where you can start configuring
|
||||
modules.
|
||||
It may look long and overwhelming, but that is just because the bar supports a lot of scenarios!
|
||||
|
||||
If you want to see some ready-to-go config files check
|
||||
the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples)
|
||||
and the example pages in the sidebar.
|
||||
|
||||
## 1. Create config file
|
||||
|
||||
The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`.
|
||||
|
||||
Ironbar supports a range of configuration formats, so you can pick your favourite:
|
||||
|
||||
- `config.json`
|
||||
- `config.toml`
|
||||
- `config.yaml`
|
||||
- `config.corn` (Experimental, includes variable support for re-using blocks.
|
||||
See [here](https://github.com/jakestanger/corn) for info)
|
||||
|
||||
You can also override the default config path using the `IRONBAR_CONFIG` environment variable.
|
||||
|
||||
## 2. Pick your use-case
|
||||
|
||||
Ironbar gives you a few ways to configure the bar to suit your needs.
|
||||
This allows you to keep your config simple and relatively flat if your use-case is simple,
|
||||
and make it more complex if required.
|
||||
|
||||
### a) I want the same bar across all monitors
|
||||
|
||||
Place the bar config inside the top-level object. This is automatically applied to each of your monitors.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"position": "bottom",
|
||||
"height": 42,
|
||||
"start": [],
|
||||
"center": [],
|
||||
"end": []
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
position = "bottom"
|
||||
height = 42
|
||||
start = []
|
||||
center = []
|
||||
end = []
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
position: "bottom"
|
||||
height: 42
|
||||
start: [ ]
|
||||
center: [ ]
|
||||
end: [ ]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```
|
||||
{
|
||||
position = "bottom"
|
||||
height = 42
|
||||
start = []
|
||||
center = []
|
||||
end = []
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### b) I want my config to differ across one or more monitors
|
||||
|
||||
Create a map/object called `monitors` inside the top-level object.
|
||||
Each of the map's keys should be an output name,
|
||||
and each value should be an object containing the bar config.
|
||||
|
||||
To find your output names, run `wayland-info | grep wl_output -A1`.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": {
|
||||
"DP-1": {
|
||||
"start": []
|
||||
},
|
||||
"DP-2": {
|
||||
"position": "bottom",
|
||||
"height": 30,
|
||||
"start": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[monitors]
|
||||
|
||||
[monitors.DP-1]
|
||||
start = []
|
||||
|
||||
[monitors.DP-2]
|
||||
position = "bottom"
|
||||
height = 30
|
||||
start = []
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
monitors:
|
||||
DP-1:
|
||||
start: [ ]
|
||||
DP-2:
|
||||
position: "bottom"
|
||||
height: 30
|
||||
start: [ ]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```
|
||||
{
|
||||
monitors.DP-1.start = []
|
||||
monitors.DP-2 = {
|
||||
position = "bottom"
|
||||
height = 30
|
||||
start = []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### c) I want one or more monitors to have multiple bars
|
||||
|
||||
Create a map/object called `monitors` inside the top-level object.
|
||||
Each of the map's keys should be an output name.
|
||||
If you want the screen to have multiple bars, use an array of bar config objects.
|
||||
If you want the screen to have a single bar, use an object.
|
||||
|
||||
To find your output names, run `wayland-info | grep wl_output -A1`.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": {
|
||||
"DP-1": [
|
||||
{
|
||||
"start": []
|
||||
},
|
||||
{
|
||||
"position": "top",
|
||||
"start": []
|
||||
}
|
||||
],
|
||||
"DP-2": {
|
||||
"position": "bottom",
|
||||
"height": 30,
|
||||
"start": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[monitors]
|
||||
|
||||
[[monitors.DP-1]]
|
||||
start = []
|
||||
|
||||
[[monitors.DP-2]]
|
||||
position = "top"
|
||||
start = []
|
||||
|
||||
[monitors.DP-2]
|
||||
position = "bottom"
|
||||
height = 30
|
||||
start = []
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
monitors:
|
||||
DP-1:
|
||||
- start: [ ]
|
||||
- position: "top"
|
||||
start: [ ]
|
||||
DP-2:
|
||||
position: "bottom"
|
||||
height: 30
|
||||
start: [ ]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
monitors.DP-1 = [
|
||||
{ start = [] }
|
||||
{ position = "top" start = [] }
|
||||
]
|
||||
monitors.DP-2 = {
|
||||
position = "bottom"
|
||||
height = 30
|
||||
start = []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 3. Write your bar config(s)
|
||||
|
||||
Once you have the basic config structure set up, it's time to actually configure your bar(s).
|
||||
|
||||
Check [here](config) for an example config file for a fully configured bar in each format.
|
||||
|
||||
### 3.1 Top-level options
|
||||
|
||||
The following table lists each of the top-level bar config options:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
|
||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
|
||||
### 3.2 Module-level options
|
||||
|
||||
The following table lists each of the module-level options that are present on **all** modules.
|
||||
For details on available modules and each of their config options, check the sidebar.
|
||||
|
||||
For information on the `Script` type, and embedding scripts in strings, see [here](script).
|
||||
|
||||
#### Events
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
|
||||
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
|
||||
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
|
||||
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
|
||||
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
|
||||
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
|
||||
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
|
||||
|
||||
#### Visibility
|
||||
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||
|
||||
#### Appearance
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
|
||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
|
||||
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
224
docs/Controlling Ironbar.md
Normal file
224
docs/Controlling Ironbar.md
Normal file
@@ -0,0 +1,224 @@
|
||||
Ironbar includes a simple IPC server which can be used to control it programmatically at runtime.
|
||||
|
||||
It also includes a command line interface, which can be used for interacting with the IPC server.
|
||||
|
||||
# CLI
|
||||
|
||||
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
|
||||
You can also view help per-command, for example using `ironbar set --help`.
|
||||
|
||||
Responses are handled by writing their type to stdout, followed by any value starting on the next line.
|
||||
Error responses are written to stderr in the same format.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
$ ironbar set subject world
|
||||
ok
|
||||
|
||||
$ ironbar get subject
|
||||
ok
|
||||
world
|
||||
```
|
||||
|
||||
# IPC
|
||||
|
||||
The server listens on a Unix socket.
|
||||
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
|
||||
|
||||
Commands and responses are sent as JSON objects, denoted by their `type` key.
|
||||
|
||||
The message buffer is currently limited to `1024` bytes.
|
||||
Particularly large messages will be truncated or cause an error.
|
||||
|
||||
The full spec can be found below.
|
||||
|
||||
## Libraries
|
||||
|
||||
- [Luajit](https://github.com/A-Cloud-Ninja/ironbar-ipc-luajit) - Maintained by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja)
|
||||
|
||||
## Commands
|
||||
|
||||
### `ping`
|
||||
|
||||
Sends a ping request to the IPC.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ping"
|
||||
}
|
||||
```
|
||||
|
||||
### `inspect`
|
||||
|
||||
Opens the GTK inspector window.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "inspect"
|
||||
}
|
||||
```
|
||||
|
||||
### `reload`
|
||||
|
||||
Restarts the bars, reloading the config in the process.
|
||||
|
||||
The IPC server and main GTK application are untouched.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "reload"
|
||||
}
|
||||
```
|
||||
|
||||
### `get`
|
||||
|
||||
Gets an [ironvar](ironvars) value.
|
||||
|
||||
Responds with `ok_value` if the value exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "get",
|
||||
"key": "foo"
|
||||
}
|
||||
```
|
||||
|
||||
### `set`
|
||||
|
||||
Sets an [ironvar](ironvars) value.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set",
|
||||
"key": "foo",
|
||||
"value": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
### `load_css`
|
||||
|
||||
Loads an additional CSS stylesheet, with hot-reloading enabled.
|
||||
|
||||
Responds with `ok` if the stylesheet exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "load_css",
|
||||
"path": "/path/to/style.css"
|
||||
}
|
||||
```
|
||||
|
||||
### `set_visible`
|
||||
|
||||
Sets a bar's visibility.
|
||||
|
||||
Responds with `ok` if the bar exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set_visible",
|
||||
"bar_name": "bar-123",
|
||||
"visible": true
|
||||
}
|
||||
```
|
||||
|
||||
### `get_visible`
|
||||
|
||||
Gets a bar's visibility.
|
||||
|
||||
Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "get_visible",
|
||||
"bar_name": "bar-123"
|
||||
}
|
||||
```
|
||||
|
||||
### `toggle_popup`
|
||||
|
||||
Toggles the open/closed state for a module's popup.
|
||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "toggle_popup",
|
||||
"bar_name": "bar-123",
|
||||
"name": "clock"
|
||||
}
|
||||
```
|
||||
|
||||
### `open_popup`
|
||||
|
||||
Sets a module's popup open, regardless of its current state.
|
||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "open_popup",
|
||||
"bar_name": "bar-123",
|
||||
"name": "clock"
|
||||
}
|
||||
```
|
||||
|
||||
### `close_popup`
|
||||
|
||||
Sets the popup on a bar closed, regardless of which module it is open for.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "close_popup",
|
||||
"bar_name": "bar-123"
|
||||
}
|
||||
```
|
||||
|
||||
## Responses
|
||||
|
||||
### `ok`
|
||||
|
||||
The operation completed successfully, with no response data.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### `ok_value`
|
||||
|
||||
The operation completed successfully, with response data.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ok_value",
|
||||
"value": "lorem ipsum"
|
||||
}
|
||||
```
|
||||
|
||||
### `error`
|
||||
|
||||
The operation failed.
|
||||
|
||||
Message is optional.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"message": "lorem ipsum"
|
||||
}
|
||||
```
|
||||
39
docs/Dynamic values.md
Normal file
39
docs/Dynamic values.md
Normal file
@@ -0,0 +1,39 @@
|
||||
In some configuration locations, Ironbar supports dynamic values,
|
||||
meaning you can inject content into the bar from an external source.
|
||||
|
||||
Currently two dynamic content sources are supported - scripts and ironvars.
|
||||
|
||||
## Dynamic String
|
||||
|
||||
Dynamic strings can contain any mixture of static string elements, scripts and variables.
|
||||
|
||||
Scripts should be placed inside `{{double braces}}`. Both polling and watching scripts are supported.
|
||||
|
||||
Variables use the standard `#name` syntax. Variables cannot be placed inside scripts.
|
||||
|
||||
To use a literal hash, use `##`. This is only necessary outside of scripts.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
label = "{{cat greeting.txt}}, #subject"
|
||||
```
|
||||
|
||||
## Dynamic Boolean
|
||||
|
||||
Dynamic booleans can use a single source of either a script or variable to control a true/false value.
|
||||
|
||||
For scripts, you can just write these directly with no notation.
|
||||
Only polling scripts are supported.
|
||||
The script exit code is used, where `0` is `true` and any other code is `false.
|
||||
|
||||
For variables, use the standard `#name` notation.
|
||||
An empty string, `0` and `false` are treated as false.
|
||||
Any other value is true.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
show_if = "exit 0" # script
|
||||
show_if = "#show_module" # variable
|
||||
```
|
||||
4
docs/Home.md
Normal file
4
docs/Home.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Welcome to the Ironbar wiki.
|
||||
|
||||
Detail about each module, and their configuration and styling options can be found on the sidebar.
|
||||
You can also find an example configuration and stylesheet there.
|
||||
15
docs/Images.md
Normal file
15
docs/Images.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Ironbar is capable of loading images from multiple sources.
|
||||
In any situation where an option takes text or an icon,
|
||||
you can use a string in any of the following formats, and it will automatically be detected as an image:
|
||||
|
||||
| Source | Example |
|
||||
|-------------------------------|---------------------------------|
|
||||
| GTK icon theme | `icon:firefox` |
|
||||
| Local file | `file:///path/to/file.jpg` |
|
||||
| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` |
|
||||
|
||||
Remote images are loaded asynchronously to avoid blocking the UI thread.
|
||||
Be aware this can cause elements to change size upon load if the image is large enough.
|
||||
|
||||
Note that mixing text and images is not supported.
|
||||
Your best option here is to use Nerd Font icons instead.
|
||||
9
docs/Ironvars.md
Normal file
9
docs/Ironvars.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Ironvars are runtime variables that can be referenced in several places in your config,
|
||||
then set using the IPC server (such as via the CLI) using the `set` command.
|
||||
|
||||
Any UTF-8 string *without whitespace* is a valid key.
|
||||
Any UTF-8 string is a valid value.
|
||||
|
||||
Reference values using `#my_variable`. These update as soon as the value changes.
|
||||
|
||||
You can set defaults using the `ironvar_defaults` key in your top-level config.
|
||||
107
docs/Scripts.md
Normal file
107
docs/Scripts.md
Normal file
@@ -0,0 +1,107 @@
|
||||
There are various places inside the configuration (other than the `script` module)
|
||||
that allow script input to dynamically set values.
|
||||
|
||||
Scripts are passed to `sh -c`.
|
||||
|
||||
Three types of scripts exist: polling, oneshot and watching:
|
||||
|
||||
- **Polling** scripts will run and wait for exit.
|
||||
Normally they will repeat this at an interval, hence the name, although in some cases they may only run on a user
|
||||
event.
|
||||
If the script exited code 0, the `stdout` will be used. Otherwise, `stderr` will be printed to the log.
|
||||
- **Oneshot** scripts are a variant of polling scripts.
|
||||
They wait for script to exit, and may do something with the output, but are only fired by user events instead of the interval.
|
||||
Generally options that accept oneshot scripts do not support the other types.
|
||||
- **Watching** scripts start a long-running process. Every time the process writes to `stdout`, the last line is captured
|
||||
and used.
|
||||
|
||||
One should prefer to use watch-mode where possible, as it removes the overhead of regularly spawning processes.
|
||||
That said, there are some cases which only support polling. These are indicated by `Script [polling]` as the option
|
||||
type.
|
||||
|
||||
## Writing script configs
|
||||
|
||||
There are two available config formats for scripts, shorthand as a string, or longhand as an object.
|
||||
Shorthand can be used in all cases, but there are some cases (such as embedding scripts inside strings) where longhand
|
||||
cannot be used.
|
||||
|
||||
In both formats, `mode` is one of `poll` or `watch` and `interval` is the number of milliseconds to wait between
|
||||
spawning the script.
|
||||
|
||||
Both `mode` and `interval` are optional and can be excluded to fall back to their defaults of `poll` and `5000`
|
||||
respectively.
|
||||
|
||||
For oneshot scripts, both the mode and interval are ignored.
|
||||
|
||||
### Shorthand (string)
|
||||
|
||||
Shorthand scripts should be written in the format:
|
||||
|
||||
```
|
||||
mode:interval:script
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
poll:5000:uptime -p | cut -d ' ' -f2-
|
||||
```
|
||||
|
||||
#### Embedding
|
||||
|
||||
Some string config options support "embedding scripts". This allows you to mix static/dynamic content.
|
||||
An example of this is the common `tooltip` option.
|
||||
|
||||
Scripts can be embedded in these cases using `{{double braces}}` and the shorthand syntax:
|
||||
|
||||
```json
|
||||
"Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
```
|
||||
|
||||
### Longhand (object)
|
||||
|
||||
An object consisting of the `cmd` key and optionally the `mode` and/or `interval` keys.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "poll",
|
||||
"interval": 5000,
|
||||
"cmd": "uptime -p | cut -d ' ' -f2-"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
mode: poll
|
||||
interval: 5000
|
||||
cmd: "uptime -p | cut -d ' ' -f2-"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```toml
|
||||
mode = "poll"
|
||||
interval = 5000
|
||||
cmd = "uptime -p | cut -d ' ' -f2-"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
mode = "poll"
|
||||
interval = 5000
|
||||
cmd = "uptime -p | cut -d ' ' -f2-"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
42
docs/Styling guide.md
Normal file
42
docs/Styling guide.md
Normal file
@@ -0,0 +1,42 @@
|
||||
Ironbar ships with no styles by default, so will fall back to the default GTK styles.
|
||||
|
||||
To style the bar, create a file at `~/.config/ironbar/style.css`.
|
||||
|
||||
Style changes are hot-loaded so there is no need to reload the bar.
|
||||
|
||||
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html),
|
||||
which only includes a subset of the full web spec (plus a few non-standard properties).
|
||||
|
||||
The below table describes the selectors provided by the bar itself.
|
||||
Information on styling individual modules can be found on their pages in the sidebar.
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------|--------------------------------------------|
|
||||
| `.background` | Top-level window. |
|
||||
| `#bar` | Bar root box. |
|
||||
| `#bar #start` | Bar left or top modules container box. |
|
||||
| `#bar #center` | Bar center modules container box. |
|
||||
| `#bar #end` | Bar right or bottom modules container box. |
|
||||
| `.container` | All of the above. |
|
||||
| `.widget-container` | The `EventBox` wrapping any widget. |
|
||||
| `.widget` | Any widget. |
|
||||
| `.popup` | Any popup box. |
|
||||
|
||||
Every widget can be selected using a `kebab-case` class name matching its name.
|
||||
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
|
||||
|
||||
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
|
||||
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
|
||||
|
||||
You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
|
||||
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
|
||||
|
||||
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
|
||||
|
||||
```css
|
||||
@define-color color_bg #2d2d2d;
|
||||
|
||||
box, menubar {
|
||||
background-color: @color_bg;
|
||||
}
|
||||
```
|
||||
37
docs/_Sidebar.md
Normal file
37
docs/_Sidebar.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Guides
|
||||
|
||||
- [Compiling from source](compiling)
|
||||
- [Configuration guide](configuration-guide)
|
||||
- [Images](images)
|
||||
- [Styling guide](styling-guide)
|
||||
|
||||
# Dynamic content
|
||||
|
||||
- [Controlling Ironbar](controlling-ironbar)
|
||||
- [Dynamic values](dynamic-values)
|
||||
- [Scripts](scripts)
|
||||
- [Ironvars](ironvars)
|
||||
|
||||
# Examples
|
||||
|
||||
- [Config](config)
|
||||
- [Stylesheet](https://github.com/JakeStanger/ironbar/blob/master/examples/style.css)
|
||||
|
||||
## Custom
|
||||
|
||||
- [Power Menu](power-menu)
|
||||
|
||||
# Modules
|
||||
|
||||
- [Clipboard](clipboard)
|
||||
- [Clock](clock)
|
||||
- [Custom](custom)
|
||||
- [Focused](focused)
|
||||
- [Label](label)
|
||||
- [Launcher](launcher)
|
||||
- [Music](music)
|
||||
- [Script](script)
|
||||
- [Sys_Info](sys-info)
|
||||
- [Tray](tray)
|
||||
- [Upower](upower)
|
||||
- [Workspaces](workspaces)
|
||||
10
docs/examples/Config.md
Normal file
10
docs/examples/Config.md
Normal file
@@ -0,0 +1,10 @@
|
||||
The configs linked below show a module of each type being used.
|
||||
|
||||
The Corn format makes heavy use of variables
|
||||
to show how module configs can be easily referenced to improve readability
|
||||
and reduce config length when using multiple bars.
|
||||
|
||||
- [JSON](https://github.com/JakeStanger/ironbar/blob/master/examples/config.json)
|
||||
- [TOML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.toml)
|
||||
- [YAML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.yaml)
|
||||
- [Corn](https://github.com/JakeStanger/ironbar/blob/master/examples/config.corn)
|
||||
241
docs/examples/custom/Power-Menu.md
Normal file
241
docs/examples/custom/Power-Menu.md
Normal file
@@ -0,0 +1,241 @@
|
||||
Creates a button on the bar, which opens a popup. The popup contains a header, shutdown button, restart button, and uptime.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "clock"
|
||||
},
|
||||
{
|
||||
"bar": [
|
||||
{
|
||||
"label": "",
|
||||
"name": "power-btn",
|
||||
"on_click": "popup:toggle",
|
||||
"type": "button"
|
||||
}
|
||||
],
|
||||
"class": "power-menu",
|
||||
"popup": [
|
||||
{
|
||||
"orientation": "vertical",
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"label": "Power menu",
|
||||
"name": "header",
|
||||
"type": "label"
|
||||
},
|
||||
{
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!shutdown now",
|
||||
"type": "button"
|
||||
},
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!reboot",
|
||||
"type": "button"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"name": "uptime",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = 'clock'
|
||||
|
||||
[[end]]
|
||||
class = 'power-menu'
|
||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
type = 'custom'
|
||||
|
||||
[[end.bar]]
|
||||
label = ''
|
||||
name = 'power-btn'
|
||||
on_click = 'popup:toggle'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup]]
|
||||
orientation = 'vertical'
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = 'Power menu'
|
||||
name = 'header'
|
||||
type = 'label'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!shutdown now'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!reboot'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
name = 'uptime'
|
||||
type = 'label'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: clock
|
||||
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
let {
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
|
||||
$clock = { type = "clock" }
|
||||
} in {
|
||||
end = [ $power_menu $clock ]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
.power-menu {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.power-menu #power-btn {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.power-menu #power-btn:hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
.popup-power-menu {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu #header {
|
||||
color: white;
|
||||
font-size: 1.4em;
|
||||
border-bottom: 1px solid white;
|
||||
padding-bottom: 0.4em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid white;
|
||||
padding: 0.6em 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn + .power-btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn:hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
```
|
||||
97
docs/modules/Clipboard.md
Normal file
97
docs/modules/Clipboard.md
Normal file
@@ -0,0 +1,97 @@
|
||||
Shows recent clipboard items, allowing you to switch between them to re-copy previous values.
|
||||
Clicking the icon button opens the popup containing all functionality.
|
||||
|
||||
Supports plain text and images.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `clipboard`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": {
|
||||
"type": "clipboard",
|
||||
"max_items": 3,
|
||||
"truncate": {
|
||||
"mode": "end",
|
||||
"length": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
|
||||
[[end.truncate]]
|
||||
mode = "end"
|
||||
length = 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: 'clipboard'
|
||||
max_items: 3
|
||||
truncate:
|
||||
mode: 'end'
|
||||
length: 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [ {
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
truncate.mode = "end"
|
||||
truncate.length = 50
|
||||
} ]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------------|------------------------------------------------------|
|
||||
| `.clipboard` | Clipboard widget. |
|
||||
| `.clipboard .btn` | Clipboard widget button. |
|
||||
| `.clipboard .btn .icon` | Clipboard widget button icon (any type). |
|
||||
| `.clipboard .btn .text-icon` | Clipboard widget button icon (textual only). |
|
||||
| `.clipboard .btn .image` | Clipboard widget button icon (image only). |
|
||||
| `.popup-clipboard` | Clipboard popup box. |
|
||||
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
||||
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
||||
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
83
docs/modules/Clock.md
Normal file
83
docs/modules/Clock.md
Normal file
@@ -0,0 +1,83 @@
|
||||
Displays the current date and time.
|
||||
Clicking on the widget opens a popup with the time and a calendar.
|
||||
|
||||

|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `clock`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. |
|
||||
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. |
|
||||
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
|
||||
|
||||
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "clock",
|
||||
"format": "%d/%m/%Y %H:%M"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "clock"
|
||||
format = "%d/%m/%Y %H:%M"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "clock"
|
||||
format: "%d/%m/%Y %H:%M"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "clock"
|
||||
format = "%d/%m/%Y %H:%M"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------|------------------------------------------------------------------------------------|
|
||||
| `.clock` | Clock widget button |
|
||||
| `.popup-clock` | Clock popup box |
|
||||
| `.popup-clock .calendar-clock` | Clock inside the popup |
|
||||
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
403
docs/modules/Custom.md
Normal file
403
docs/modules/Custom.md
Normal file
@@ -0,0 +1,403 @@
|
||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
||||
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||
|
||||
If you only intend to run a single script, prefer the [script](script) module,
|
||||
or [label](label) if you only need a single text label.
|
||||
|
||||

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

|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `focused`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
||||
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
||||
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "focused",
|
||||
"show_icon": true,
|
||||
"show_title": true,
|
||||
"icon_size": 32,
|
||||
"truncate": "end"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "focused"
|
||||
show_icon = true
|
||||
show_title = true
|
||||
icon_size = 32
|
||||
truncate = "end"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "focused"
|
||||
show_icon: true
|
||||
show_title: true
|
||||
icon_size: 32
|
||||
truncate: "end"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "focused"
|
||||
show_icon = true
|
||||
show_title = true
|
||||
icon_size = 32
|
||||
truncate = "end"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------|--------------------|
|
||||
| `.focused` | Focused widget box |
|
||||
| `.focused .icon` | App icon |
|
||||
| `.focused .label` | App name |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
75
docs/modules/Label.md
Normal file
75
docs/modules/Label.md
Normal file
@@ -0,0 +1,75 @@
|
||||
Displays custom text, with markup support.
|
||||
|
||||
If you only intend to run a single script, prefer the [script](script) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------|---------|------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "label"
|
||||
label: "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|----------|------------------------------------------------------------------------------------|
|
||||
| `.label` | Label widget |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
101
docs/modules/Launcher.md
Normal file
101
docs/modules/Launcher.md
Normal file
@@ -0,0 +1,101 @@
|
||||
Windows-style taskbar that displays running windows, grouped by program.
|
||||
Hovering over a program with multiple windows open shows a popup with each window.
|
||||
Clicking an icon/popup item focuses or launches the program.
|
||||
Optionally displays a launchable set of favourites.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `launcher`
|
||||
|
||||
| | Type | Default | Description |
|
||||
|--------------|------------|---------|-----------------------------------------------------------------------------------------------------|
|
||||
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
|
||||
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
|
||||
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"start": [
|
||||
{
|
||||
"type": "launcher",
|
||||
"favourites": [
|
||||
"firefox",
|
||||
"discord"
|
||||
],
|
||||
"show_names": false,
|
||||
"show_icons": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[start]]
|
||||
type = "launcher"
|
||||
favorites = ["firefox", "discord"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
start:
|
||||
- type: "launcher"
|
||||
favorites:
|
||||
- firefox
|
||||
- discord
|
||||
show_names: false
|
||||
show_icons: true
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
start = [
|
||||
{
|
||||
type = "launcher"
|
||||
favorites = [ "firefox" "discord" ]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------------------|--------------------------|
|
||||
| `.launcher` | Launcher widget box |
|
||||
| `.launcher .item` | App button |
|
||||
| `.launcher .item.open` | App button (open app) |
|
||||
| `.launcher .item.focused` | App button (focused app) |
|
||||
| `.launcher .item.urgent` | App button (urgent app) |
|
||||
| `.popup-launcher` | Popup container |
|
||||
| `.popup-launcher .popup-item` | Window button in popup |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
173
docs/modules/Music.md
Normal file
173
docs/modules/Music.md
Normal file
@@ -0,0 +1,173 @@
|
||||
Displays currently playing song from your music player.
|
||||
This module supports both MPRIS players and MPD servers.
|
||||
Clicking on the widget opens a popout displaying info about the current song, album art
|
||||
and playback controls.
|
||||
|
||||
in MPRIS mode, the widget will listen to all players and automatically detect/display the active one.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `music`
|
||||
|
||||
| | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
|
||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
|
||||
See [here](images) for information on images.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"start": [
|
||||
{
|
||||
"type": "music",
|
||||
"player_type": "mpd",
|
||||
"format": "{title} / {artist}",
|
||||
"truncate": "end",
|
||||
"icons": {
|
||||
"play": "",
|
||||
"pause": ""
|
||||
},
|
||||
"music_dir": "/home/jake/Music"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[start]]
|
||||
type = "music"
|
||||
player_type = "mpd"
|
||||
format = "{title} / {artist}"
|
||||
music_dir = "/home/jake/Music"
|
||||
truncate = "end"
|
||||
|
||||
[[start.icons]]
|
||||
play = ""
|
||||
pause = ""
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
start:
|
||||
- type: "music"
|
||||
player_type: "mpd"
|
||||
format: "{title} / {artist}"
|
||||
truncate: "end"
|
||||
icons:
|
||||
play: ""
|
||||
pause: ""
|
||||
music_dir: "/home/jake/Music"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
start = [
|
||||
{
|
||||
type = "music"
|
||||
player_type = "mpd"
|
||||
format = "{title} / {artist}"
|
||||
truncate = "end"
|
||||
icons.play = ""
|
||||
icons.pause = ""
|
||||
music_dir = "/home/jake/Music"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Formatting Tokens
|
||||
|
||||
The following tokens can be used in the `format` config option,
|
||||
and will be replaced with values from the currently playing track:
|
||||
|
||||
| Token | Description |
|
||||
|--------------|--------------------------------------|
|
||||
| `{title}` | Title |
|
||||
| `{album}` | Album name |
|
||||
| `{artist}` | Artist name |
|
||||
| `{date}` | Release date |
|
||||
| `{track}` | Track number |
|
||||
| `{disc}` | Disc number |
|
||||
| `{genre}` | Genre |
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------------------------------|-------------------------------------------------------|
|
||||
| `.music` | Tray widget button |
|
||||
| `.music .contents` | Tray widget button contents box |
|
||||
| `.music .contents .icon` | Tray widget button icon (any type) |
|
||||
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
|
||||
| `.music .contents .image` | Tray widget button icon (image only) |
|
||||
| `.popup-music` | Popup box |
|
||||
| `.popup-music .album-art` | Album art image inside popup box |
|
||||
| `.popup-music .title` | Track title container inside popup box |
|
||||
| `.popup-music .title .icon-box` | Track title icon container inside popup box |
|
||||
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
|
||||
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
|
||||
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
|
||||
| `.popup-music .title .label` | Track title label inside popup box |
|
||||
| `.popup-music .album` | Track album container inside popup box |
|
||||
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
|
||||
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
|
||||
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
|
||||
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
|
||||
| `.popup-music .album .label` | Track album label inside popup box |
|
||||
| `.popup-music .artist` | Track artist container inside popup box |
|
||||
| `.popup-music .artist .icon-box` | Track artist icon container inside popup box |
|
||||
| `.popup-music .artist .icon-box .icon` | Track artist icon inside its container (any type) |
|
||||
| `.popup-music .artist .icon-box .text-icon` | Track artist icon inside its container (textual only) |
|
||||
| `.popup-music .artist .icon-box .image` | Track artist icon inside its container (image only) |
|
||||
| `.popup-music .artist .label` | Track artist label inside popup box |
|
||||
| `.popup-music .controls` | Controls container inside popup box |
|
||||
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
|
||||
| `.popup-music .controls .btn-play` | Play button inside popup box |
|
||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
||||
| `.popup-music .volume` | Volume container inside popup box |
|
||||
| `.popup-music .volume .slider` | Slider inside volume container |
|
||||
| `.popup-music .volume .icon` | Icon inside volume container |
|
||||
| `.popup-music .progress` | Progress (seek) bar container |
|
||||
| `.popup-music .progress .slider` | Slider inside progress container |
|
||||
| `.popup-music .progress .label` | Duration label inside progress container |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
92
docs/modules/Script.md
Normal file
92
docs/modules/Script.md
Normal file
@@ -0,0 +1,92 @@
|
||||
Executes a script and shows the result of `stdout` on a label.
|
||||
Pango markup is supported.
|
||||
|
||||
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `script`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------------|-----------------------|---------|---------------------------------------------------------|
|
||||
| `cmd` | `string` | `null` | Path to the script on disk |
|
||||
| `mode` | `'poll'` or `'watch'` | `poll` | See [#modes](#modes) |
|
||||
| `interval` | `number` | `5000` | Number of milliseconds to wait between executing script |
|
||||
|
||||
### Modes
|
||||
|
||||
- Use `poll` to run the script wait for it to exit. On exit, the label is updated to show everything the script wrote to `stdout`.
|
||||
- Use `watch` to start a long-running script. Every time the script writes to `stdout`, the label is updated to show the latest line.
|
||||
Note this does not work for all programs as they may use block-buffering instead of line-buffering when they detect output being piped.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "script",
|
||||
"cmd": "/home/jake/.local/bin/phone-battery",
|
||||
"mode": "poll",
|
||||
"interval": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "script"
|
||||
cmd = "/home/jake/.local/bin/phone-battery"
|
||||
mode = "poll"
|
||||
interval = 5000
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "script"
|
||||
cmd: "/home/jake/.local/bin/phone-battery"
|
||||
mode: 'poll'
|
||||
interval : 5000
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "script"
|
||||
cmd = "/home/jake/.local/bin/phone-battery"
|
||||
mode = "poll"
|
||||
interval = 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-----------|---------------------|
|
||||
| `.script` | Script widget label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
180
docs/modules/Sys-Info.md
Normal file
180
docs/modules/Sys-Info.md
Normal file
@@ -0,0 +1,180 @@
|
||||
Displays one or more labels containing system information.
|
||||
|
||||
Separating information across several labels allows for styling each one independently.
|
||||
Pango markup is supported.
|
||||
|
||||

|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `sys_info`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. |
|
||||
| `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. |
|
||||
| `interval.memory` | `integer` | `5` | Seconds between refreshing memory data |
|
||||
| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data |
|
||||
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data |
|
||||
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data |
|
||||
| `interval.network` | `integer` | `5` | Seconds between refreshing network data |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"format": [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
],
|
||||
"interval": {
|
||||
"cpu": 1,
|
||||
"disks": 300,
|
||||
"memory": 30,
|
||||
"networks": 3,
|
||||
"temps": 5
|
||||
},
|
||||
"type": "sys_info"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = 'sys_info'
|
||||
format = [
|
||||
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
]
|
||||
|
||||
[end.interval]
|
||||
cpu = 1
|
||||
disks = 300
|
||||
memory = 30
|
||||
networks = 3
|
||||
temps = 5
|
||||
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- format:
|
||||
- ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C'
|
||||
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
|
||||
- '李 {net_down:enp39s0} / {net_up:enp39s0} Mbps'
|
||||
- '猪 {load_average:1} | {load_average:5} | {load_average:15}'
|
||||
- ' {uptime}'
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "sys_info"
|
||||
|
||||
interval.memory = 30
|
||||
interval.cpu = 1
|
||||
interval.temps = 5
|
||||
interval.disks = 300
|
||||
interval.networks = 3
|
||||
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Formatting Tokens
|
||||
|
||||
The following tokens can be used in the `format` configuration option:
|
||||
|
||||
| Token | Description |
|
||||
|--------------------------|------------------------------------------------------------------------------------|
|
||||
| **CPU** | |
|
||||
| `{cpu_percent}` | Total CPU utilisation percentage |
|
||||
| **Memory** | |
|
||||
| `{memory_free}` | Memory free in GB. |
|
||||
| `{memory_used}` | Memory used in GB. |
|
||||
| `{memory_total}` | Memory total in GB. |
|
||||
| `{memory_percent}` | Memory utilisation percentage. |
|
||||
| `{swap_free}` | Swap free in GB. |
|
||||
| `{swap_used}` | Swap used in GB. |
|
||||
| `{swap_total}` | Swap total in GB. |
|
||||
| `{swap_percent}` | Swap utilisation percentage. |
|
||||
| **Temperature** | |
|
||||
| `{temp_c:[sensor]}` | Temperature in degrees C. Replace `[sensor]` with the sensor label. |
|
||||
| `{temp_f:[sensor]}` | Temperature in degrees F. Replace `[sensor]` with the sensor label. |
|
||||
| **Disk** | |
|
||||
| `{disk_free:[mount]}` | Disk free space in GB. Replace `[mount]` with the disk mountpoint. |
|
||||
| `{disk_used:[mount]}` | Disk used space in GB. Replace `[mount]` with the disk mountpoint. |
|
||||
| `{disk_total:[mount]}` | Disk total space in GB. Replace `[mount]` with the disk mountpoint. |
|
||||
| `{disk_percent:[mount]}` | Disk utilisation percentage. Replace `[mount]` with the disk mountpoint. |
|
||||
| **Network** | |
|
||||
| `{net_down:[adapter]}` | Average network download speed in Mbps. Replace `[adapter]` with the adapter name. |
|
||||
| `{net_up:[adapter]}` | Average network upload speed in Mbps. Replace `[adapter]` with the adapter name. |
|
||||
| **System** | |
|
||||
| `{load_average:1}` | 1-minute load average. |
|
||||
| `{load_average:5}` | 5-minute load average. |
|
||||
| `{load_average:15}` | 15-minute load average. |
|
||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
||||
|
||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|------------------|------------------------------|
|
||||
| `.sysinfo` | Sysinfo widget box |
|
||||
| `.sysinfo .item` | Individual information label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
66
docs/modules/Tray.md
Normal file
66
docs/modules/Tray.md
Normal file
@@ -0,0 +1,66 @@
|
||||
Displays a fully interactive icon tray using the KDE `libappindicator` protocol.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `tray`
|
||||
|
||||
***This module provides no configuration options.***
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "tray"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "tray"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "tray"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{ type = "tray" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------|------------------|
|
||||
| `.tray` | Tray widget box |
|
||||
| `.tray .item` | Tray icon button |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
84
docs/modules/Upower.md
Normal file
84
docs/modules/Upower.md
Normal file
@@ -0,0 +1,84 @@
|
||||
Displays system power information such as the battery percentage, and estimated time to empty.
|
||||
|
||||
`TODO: ADD SCREENSHOT`
|
||||
|
||||
[//]: # ()
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `upower`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------|-----------|-----------------|---------------------------------------------------|
|
||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "upower",
|
||||
"format": "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "upower"
|
||||
format: "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------------------|--------------------------------|
|
||||
| `.upower` | Upower widget container. |
|
||||
| `.upower .button` | Upower widget button. |
|
||||
| `.upower .button .contents` | Upower widget button contents. |
|
||||
| `.upower .button .icon` | Upower widget battery icon. |
|
||||
| `.upower .button .label` | Upower widget button label. |
|
||||
| `.popup-upower` | Upower popup box. |
|
||||
| `.popup-upower .upower-details` | Label inside the popup. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
112
docs/modules/Workspaces.md
Normal file
112
docs/modules/Workspaces.md
Normal file
@@ -0,0 +1,112 @@
|
||||
> ⚠ **This module is currently only supported on Sway and Hyprland**
|
||||
|
||||
Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `workspaces`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
|
||||
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "workspaces",
|
||||
"name_map": {
|
||||
"1": "",
|
||||
"2": "",
|
||||
"3": ""
|
||||
},
|
||||
"favorites": ["1", "2", "3"],
|
||||
"all_monitors": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
favorites = ["1", "2", "3"]
|
||||
|
||||
[[end.name_map]]
|
||||
1 = ""
|
||||
2 = ""
|
||||
3 = ""
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "workspaces"
|
||||
name_map:
|
||||
1: ""
|
||||
2: ""
|
||||
3: ""
|
||||
favorites:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
all_monitors: false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "workspaces",
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
favorites = [ "1" "2" "3" ]
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------|--------------------------------------|
|
||||
| `.workspaces` | Workspaces widget box |
|
||||
| `.workspaces .item` | Workspace button |
|
||||
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
||||
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
|
||||
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
|
||||
| `.workspaces .item .icon` | Workspace button icon (any type) |
|
||||
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
|
||||
| `.workspaces .item .image` | Workspace button icon (image only) |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -3,41 +3,110 @@ let {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
2 = ""
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Games = ""
|
||||
Games = "icon:steam"
|
||||
Code = ""
|
||||
}
|
||||
}
|
||||
|
||||
$focused = { type = "focused" }
|
||||
|
||||
$launcher = {
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord" "Steam"]
|
||||
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" }
|
||||
$mpris = {
|
||||
type = "music"
|
||||
player_type = "mpris"
|
||||
|
||||
on_click_middle = "playerctl play-pause"
|
||||
on_scroll_up = "playerctl volume +5"
|
||||
on_scroll_down = "playerctl volume -5"
|
||||
|
||||
}
|
||||
|
||||
$mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
|
||||
$mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
|
||||
|
||||
$sys_info = {
|
||||
type = "sys-info"
|
||||
format = ["{cpu-percent}% " "{memory-percent}% "]
|
||||
type = "sys_info"
|
||||
|
||||
interval.memory = 30
|
||||
interval.cpu = 1
|
||||
interval.temps = 5
|
||||
interval.disks = 300
|
||||
interval.networks = 3
|
||||
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
|
||||
$tray = { type = "tray" }
|
||||
|
||||
$clock = { type = "clock" }
|
||||
|
||||
$phone_battery = {
|
||||
type = "script"
|
||||
path = "/home/jake/bin/phone-battery"
|
||||
cmd = "/home/jake/bin/phone-battery"
|
||||
|
||||
show_if.cmd = "/home/jake/bin/phone-connected"
|
||||
show_if.interval = 500
|
||||
}
|
||||
|
||||
$left = [ $workspaces $launcher ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
|
||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||
|
||||
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
|
||||
|
||||
// -- begin custom --
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
// -- end custom --
|
||||
|
||||
$left = [ $workspaces $launcher $label ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
|
||||
}
|
||||
in {
|
||||
left = $left right = $right
|
||||
anchor_to_edges = true
|
||||
position = "bottom"
|
||||
icon_theme = "Paper"
|
||||
|
||||
start = $left
|
||||
end = $right
|
||||
}
|
||||
|
||||
@@ -1,18 +1,135 @@
|
||||
{
|
||||
"monitors": {
|
||||
"DP-1": [
|
||||
"anchor_to_edges": true,
|
||||
"end": [
|
||||
{
|
||||
"left": [{"type": "clock"}]
|
||||
"music_dir": "/home/jake/Music",
|
||||
"player_type": "mpd",
|
||||
"truncate": {
|
||||
"max_length": 100,
|
||||
"mode": "end"
|
||||
},
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"position": "top",
|
||||
"left": []
|
||||
"host": "chloe:6600",
|
||||
"player_type": "mpd",
|
||||
"truncate": "end",
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"cmd": "/home/jake/bin/phone-battery",
|
||||
"show_if": {
|
||||
"cmd": "/home/jake/bin/phone-connected",
|
||||
"interval": 500
|
||||
},
|
||||
"type": "script"
|
||||
},
|
||||
{
|
||||
"format": [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
],
|
||||
"interval": {
|
||||
"cpu": 1,
|
||||
"disks": 300,
|
||||
"memory": 30,
|
||||
"networks": 3,
|
||||
"temps": 5
|
||||
},
|
||||
"type": "sys_info"
|
||||
},
|
||||
{
|
||||
"max_items": 3,
|
||||
"truncate": {
|
||||
"length": 50,
|
||||
"mode": "end"
|
||||
},
|
||||
"type": "clipboard"
|
||||
},
|
||||
{
|
||||
"bar": [
|
||||
{
|
||||
"label": "",
|
||||
"name": "power-btn",
|
||||
"on_click": "popup:toggle",
|
||||
"type": "button"
|
||||
}
|
||||
],
|
||||
"DP-2": {
|
||||
"class": "power-menu",
|
||||
"popup": [
|
||||
{
|
||||
"orientation": "vertical",
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"label": "Power menu",
|
||||
"name": "header",
|
||||
"type": "label"
|
||||
},
|
||||
{
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!shutdown now",
|
||||
"type": "button"
|
||||
},
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!reboot",
|
||||
"type": "button"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"name": "uptime",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"type": "clock"
|
||||
}
|
||||
],
|
||||
"icon_theme": "Paper",
|
||||
"position": "bottom",
|
||||
"height": 30,
|
||||
"left": []
|
||||
}
|
||||
"start": [
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "",
|
||||
"2": "icon:firefox",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
"Games": "icon:steam"
|
||||
},
|
||||
"type": "workspaces"
|
||||
},
|
||||
{
|
||||
"favorites": [
|
||||
"firefox",
|
||||
"discord",
|
||||
"steam"
|
||||
],
|
||||
"show_icons": true,
|
||||
"show_names": false,
|
||||
"type": "launcher"
|
||||
},
|
||||
{
|
||||
"label": "random num: {{500:echo FIXME}}",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
122
examples/config.toml
Normal file
122
examples/config.toml
Normal file
@@ -0,0 +1,122 @@
|
||||
anchor_to_edges = true
|
||||
icon_theme = "Paper"
|
||||
position = "bottom"
|
||||
|
||||
[[end]]
|
||||
music_dir = "/home/jake/Music"
|
||||
player_type = "mpd"
|
||||
type = "music"
|
||||
|
||||
[end.truncate]
|
||||
max_length = 100
|
||||
mode = "end"
|
||||
|
||||
[[end]]
|
||||
host = "chloe:6600"
|
||||
player_type = "mpd"
|
||||
truncate = "end"
|
||||
type = "music"
|
||||
|
||||
[[end]]
|
||||
cmd = "/home/jake/bin/phone-battery"
|
||||
type = "script"
|
||||
|
||||
[end.show_if]
|
||||
cmd = "/home/jake/bin/phone-connected"
|
||||
interval = 500
|
||||
|
||||
[[end]]
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}",
|
||||
]
|
||||
type = "sys_info"
|
||||
|
||||
[end.interval]
|
||||
cpu = 1
|
||||
disks = 300
|
||||
memory = 30
|
||||
networks = 3
|
||||
temps = 5
|
||||
|
||||
[[end]]
|
||||
max_items = 3
|
||||
type = "clipboard"
|
||||
|
||||
[end.truncate]
|
||||
length = 50
|
||||
mode = "end"
|
||||
|
||||
[[end]]
|
||||
class = "power-menu"
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
type = "custom"
|
||||
|
||||
[[end.bar]]
|
||||
label = ""
|
||||
name = "power-btn"
|
||||
on_click = "popup:toggle"
|
||||
type = "button"
|
||||
|
||||
[[end.popup]]
|
||||
orientation = "vertical"
|
||||
type = "box"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = "Power menu"
|
||||
name = "header"
|
||||
type = "label"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
type = "box"
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!shutdown now"
|
||||
type = "button"
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!reboot"
|
||||
type = "button"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
name = "uptime"
|
||||
type = "label"
|
||||
|
||||
[[end]]
|
||||
type = "clock"
|
||||
|
||||
[[start]]
|
||||
all_monitors = false
|
||||
type = "workspaces"
|
||||
|
||||
[start.name_map]
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Code = ""
|
||||
Games = "icon:steam"
|
||||
|
||||
[[start]]
|
||||
favorites = [
|
||||
"firefox",
|
||||
"discord",
|
||||
"steam",
|
||||
]
|
||||
show_icons = true
|
||||
show_names = false
|
||||
type = "launcher"
|
||||
|
||||
[[start]]
|
||||
label = "random num: {{500:echo FIXME}}"
|
||||
type = "label"
|
||||
|
||||
87
examples/config.yaml
Normal file
87
examples/config.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
anchor_to_edges: true
|
||||
end:
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- format:
|
||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
||||
- 李 {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||
- 猪 {load_average:1} | {load_average:5} | {load_average:15}
|
||||
- {uptime}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
icon_theme: Paper
|
||||
position: bottom
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1':
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo FIXME}}'
|
||||
type: label
|
||||
|
||||
@@ -1,148 +1,201 @@
|
||||
@define-color color_bg #2d2d2d;
|
||||
@define-color color_bg_dark #1c1c1c;
|
||||
@define-color color_border #424242;
|
||||
@define-color color_border_active #6699cc;
|
||||
@define-color color_text #ffffff;
|
||||
@define-color color_urgent #8f0a0a;
|
||||
|
||||
/* -- base styles -- */
|
||||
|
||||
* {
|
||||
/* `otf-font-awesome` is required to be installed for icons */
|
||||
font-family: Noto Sans Nerd Font, sans-serif;
|
||||
/* font-family: 'Jetbrains Mono', monospace;*/
|
||||
font-size: 16px;
|
||||
|
||||
/*color: white;*/
|
||||
/*background-color: #2d2d2d;*/
|
||||
/*background-color: red;*/
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/*opacity: 0.4;*/
|
||||
box, menubar, button {
|
||||
background-color: @color_bg;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
button, label {
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
#bar {
|
||||
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;
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clock {
|
||||
border: 1px solid @color_border;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#calendar-clock {
|
||||
color: white;
|
||||
|
||||
/* -- clipboard -- */
|
||||
|
||||
.clipboard {
|
||||
margin-left: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.popup-clipboard .item {
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
|
||||
/* -- clock -- */
|
||||
|
||||
.clock {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.popup-clock .calendar-clock {
|
||||
color: @color_text;
|
||||
font-size: 2.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-clock .calendar {
|
||||
background-color: @color_bg;
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
#calendar .header {
|
||||
.popup-clock .calendar .header {
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #424242;
|
||||
border-top: 1px solid @color_border;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#calendar:selected {
|
||||
background-color: #6699cc;
|
||||
.popup-clock .calendar:selected {
|
||||
background-color: @color_border_active;
|
||||
}
|
||||
|
||||
#popup-mpd {
|
||||
color: white;
|
||||
padding: 1em;
|
||||
|
||||
/* -- launcher -- */
|
||||
|
||||
.launcher .item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#popup-mpd #album-art {
|
||||
/*border: 1px solid #424242;*/
|
||||
.launcher .item:not(.focused):hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.launcher .open {
|
||||
border-bottom: 1px solid @color_text;
|
||||
}
|
||||
|
||||
.launcher .focused {
|
||||
border-bottom: 2px solid @color_border_active;
|
||||
}
|
||||
|
||||
.launcher .urgent {
|
||||
border-bottom-color: @color_urgent;
|
||||
}
|
||||
|
||||
.popup-launcher {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popup-launcher .popup-item:not(:first-child) {
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
|
||||
/* -- music -- */
|
||||
|
||||
.music:hover * {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.popup-music .album-art {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #title .icon, #popup-mpd #title .label {
|
||||
.popup-music .icon-box {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.popup-music .title .icon, .popup-music .title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
#popup-mpd #controls * {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-music .controls *:disabled {
|
||||
color: @color_border;
|
||||
}
|
||||
|
||||
#popup-mpd #controls *:disabled {
|
||||
color: #424242;
|
||||
.popup-music .volume .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#focused {
|
||||
color: white;
|
||||
.popup-music .volume .icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.popup-music .progress .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* -- script -- */
|
||||
|
||||
.script {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* -- sys_info -- */
|
||||
|
||||
.sysinfo {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sysinfo .item {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* -- tray -- */
|
||||
|
||||
.tray {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* -- workspaces -- */
|
||||
|
||||
.workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.workspaces .item:hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
|
||||
/* -- custom: power menu -- */
|
||||
|
||||
.popup-power-menu #header {
|
||||
font-size: 1.4em;
|
||||
padding-bottom: 0.4em;
|
||||
margin-bottom: 0.6em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn {
|
||||
border: 1px solid @color_border;
|
||||
padding: 0.6em 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu #buttons > *:nth-child(1) .power-btn {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
|
||||
213
flake.lock
generated
Normal file
213
flake.lock
generated
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1693439040,
|
||||
"narHash": "sha256-t2nOxBcP0Q/XJt6Ild4v0hJ49OSl9F3nE1cdIT4xsDg=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "174604795d316b75777e28185c3a4918bc69b399",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692351612,
|
||||
"narHash": "sha256-KTGonidcdaLadRnv9KFgwSMh1ZbXoR/OBmPjeNMhFwU=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "78789c30d64dea2396c9da516bbcc8db3a475207",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1693355128,
|
||||
"narHash": "sha256-+ZoAny3ZxLcfMaUoLVgL9Ywb/57wP+EtsdNGuXUJrwg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a63a64b593dcf2fe05f7c5d666eb395950f36bc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1693377291,
|
||||
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"crane",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"crane",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1691374719,
|
||||
"narHash": "sha256-HCodqnx1Mi2vN4f3hjRPc7+lSQy18vRn8xWW68GeQOg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b520a3889b24aaf909e287d19d406862ced9ffc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay_2": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1693447852,
|
||||
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
207
flake.nix
Normal file
207
flake.nix
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
description = "Nix Flake for ironbar";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
crane,
|
||||
naersk,
|
||||
...
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
genSystems = lib.genAttrs [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
];
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
overlays = [
|
||||
self.overlays.default
|
||||
rust-overlay.overlays.default
|
||||
];
|
||||
};
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
in {
|
||||
overlays.default = final: prev: let
|
||||
rust = mkRustToolchain final;
|
||||
craneLib = (crane.mkLib final).overrideToolchain rust;
|
||||
naersk' = prev.callPackage naersk {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
|
||||
rustPlatform = prev.makeRustPlatform {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
mkDate = longDate: (lib.concatStringsSep "-" [
|
||||
(builtins.substring 0 4 longDate)
|
||||
(builtins.substring 4 2 longDate)
|
||||
(builtins.substring 6 2 longDate)
|
||||
]);
|
||||
builder = "naersk";
|
||||
in {
|
||||
ironbar = let
|
||||
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
||||
in
|
||||
if builder == "crane"
|
||||
then
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
builder = craneLib;
|
||||
}
|
||||
else if builder == "naersk"
|
||||
then
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
builder = naersk';
|
||||
}
|
||||
else
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
};
|
||||
};
|
||||
packages = genSystems (
|
||||
system: let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
(self.overlays.default pkgs pkgs)
|
||||
// {
|
||||
default = self.packages.${system}.ironbar;
|
||||
}
|
||||
);
|
||||
apps = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
in {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
ironbar = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
});
|
||||
devShells = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
rust
|
||||
rust-analyzer-unwrapped
|
||||
gcc
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
pkg-config
|
||||
openssl
|
||||
gdk-pixbuf
|
||||
glib
|
||||
glib-networking
|
||||
shared-mime-info
|
||||
gnome.adwaita-icon-theme
|
||||
hicolor-icon-theme
|
||||
gsettings-desktop-schemas
|
||||
libxkbcommon
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
});
|
||||
homeManagerModules.default = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
cfg = config.programs.ironbar;
|
||||
defaultIronbarPackage = self.packages.${pkgs.hostPlatform.system}.default;
|
||||
jsonFormat = pkgs.formats.json {};
|
||||
in {
|
||||
options.programs.ironbar = {
|
||||
enable = lib.mkEnableOption "ironbar status bar";
|
||||
package = lib.mkOption {
|
||||
type = with lib.types; package;
|
||||
default = defaultIronbarPackage;
|
||||
description = "The package for ironbar to use.";
|
||||
};
|
||||
systemd = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = pkgs.stdenv.isLinux;
|
||||
description = "Whether to enable to systemd service for ironbar.";
|
||||
};
|
||||
style = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
default = "";
|
||||
description = "The stylesheet to apply to ironbar.";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = jsonFormat.type;
|
||||
default = {};
|
||||
description = "The config to pass to ironbar.";
|
||||
};
|
||||
features = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.nonEmptyStr;
|
||||
default = [];
|
||||
description = "The features to be used.";
|
||||
};
|
||||
};
|
||||
config = let
|
||||
pkg = cfg.package.override {features = cfg.features;};
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
home.packages = [pkg];
|
||||
xdg.configFile = {
|
||||
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
||||
source = jsonFormat.generate "ironbar-config" cfg.config;
|
||||
};
|
||||
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
|
||||
text = cfg.style;
|
||||
};
|
||||
};
|
||||
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
|
||||
Unit = {
|
||||
Description = "Systemd service for Ironbar";
|
||||
Requires = ["graphical-session.target"];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkg}/bin/ironbar";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
nixConfig = {
|
||||
extra-substituters = ["https://jakestanger.cachix.org"];
|
||||
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
|
||||
};
|
||||
}
|
||||
95
nix/default.nix
Normal file
95
nix/default.nix
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
gtk3,
|
||||
gdk-pixbuf,
|
||||
librsvg,
|
||||
webp-pixbuf-loader,
|
||||
gobject-introspection,
|
||||
glib-networking,
|
||||
glib,
|
||||
shared-mime-info,
|
||||
gsettings-desktop-schemas,
|
||||
wrapGAppsHook,
|
||||
gtk-layer-shell,
|
||||
gnome,
|
||||
libxkbcommon,
|
||||
openssl,
|
||||
pkg-config,
|
||||
hicolor-icon-theme,
|
||||
rustPlatform,
|
||||
lib,
|
||||
version ? "git",
|
||||
features ? [],
|
||||
builderName ? "nix",
|
||||
builder ? {},
|
||||
}: let
|
||||
basePkg = rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||
propagatedBuildInputs = [
|
||||
gtk3
|
||||
];
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# Thumbnailers
|
||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||
)
|
||||
'';
|
||||
passthru = {
|
||||
updateScript = gnome.updateScript {
|
||||
packageName = pname;
|
||||
attrPath = "gnome.${pname}";
|
||||
};
|
||||
};
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
};
|
||||
};
|
||||
flags = let
|
||||
noDefault =
|
||||
if features == []
|
||||
then ""
|
||||
else "--no-default-features";
|
||||
featuresStr =
|
||||
if features == []
|
||||
then ""
|
||||
else ''-F "${builtins.concatStringsSep "," features}"'';
|
||||
in [noDefault featuresStr];
|
||||
in
|
||||
if builderName == "naersk"
|
||||
then
|
||||
builder.buildPackage (basePkg
|
||||
// {
|
||||
cargoOptions = old: old ++ flags;
|
||||
})
|
||||
else if builderName == "crane"
|
||||
then
|
||||
builder.buildPackage (basePkg
|
||||
// {
|
||||
cargoExtraArgs = builtins.concatStringsSep " " flags;
|
||||
doCheck = false;
|
||||
})
|
||||
else
|
||||
rustPlatform.buildRustPackage (basePkg
|
||||
// {
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||
})
|
||||
5
scripts/generate-examples.sh
Executable file
5
scripts/generate-examples.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
corn examples/config.corn -t json > examples/config.json
|
||||
corn examples/config.corn -t toml > examples/config.toml
|
||||
corn examples/config.corn -t yaml > examples/config.yaml
|
||||
72
scripts/migrate-styles.sh
Executable file
72
scripts/migrate-styles.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Migrates CSS selectors from widget names to CSS classes.
|
||||
# These changed as part of the 0.12 release.
|
||||
|
||||
# ⚠ This script will **NOT** check for custom styles and may mangle them!
|
||||
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
|
||||
|
||||
style_path="$HOME/.config/ironbar/style.css"
|
||||
|
||||
# general
|
||||
sed -i 's/#icon/.icon/g' "$style_path"
|
||||
sed -i 's/#label/.label/g' "$style_path"
|
||||
sed -i 's/#image/.image/g' "$style_path"
|
||||
|
||||
# clipboard
|
||||
sed -i 's/#clipboard/.clipboard/g' "$style_path"
|
||||
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
|
||||
|
||||
# clock
|
||||
sed -i 's/#clock/.clock/g' "$style_path"
|
||||
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
|
||||
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
|
||||
sed -i 's/#calendar/.calendar/g' "$style_path"
|
||||
|
||||
# custom
|
||||
sed -i 's/#custom/.custom/g' "$style_path"
|
||||
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
|
||||
|
||||
# focused
|
||||
sed -i 's/#focused/.focused/g' "$style_path"
|
||||
|
||||
# launcher
|
||||
sed -i 's/#launcher/.launcher/g' "$style_path"
|
||||
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
|
||||
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
|
||||
|
||||
# music
|
||||
sed -i 's/#music/.music/g' "$style_path"
|
||||
sed -i 's/#contents/.contents/g' "$style_path"
|
||||
sed -i 's/#popup-music/.popup-music/g' "$style_path"
|
||||
sed -i 's/#album-art/.album-art/g' "$style_path"
|
||||
sed -i 's/#title/.title/g' "$style_path"
|
||||
sed -i 's/#album/.album/g' "$style_path"
|
||||
sed -i 's/#artist/.artist/g' "$style_path"
|
||||
sed -i 's/#controls/.controls/g' "$style_path"
|
||||
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
|
||||
sed -i 's/#btn-play/.btn-play/g' "$style_path"
|
||||
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
|
||||
sed -i 's/#btn-next/.btn-next/g' "$style_path"
|
||||
sed -i 's/#volume/.volume/g' "$style_path"
|
||||
sed -i 's/#slider/.slider/g' "$style_path"
|
||||
|
||||
# script
|
||||
sed -i 's/#script/.script/g' "$style_path"
|
||||
|
||||
# sys_info
|
||||
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
|
||||
# tray
|
||||
sed -i 's/#tray/.tray/g' "$style_path"
|
||||
|
||||
# upower
|
||||
sed -i 's/#upower/.upower/g' "$style_path"
|
||||
sed -i 's/#button/.button/g' "$style_path"
|
||||
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
|
||||
sed -i 's/#upower-details/.upower-details/g' "$style_path"
|
||||
|
||||
# workspaces
|
||||
sed -i 's/#workspaces/.workspaces/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
17
shell.nix
Normal file
17
shell.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustfmt
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
gcc
|
||||
openssl
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
];
|
||||
}
|
||||
280
src/bar.rs
280
src/bar.rs
@@ -1,55 +1,161 @@
|
||||
use crate::config::{BarPosition, ModuleConfig};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleLocation};
|
||||
use crate::Config;
|
||||
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
||||
use crate::modules::{
|
||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
||||
};
|
||||
use crate::popup::Popup;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{Config, GlobalState};
|
||||
use color_eyre::Result;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, Orientation};
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Creates a new window for a bar,
|
||||
/// sets it up and adds its widgets.
|
||||
pub fn create_bar(
|
||||
app: &Application,
|
||||
monitor: &Monitor,
|
||||
monitor_name: &str,
|
||||
config: Config,
|
||||
global_state: &Rc<RefCell<GlobalState>>,
|
||||
) -> Result<()> {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
let bar_name = config
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("bar-{}", get_unique_usize()));
|
||||
|
||||
setup_layer_shell(&win, monitor, &config.position);
|
||||
win.set_widget_name(&bar_name);
|
||||
info!("Creating bar {}", bar_name);
|
||||
|
||||
setup_layer_shell(
|
||||
&win,
|
||||
monitor,
|
||||
config.position,
|
||||
config.anchor_to_edges,
|
||||
config.margin,
|
||||
);
|
||||
|
||||
let orientation = config.position.get_orientation();
|
||||
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.orientation(orientation)
|
||||
.spacing(0)
|
||||
.hexpand(false)
|
||||
.height_request(config.height)
|
||||
.name("bar")
|
||||
.name("bar");
|
||||
|
||||
let content = if orientation == Orientation::Horizontal {
|
||||
content.height_request(config.height)
|
||||
} else {
|
||||
content.width_request(config.height)
|
||||
}
|
||||
.build();
|
||||
|
||||
let left = gtk::Box::builder().spacing(0).name("left").build();
|
||||
let center = gtk::Box::builder().spacing(0).name("center").build();
|
||||
let right = gtk::Box::builder().spacing(0).name("right").build();
|
||||
|
||||
content.style_context().add_class("container");
|
||||
left.style_context().add_class("container");
|
||||
center.style_context().add_class("container");
|
||||
right.style_context().add_class("container");
|
||||
|
||||
content.add(&left);
|
||||
let start = create_container("start", orientation);
|
||||
let center = create_container("center", orientation);
|
||||
let end = create_container("end", orientation);
|
||||
|
||||
content.add(&start);
|
||||
content.set_center_widget(Some(¢er));
|
||||
content.pack_end(&right, false, false, 0);
|
||||
content.pack_end(&end, false, false, 0);
|
||||
|
||||
let load_result = load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||
global_state
|
||||
.borrow_mut()
|
||||
.popups_mut()
|
||||
.insert(bar_name.into(), load_result.popup);
|
||||
|
||||
load_modules(&left, ¢er, &right, app, config, monitor, monitor_name)?;
|
||||
win.add(&content);
|
||||
|
||||
win.connect_destroy_event(|_, _| {
|
||||
info!("Shutting down");
|
||||
gtk::main_quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
win.show_all();
|
||||
debug!("Showing bar");
|
||||
|
||||
// show each box but do not use `show_all`.
|
||||
// this ensures `show_if` option works as intended.
|
||||
start.show();
|
||||
center.show();
|
||||
end.show();
|
||||
content.show();
|
||||
win.show();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets up GTK layer shell for a provided application window.
|
||||
fn setup_layer_shell(
|
||||
win: &ApplicationWindow,
|
||||
monitor: &Monitor,
|
||||
position: BarPosition,
|
||||
anchor_to_edges: bool,
|
||||
margin: MarginConfig,
|
||||
) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
||||
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
|
||||
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
|
||||
|
||||
let bar_orientation = position.get_orientation();
|
||||
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
position == BarPosition::Top
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
position == BarPosition::Bottom
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Left,
|
||||
position == BarPosition::Left
|
||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Right,
|
||||
position == BarPosition::Right
|
||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Box` container to place widgets inside.
|
||||
fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(orientation)
|
||||
.spacing(0)
|
||||
.name(name)
|
||||
.build();
|
||||
|
||||
container.style_context().add_class("container");
|
||||
container
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BarLoadResult {
|
||||
popup: Rc<RefCell<Popup>>,
|
||||
}
|
||||
|
||||
/// Loads the configured modules onto a bar.
|
||||
fn load_modules(
|
||||
left: >k::Box,
|
||||
center: >k::Box,
|
||||
@@ -58,92 +164,100 @@ fn load_modules(
|
||||
config: Config,
|
||||
monitor: &Monitor,
|
||||
output_name: &str,
|
||||
) -> Result<()> {
|
||||
if let Some(modules) = config.left {
|
||||
let info = ModuleInfo {
|
||||
) -> Result<BarLoadResult> {
|
||||
let icon_theme = IconTheme::new();
|
||||
if let Some(ref theme) = config.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(theme));
|
||||
}
|
||||
|
||||
macro_rules! info {
|
||||
($location:expr) => {
|
||||
ModuleInfo {
|
||||
app,
|
||||
location: ModuleLocation::Left,
|
||||
bar_position: &config.position,
|
||||
bar_position: config.position,
|
||||
monitor,
|
||||
output_name,
|
||||
location: $location,
|
||||
icon_theme: &icon_theme,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
add_modules(left, modules, &info)?;
|
||||
// popup ignores module location so can bodge this for now
|
||||
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
|
||||
let popup = Rc::new(RefCell::new(popup));
|
||||
|
||||
if let Some(modules) = config.start {
|
||||
let info = info!(ModuleLocation::Left);
|
||||
add_modules(left, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
if let Some(modules) = config.center {
|
||||
let info = ModuleInfo {
|
||||
app,
|
||||
location: ModuleLocation::Center,
|
||||
bar_position: &config.position,
|
||||
monitor,
|
||||
output_name,
|
||||
};
|
||||
|
||||
add_modules(center, modules, &info)?;
|
||||
let info = info!(ModuleLocation::Center);
|
||||
add_modules(center, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
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)?;
|
||||
if let Some(modules) = config.end {
|
||||
let info = info!(ModuleLocation::Right);
|
||||
add_modules(right, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let result = BarLoadResult { popup };
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
|
||||
/// Adds modules into a provided GTK box,
|
||||
/// which should be one of its left, center or right containers.
|
||||
fn add_modules(
|
||||
content: >k::Box,
|
||||
modules: Vec<ModuleConfig>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
) -> Result<()> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
macro_rules! add_module {
|
||||
($module:expr, $name:literal) => {{
|
||||
let widget = $module.into_widget(&info)?;
|
||||
widget.set_widget_name($name);
|
||||
content.add(&widget);
|
||||
($module:expr, $id:expr) => {{
|
||||
let common = $module.common.take().expect("common config to exist");
|
||||
let widget_parts = create_module(
|
||||
*$module,
|
||||
$id,
|
||||
common.name.clone(),
|
||||
&info,
|
||||
&Rc::clone(&popup),
|
||||
)?;
|
||||
set_widget_identifiers(&widget_parts, &common);
|
||||
|
||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
||||
content.add(&container);
|
||||
}};
|
||||
}
|
||||
|
||||
for config in modules {
|
||||
let id = get_unique_usize();
|
||||
match config {
|
||||
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"),
|
||||
#[cfg(feature = "clipboard")]
|
||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "clock")]
|
||||
ModuleConfig::Clock(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Focused(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Label(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Launcher(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "music")]
|
||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Script(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "sys_info")]
|
||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "tray")]
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "upower")]
|
||||
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "workspaces")]
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
||||
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
|
||||
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
position == &BarPosition::Top,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
position == &BarPosition::Bottom,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Left, true);
|
||||
gtk_layer_shell::set_anchor(win, gtk_layer_shell::Edge::Right, true);
|
||||
}
|
||||
|
||||
44
src/bridge_channel.rs
Normal file
44
src/bridge_channel.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::send;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// MPSC async -> GTK sync channel.
|
||||
/// The sender uses `tokio::sync::mpsc`
|
||||
/// while the receiver uses `glib::MainContext::channel`.
|
||||
///
|
||||
/// This makes it possible to send events asynchronously
|
||||
/// and receive them on the main thread,
|
||||
/// allowing UI updates to be handled on the receiving end.
|
||||
pub struct BridgeChannel<T> {
|
||||
async_tx: mpsc::Sender<T>,
|
||||
sync_rx: glib::Receiver<T>,
|
||||
}
|
||||
|
||||
impl<T: Send + 'static> BridgeChannel<T> {
|
||||
/// Creates a new channel
|
||||
pub fn new() -> Self {
|
||||
let (async_tx, mut async_rx) = mpsc::channel(32);
|
||||
let (sync_tx, sync_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
while let Some(val) = async_rx.recv().await {
|
||||
send!(sync_tx, val);
|
||||
}
|
||||
});
|
||||
|
||||
Self { async_tx, sync_rx }
|
||||
}
|
||||
|
||||
/// Gets a clone of the sender.
|
||||
pub fn create_sender(&self) -> mpsc::Sender<T> {
|
||||
self.async_tx.clone()
|
||||
}
|
||||
|
||||
/// Attaches a callback to the receiver.
|
||||
pub fn recv<F>(self, f: F) -> glib::SourceId
|
||||
where
|
||||
F: FnMut(T) -> glib::Continue + 'static,
|
||||
{
|
||||
self.sync_rx.attach(None, f)
|
||||
}
|
||||
}
|
||||
145
src/cached_broadcast.rs
Normal file
145
src/cached_broadcast.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::{arc_rw, read_lock, send_async, write_lock};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, RwLock};
|
||||
// use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::sleep;
|
||||
use tracing::trace;
|
||||
|
||||
pub trait Cacheable: Debug + Clone + Send + Sync {
|
||||
type Key: Debug + Clone + Send + Sync + Eq;
|
||||
|
||||
fn get_key(&self) -> Self::Key;
|
||||
}
|
||||
|
||||
pub type Sender<T> = mpsc::Sender<Event<T>>;
|
||||
pub type Receiver<T> = mpsc::Receiver<Event<T>>;
|
||||
|
||||
pub struct CachedBroadcastChannel<T>
|
||||
where
|
||||
T: Cacheable,
|
||||
{
|
||||
capacity: usize,
|
||||
data: Vec<T>,
|
||||
channels: Arc<RwLock<Vec<mpsc::Sender<Event<T>>>>>,
|
||||
base_tx: mpsc::Sender<Event<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event<T>
|
||||
where
|
||||
T: Cacheable,
|
||||
{
|
||||
Add(T),
|
||||
Remove(T::Key),
|
||||
Replace(T::Key, T),
|
||||
}
|
||||
|
||||
impl<T> CachedBroadcastChannel<T>
|
||||
where
|
||||
T: Cacheable + 'static,
|
||||
{
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
let (tx, rx) = mpsc::channel::<Event<T>>(capacity);
|
||||
let mut rx = DropDetector(rx);
|
||||
|
||||
// spawn_blocking(move || loop {
|
||||
// let ev = rx.0.try_recv();
|
||||
// println!("{ev:?}");
|
||||
// sleep(Duration::from_secs(1))
|
||||
// });
|
||||
|
||||
let channels = arc_rw!(Vec::<Sender<T>>::new());
|
||||
|
||||
let channels = Arc::clone(&channels);
|
||||
spawn(async move {
|
||||
println!("hello");
|
||||
|
||||
while let Some(event) = rx.0.recv().await {
|
||||
println!("ev");
|
||||
// trace!("{event:?}");
|
||||
// let iter = read_lock!(channels).clone().into_iter();
|
||||
// for channel in iter {
|
||||
// send_async!(channel, event.clone());
|
||||
// }
|
||||
}
|
||||
println!("goodbye");
|
||||
});
|
||||
|
||||
Self {
|
||||
capacity,
|
||||
data: vec![],
|
||||
channels,
|
||||
base_tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, event: Event<T>) {
|
||||
match event.clone() {
|
||||
Event::Add(data) => {
|
||||
self.data.push(data);
|
||||
}
|
||||
Event::Remove(key) => {
|
||||
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
|
||||
return;
|
||||
};
|
||||
self.data.remove(index);
|
||||
}
|
||||
Event::Replace(key, data) => {
|
||||
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
|
||||
return;
|
||||
};
|
||||
let _ = std::mem::replace(&mut self.data[index], data);
|
||||
}
|
||||
}
|
||||
|
||||
send_async!(self.base_tx, event);
|
||||
|
||||
// let mut closed = vec![];
|
||||
// for (i, channel) in read_lock!(self.channels).iter().enumerate() {
|
||||
// if channel.is_closed() {
|
||||
// closed.push(i);
|
||||
// } else {
|
||||
// send_async!(channel, event.clone());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for channel in closed.into_iter().rev() {
|
||||
// write_lock!(self.channels).remove(channel);
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> mpsc::Sender<Event<T>> {
|
||||
self.base_tx.clone()
|
||||
}
|
||||
|
||||
pub fn receiver(&mut self) -> mpsc::Receiver<Event<T>> {
|
||||
let (tx, rx) = mpsc::channel(self.capacity);
|
||||
write_lock!(self.channels).push(tx);
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Vec<T> {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DropDetector<T>(T);
|
||||
|
||||
impl<T> Drop for DropDetector<T> {
|
||||
fn drop(&mut self) {
|
||||
println!("DROPPED")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Cacheable> Drop for CachedBroadcastChannel<T> {
|
||||
fn drop(&mut self) {
|
||||
println!("Channel DROPPED")
|
||||
}
|
||||
}
|
||||
19
src/cli/mod.rs
Normal file
19
src/cli/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::ipc::commands::Command;
|
||||
use crate::ipc::responses::Response;
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Parser, Debug, Serialize, Deserialize)]
|
||||
#[command(version)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
pub fn handle_response(response: Response) {
|
||||
match response {
|
||||
Response::Ok => println!("ok"),
|
||||
Response::OkValue { value } => println!("ok\n{value}"),
|
||||
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
239
src/clients/clipboard.rs
Normal file
239
src/clients/clipboard.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use super::wayland::{self, ClipboardItem};
|
||||
use crate::{arc_mut, lock, try_send};
|
||||
use indexmap::map::Iter;
|
||||
use indexmap::IndexMap;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClipboardEvent {
|
||||
Add(Arc<ClipboardItem>),
|
||||
Remove(usize),
|
||||
Activate(usize),
|
||||
}
|
||||
|
||||
type EventSender = mpsc::Sender<ClipboardEvent>;
|
||||
|
||||
/// Clipboard client singleton,
|
||||
/// to ensure bars don't duplicate requests to the compositor.
|
||||
pub struct ClipboardClient {
|
||||
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
|
||||
cache: Arc<Mutex<ClipboardCache>>,
|
||||
}
|
||||
|
||||
impl ClipboardClient {
|
||||
fn new() -> Self {
|
||||
trace!("Initializing clipboard client");
|
||||
|
||||
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
|
||||
|
||||
let cache = arc_mut!(ClipboardCache::new());
|
||||
|
||||
{
|
||||
let senders = senders.clone();
|
||||
let cache = cache.clone();
|
||||
|
||||
spawn(async move {
|
||||
let (mut rx, item) = {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_clipboard()
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
|
||||
lock!(cache).insert(item, senders.len());
|
||||
}
|
||||
|
||||
while let Ok(item) = rx.recv().await {
|
||||
debug!("Received clipboard item (ID: {})", item.id);
|
||||
|
||||
let (existing_id, cache_size) = {
|
||||
let cache = lock!(cache);
|
||||
(cache.contains(&item), cache.len())
|
||||
};
|
||||
|
||||
existing_id.map_or_else(
|
||||
|| {
|
||||
{
|
||||
let mut cache = lock!(cache);
|
||||
let senders = lock!(senders);
|
||||
cache.insert(item.clone(), senders.len());
|
||||
}
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, sender_cache_size) in iter {
|
||||
if cache_size == *sender_cache_size {
|
||||
let removed_id = lock!(cache)
|
||||
.remove_ref_first()
|
||||
.expect("Clipboard cache unexpectedly empty");
|
||||
try_send!(tx, ClipboardEvent::Remove(removed_id));
|
||||
}
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
},
|
||||
|existing_id| {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(existing_id));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self { senders, cache }
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
|
||||
{
|
||||
let cache = lock!(self.cache);
|
||||
|
||||
let iter = cache.iter();
|
||||
for (_, (item, _)) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
lock!(self.senders).push((tx, cache_size));
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn copy(&self, id: usize) {
|
||||
debug!("Copying item with id {id}");
|
||||
|
||||
let item = {
|
||||
let cache = lock!(self.cache);
|
||||
cache.get(id)
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.copy_to_clipboard(item);
|
||||
}
|
||||
|
||||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: usize) {
|
||||
lock!(self.cache).remove(id);
|
||||
|
||||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Remove(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared clipboard item cache.
|
||||
///
|
||||
/// Items are stored with a number of references,
|
||||
/// allowing different consumers to 'remove' cached items
|
||||
/// at different times.
|
||||
#[derive(Debug)]
|
||||
struct ClipboardCache {
|
||||
cache: IndexMap<usize, (Arc<ClipboardItem>, usize)>,
|
||||
}
|
||||
|
||||
impl ClipboardCache {
|
||||
/// Creates a new empty cache.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cache: IndexMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the entry with key `id` from the cache.
|
||||
fn get(&self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache.get(&id).map(|(item, _)| item).cloned()
|
||||
}
|
||||
|
||||
/// Inserts an entry with `ref_count` initial references.
|
||||
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache
|
||||
.insert(item.id, (item, ref_count))
|
||||
.map(|(item, _)| item)
|
||||
}
|
||||
|
||||
/// Removes the entry with key `id`.
|
||||
/// This ignores references.
|
||||
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache.shift_remove(&id).map(|(item, _)| item)
|
||||
}
|
||||
|
||||
/// Removes a reference to the entry with key `id`.
|
||||
///
|
||||
/// If the reference count reaches zero, the entry
|
||||
/// is removed from the cache.
|
||||
fn remove_ref(&mut self, id: usize) {
|
||||
if let Some(entry) = self.cache.get_mut(&id) {
|
||||
entry.1 -= 1;
|
||||
|
||||
if entry.1 == 0 {
|
||||
self.cache.shift_remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a reference to the first entry.
|
||||
///
|
||||
/// If the reference count reaches zero, the entry
|
||||
/// is removed from the cache.
|
||||
fn remove_ref_first(&mut self) -> Option<usize> {
|
||||
if let Some((id, _)) = self.cache.first() {
|
||||
let id = *id;
|
||||
self.remove_ref(id);
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an item with matching mime type and value
|
||||
/// already exists in the cache.
|
||||
fn contains(&self, item: &ClipboardItem) -> Option<usize> {
|
||||
self.cache.values().find_map(|(it, _)| {
|
||||
if it.mime_type == item.mime_type && it.value == item.value {
|
||||
Some(it.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the current number of items in the cache.
|
||||
fn len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, usize)> {
|
||||
self.cache.iter()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: ClipboardClient = ClipboardClient::new();
|
||||
}
|
||||
|
||||
pub fn get_client() -> &'static ClipboardClient {
|
||||
&CLIENT
|
||||
}
|
||||
295
src/clients/compositor/hyprland.rs
Normal file
295
src/clients/compositor/hyprland.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{arc_mut, lock, send};
|
||||
use color_eyre::Result;
|
||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
use hyprland::event_listener::EventListener;
|
||||
use hyprland::prelude::*;
|
||||
use hyprland::shared::WorkspaceType;
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub struct EventClient {
|
||||
workspace_tx: Sender<WorkspaceUpdate>,
|
||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||
}
|
||||
|
||||
impl EventClient {
|
||||
fn new() -> Self {
|
||||
let (workspace_tx, workspace_rx) = channel(16);
|
||||
|
||||
Self {
|
||||
workspace_tx,
|
||||
_workspace_rx: workspace_rx,
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_workspace_events(&self) {
|
||||
info!("Starting Hyprland event listener");
|
||||
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let mut event_listener = EventListener::new();
|
||||
|
||||
// we need a lock to ensure events don't run at the same time
|
||||
let lock = arc_mut!(());
|
||||
|
||||
// cache the active workspace since Hyprland doesn't give us the prev active
|
||||
let active = Self::get_active_workspace().expect("Failed to get active workspace");
|
||||
let active = arc_mut!(Some(active));
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_added_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Added workspace: {workspace_type:?}");
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let prev_workspace = lock!(active);
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_change_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
debug!(
|
||||
"Received workspace change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.id)
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
workspace.map_or_else(
|
||||
|| {
|
||||
error!("Unable to locate workspace");
|
||||
},
|
||||
|workspace| {
|
||||
// there may be another type of update so dispatch that regardless of focus change
|
||||
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_active_monitor_change_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.workspace;
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
debug!(
|
||||
"Received active monitor change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.name)
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some((false, workspace)) =
|
||||
workspace.map(|w| (w.visibility.is_focused(), w))
|
||||
{
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
} else {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.workspace;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
event_listener.add_workspace_destroy_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace destroy: {workspace_type:?}");
|
||||
|
||||
let name = get_workspace_name(workspace_type);
|
||||
send!(tx, WorkspaceUpdate::Remove(name));
|
||||
});
|
||||
}
|
||||
|
||||
event_listener
|
||||
.start_listener()
|
||||
.expect("Failed to start listener");
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends a `WorkspaceUpdate::Focus` event
|
||||
/// and updates the active workspace cache.
|
||||
fn send_focus_change(
|
||||
prev_workspace: &mut Option<Workspace>,
|
||||
workspace: Workspace,
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
) {
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Focus {
|
||||
old: prev_workspace.take(),
|
||||
new: workspace.clone(),
|
||||
}
|
||||
);
|
||||
|
||||
prev_workspace.replace(workspace);
|
||||
}
|
||||
|
||||
/// Gets a workspace by name from the server, given the active workspace if known.
|
||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
|
||||
Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.find_map(|w| {
|
||||
if w.name == name {
|
||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
||||
create_is_visible()(w)
|
||||
}));
|
||||
|
||||
Some(Workspace::from((vis, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the active workspace from the server.
|
||||
fn get_active_workspace() -> Result<Workspace> {
|
||||
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?;
|
||||
Ok(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for EventClient {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
let identifier = match id.parse::<i32>() {
|
||||
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
|
||||
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
};
|
||||
|
||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let rx = self.workspace_tx.subscribe();
|
||||
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||
let is_visible = create_is_visible();
|
||||
|
||||
let workspaces = Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.map(|w| {
|
||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||
|
||||
Workspace::from((vis, w))
|
||||
})
|
||||
.collect();
|
||||
|
||||
send!(tx, WorkspaceUpdate::Init(workspaces));
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: EventClient = {
|
||||
let client = EventClient::new();
|
||||
client.listen_workspace_events();
|
||||
client
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_client() -> &'static EventClient {
|
||||
&CLIENT
|
||||
}
|
||||
|
||||
fn get_workspace_name(name: WorkspaceType) -> String {
|
||||
match name {
|
||||
WorkspaceType::Regular(name) => name,
|
||||
WorkspaceType::Special(name) => name.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a function which determines if a workspace is visible. This function makes a Hyprland call that allocates so it should be cached when possible, but it is only valid so long as workspaces do not change so it should not be stored long term
|
||||
fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
|
||||
let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), |ms| ms.to_vec());
|
||||
|
||||
move |w| monitors.iter().any(|m| m.active_workspace.id == w.id)
|
||||
}
|
||||
|
||||
impl From<(Visibility, HWorkspace)> for Workspace {
|
||||
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.monitor,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility
|
||||
where
|
||||
F: FnOnce(&'f HWorkspace) -> bool,
|
||||
'a: 'f,
|
||||
{
|
||||
fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self {
|
||||
if Some(workspace.name.as_str()) == active_name {
|
||||
Self::focused()
|
||||
} else if is_visible(workspace) {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/clients/compositor/mod.rs
Normal file
134
src/clients/compositor/mod.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::debug;
|
||||
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
pub mod hyprland;
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
pub mod sway;
|
||||
|
||||
pub enum Compositor {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Sway,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Hyprland,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl Display for Compositor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Self::Sway => "Sway",
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::Hyprland => "Hyprland",
|
||||
Self::Unsupported => "Unsupported",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Compositor {
|
||||
/// Attempts to get the current compositor.
|
||||
/// This is done by checking system env vars.
|
||||
fn get_current() -> Self {
|
||||
if std::env::var("SWAYSOCK").is_ok() {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+sway")] { Self::Sway }
|
||||
else { tracing::error!("Not compiled with Sway support"); Self::Unsupported }
|
||||
}
|
||||
} else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland}
|
||||
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
|
||||
}
|
||||
} else {
|
||||
Self::Unsupported
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the workspace client for the current compositor
|
||||
pub fn get_workspace_client() -> Result<&'static (dyn WorkspaceClient + Send)> {
|
||||
let current = Self::get_current();
|
||||
debug!("Getting workspace client for: {current}");
|
||||
match current {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Self::Sway => Ok(sway::get_sub_client()),
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::Hyprland => Ok(hyprland::get_client()),
|
||||
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
||||
.note("Currently workspaces are only supported by Sway and Hyprland")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Workspace {
|
||||
/// Unique identifier
|
||||
pub id: String,
|
||||
/// Workspace friendly name
|
||||
pub name: String,
|
||||
/// Name of the monitor (output) the workspace is located on
|
||||
pub monitor: String,
|
||||
/// How visible the workspace is
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
|
||||
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Visibility {
|
||||
Visible(bool),
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
pub fn visible() -> Self {
|
||||
Self::Visible(false)
|
||||
}
|
||||
|
||||
pub fn focused() -> Self {
|
||||
Self::Visible(true)
|
||||
}
|
||||
|
||||
pub fn is_visible(self) -> bool {
|
||||
matches!(self, Self::Visible(_))
|
||||
}
|
||||
|
||||
pub fn is_focused(self) -> bool {
|
||||
if let Self::Visible(focused) = self {
|
||||
focused
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WorkspaceUpdate {
|
||||
/// Provides an initial list of workspaces.
|
||||
/// This is re-sent to all subscribers when a new subscription is created.
|
||||
Init(Vec<Workspace>),
|
||||
Add(Workspace),
|
||||
Remove(String),
|
||||
Update(Workspace),
|
||||
Move(Workspace),
|
||||
/// Declares focus moved from the old workspace to the new.
|
||||
Focus {
|
||||
old: Option<Workspace>,
|
||||
new: Workspace,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait WorkspaceClient {
|
||||
/// Requests the workspace with this name is focused.
|
||||
fn focus(&self, name: String) -> Result<()>;
|
||||
|
||||
/// Creates a new to workspace event receiver.
|
||||
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
|
||||
}
|
||||
179
src/clients/compositor/sway.rs
Normal file
179
src/clients/compositor/sway.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{await_sync, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use futures_util::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, trace};
|
||||
|
||||
pub struct SwayEventClient {
|
||||
workspace_tx: Sender<WorkspaceUpdate>,
|
||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||
}
|
||||
|
||||
impl SwayEventClient {
|
||||
fn new() -> Self {
|
||||
let (workspace_tx, workspace_rx) = channel(16);
|
||||
|
||||
{
|
||||
let workspace_tx = workspace_tx.clone();
|
||||
spawn(async move {
|
||||
let client = Connection::new().await?;
|
||||
info!("Sway IPC subscription client connected");
|
||||
|
||||
let event_types = [EventType::Workspace];
|
||||
|
||||
let mut events = client.subscribe(event_types).await?;
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
trace!("event: {:?}", event);
|
||||
if let Event::Workspace(ev) = event? {
|
||||
workspace_tx.send(WorkspaceUpdate::from(*ev))?;
|
||||
};
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
workspace_tx,
|
||||
_workspace_rx: workspace_rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for SwayEventClient {
|
||||
fn focus(&self, id: String) -> color_eyre::Result<()> {
|
||||
await_sync(async move {
|
||||
let client = get_client().await;
|
||||
let mut client = client.lock().await;
|
||||
client.run_command(format!("workspace {id}")).await
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let rx = self.workspace_tx.subscribe();
|
||||
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
await_sync(async {
|
||||
let client = get_client().await;
|
||||
let mut client = client.lock().await;
|
||||
|
||||
let workspaces = client
|
||||
.get_workspaces()
|
||||
.await
|
||||
.expect("Failed to get workspaces");
|
||||
let event =
|
||||
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
|
||||
|
||||
send!(tx, event);
|
||||
});
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: AsyncOnce<Arc<Mutex<Connection>>> = AsyncOnce::new(async {
|
||||
let client = Connection::new()
|
||||
.await
|
||||
.expect("Failed to connect to Sway socket");
|
||||
Arc::new(Mutex::new(client))
|
||||
});
|
||||
static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new();
|
||||
}
|
||||
|
||||
/// Gets the sway IPC client
|
||||
async fn get_client() -> Arc<Mutex<Connection>> {
|
||||
let client = CLIENT.get().await;
|
||||
Arc::clone(client)
|
||||
}
|
||||
|
||||
/// Gets the sway IPC event subscription client
|
||||
pub fn get_sub_client() -> &'static SwayEventClient {
|
||||
&SUB_CLIENT
|
||||
}
|
||||
|
||||
impl From<Node> for Workspace {
|
||||
fn from(node: Node) -> Self {
|
||||
let visibility = Visibility::from(&node);
|
||||
|
||||
Self {
|
||||
id: node.id.to_string(),
|
||||
name: node.name.unwrap_or_default(),
|
||||
monitor: node.output.unwrap_or_default(),
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<swayipc_async::Workspace> for Workspace {
|
||||
fn from(workspace: swayipc_async::Workspace) -> Self {
|
||||
let visibility = Visibility::from(&workspace);
|
||||
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.output,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Node> for Visibility {
|
||||
fn from(node: &Node) -> Self {
|
||||
if node.focused {
|
||||
Self::focused()
|
||||
} else if node.visible.unwrap_or(false) {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&swayipc_async::Workspace> for Visibility {
|
||||
fn from(workspace: &swayipc_async::Workspace) -> Self {
|
||||
if workspace.focused {
|
||||
Self::focused()
|
||||
} else if workspace.visible {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorkspaceEvent> for WorkspaceUpdate {
|
||||
fn from(event: WorkspaceEvent) -> Self {
|
||||
match event.change {
|
||||
WorkspaceChange::Init => {
|
||||
Self::Add(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
WorkspaceChange::Empty => Self::Remove(
|
||||
event
|
||||
.current
|
||||
.expect("Missing current workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
WorkspaceChange::Focus => Self::Focus {
|
||||
old: event.old.map(Workspace::from),
|
||||
new: Workspace::from(event.current.expect("Missing current workspace")),
|
||||
},
|
||||
WorkspaceChange::Move => {
|
||||
Self::Move(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
_ => Self::Update(event.current.expect("Missing current workspace").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/clients/mod.rs
Normal file
11
src/clients/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub mod clipboard;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod compositor;
|
||||
#[cfg(feature = "music")]
|
||||
pub mod music;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod system_tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
pub mod wayland;
|
||||
85
src/clients/music/mod.rs
Normal file
85
src/clients/music/mod.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use color_eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
#[cfg(feature = "music+mpd")]
|
||||
pub mod mpd;
|
||||
#[cfg(feature = "music+mpris")]
|
||||
pub mod mpris;
|
||||
|
||||
pub const TICK_INTERVAL_MS: u64 = 200;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerUpdate {
|
||||
/// Triggered when the track or player state notably changes,
|
||||
/// such as a new track playing, the player being paused, or a volume change.
|
||||
Update(Box<Option<Track>>, Status),
|
||||
/// Triggered at regular intervals while a track is playing.
|
||||
/// Used to keep track of the progress through the current track.
|
||||
ProgressTick(ProgressTick),
|
||||
/// Triggered when the client disconnects from the player.
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Track {
|
||||
pub title: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub date: Option<String>,
|
||||
pub disc: Option<u64>,
|
||||
pub genre: Option<String>,
|
||||
pub track: Option<u64>,
|
||||
pub cover_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PlayerState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Status {
|
||||
pub state: PlayerState,
|
||||
pub volume_percent: Option<u8>,
|
||||
pub playlist_position: u32,
|
||||
pub playlist_length: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProgressTick {
|
||||
pub duration: Option<Duration>,
|
||||
pub elapsed: Option<Duration>,
|
||||
}
|
||||
|
||||
pub trait MusicClient {
|
||||
fn play(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
fn next(&self) -> Result<()>;
|
||||
fn prev(&self) -> Result<()>;
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||
fn seek(&self, duration: Duration) -> Result<()>;
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||
}
|
||||
|
||||
pub enum ClientType<'a> {
|
||||
Mpd { host: &'a str, music_dir: PathBuf },
|
||||
Mpris,
|
||||
}
|
||||
|
||||
pub async fn get_client(client_type: ClientType<'_>) -> Box<Arc<dyn MusicClient>> {
|
||||
match client_type {
|
||||
ClientType::Mpd { host, music_dir } => Box::new(
|
||||
mpd::get_client(host, music_dir)
|
||||
.await
|
||||
.expect("Failed to connect to MPD client"),
|
||||
),
|
||||
ClientType::Mpris => Box::new(mpris::get_client()),
|
||||
}
|
||||
}
|
||||
344
src/clients/music/mpd.rs
Normal file
344
src/clients/music/mpd.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use super::{
|
||||
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
||||
};
|
||||
use crate::{await_sync, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||
use mpd_client::commands::SeekMode;
|
||||
use mpd_client::protocol::MpdProtocolError;
|
||||
use mpd_client::responses::{PlayState, Song};
|
||||
use mpd_client::tag::Tag;
|
||||
use mpd_client::{commands, Client};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::{TcpStream, UnixStream};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
lazy_static! {
|
||||
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
music_dir: PathBuf,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MpdConnectionError {
|
||||
MaxRetries,
|
||||
ProtocolError(MpdProtocolError),
|
||||
}
|
||||
|
||||
impl Display for MpdConnectionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MaxRetries => write!(f, "Reached max retries"),
|
||||
Self::ProtocolError(e) => write!(f, "{e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MpdConnectionError {}
|
||||
|
||||
impl MpdClient {
|
||||
async fn new(host: &str, music_dir: PathBuf) -> Result<Self, MpdConnectionError> {
|
||||
debug!("Creating new MPD connection to {}", host);
|
||||
|
||||
let (client, mut state_changes) =
|
||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||
|
||||
let (tx, rx) = broadcast::channel(16);
|
||||
|
||||
{
|
||||
let music_dir = music_dir.clone();
|
||||
let tx = tx.clone();
|
||||
let client = client.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(change) = state_changes.next().await {
|
||||
debug!("Received state change: {:?}", change);
|
||||
|
||||
if let ConnectionEvent::SubsystemChange(
|
||||
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||
) = change
|
||||
{
|
||||
Self::send_update(&client, &tx, &music_dir)
|
||||
.await
|
||||
.expect("Failed to send update");
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn(async move {
|
||||
loop {
|
||||
Self::send_tick_update(&client, &tx).await;
|
||||
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
music_dir,
|
||||
tx,
|
||||
_rx: rx,
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_update(
|
||||
client: &Client,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
music_dir: &Path,
|
||||
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
let current_song = client.command(commands::CurrentSong).await;
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let (Ok(current_song), Ok(status)) = (current_song, status) {
|
||||
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
||||
let status = Status::from(status);
|
||||
|
||||
let update = PlayerUpdate::Update(Box::new(track), status);
|
||||
send!(tx, update);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let Ok(status) = status {
|
||||
if status.state == PlayState::Playing {
|
||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||
duration: status.duration,
|
||||
elapsed: status.elapsed,
|
||||
});
|
||||
|
||||
send!(tx, update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
!self.client.is_connection_closed()
|
||||
}
|
||||
|
||||
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
info!("Connection to MPD server lost");
|
||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_song(song: &Song, music_dir: &Path) -> Track {
|
||||
let (track, disc) = song.number();
|
||||
|
||||
let cover_path = music_dir
|
||||
.join(
|
||||
song.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok();
|
||||
|
||||
Track {
|
||||
title: song.title().map(std::string::ToString::to_string),
|
||||
album: song.album().map(std::string::ToString::to_string),
|
||||
artist: Some(song.artists().join(", ")),
|
||||
date: try_get_first_tag(song, &Tag::Date).map(std::string::ToString::to_string),
|
||||
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
|
||||
disc: Some(disc),
|
||||
track: Some(track),
|
||||
cover_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! async_command {
|
||||
($client:expr, $command:expr) => {
|
||||
await_sync(async {
|
||||
$client
|
||||
.command($command)
|
||||
.await
|
||||
.unwrap_or_else(|err| error!("Failed to send command: {err:?}"))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
impl MusicClient for MpdClient {
|
||||
fn play(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(false));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Previous);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
async_command!(self.client, commands::SetVolume(vol));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
await_sync(async {
|
||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||
.await
|
||||
.expect("Failed to send player update");
|
||||
});
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client(
|
||||
host: &str,
|
||||
music_dir: PathBuf,
|
||||
) -> Result<Arc<MpdClient>, MpdConnectionError> {
|
||||
let mut connections = CONNECTIONS.lock().await;
|
||||
match connections.get(host) {
|
||||
None => {
|
||||
let client = MpdClient::new(host, music_dir).await?;
|
||||
let client = Arc::new(client);
|
||||
connections.insert(host.to_string(), Arc::clone(&client));
|
||||
Ok(client)
|
||||
}
|
||||
Some(client) => {
|
||||
if client.is_connected() {
|
||||
Ok(Arc::clone(client))
|
||||
} else {
|
||||
client
|
||||
.send_disconnect_update()
|
||||
.expect("Failed to send disconnect update");
|
||||
|
||||
let client = MpdClient::new(host, music_dir).await?;
|
||||
let client = Arc::new(client);
|
||||
connections.insert(host.to_string(), Arc::clone(&client));
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_connection(
|
||||
host: &str,
|
||||
interval: Duration,
|
||||
max_retries: Option<usize>,
|
||||
) -> Result<Connection, MpdConnectionError> {
|
||||
let mut retries = 0;
|
||||
let max_retries = max_retries.unwrap_or(usize::MAX);
|
||||
|
||||
loop {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::MaxRetries);
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
|
||||
match try_get_mpd_conn(host).await {
|
||||
Ok(conn) => break Ok(conn),
|
||||
Err(err) => {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::ProtocolError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycles through each MPD host and
|
||||
/// returns the first one which connects,
|
||||
/// or none if there are none
|
||||
async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
if is_unix_socket(host) {
|
||||
connect_unix(host).await
|
||||
} else {
|
||||
connect_tcp(host).await
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unix_socket(host: &str) -> bool {
|
||||
let path = PathBuf::from(host);
|
||||
path.exists()
|
||||
&& path
|
||||
.metadata()
|
||||
.map_or(false, |metadata| metadata.file_type().is_socket())
|
||||
}
|
||||
|
||||
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = UnixStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = TcpStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
/// Attempts to read the first value for a tag
|
||||
/// (since the MPD client returns a vector of tags, or None)
|
||||
pub fn try_get_first_tag<'a>(song: &'a Song, tag: &'a Tag) -> Option<&'a str> {
|
||||
song.tags
|
||||
.get(tag)
|
||||
.and_then(|vec| vec.first().map(String::as_str))
|
||||
}
|
||||
|
||||
impl From<mpd_client::responses::Status> for Status {
|
||||
fn from(status: mpd_client::responses::Status) -> Self {
|
||||
Self {
|
||||
state: PlayerState::from(status.state),
|
||||
volume_percent: Some(status.volume),
|
||||
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
||||
playlist_length: status.playlist_length as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayState> for PlayerState {
|
||||
fn from(value: PlayState) -> Self {
|
||||
match value {
|
||||
PlayState::Stopped => Self::Stopped,
|
||||
PlayState::Playing => Self::Playing,
|
||||
PlayState::Paused => Self::Paused,
|
||||
}
|
||||
}
|
||||
}
|
||||
346
src/clients/music/mpris.rs
Normal file
346
src/clients/music/mpris.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
|
||||
use crate::clients::music::ProgressTick;
|
||||
use crate::{arc_mut, lock, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{cmp, string};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Client> = Arc::new(Client::new());
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
||||
let current_player = arc_mut!(None);
|
||||
|
||||
{
|
||||
let players_list = arc_mut!(HashSet::new());
|
||||
let current_player = current_player.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
|
||||
|
||||
// D-Bus gives no event for new players,
|
||||
// so we have to keep polling the player list
|
||||
loop {
|
||||
let players = player_finder
|
||||
.find_all()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
let mut players_list_val = lock!(players_list);
|
||||
for player in players {
|
||||
let identity = player.identity();
|
||||
|
||||
if !players_list_val.contains(identity) {
|
||||
debug!("Adding MPRIS player '{identity}'");
|
||||
players_list_val.insert(identity.to_string());
|
||||
|
||||
let status = player
|
||||
.get_playback_status()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
{
|
||||
let mut current_player = lock!(current_player);
|
||||
|
||||
if status == PlaybackStatus::Playing || current_player.is_none() {
|
||||
debug!("Setting active player to '{identity}'");
|
||||
|
||||
current_player.replace(identity.to_string());
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::listen_player_events(
|
||||
identity.to_string(),
|
||||
players_list.clone(),
|
||||
current_player.clone(),
|
||||
tx.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// wait 1 second before re-checking players
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let current_player = current_player.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new().expect("to get new player finder");
|
||||
|
||||
loop {
|
||||
Self::send_tick_update(&player_finder, ¤t_player, &tx);
|
||||
sleep(Duration::from_millis(TICK_INTERVAL_MS));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
current_player,
|
||||
tx,
|
||||
_rx: rx,
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_player_events(
|
||||
player_id: String,
|
||||
players: Arc<Mutex<HashSet<String>>>,
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
) {
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new()?;
|
||||
|
||||
if let Ok(player) = player_finder.find_by_name(&player_id) {
|
||||
let identity = player.identity();
|
||||
|
||||
for event in player.events()? {
|
||||
trace!("Received player event from '{identity}': {event:?}");
|
||||
match event {
|
||||
Ok(Event::PlayerShutDown) => {
|
||||
lock!(current_player).take();
|
||||
lock!(players).remove(identity);
|
||||
break;
|
||||
}
|
||||
Ok(Event::Playing) => {
|
||||
lock!(current_player).replace(identity.to_string());
|
||||
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
let current_player = lock!(current_player);
|
||||
let current_player = current_player.as_ref();
|
||||
if let Some(current_player) = current_player {
|
||||
if current_player == identity {
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), DBusError>(())
|
||||
});
|
||||
}
|
||||
|
||||
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
|
||||
debug!("Sending update using '{}'", player.identity());
|
||||
|
||||
let metadata = player.get_metadata()?;
|
||||
let playback_status = player
|
||||
.get_playback_status()
|
||||
.unwrap_or(PlaybackStatus::Stopped);
|
||||
|
||||
let track_list = player.get_track_list();
|
||||
|
||||
let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok();
|
||||
|
||||
let status = Status {
|
||||
// MRPIS doesn't seem to provide playlist info reliably,
|
||||
// so we can just assume next/prev will work by bodging the numbers
|
||||
playlist_position: 1,
|
||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
||||
state: PlayerState::from(playback_status),
|
||||
volume_percent,
|
||||
};
|
||||
|
||||
let track = Track::from(metadata);
|
||||
|
||||
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
|
||||
send!(tx, player_update);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_player(&self) -> Option<Player> {
|
||||
let player_name = lock!(self.current_player);
|
||||
let player_name = player_name.as_ref();
|
||||
|
||||
player_name.and_then(|player_name| {
|
||||
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
|
||||
player_finder.find_by_name(player_name).ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn send_tick_update(
|
||||
player_finder: &PlayerFinder,
|
||||
current_player: &Mutex<Option<String>>,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
) {
|
||||
if let Some(player) = lock!(current_player)
|
||||
.as_ref()
|
||||
.and_then(|name| player_finder.find_by_name(name).ok())
|
||||
{
|
||||
if let Ok(metadata) = player.get_metadata() {
|
||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||
elapsed: player.get_position().ok(),
|
||||
duration: metadata.length(),
|
||||
});
|
||||
|
||||
send!(tx, update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! command {
|
||||
($self:ident, $func:ident) => {
|
||||
if let Some(player) = Self::get_player($self) {
|
||||
player.$func()?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl MusicClient for Client {
|
||||
fn play(&self) -> Result<()> {
|
||||
command!(self, play);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<()> {
|
||||
command!(self, pause);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&self) -> Result<()> {
|
||||
command!(self, next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&self) -> Result<()> {
|
||||
command!(self, previous);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
if let Some(player) = Self::get_player(self) {
|
||||
player.set_volume(vol as f64 / 100.0)?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
if let Some(player) = Self::get_player(self) {
|
||||
let pos = player.get_position().unwrap_or_default();
|
||||
|
||||
let duration = duration.as_micros() as i64;
|
||||
let position = pos.as_micros() as i64;
|
||||
|
||||
let seek = cmp::max(duration, 0) - position;
|
||||
|
||||
player.seek(seek)?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
debug!("Creating new subscription");
|
||||
let rx = self.tx.subscribe();
|
||||
|
||||
if let Some(player) = self.get_player() {
|
||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else {
|
||||
let status = Status {
|
||||
playlist_position: 0,
|
||||
playlist_length: 0,
|
||||
state: PlayerState::Stopped,
|
||||
volume_percent: None,
|
||||
};
|
||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Client> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
impl From<Metadata> for Track {
|
||||
fn from(value: Metadata) -> Self {
|
||||
const KEY_DATE: &str = "xesam:contentCreated";
|
||||
const KEY_GENRE: &str = "xesam:genre";
|
||||
|
||||
Self {
|
||||
title: value
|
||||
.title()
|
||||
.map(std::string::ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
album: value
|
||||
.album_name()
|
||||
.map(std::string::ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
artist: value
|
||||
.artists()
|
||||
.map(|artists| artists.join(", "))
|
||||
.and_then(replace_empty_none),
|
||||
date: value
|
||||
.get(KEY_DATE)
|
||||
.and_then(mpris::MetadataValue::as_string)
|
||||
.map(std::string::ToString::to_string),
|
||||
disc: value.disc_number().map(|disc| disc as u64),
|
||||
genre: value
|
||||
.get(KEY_GENRE)
|
||||
.and_then(mpris::MetadataValue::as_str_array)
|
||||
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
||||
track: value.track_number().map(|track| track as u64),
|
||||
cover_path: value.art_url().map(string::ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlaybackStatus> for PlayerState {
|
||||
fn from(value: PlaybackStatus) -> Self {
|
||||
match value {
|
||||
PlaybackStatus::Playing => Self::Playing,
|
||||
PlaybackStatus::Paused => Self::Paused,
|
||||
PlaybackStatus::Stopped => Self::Stopped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_empty_none(string: String) -> Option<String> {
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
127
src/clients/system_tray.rs
Normal file
127
src/clients/system_tray.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{arc_mut, lock, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use system_tray::message::menu::TrayMenu;
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::StatusNotifierWatcher;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
|
||||
|
||||
pub struct TrayEventReceiver {
|
||||
tx: mpsc::Sender<NotifierItemCommand>,
|
||||
b_tx: broadcast::Sender<NotifierItemMessage>,
|
||||
_b_rx: broadcast::Receiver<NotifierItemMessage>,
|
||||
|
||||
tray: Arc<Mutex<Tray>>,
|
||||
}
|
||||
|
||||
impl TrayEventReceiver {
|
||||
async fn new() -> system_tray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", get_unique_usize());
|
||||
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let (b_tx, b_rx) = broadcast::channel(16);
|
||||
|
||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
||||
|
||||
let tray = arc_mut!(BTreeMap::new());
|
||||
|
||||
{
|
||||
let b_tx = b_tx.clone();
|
||||
let tray = tray.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Ok(message) = host.recv().await {
|
||||
trace!("Received message: {message:?} ");
|
||||
|
||||
send!(b_tx, message.clone());
|
||||
let mut tray = lock!(tray);
|
||||
match message {
|
||||
NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
} => {
|
||||
debug!("Adding item with address '{address}'");
|
||||
tray.insert(address, (item, menu));
|
||||
}
|
||||
NotifierItemMessage::Remove { address } => {
|
||||
debug!("Removing item with address '{address}'");
|
||||
tray.remove(&address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tx,
|
||||
b_tx,
|
||||
_b_rx: b_rx,
|
||||
tray,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subscribe(
|
||||
&self,
|
||||
) -> (
|
||||
mpsc::Sender<NotifierItemCommand>,
|
||||
broadcast::Receiver<NotifierItemMessage>,
|
||||
) {
|
||||
let tx = self.tx.clone();
|
||||
let b_rx = self.b_tx.subscribe();
|
||||
|
||||
let tray = lock!(self.tray).clone();
|
||||
for (address, (item, menu)) in tray {
|
||||
let update = NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
};
|
||||
send!(self.b_tx, update);
|
||||
}
|
||||
|
||||
(tx, b_rx)
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: AsyncOnce<TrayEventReceiver> = AsyncOnce::new(async {
|
||||
const MAX_RETRIES: i32 = 10;
|
||||
|
||||
// sometimes this can fail
|
||||
let mut retries = 0;
|
||||
|
||||
let value = loop {
|
||||
retries += 1;
|
||||
|
||||
let tray = Box::pin(TrayEventReceiver::new()).await;
|
||||
|
||||
match tray {
|
||||
Ok(tray) => break Some(tray),
|
||||
Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})")))
|
||||
}
|
||||
|
||||
if retries == MAX_RETRIES {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
value.expect("Failed to create StatusNotifierWatcher")
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_tray_event_client() -> &'static TrayEventReceiver {
|
||||
CLIENT.get().await
|
||||
}
|
||||
40
src/clients/upower.rs
Normal file
40
src/clients/upower.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use async_once::AsyncOnce;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use upower_dbus::UPowerProxy;
|
||||
use zbus::fdo::PropertiesProxy;
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||
let dbus = Box::pin(zbus::Connection::system())
|
||||
.await
|
||||
.expect("failed to create connection to system bus");
|
||||
|
||||
let device_proxy = UPowerProxy::new(&dbus)
|
||||
.await
|
||||
.expect("failed to create upower proxy");
|
||||
|
||||
let display_device = device_proxy
|
||||
.get_display_device()
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
|
||||
|
||||
let path = display_device.path().to_owned();
|
||||
|
||||
let proxy = PropertiesProxy::builder(&dbus)
|
||||
.destination("org.freedesktop.UPower")
|
||||
.expect("failed to set proxy destination address")
|
||||
.path(path)
|
||||
.expect("failed to set proxy path")
|
||||
.cache_properties(zbus::CacheProperties::No)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to build proxy");
|
||||
|
||||
Arc::new(proxy)
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
|
||||
DISPLAY_PROXY.get().await
|
||||
}
|
||||
276
src/clients/wayland/client.rs
Normal file
276
src/clients/wayland/client.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use super::wlr_foreign_toplevel::handle::ToplevelHandle;
|
||||
use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use super::wlr_foreign_toplevel::ToplevelEvent;
|
||||
use super::Environment;
|
||||
use crate::cached_broadcast::CachedBroadcastChannel;
|
||||
use crate::error::ERR_CHANNEL_RECV;
|
||||
use crate::{cached_broadcast, send};
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Report;
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
use smithay_client_toolkit::registry::RegistryState;
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::globals::registry_queue_init;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, WaylandSource};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
use super::ClipboardItem;
|
||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use std::sync::Arc;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
/// Sends a request for all the seats.
|
||||
/// These are then sent on the `seat` channel.
|
||||
Seats,
|
||||
/// Sends a request for all the toplevels.
|
||||
/// These are then sent on the `toplevel_init` channel.
|
||||
Toplevels,
|
||||
/// Sends a request for the current clipboard item.
|
||||
/// This is then sent on the `clipboard_init` channel.
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard,
|
||||
/// Copies the value to the clipboard
|
||||
#[cfg(feature = "clipboard")]
|
||||
CopyToClipboard(Arc<ClipboardItem>),
|
||||
/// Forces a dispatch, flushing any currently queued events
|
||||
Roundtrip,
|
||||
}
|
||||
|
||||
pub struct WaylandClient {
|
||||
// External channels
|
||||
output_channel: CachedBroadcastChannel<OutputInfo>,
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
|
||||
// Internal channels
|
||||
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
|
||||
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
|
||||
|
||||
request_tx: Sender<Request>,
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(super) fn new() -> Self {
|
||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||
|
||||
let mut output_channel = CachedBroadcastChannel::new(8);
|
||||
let output_tx = output_channel.sender();
|
||||
|
||||
let tx2 = output_tx.clone();
|
||||
|
||||
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
||||
#[cfg(feature = "clipboard")]
|
||||
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
|
||||
let (seat_tx, seat_rx) = mpsc::channel();
|
||||
|
||||
let toplevel_tx2 = toplevel_tx.clone();
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
|
||||
let clipboard_tx2 = clipboard_tx.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let (ev_tx, ev_rx) = channel::<Request>();
|
||||
|
||||
// `queue` is not `Send` so we need to handle everything inside the task
|
||||
spawn_blocking(move || {
|
||||
let toplevel_tx = toplevel_tx2;
|
||||
#[cfg(feature = "clipboard")]
|
||||
let clipboard_tx = clipboard_tx2;
|
||||
|
||||
let conn =
|
||||
Connection::connect_to_env().expect("Failed to connect to Wayland compositor");
|
||||
|
||||
let (globals, queue) =
|
||||
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
|
||||
|
||||
let qh = queue.handle();
|
||||
let mut event_loop =
|
||||
EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
|
||||
|
||||
WaylandSource::new(queue)
|
||||
.expect("Failed to create Wayland source from queue")
|
||||
.insert(event_loop.handle())
|
||||
.expect("Failed to insert Wayland event queue into event loop");
|
||||
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
// Initialize the registry handling
|
||||
// so other parts of Smithay's client toolkit may bind globals.
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
|
||||
let output_delegate = OutputState::new(&globals, &qh);
|
||||
let seat_delegate = SeatState::new(&globals, &qh);
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
let data_control_device_manager_delegate =
|
||||
DataControlDeviceManagerState::bind(&globals, &qh)
|
||||
.expect("data device manager is not available");
|
||||
|
||||
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
|
||||
.expect("foreign toplevel manager is not available");
|
||||
|
||||
let mut env = Environment {
|
||||
registry_state,
|
||||
output_state: output_delegate,
|
||||
seat_state: seat_delegate,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_device_manager_state: data_control_device_manager_delegate,
|
||||
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
|
||||
seats: vec![],
|
||||
handles: HashMap::new(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: crate::arc_mut!(None),
|
||||
output_tx,
|
||||
toplevel_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_devices: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
selection_offers: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
copy_paste_sources: vec![],
|
||||
loop_handle: event_loop.handle(),
|
||||
};
|
||||
|
||||
loop_handle
|
||||
.insert_source(ev_rx, move |event, _metadata, env| {
|
||||
trace!("{event:?}");
|
||||
match event {
|
||||
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
|
||||
Event::Msg(Request::Seats) => {
|
||||
trace!("Receive get seats request");
|
||||
send!(seat_tx, env.seats.clone());
|
||||
}
|
||||
Event::Msg(Request::Toplevels) => {
|
||||
trace!("Receive get toplevels request");
|
||||
send!(toplevel_init_tx, env.handles.clone());
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::Clipboard) => {
|
||||
trace!("Receive get clipboard requests");
|
||||
let clipboard = lock!(env.clipboard).clone();
|
||||
send!(clipboard_init_tx, clipboard);
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::CopyToClipboard(value)) => {
|
||||
env.copy_to_clipboard(value, &qh);
|
||||
}
|
||||
Event::Closed => panic!("Channel unexpectedly closed"),
|
||||
}
|
||||
})
|
||||
.expect("Failed to insert channel into event queue");
|
||||
|
||||
loop {
|
||||
trace!("Dispatching event loop");
|
||||
if let Err(err) = event_loop.dispatch(None, &mut env) {
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
output_channel,
|
||||
toplevel_tx,
|
||||
_toplevel_rx: toplevel_rx,
|
||||
toplevel_init_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx,
|
||||
seat_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
_clipboard_rx: clipboard_rx,
|
||||
request_tx: ev_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_toplevels(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<ToplevelEvent>,
|
||||
HashMap<usize, ToplevelHandle>,
|
||||
) {
|
||||
let rx = self.toplevel_tx.subscribe();
|
||||
|
||||
let receiver = &self.toplevel_init_rx;
|
||||
send!(self.request_tx, Request::Toplevels);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn subscribe_clipboard(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
Option<Arc<ClipboardItem>>,
|
||||
) {
|
||||
let rx = self.clipboard_tx.subscribe();
|
||||
|
||||
let receiver = &self.clipboard_init_rx;
|
||||
send!(self.request_tx, Request::Clipboard);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
pub fn subscribe_outputs(&mut self) -> cached_broadcast::Receiver<OutputInfo> {
|
||||
self.output_channel.receiver()
|
||||
}
|
||||
|
||||
/// Force a roundtrip on the wayland connection,
|
||||
/// flushing any queued events and immediately receiving any new ones.
|
||||
pub fn roundtrip(&self) {
|
||||
trace!("Sending roundtrip request");
|
||||
send!(self.request_tx, Request::Roundtrip);
|
||||
}
|
||||
|
||||
/// Gets a list of all outputs.
|
||||
///
|
||||
/// This should only be used in a scenario
|
||||
/// where you need a snapshot of outputs at the current time.
|
||||
/// Prefer to listen to output events with `subscribe_output` where possible.
|
||||
pub fn get_outputs(&self) -> &Vec<OutputInfo> {
|
||||
self.output_channel.data()
|
||||
}
|
||||
|
||||
pub fn get_seats(&self) -> Vec<WlSeat> {
|
||||
trace!("Sending get seats request");
|
||||
|
||||
send!(self.request_tx, Request::Seats);
|
||||
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
||||
send!(self.request_tx, Request::CopyToClipboard(item));
|
||||
}
|
||||
}
|
||||
101
src/clients/wayland/macros.rs
Normal file
101
src/clients/wayland/macros.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
/// It is necessary to store macros in a separate file due to a compilation error.
|
||||
/// I believe this stems from the feature flags.
|
||||
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
|
||||
|
||||
// --- Data Control Device --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_offer {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_source {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Foreign Toplevel --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_handle {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
114
src/clients/wayland/mod.rs
Normal file
114
src/clients/wayland/mod.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
mod client;
|
||||
|
||||
mod macros;
|
||||
mod wl_output;
|
||||
mod wl_seat;
|
||||
mod wlr_foreign_toplevel;
|
||||
|
||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use crate::{arc_mut, cached_broadcast, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||
use cfg_if::cfg_if;
|
||||
use lazy_static::lazy_static;
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
||||
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use smithay_client_toolkit::{
|
||||
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
|
||||
pub use self::client::WaylandClient;
|
||||
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
mod wlr_data_control;
|
||||
|
||||
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
|
||||
use self::wlr_data_control::device::DataControlDevice;
|
||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use self::wlr_data_control::source::CopyPasteSource;
|
||||
use self::wlr_data_control::SelectionOfferItem;
|
||||
|
||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||
|
||||
pub struct DataControlDeviceEntry {
|
||||
seat: WlSeat,
|
||||
device: DataControlDevice,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Environment {
|
||||
pub registry_state: RegistryState,
|
||||
pub output_state: OutputState,
|
||||
pub seat_state: SeatState,
|
||||
pub foreign_toplevel_manager_state: ToplevelManagerState,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_device_manager_state: DataControlDeviceManagerState,
|
||||
pub loop_handle: LoopHandle<'static, Self>,
|
||||
|
||||
pub seats: Vec<WlSeat>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_devices: Vec<DataControlDeviceEntry>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub selection_offers: Vec<SelectionOfferItem>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub copy_paste_sources: Vec<CopyPasteSource>,
|
||||
|
||||
pub handles: HashMap<usize, ToplevelHandle>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
|
||||
|
||||
output_tx: cached_broadcast::Sender<OutputInfo>,
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
}
|
||||
|
||||
// Now we need to say we are delegating the responsibility of output related events for our application data
|
||||
// type to the requisite delegate.
|
||||
delegate_output!(Environment);
|
||||
delegate_seat!(Environment);
|
||||
|
||||
delegate_foreign_toplevel_manager!(Environment);
|
||||
delegate_foreign_toplevel_handle!(Environment);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
delegate_data_control_device_manager!(Environment);
|
||||
delegate_data_control_device!(Environment);
|
||||
delegate_data_control_source!(Environment);
|
||||
delegate_data_control_offer!(Environment);
|
||||
}
|
||||
}
|
||||
|
||||
// In order for our delegate to know of the existence of globals, we need to implement registry
|
||||
// handling for the program. This trait will forward events to the RegistryHandler trait
|
||||
// implementations.
|
||||
delegate_registry!(Environment);
|
||||
|
||||
// In order for delegate_registry to work, our application data type needs to provide a way for the
|
||||
// implementation to access the registry state.
|
||||
//
|
||||
// We also need to indicate which delegates will get told about globals being created. We specify
|
||||
// the types of the delegates inside the array.
|
||||
impl ProvidesRegistryState for Environment {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![OutputState, SeatState];
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
72
src/clients/wayland/wl_output.rs
Normal file
72
src/clients/wayland/wl_output.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use super::Environment;
|
||||
use crate::cached_broadcast::Cacheable;
|
||||
use crate::{cached_broadcast, try_send};
|
||||
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_output;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl Environment {
|
||||
pub fn output_info(&mut self) -> Vec<OutputInfo> {
|
||||
self.output_state
|
||||
.outputs()
|
||||
.filter_map(|output| self.output_state.info(&output))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
|
||||
// output and to provide an instance of the output state to the delegate when dispatching events.
|
||||
impl OutputHandler for Environment {
|
||||
// First we need to provide a way to access the delegate.
|
||||
//
|
||||
// This is needed because delegate implementations for handling events use the application data type in
|
||||
// their function signatures. This allows the implementation to access an instance of the type.
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
// Then there exist these functions that indicate the lifecycle of an output.
|
||||
// These will be called as appropriate by the delegate implementation.
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handler received new output");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(self.output_tx, cached_broadcast::Event::Add(info));
|
||||
};
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handle received output update");
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handle received output destruction");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(self.output_tx, cached_broadcast::Event::Remove(info.id));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Cacheable for OutputInfo {
|
||||
type Key = u32;
|
||||
|
||||
fn get_key(&self) -> Self::Key {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
63
src/clients/wayland/wl_seat.rs
Normal file
63
src/clients/wayland/wl_seat.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::Environment;
|
||||
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_seat;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl SeatHandler for Environment {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received new seat");
|
||||
self.seats.push(seat);
|
||||
}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received new capability");
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
if !self
|
||||
.data_control_devices
|
||||
.iter_mut()
|
||||
.any(|entry| entry.seat == seat)
|
||||
{
|
||||
debug!("Adding new data control device");
|
||||
// create the data device here for this seat
|
||||
let data_control_device_manager = &self.data_control_device_manager_state;
|
||||
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
|
||||
self.data_control_devices
|
||||
.push(super::DataControlDeviceEntry {
|
||||
seat: seat.clone(),
|
||||
device: data_control_device,
|
||||
});
|
||||
}
|
||||
|
||||
if !self.seats.iter().any(|s| s == &seat) {
|
||||
self.seats.push(seat);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received capability removal");
|
||||
// Not applicable
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received seat removal");
|
||||
self.seats.retain(|s| s != &seat);
|
||||
}
|
||||
}
|
||||
166
src/clients/wayland/wlr_data_control/device.rs
Normal file
166
src/clients/wayland/wlr_data_control/device.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use super::offer::{
|
||||
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
|
||||
};
|
||||
use crate::error::ERR_WAYLAND_DATA;
|
||||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::warn;
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
||||
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
||||
};
|
||||
|
||||
pub struct DataControlDevice {
|
||||
pub device: ZwlrDataControlDeviceV1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceInner {
|
||||
/// the active selection offer and its data
|
||||
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
|
||||
/// the active undetermined offers and their data
|
||||
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceData {
|
||||
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
|
||||
}
|
||||
|
||||
pub trait DataControlDeviceDataExt: Send + Sync {
|
||||
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
|
||||
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData;
|
||||
|
||||
fn selection_mime_types(&self) -> Vec<String> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.map(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.mime_types()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the active selection offer if it exists.
|
||||
fn selection_offer(&self) -> Option<SelectionOffer> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.and_then(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.as_selection_offer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDevice {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self.device.data().expect(ERR_WAYLAND_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDeviceData {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataDevice` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
|
||||
pub trait DataControlDeviceHandler: Sized {
|
||||
/// Advertises a new selection.
|
||||
fn selection(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U>
|
||||
+ Dispatch<ZwlrDataControlOfferV1, V>
|
||||
+ DataControlDeviceHandler
|
||||
+ DataControlOfferHandler
|
||||
+ 'static,
|
||||
U: DataControlDeviceDataExt,
|
||||
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrDataControlDeviceV1, [
|
||||
0 => (ZwlrDataControlOfferV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
data_device: &ZwlrDataControlDeviceV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_device_data();
|
||||
let inner = lock!(data.inner);
|
||||
|
||||
match event {
|
||||
Event::DataOffer { id } => {
|
||||
// XXX Drop done here to prevent Mutex deadlocks.S
|
||||
|
||||
lock!(inner.undetermined_offers).push(id.clone());
|
||||
let data = id
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.init_undetermined_offer(&id);
|
||||
|
||||
// Append the data offer to our list of offers.
|
||||
drop(inner);
|
||||
}
|
||||
Event::Selection { id } => {
|
||||
let mut selection_offer = lock!(inner.selection_offer);
|
||||
|
||||
if let Some(offer) = id {
|
||||
let mut undetermined = lock!(inner.undetermined_offers);
|
||||
if let Some(i) = undetermined.iter().position(|o| o == &offer) {
|
||||
undetermined.remove(i);
|
||||
}
|
||||
drop(undetermined);
|
||||
|
||||
let data = offer
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.to_selection_offer();
|
||||
// XXX Drop done here to prevent Mutex deadlocks.
|
||||
*selection_offer = Some(offer.clone());
|
||||
drop(selection_offer);
|
||||
drop(inner);
|
||||
state.selection(
|
||||
conn,
|
||||
qh,
|
||||
DataControlDevice {
|
||||
device: data_device.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
*selection_offer = None;
|
||||
}
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/clients/wayland/wlr_data_control/manager.rs
Normal file
132
src/clients/wayland/wlr_data_control/manager.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
|
||||
use super::offer::DataControlOfferData;
|
||||
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::debug;
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
|
||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
|
||||
manager: ZwlrDataControlManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl DataControlDeviceManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let manager = globals.bind(qh, 1..=2, GlobalData)?;
|
||||
debug!("Bound to ZwlDataControlManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// creates a data source for copy paste
|
||||
pub fn create_copy_paste_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> CopyPasteSource
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
CopyPasteSource {
|
||||
inner: self.create_data_control_source(qh, mime_types),
|
||||
}
|
||||
}
|
||||
|
||||
/// creates a data source
|
||||
fn create_data_control_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
let source =
|
||||
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
|
||||
|
||||
for mime in mime_types {
|
||||
source.offer(mime.to_string());
|
||||
}
|
||||
|
||||
source
|
||||
}
|
||||
|
||||
/// create a new data source for a given seat with some user data
|
||||
pub fn create_data_control_source_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
data: U,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
|
||||
U: DataControlSourceDataExt + 'static,
|
||||
{
|
||||
self.manager.create_data_source(qh, data)
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat
|
||||
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
|
||||
{
|
||||
DataControlDevice {
|
||||
device: self.get_data_control_device_with_data(
|
||||
qh,
|
||||
seat,
|
||||
DataControlDeviceData::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat with some user data
|
||||
pub fn get_data_control_device_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
seat: &WlSeat,
|
||||
data: U,
|
||||
) -> ZwlrDataControlDeviceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
|
||||
U: DataControlDeviceDataExt + 'static,
|
||||
{
|
||||
self.manager.get_data_device(seat, qh, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>,
|
||||
{
|
||||
fn event(
|
||||
_state: &mut D,
|
||||
_proxy: &ZwlrDataControlManagerV1,
|
||||
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
|
||||
_data: &GlobalData,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
375
src/clients/wayland/wlr_data_control/mod.rs
Normal file
375
src/clients/wayland/wlr_data_control/mod.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
pub mod device;
|
||||
pub mod manager;
|
||||
pub mod offer;
|
||||
pub mod source;
|
||||
|
||||
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||
use self::source::DataControlSourceHandler;
|
||||
use crate::clients::wayland::Environment;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{lock, send};
|
||||
use device::DataControlDevice;
|
||||
use glib::Bytes;
|
||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
||||
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||
use std::cmp::min;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
use std::sync::Arc;
|
||||
use std::{fs, io};
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
||||
|
||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||
|
||||
pub struct SelectionOfferItem {
|
||||
offer: SelectionOffer,
|
||||
token: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct ClipboardItem {
|
||||
pub id: usize,
|
||||
pub value: ClipboardValue,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for ClipboardItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardValue {
|
||||
Text(String),
|
||||
Image(Bytes),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Debug for ClipboardValue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Text(text) => text.clone(),
|
||||
Self::Image(bytes) => {
|
||||
format!("[{} Bytes]", bytes.len())
|
||||
}
|
||||
Self::Other => "[Unknown]".to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MimeType {
|
||||
value: String,
|
||||
category: MimeTypeCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MimeTypeCategory {
|
||||
Text,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
fn parse(mime_type: &str) -> Option<Self> {
|
||||
match mime_type.to_lowercase().as_str() {
|
||||
"text"
|
||||
| "string"
|
||||
| "utf8_string"
|
||||
| "text/plain"
|
||||
| "text/plain;charset=utf-8"
|
||||
| "text/plain;charset=iso-8859-1"
|
||||
| "text/plain;charset=us-ascii"
|
||||
| "text/plain;charset=unicode" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Text,
|
||||
}),
|
||||
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
||||
| "image/x-bmp" | "image/icon" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Image,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
|
||||
mime_types.iter().find_map(|mime| Self::parse(mime))
|
||||
}
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
|
||||
debug!("Copying item to clipboard: {item:?}");
|
||||
|
||||
// TODO: Proper device tracking
|
||||
let device = self.data_control_devices.first();
|
||||
if let Some(device) = device {
|
||||
let source = self
|
||||
.data_control_device_manager_state
|
||||
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
|
||||
|
||||
source.set_selection(&device.device);
|
||||
self.copy_paste_sources.push(source);
|
||||
|
||||
lock!(self.clipboard).replace(item);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||
let value = match mime_type.category {
|
||||
MimeTypeCategory::Text => {
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)?;
|
||||
|
||||
ClipboardValue::Text(txt)
|
||||
}
|
||||
MimeTypeCategory::Image => {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)?;
|
||||
let bytes = Bytes::from(&bytes);
|
||||
|
||||
ClipboardValue::Image(bytes)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ClipboardItem {
|
||||
id: get_unique_usize(),
|
||||
value,
|
||||
mime_type: mime_type.value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandler for Environment {
|
||||
fn selection(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
) {
|
||||
debug!("Handler received selection event");
|
||||
|
||||
let mime_types = data_device.selection_mime_types();
|
||||
|
||||
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(offer) = data_device.selection_offer() {
|
||||
self.selection_offers
|
||||
.push(SelectionOfferItem { offer, token: None });
|
||||
|
||||
let cur_offer = self
|
||||
.selection_offers
|
||||
.last_mut()
|
||||
.expect("Failed to get current offer");
|
||||
|
||||
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
||||
lock!(self.clipboard).take();
|
||||
// send an event so the clipboard module is aware it's changed
|
||||
send!(
|
||||
self.clipboard_tx,
|
||||
Arc::new(ClipboardItem {
|
||||
id: usize::MAX,
|
||||
mime_type: String::new(),
|
||||
value: ClipboardValue::Other
|
||||
})
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
||||
let offer_clone = cur_offer.offer.clone();
|
||||
|
||||
let tx = self.clipboard_tx.clone();
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
let token = self
|
||||
.loop_handle
|
||||
.insert_source(read_pipe, move |_, file, state| {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
|
||||
match Self::read_file(&mime_type, file) {
|
||||
Ok(item) => {
|
||||
let item = Arc::new(item);
|
||||
lock!(clipboard).replace(item.clone());
|
||||
send!(tx, item);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
});
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
cur_offer.token.replace(token);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlOfferHandler for Environment {
|
||||
fn offer(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_offer: &mut DataControlDeviceOffer,
|
||||
_mime_type: String,
|
||||
) {
|
||||
trace!("Handler received offer");
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlSourceHandler for Environment {
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
) {
|
||||
debug!("Accepted mime type: {mime:?}");
|
||||
}
|
||||
|
||||
fn send_request(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
debug!("Handler received source send request event ({mime})");
|
||||
|
||||
if let Some(item) = lock!(self.clipboard).clone() {
|
||||
let fd = OwnedFd::from(write_pipe);
|
||||
if self
|
||||
.copy_paste_sources
|
||||
.iter_mut()
|
||||
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
|
||||
{
|
||||
trace!("Source found, writing to file");
|
||||
|
||||
let mut bytes = match &item.value {
|
||||
ClipboardValue::Text(text) => text.as_bytes(),
|
||||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||
ClipboardValue::Other => panic!(
|
||||
"{:?}",
|
||||
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",)
|
||||
),
|
||||
};
|
||||
|
||||
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
|
||||
.expect("Failed to increase pipe size");
|
||||
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
|
||||
|
||||
trace!("Num bytes: {}", bytes.len());
|
||||
|
||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
|
||||
let epoll_fd =
|
||||
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
|
||||
epoll_fd
|
||||
.add(fd, epoll_event)
|
||||
.expect("to send valid epoll operation");
|
||||
|
||||
while !bytes.is_empty() {
|
||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||
|
||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
||||
|
||||
epoll_fd
|
||||
.wait(&mut events, 100)
|
||||
.expect("Failed to wait to epoll");
|
||||
|
||||
match file.write(chunk) {
|
||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to find source");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
) {
|
||||
debug!("Handler received source cancelled event");
|
||||
|
||||
self.copy_paste_sources
|
||||
.iter()
|
||||
.position(|s| s.inner() == source)
|
||||
.map(|pos| self.copy_paste_sources.remove(pos));
|
||||
source.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to increase the fd pipe size to the requested number of bytes.
|
||||
/// The kernel will automatically round this up to the nearest page size.
|
||||
/// If the requested size is larger than the kernel max (normally 1MB),
|
||||
/// it will be clamped at this.
|
||||
///
|
||||
/// Returns the new size if succeeded
|
||||
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||
// clamp size at kernel max
|
||||
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
||||
.expect("Failed to find pipe-max-size virtual kernel file")
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.expect("Failed to parse pipe-max-size contents");
|
||||
|
||||
let size = min(size, max_pipe_size);
|
||||
|
||||
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
|
||||
|
||||
trace!("Current pipe size: {curr_size}");
|
||||
|
||||
let new_size = if size > curr_size {
|
||||
trace!("Requesting pipe size increase to (at least): {size}");
|
||||
|
||||
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
|
||||
trace!("New pipe size: {res}");
|
||||
|
||||
if res < size as i32 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
size as i32
|
||||
};
|
||||
|
||||
Ok(new_size)
|
||||
}
|
||||
183
src/clients/wayland/wlr_data_control/offer.rs
Normal file
183
src/clients/wayland/wlr_data_control/offer.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::unistd::{close, pipe2};
|
||||
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
|
||||
use smithay_client_toolkit::data_device_manager::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{trace, warn};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
Event, ZwlrDataControlOfferV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UndeterminedOffer {
|
||||
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>,
|
||||
}
|
||||
|
||||
impl PartialEq for UndeterminedOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionOffer {
|
||||
pub data_offer: ZwlrDataControlOfferV1,
|
||||
}
|
||||
|
||||
impl PartialEq for SelectionOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionOffer {
|
||||
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
|
||||
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DataControlDeviceOffer {
|
||||
Selection(SelectionOffer),
|
||||
Undetermined(UndeterminedOffer),
|
||||
}
|
||||
|
||||
impl Default for DataControlDeviceOffer {
|
||||
fn default() -> Self {
|
||||
Self::Undetermined(UndeterminedOffer { data_offer: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlOfferData {
|
||||
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceOfferInner {
|
||||
pub(crate) offer: DataControlDeviceOffer,
|
||||
pub(crate) mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl DataControlOfferData {
|
||||
pub(crate) fn push_mime_type(&self, mime_type: String) {
|
||||
lock!(self.inner).mime_types.push(mime_type);
|
||||
}
|
||||
|
||||
pub(crate) fn to_selection_offer(&self) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
|
||||
data_offer: o.data_offer.clone().expect("Missing current data offer"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {
|
||||
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
|
||||
data_offer: Some(offer.clone()),
|
||||
});
|
||||
}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
o.data_offer = Some(offer.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataControlOfferDataExt {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData;
|
||||
fn mime_types(&self) -> Vec<String>;
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer>;
|
||||
}
|
||||
|
||||
impl DataControlOfferDataExt for DataControlOfferData {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData {
|
||||
self
|
||||
}
|
||||
|
||||
fn mime_types(&self) -> Vec<String> {
|
||||
lock!(self.inner).mime_types.clone()
|
||||
}
|
||||
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer> {
|
||||
match &lock!(self.inner).offer {
|
||||
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
|
||||
DataControlDeviceOffer::Undetermined(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataOffer` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
|
||||
pub trait DataControlOfferHandler: Sized {
|
||||
// Called for each mime type the data offer advertises.
|
||||
fn offer(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
offer: &mut DataControlDeviceOffer,
|
||||
mime_type: String,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
|
||||
U: DataControlOfferDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
_offer: &ZwlrDataControlOfferV1,
|
||||
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_offer_data();
|
||||
|
||||
if let Event::Offer { mime_type } = event {
|
||||
trace!("Adding new offer with type '{mime_type}'");
|
||||
data.push_mime_type(mime_type.clone());
|
||||
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to receive the data of a given mime type.
|
||||
///
|
||||
/// You can do this several times, as a reaction to motion of
|
||||
/// the dnd cursor, or to inspect the data in order to choose your
|
||||
/// response.
|
||||
///
|
||||
/// Note that you should *not* read the contents right away in a
|
||||
/// blocking way, as you may deadlock your application doing so.
|
||||
/// At least make sure you flush your events to the server before
|
||||
/// doing so.
|
||||
///
|
||||
/// Fails if too many file descriptors were already open and a pipe
|
||||
/// could not be created.
|
||||
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
|
||||
offer.receive(mime_type, writefd);
|
||||
|
||||
if let Err(err) = close(writefd) {
|
||||
warn!("Failed to close write pipe: {}", err);
|
||||
}
|
||||
|
||||
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||
}
|
||||
101
src/clients/wayland/wlr_data_control/source.rs
Normal file
101
src/clients/wayland/wlr_data_control/source.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use super::device::DataControlDevice;
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
|
||||
Event, ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlSourceData {}
|
||||
|
||||
pub trait DataControlSourceDataExt: Send + Sync {
|
||||
fn data_source_data(&self) -> &DataControlSourceData;
|
||||
}
|
||||
|
||||
impl DataControlSourceDataExt for DataControlSourceData {
|
||||
fn data_source_data(&self) -> &DataControlSourceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataSource` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
|
||||
pub trait DataControlSourceHandler: Sized {
|
||||
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
);
|
||||
|
||||
/// The client has requested the data for this source to be sent.
|
||||
/// Send the data, then close the fd.
|
||||
fn send_request(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
fd: WritePipe,
|
||||
);
|
||||
|
||||
/// The data source is no longer valid
|
||||
/// Cleanup & destroy this resource
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
|
||||
U: DataControlSourceDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
|
||||
_data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Send { mime_type, fd } => {
|
||||
state.send_request(conn, qh, source, mime_type, fd.into());
|
||||
}
|
||||
Event::Cancelled => {
|
||||
state.cancelled(conn, qh, source);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CopyPasteSource {
|
||||
pub(crate) inner: ZwlrDataControlSourceV1,
|
||||
}
|
||||
|
||||
impl CopyPasteSource {
|
||||
/// Set the selection of the provided data device as a response to the event with with provided serial.
|
||||
pub fn set_selection(&self, device: &DataControlDevice) {
|
||||
device.device.set_selection(Some(&self.inner));
|
||||
}
|
||||
|
||||
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CopyPasteSource {
|
||||
fn drop(&mut self) {
|
||||
self.inner.destroy();
|
||||
}
|
||||
}
|
||||
178
src/clients/wayland/wlr_foreign_toplevel/handle.rs
Normal file
178
src/clients/wayland/wlr_foreign_toplevel/handle.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use super::manager::ToplevelManagerState;
|
||||
use crate::lock;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::trace;
|
||||
use wayland_client::protocol::wl_output::WlOutput;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
|
||||
Event, ZwlrForeignToplevelHandleV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelHandle {
|
||||
pub handle: ZwlrForeignToplevelHandleV1,
|
||||
}
|
||||
|
||||
impl PartialEq for ToplevelHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.handle == other.handle
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelHandle {
|
||||
pub fn info(&self) -> Option<ToplevelInfo> {
|
||||
trace!("Retrieving handle info");
|
||||
|
||||
let data = self.handle.data::<ToplevelHandleData>()?;
|
||||
data.info()
|
||||
}
|
||||
|
||||
pub fn focus(&self, seat: &WlSeat) {
|
||||
trace!("Activating handle");
|
||||
self.handle.activate(seat);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleData {
|
||||
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
|
||||
}
|
||||
|
||||
impl ToplevelHandleData {
|
||||
fn info(&self) -> Option<ToplevelInfo> {
|
||||
lock!(self.inner).current_info.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleDataInner {
|
||||
initial_done: bool,
|
||||
output: Option<WlOutput>,
|
||||
|
||||
current_info: Option<ToplevelInfo>,
|
||||
pending_info: ToplevelInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelInfo {
|
||||
pub id: usize,
|
||||
pub app_id: String,
|
||||
pub title: String,
|
||||
pub fullscreen: bool,
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl Default for ToplevelInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: get_unique_usize(),
|
||||
app_id: String::new(),
|
||||
title: String::new(),
|
||||
fullscreen: false,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelHandleDataExt {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
|
||||
}
|
||||
|
||||
impl ToplevelHandleDataExt for ToplevelHandleData {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelHandleHandler: Sized {
|
||||
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
|
||||
U: ToplevelHandleDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
handle: &ZwlrForeignToplevelHandleV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
const STATE_ACTIVE: u32 = 2;
|
||||
const STATE_FULLSCREEN: u32 = 3;
|
||||
|
||||
let data = data.toplevel_handle_data();
|
||||
|
||||
trace!("Processing handle event: {event:?}");
|
||||
|
||||
match event {
|
||||
Event::Title { title } => {
|
||||
lock!(data.inner).pending_info.title = title;
|
||||
}
|
||||
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
|
||||
Event::State { state } => {
|
||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||
// the u32 then represents a value in the `State` enum.
|
||||
assert_eq!(state.len() % 4, 0);
|
||||
let state = (0..state.len() / 4)
|
||||
.map(|i| {
|
||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||
.try_into()
|
||||
.expect("Received invalid state length");
|
||||
u32::from_le_bytes(slice)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
|
||||
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||
}
|
||||
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
|
||||
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
|
||||
Event::Closed => state.remove_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
),
|
||||
Event::Done => {
|
||||
{
|
||||
let pending_info = lock!(data.inner).pending_info.clone();
|
||||
lock!(data.inner).current_info = Some(pending_info);
|
||||
}
|
||||
|
||||
if lock!(data.inner).initial_done {
|
||||
state.update_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
lock!(data.inner).initial_done = true;
|
||||
state.new_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
trace!("Event processed");
|
||||
}
|
||||
}
|
||||
86
src/clients/wayland/wlr_foreign_toplevel/manager.rs
Normal file
86
src/clients/wayland/wlr_foreign_toplevel/manager.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
|
||||
pub struct ToplevelManagerState<V = ToplevelHandleData> {
|
||||
manager: ZwlrForeignToplevelManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl ToplevelManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let manager = globals.bind(qh, 1..=3, GlobalData)?;
|
||||
debug!("Bound to ZwlForeignToplevelManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelManagerHandler: Sized {
|
||||
/// Advertises a new toplevel.
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
manager: ToplevelManagerState,
|
||||
);
|
||||
}
|
||||
|
||||
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
|
||||
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
|
||||
+ ToplevelManagerHandler
|
||||
+ ToplevelHandleHandler
|
||||
+ 'static,
|
||||
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
|
||||
0 => (ZwlrForeignToplevelHandleV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
toplevel_manager: &ZwlrForeignToplevelManagerV1,
|
||||
event: Event,
|
||||
_data: &GlobalData,
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Toplevel { toplevel: _ } => {
|
||||
state.toplevel(
|
||||
conn,
|
||||
qhandle,
|
||||
ToplevelManagerState {
|
||||
manager: toplevel_manager.clone(),
|
||||
_phantom: PhantomData,
|
||||
},
|
||||
);
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
84
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
pub mod handle;
|
||||
pub mod manager;
|
||||
|
||||
use self::handle::ToplevelHandleHandler;
|
||||
use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
|
||||
use crate::clients::wayland::Environment;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
use crate::send;
|
||||
pub use handle::{ToplevelHandle, ToplevelInfo};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelEvent {
|
||||
New(ToplevelHandle),
|
||||
Update(ToplevelHandle),
|
||||
Remove(ToplevelHandle),
|
||||
}
|
||||
|
||||
impl ToplevelManagerHandler for Environment {
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_manager: ToplevelManagerState,
|
||||
) {
|
||||
debug!("Manager received new handle");
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelHandleHandler for Environment {
|
||||
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
|
||||
trace!("Handler received new handle");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Adding new handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::New(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
trace!("Handler received handle update");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Updating handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
debug!("Handler received handle close");
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
self.handles.remove(&info.id);
|
||||
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use std::slice::{Iter, IterMut};
|
||||
|
||||
/// An ordered map.
|
||||
/// Internally this is just two vectors -
|
||||
/// one for keys and one for values.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Collection<TKey, TData> {
|
||||
keys: Vec<TKey>,
|
||||
values: Vec<TData>,
|
||||
}
|
||||
|
||||
impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
keys: vec![],
|
||||
values: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: TKey, value: TData) {
|
||||
self.keys.push(key);
|
||||
self.values.push(value);
|
||||
|
||||
assert_eq!(self.keys.len(), self.values.len());
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &TKey) -> Option<&TData> {
|
||||
let index = self.keys.iter().position(|k| k == key);
|
||||
match index {
|
||||
Some(index) => self.values.get(index),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
|
||||
let index = self.keys.iter().position(|k| k == key);
|
||||
match index {
|
||||
Some(index) => self.values.get_mut(index),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &TKey) -> Option<TData> {
|
||||
assert_eq!(self.keys.len(), self.values.len());
|
||||
|
||||
let index = self.keys.iter().position(|k| k == key);
|
||||
if let Some(index) = index {
|
||||
self.keys.remove(index);
|
||||
Some(self.values.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
}
|
||||
|
||||
pub fn first(&self) -> Option<&TData> {
|
||||
self.values.first()
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[TData] {
|
||||
self.values.as_slice()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, TData> {
|
||||
self.values.iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
|
||||
self.values.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TKey: PartialEq, TData> From<(TKey, TData)> for Collection<TKey, TData> {
|
||||
fn from((key, value): (TKey, TData)) -> Self {
|
||||
let mut collection = Self::new();
|
||||
collection.insert(key, value);
|
||||
collection
|
||||
}
|
||||
}
|
||||
|
||||
impl<TKey: PartialEq, TData> FromIterator<(TKey, TData)> for Collection<TKey, TData> {
|
||||
fn from_iter<T: IntoIterator<Item = (TKey, TData)>>(iter: T) -> Self {
|
||||
let mut collection = Self::new();
|
||||
for (key, value) in iter {
|
||||
collection.insert(key, value);
|
||||
}
|
||||
|
||||
collection
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, TKey: PartialEq, TData> IntoIterator for &'a Collection<TKey, TData> {
|
||||
type Item = &'a TData;
|
||||
type IntoIter = CollectionIntoIterator<'a, TKey, TData>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CollectionIntoIterator {
|
||||
collection: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CollectionIntoIterator<'a, TKey, TData> {
|
||||
collection: &'a Collection<TKey, TData>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, TKey: PartialEq, TData> Iterator for CollectionIntoIterator<'a, TKey, TData> {
|
||||
type Item = &'a TData;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let res = self.collection.values.get(self.index);
|
||||
self.index += 1;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<TKey: PartialEq, TData> Default for Collection<TKey, TData> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
143
src/config.rs
143
src/config.rs
@@ -1,143 +0,0 @@
|
||||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
use crate::modules::mpd::MpdModule;
|
||||
use crate::modules::script::ScriptModule;
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
use crate::modules::tray::TrayModule;
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use color_eyre::eyre::{Context, ContextCompat};
|
||||
use color_eyre::{eyre, Help, Report};
|
||||
use dirs::config_dir;
|
||||
use eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ModuleConfig {
|
||||
Clock(ClockModule),
|
||||
Mpd(MpdModule),
|
||||
Tray(TrayModule),
|
||||
Workspaces(WorkspacesModule),
|
||||
SysInfo(SysInfoModule),
|
||||
Launcher(LauncherModule),
|
||||
Script(ScriptModule),
|
||||
Focused(FocusedModule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum MonitorConfig {
|
||||
Single(Config),
|
||||
Multiple(Vec<Config>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BarPosition {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Default for BarPosition {
|
||||
fn default() -> Self {
|
||||
Self::Bottom
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_bar_position")]
|
||||
pub position: BarPosition,
|
||||
#[serde(default = "default_bar_height")]
|
||||
pub height: i32,
|
||||
|
||||
pub left: Option<Vec<ModuleConfig>>,
|
||||
pub center: Option<Vec<ModuleConfig>>,
|
||||
pub right: Option<Vec<ModuleConfig>>,
|
||||
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
}
|
||||
|
||||
const fn default_bar_position() -> BarPosition {
|
||||
BarPosition::Bottom
|
||||
}
|
||||
|
||||
const fn default_bar_height() -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
||||
let path = PathBuf::from(config_path);
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(Report::msg("Specified config file does not exist")
|
||||
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
||||
}
|
||||
} else {
|
||||
Self::try_find_config()
|
||||
}?;
|
||||
|
||||
Self::load_file(&config_path)
|
||||
}
|
||||
|
||||
fn try_find_config() -> Result<PathBuf> {
|
||||
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
||||
|
||||
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
|
||||
|
||||
let file = extensions.into_iter().find_map(|extension| {
|
||||
let full_path = config_dir
|
||||
.join("ironbar")
|
||||
.join(format!("config.{extension}"));
|
||||
|
||||
if Path::exists(&full_path) {
|
||||
Some(full_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match file {
|
||||
Some(file) => Ok(file),
|
||||
None => Err(Report::msg("Could not find config file")),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file(path: &Path) -> Result<Self> {
|
||||
let file = fs::read(path).wrap_err("Failed to read config file")?;
|
||||
let extension = path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
match extension {
|
||||
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
|
||||
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
|
||||
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
|
||||
"corn" => {
|
||||
// corn doesn't support deserialization yet
|
||||
// so serialize the interpreted result then deserialize that
|
||||
let file =
|
||||
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
|
||||
let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
|
||||
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
pub const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
151
src/config/common.rs
Normal file
151
src/config/common.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
/// Common configuration options
|
||||
/// which can be set on every module.
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
pub struct CommonConfig {
|
||||
pub class: Option<String>,
|
||||
pub name: Option<String>,
|
||||
|
||||
pub show_if: Option<DynamicBool>,
|
||||
pub transition_type: Option<TransitionType>,
|
||||
pub transition_duration: Option<u32>,
|
||||
|
||||
pub on_click_left: Option<ScriptInput>,
|
||||
pub on_click_right: Option<ScriptInput>,
|
||||
pub on_click_middle: Option<ScriptInput>,
|
||||
pub on_scroll_up: Option<ScriptInput>,
|
||||
pub on_scroll_down: Option<ScriptInput>,
|
||||
pub on_mouse_enter: Option<ScriptInput>,
|
||||
pub on_mouse_exit: Option<ScriptInput>,
|
||||
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TransitionType {
|
||||
None,
|
||||
Crossfade,
|
||||
SlideStart,
|
||||
SlideEnd,
|
||||
}
|
||||
|
||||
impl TransitionType {
|
||||
pub const fn to_revealer_transition_type(
|
||||
&self,
|
||||
orientation: Orientation,
|
||||
) -> RevealerTransitionType {
|
||||
match (self, orientation) {
|
||||
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft,
|
||||
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
||||
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight,
|
||||
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
||||
(Self::Crossfade, _) => RevealerTransitionType::Crossfade,
|
||||
_ => RevealerTransitionType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonConfig {
|
||||
/// Configures the module's container according to the common config options.
|
||||
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.install_show_if(container, revealer);
|
||||
|
||||
let left_click_script = self.on_click_left.map(Script::new_polling);
|
||||
let middle_click_script = self.on_click_middle.map(Script::new_polling);
|
||||
let right_click_script = self.on_click_right.map(Script::new_polling);
|
||||
|
||||
container.connect_button_press_event(move |_, event| {
|
||||
let script = match event.button() {
|
||||
1 => left_click_script.as_ref(),
|
||||
2 => middle_click_script.as_ref(),
|
||||
3 => right_click_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-click script: {}", event.button());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
||||
|
||||
container.connect_scroll_event(move |_, event| {
|
||||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-scroll script: {}", event.direction());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
macro_rules! install_oneshot {
|
||||
($option:expr, $method:ident) => {
|
||||
$option.map(Script::new_polling).map(|script| {
|
||||
container.$method(move |_, _| {
|
||||
script.run_as_oneshot(None);
|
||||
Inhibit(false)
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
install_oneshot!(self.on_mouse_enter, connect_enter_notify_event);
|
||||
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
let container = container.clone();
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.show_if.take().map_or_else(
|
||||
|| {
|
||||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
let container = container.clone();
|
||||
|
||||
{
|
||||
let revealer = revealer.clone();
|
||||
let container = container.clone();
|
||||
|
||||
show_if.subscribe(move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
revealer.connect_child_revealed_notify(move |revealer| {
|
||||
if !revealer.reveals_child() {
|
||||
container.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
58
src/config/impl.rs
Normal file
58
src/config/impl.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::{BarPosition, Config, MonitorConfig};
|
||||
use color_eyre::{Help, Report};
|
||||
use gtk::Orientation;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
// Manually implement for better untagged enum error handling:
|
||||
// currently open pr: https://github.com/serde-rs/serde/pull/1544
|
||||
impl<'de> Deserialize<'de> for MonitorConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let content =
|
||||
<serde::__private::de::Content as serde::Deserialize>::deserialize(deserializer)?;
|
||||
|
||||
match <Config as serde::Deserialize>::deserialize(
|
||||
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
|
||||
) {
|
||||
Ok(config) => Ok(Self::Single(config)),
|
||||
Err(outer) => match <Vec<Config> as serde::Deserialize>::deserialize(
|
||||
serde::__private::de::ContentRefDeserializer::<D::Error>::new(&content),
|
||||
) {
|
||||
Ok(config) => Ok(Self::Multiple(config)),
|
||||
Err(inner) => {
|
||||
let report = Report::msg(format!(" multi-bar (c): {inner}").replace("An error occurred when deserializing: ", ""))
|
||||
.wrap_err(format!("single-bar (b): {outer}").replace("An error occurred when deserializing: ", ""))
|
||||
.wrap_err("An invalid config was found. The following errors were encountered:")
|
||||
.note("Both the single-bar (type b / error 1) and multi-bar (type c / error 2) config variants were tried. You can likely ignore whichever of these is not relevant to you.")
|
||||
.suggestion("Please see https://github.com/JakeStanger/ironbar/wiki/configuration-guide#2-pick-your-use-case for more info on the above");
|
||||
|
||||
Err(serde::de::Error::custom(format!("{report:?}")))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarPosition {
|
||||
/// Gets the orientation the bar and widgets should use
|
||||
/// based on this position.
|
||||
pub fn get_orientation(self) -> Orientation {
|
||||
if self == Self::Top || self == Self::Bottom {
|
||||
Orientation::Horizontal
|
||||
} else {
|
||||
Orientation::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the angle that label text should be displayed at
|
||||
/// based on this position.
|
||||
pub const fn get_angle(self) -> f64 {
|
||||
match self {
|
||||
Self::Top | Self::Bottom => 0.0,
|
||||
Self::Left => 90.0,
|
||||
Self::Right => 270.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/config/mod.rs
Normal file
158
src/config/mod.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
mod common;
|
||||
mod r#impl;
|
||||
mod truncate;
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
use crate::modules::clipboard::ClipboardModule;
|
||||
#[cfg(feature = "clock")]
|
||||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::label::LabelModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
#[cfg(feature = "music")]
|
||||
use crate::modules::music::MusicModule;
|
||||
use crate::modules::script::ScriptModule;
|
||||
#[cfg(feature = "sys_info")]
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
#[cfg(feature = "tray")]
|
||||
use crate::modules::tray::TrayModule;
|
||||
#[cfg(feature = "upower")]
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use cfg_if::cfg_if;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use self::common::{CommonConfig, TransitionType};
|
||||
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ModuleConfig {
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
Custom(Box<CustomModule>),
|
||||
Focused(Box<FocusedModule>),
|
||||
Label(Box<LabelModule>),
|
||||
Launcher(Box<LauncherModule>),
|
||||
#[cfg(feature = "music")]
|
||||
Music(Box<MusicModule>),
|
||||
Script(Box<ScriptModule>),
|
||||
#[cfg(feature = "sys_info")]
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
#[cfg(feature = "tray")]
|
||||
Tray(Box<TrayModule>),
|
||||
#[cfg(feature = "upower")]
|
||||
Upower(Box<UpowerModule>),
|
||||
#[cfg(feature = "workspaces")]
|
||||
Workspaces(Box<WorkspacesModule>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MonitorConfig {
|
||||
Single(Config),
|
||||
Multiple(Vec<Config>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BarPosition {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Default for BarPosition {
|
||||
fn default() -> Self {
|
||||
Self::Bottom
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct MarginConfig {
|
||||
#[serde(default)]
|
||||
pub bottom: i32,
|
||||
#[serde(default)]
|
||||
pub left: i32,
|
||||
#[serde(default)]
|
||||
pub right: i32,
|
||||
#[serde(default)]
|
||||
pub top: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub position: BarPosition,
|
||||
#[serde(default = "default_true")]
|
||||
pub anchor_to_edges: bool,
|
||||
#[serde(default = "default_bar_height")]
|
||||
pub height: i32,
|
||||
#[serde(default)]
|
||||
pub margin: MarginConfig,
|
||||
#[serde(default = "default_popup_gap")]
|
||||
pub popup_gap: i32,
|
||||
pub name: Option<String>,
|
||||
|
||||
/// GTK icon theme to use.
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
||||
|
||||
pub start: Option<Vec<ModuleConfig>>,
|
||||
pub center: Option<Vec<ModuleConfig>>,
|
||||
pub end: Option<Vec<ModuleConfig>>,
|
||||
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clock")] {
|
||||
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
|
||||
}
|
||||
else {
|
||||
let end = None;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
position: BarPosition::default(),
|
||||
height: default_bar_height(),
|
||||
margin: MarginConfig::default(),
|
||||
name: None,
|
||||
popup_gap: default_popup_gap(),
|
||||
icon_theme: None,
|
||||
ironvar_defaults: None,
|
||||
start: Some(vec![ModuleConfig::Label(
|
||||
LabelModule::new("ℹ️ Using default config".to_string()).into(),
|
||||
)]),
|
||||
center: Some(vec![ModuleConfig::Focused(Box::default())]),
|
||||
end,
|
||||
anchor_to_edges: default_true(),
|
||||
monitors: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_bar_height() -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
const fn default_popup_gap() -> i32 {
|
||||
5
|
||||
}
|
||||
|
||||
pub const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
66
src/config/truncate.rs
Normal file
66
src/config/truncate.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EllipsizeMode {
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
|
||||
impl From<EllipsizeMode> for GtkEllipsizeMode {
|
||||
fn from(value: EllipsizeMode) -> Self {
|
||||
match value {
|
||||
EllipsizeMode::Start => Self::Start,
|
||||
EllipsizeMode::Middle => Self::Middle,
|
||||
EllipsizeMode::End => Self::End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(untagged)]
|
||||
pub enum TruncateMode {
|
||||
Auto(EllipsizeMode),
|
||||
Length {
|
||||
mode: EllipsizeMode,
|
||||
length: Option<i32>,
|
||||
max_length: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TruncateMode {
|
||||
const fn mode(&self) -> EllipsizeMode {
|
||||
match self {
|
||||
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
|
||||
}
|
||||
}
|
||||
|
||||
const fn length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Length { length, .. } => *length,
|
||||
}
|
||||
}
|
||||
|
||||
const fn max_length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Length { max_length, .. } => *max_length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate_label(&self, label: >k::Label) {
|
||||
label.set_ellipsize(self.mode().into());
|
||||
|
||||
if let Some(length) = self.length() {
|
||||
label.set_width_chars(length);
|
||||
}
|
||||
|
||||
if let Some(length) = self.max_length() {
|
||||
label.set_max_width_chars(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
src/desktop_file.rs
Normal file
208
src/desktop_file.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::lock;
|
||||
|
||||
type DesktopFile = HashMap<String, Vec<String>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> =
|
||||
Mutex::new(HashMap::new());
|
||||
|
||||
/// These are the keys that in the cache
|
||||
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> =
|
||||
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]);
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![
|
||||
PathBuf::from("/usr/share/applications"), // system installed apps
|
||||
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
|
||||
];
|
||||
|
||||
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
|
||||
if let Some(xdg_dirs) = xdg_dirs {
|
||||
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
|
||||
xdg_dir.push("applications");
|
||||
dirs.push(xdg_dir);
|
||||
}
|
||||
}
|
||||
|
||||
let user_dir = dirs::data_local_dir(); // user installed apps
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Finds all the desktop files
|
||||
fn find_desktop_files() -> Vec<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
dirs.into_iter()
|
||||
.flat_map(|dir| {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
// this is necessary to invalidate the cache
|
||||
let files = find_desktop_files();
|
||||
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using a simple condition check
|
||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let with_names = files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f,
|
||||
f.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
with_names
|
||||
.iter()
|
||||
// first pass - check for exact match
|
||||
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
|
||||
// second pass - check for substring
|
||||
.or_else(|| {
|
||||
with_names.iter().find(|(_, name)| {
|
||||
// this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
app_id
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part))
|
||||
})
|
||||
})
|
||||
.map(|(file, _)| file.into())
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
|
||||
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let app_id = &app_id.to_lowercase();
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let files = files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let Some(parsed_desktop_file) = parse_desktop_file(file) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = files
|
||||
.iter()
|
||||
// first pass - check name key for exact match
|
||||
.find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
// second pass - check name key for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.map(|names| {
|
||||
names
|
||||
.iter()
|
||||
.any(|name| name.to_lowercase().contains(app_id))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
})
|
||||
// third pass - check all keys for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
});
|
||||
|
||||
file.map(|(path, _)| path).cloned()
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||
let Ok(file) = fs::read_to_string(path) else {
|
||||
warn!("Couldn't Open File: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_file: DesktopFile = DesktopFile::new();
|
||||
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(value.to_string());
|
||||
});
|
||||
|
||||
Some(desktop_file)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
let Some(path) = find_desktop_file(app_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let desktop_file = match desktop_files_cache.get(&path) {
|
||||
Some(desktop_file) => desktop_file,
|
||||
_ => desktop_files_cache
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||
};
|
||||
|
||||
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||
|
||||
icons.next().map(std::string::ToString::to_string)
|
||||
}
|
||||
78
src/dynamic_value/dynamic_bool.rs
Normal file
78
src/dynamic_value/dynamic_bool.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::Script;
|
||||
use crate::send;
|
||||
use cfg_if::cfg_if;
|
||||
use glib::Continue;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum DynamicBool {
|
||||
/// Either a script or variable, to be determined.
|
||||
Unknown(String),
|
||||
Script(Script),
|
||||
#[cfg(feature = "ipc")]
|
||||
Variable(Box<str>),
|
||||
}
|
||||
|
||||
impl DynamicBool {
|
||||
pub fn subscribe<F>(self, f: F)
|
||||
where
|
||||
F: FnMut(bool) -> Continue + 'static,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown(input) => {
|
||||
if input.starts_with('#') {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
Self::Variable(input.into())
|
||||
} else {
|
||||
Self::Unknown(input)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let script = Script::from(input.as_str());
|
||||
Self::Script(script)
|
||||
}
|
||||
}
|
||||
_ => self,
|
||||
};
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
spawn(async move {
|
||||
match value {
|
||||
DynamicBool::Script(script) => {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicBool::Variable(variable) => {
|
||||
let variable_manager = get_variable_manager();
|
||||
|
||||
let variable_name = variable[1..].into(); // remove hash
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||
send!(tx, has_value);
|
||||
}
|
||||
}
|
||||
DynamicBool::Unknown(_) => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a string ironvar is 'truthy'
|
||||
#[cfg(feature = "ipc")]
|
||||
fn is_truthy(string: &str) -> bool {
|
||||
!(string.is_empty() || string == "0" || string == "false")
|
||||
}
|
||||
321
src/dynamic_value/dynamic_string.rs
Normal file
321
src/dynamic_value/dynamic_string.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{arc_mut, lock, send};
|
||||
use gtk::prelude::*;
|
||||
use tokio::spawn;
|
||||
|
||||
/// A segment of a dynamic string,
|
||||
/// containing either a static string
|
||||
/// or a script.
|
||||
#[derive(Debug)]
|
||||
enum DynamicStringSegment {
|
||||
Static(String),
|
||||
Script(Script),
|
||||
#[cfg(feature = "ipc")]
|
||||
Variable(Box<str>),
|
||||
}
|
||||
|
||||
/// Creates a new dynamic string, based off the input template.
|
||||
/// Runs `f` with the compiled string each time one of the scripts or variables updates.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rs
|
||||
/// dynamic_string(&text, move |string| {
|
||||
/// label.set_markup(&string);
|
||||
/// Continue(true)
|
||||
/// });
|
||||
/// ```
|
||||
pub fn dynamic_string<F>(input: &str, f: F)
|
||||
where
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let tokens = parse_input(input);
|
||||
|
||||
let label_parts = arc_mut!(vec![]);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
for (i, segment) in tokens.into_iter().enumerate() {
|
||||
match segment {
|
||||
DynamicStringSegment::Static(str) => {
|
||||
lock!(label_parts).push(str);
|
||||
}
|
||||
DynamicStringSegment::Script(script) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
// insert blank value to preserve segment order
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, |out, _| {
|
||||
if let OutputStream::Stdout(out) = out {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||
|
||||
let string = label_parts.join("");
|
||||
send!(tx, string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicStringSegment::Variable(name) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
// insert blank value to preserve segment order
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
spawn(async move {
|
||||
let variable_manager = get_variable_manager();
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
if let Some(value) = value {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _: String = std::mem::replace(&mut label_parts[i], value);
|
||||
|
||||
let string = label_parts.join("");
|
||||
send!(tx, string);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
// initialize
|
||||
{
|
||||
let label_parts = lock!(label_parts).join("");
|
||||
send!(tx, label_parts);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the input string into static and dynamic segments
|
||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||
// short-circuit parser if it's all static
|
||||
if !input.contains("{{") && !input.contains('#') {
|
||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||
}
|
||||
|
||||
let mut tokens = vec![];
|
||||
|
||||
let mut chars = input.chars().collect::<Vec<_>>();
|
||||
while !chars.is_empty() {
|
||||
let char_pair = if chars.len() > 1 {
|
||||
Some(&chars[..=1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (token, skip) = match char_pair {
|
||||
Some(['{', '{']) => parse_script(&chars),
|
||||
Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2),
|
||||
#[cfg(feature = "ipc")]
|
||||
Some(['#', _]) => parse_variable(&chars),
|
||||
_ => parse_static(&chars),
|
||||
};
|
||||
|
||||
// quick runtime check to make sure the parser is working as expected
|
||||
assert_ne!(skip, 0);
|
||||
|
||||
tokens.push(token);
|
||||
chars.drain(..skip);
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||
|
||||
let str = chars
|
||||
.windows(2)
|
||||
.skip(2)
|
||||
.take_while(|win| win != &['}', '}'])
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_BRACKETS;
|
||||
let script = Script::from(str.as_str());
|
||||
|
||||
(DynamicStringSegment::Script(script), len)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
const SKIP_HASH: usize = 1;
|
||||
|
||||
let str = chars
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take_while(|&c| !c.is_whitespace())
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_HASH;
|
||||
let value = str.into();
|
||||
|
||||
(DynamicStringSegment::Variable(value), len)
|
||||
}
|
||||
|
||||
fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
let mut str = chars
|
||||
.windows(2)
|
||||
.take_while(|&win| win != ['{', '{'] && win[0] != '#')
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
let mut char_count = str.chars().count();
|
||||
|
||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||
if chars.len() == char_count + 1 {
|
||||
let remaining_char = *chars.get(char_count).expect("Failed to find last char");
|
||||
str.push(remaining_char);
|
||||
char_count += 1;
|
||||
}
|
||||
|
||||
(DynamicStringSegment::Static(str), char_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_static() {
|
||||
const INPUT: &str = "hello world";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_odd_char_count() {
|
||||
const INPUT: &str = "hello";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script() {
|
||||
const INPUT: &str = "{{echo hello}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable() {
|
||||
const INPUT: &str = "#variable";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script() {
|
||||
const INPUT: &str = "hello {{echo world}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_variable() {
|
||||
const INPUT: &str = "hello #subject";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script_static() {
|
||||
const INPUT: &str = "hello {{echo world}} foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_variable_static() {
|
||||
const INPUT: &str = "hello #subject foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script_variable() {
|
||||
const INPUT: &str = "hello {{echo world}} #foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 4);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
|
||||
assert!(
|
||||
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_hash() {
|
||||
const INPUT: &str = "number ###num";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
|
||||
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
|
||||
assert!(
|
||||
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_with_hash() {
|
||||
const INPUT: &str = "{{echo #hello}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/dynamic_value/mod.rs
Normal file
7
src/dynamic_value/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#![doc = include_str!("../../docs/Dynamic values.md")]
|
||||
|
||||
mod dynamic_bool;
|
||||
mod dynamic_string;
|
||||
|
||||
pub use dynamic_bool::DynamicBool;
|
||||
pub use dynamic_string::dynamic_string;
|
||||
14
src/error.rs
Normal file
14
src/error.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#[repr(i32)]
|
||||
pub enum ExitCode {
|
||||
GtkDisplay = 1,
|
||||
CreateBars = 2,
|
||||
}
|
||||
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
||||
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
|
||||
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
|
||||
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
|
||||
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
|
||||
|
||||
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";
|
||||
43
src/global_state.rs
Normal file
43
src/global_state.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::popup::Popup;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Global application state shared across all bars.
|
||||
///
|
||||
/// Data that needs to be accessed from anywhere
|
||||
/// that is not otherwise accessible should be placed on here.
|
||||
#[derive(Debug)]
|
||||
pub struct GlobalState {
|
||||
popups: HashMap<Box<str>, Rc<RefCell<Popup>>>,
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
popups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn popups(&self) -> &HashMap<Box<str>, Rc<RefCell<Popup>>> {
|
||||
&self.popups
|
||||
}
|
||||
|
||||
pub fn popups_mut(&mut self) -> &mut HashMap<Box<str>, Rc<RefCell<Popup>>> {
|
||||
&mut self.popups
|
||||
}
|
||||
|
||||
pub fn with_popup_mut<F, T>(&self, monitor_name: &str, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(RefMut<Popup>) -> T,
|
||||
{
|
||||
let popup = self.popups().get(monitor_name);
|
||||
|
||||
if let Some(popup) = popup {
|
||||
let popup = popup.borrow_mut();
|
||||
Some(f(popup))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/gtk_helpers.rs
Normal file
77
src/gtk_helpers.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use glib::IsA;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Orientation, Widget};
|
||||
|
||||
/// Represents a widget's size
|
||||
/// and location relative to the bar's start edge.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct WidgetGeometry {
|
||||
/// Position of the start edge of the widget
|
||||
/// from the start edge of the bar.
|
||||
pub position: i32,
|
||||
/// The length of the widget.
|
||||
pub size: i32,
|
||||
/// The length of the bar.
|
||||
pub bar_size: i32,
|
||||
}
|
||||
|
||||
pub trait IronbarGtkExt {
|
||||
/// Adds a new CSS class to the widget.
|
||||
fn add_class(&self, class: &str);
|
||||
/// Gets the geometry for the widget
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
||||
|
||||
/// Gets a data tag on a widget, if it exists.
|
||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
|
||||
/// Sets a data tag on a widget.
|
||||
fn set_tag<V: 'static>(&self, key: &str, value: V);
|
||||
}
|
||||
|
||||
impl<W: IsA<Widget>> IronbarGtkExt for W {
|
||||
fn add_class(&self, class: &str) {
|
||||
self.style_context().add_class(class);
|
||||
}
|
||||
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
|
||||
let allocation = self.allocation();
|
||||
|
||||
let widget_size = if orientation == Orientation::Horizontal {
|
||||
allocation.width()
|
||||
} else {
|
||||
allocation.height()
|
||||
};
|
||||
|
||||
let top_level = self.toplevel().expect("Failed to get top-level widget");
|
||||
let top_level_allocation = top_level.allocation();
|
||||
|
||||
let bar_size = if orientation == Orientation::Horizontal {
|
||||
top_level_allocation.width()
|
||||
} else {
|
||||
top_level_allocation.height()
|
||||
};
|
||||
|
||||
let (widget_x, widget_y) = self
|
||||
.translate_coordinates(&top_level, 0, 0)
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
let widget_pos = if orientation == Orientation::Horizontal {
|
||||
widget_x
|
||||
} else {
|
||||
widget_y
|
||||
};
|
||||
|
||||
WidgetGeometry {
|
||||
position: widget_pos,
|
||||
size: widget_size,
|
||||
bar_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
|
||||
unsafe { self.data(key).map(|val| val.as_ref()) }
|
||||
}
|
||||
|
||||
fn set_tag<V: 'static>(&self, key: &str, value: V) {
|
||||
unsafe { self.set_data(key, value) }
|
||||
}
|
||||
}
|
||||
145
src/icon.rs
145
src/icon.rs
@@ -1,145 +0,0 @@
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Gets directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
||||
let user_dir = dirs::data_local_dir();
|
||||
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
/// (or app class).
|
||||
///
|
||||
/// A simple case-insensitive check is performed on filename == `app_id`.
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
|
||||
for dir in dirs {
|
||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||
|
||||
let entry = walker.find(|entry| match entry {
|
||||
Ok(entry) => {
|
||||
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
||||
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
||||
file_name == test_name
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(Ok(entry)) = entry {
|
||||
let path = entry.path().to_owned();
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a flat hashmap of keys/values.
|
||||
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
||||
let file = File::open(path)?;
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for line in lines.flatten() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
match find_desktop_file(app_id) {
|
||||
Some(file) => {
|
||||
let map = parse_desktop_file(file);
|
||||
|
||||
match map {
|
||||
Ok(map) => map.get("Icon").map(std::string::ToString::to_string),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
enum IconLocation {
|
||||
Theme(String),
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
|
||||
let has_icon = theme
|
||||
.lookup_icon(app_id, size, IconLookupFlags::empty())
|
||||
.is_some();
|
||||
|
||||
if has_icon {
|
||||
return Some(IconLocation::Theme(app_id.to_string()));
|
||||
}
|
||||
|
||||
let is_steam_game = app_id.starts_with("steam_app_");
|
||||
if is_steam_game {
|
||||
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
|
||||
|
||||
return match dirs::data_dir() {
|
||||
Some(dir) => {
|
||||
let path = dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{}.png",
|
||||
steam_id
|
||||
));
|
||||
|
||||
return Some(IconLocation::File(path));
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
let icon_name = get_desktop_icon_name(app_id);
|
||||
if let Some(icon_name) = icon_name {
|
||||
let is_path = PathBuf::from(&icon_name).exists();
|
||||
|
||||
return if is_path {
|
||||
Some(IconLocation::File(PathBuf::from(icon_name)))
|
||||
} else {
|
||||
return Some(IconLocation::Theme(icon_name));
|
||||
};
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Gets the icon associated with an app.
|
||||
pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
|
||||
let icon_location = get_icon_location(theme, app_id, size);
|
||||
|
||||
match icon_location {
|
||||
Some(IconLocation::Theme(icon_name)) => {
|
||||
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
|
||||
|
||||
match icon {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
55
src/image/gtk.rs
Normal file
55
src/image/gtk.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
let button = Button::new();
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Some(_) => {
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
None => {
|
||||
button.set_label(input);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button.set_label(input);
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
#[cfg(feature = "music")]
|
||||
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
7
src/image/mod.rs
Normal file
7
src/image/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
mod gtk;
|
||||
mod provider;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
pub use self::gtk::*;
|
||||
pub use provider::ImageProvider;
|
||||
284
src/image/provider.rs
Normal file
284
src/image/provider.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use crate::desktop_file::get_desktop_icon_name;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::warn;
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use crate::send;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ImageLocation<'a> {
|
||||
Icon {
|
||||
name: String,
|
||||
theme: &'a IconTheme,
|
||||
},
|
||||
Local(PathBuf),
|
||||
Steam(String),
|
||||
#[cfg(feature = "http")]
|
||||
Remote(reqwest::Url),
|
||||
}
|
||||
|
||||
pub struct ImageProvider<'a> {
|
||||
location: ImageLocation<'a>,
|
||||
size: i32,
|
||||
}
|
||||
|
||||
impl<'a> ImageProvider<'a> {
|
||||
/// Attempts to parse the image input to find its location.
|
||||
/// Errors if no valid location type can be found.
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
||||
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
fn get_location(
|
||||
input: &str,
|
||||
theme: &'a IconTheme,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
recurse_depth: usize,
|
||||
) -> Option<ImageLocation<'a>> {
|
||||
macro_rules! fallback {
|
||||
() => {
|
||||
if use_fallback {
|
||||
Some(Self::get_fallback_icon(theme))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_RECURSE_DEPTH: usize = 2;
|
||||
|
||||
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
|
||||
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
}),
|
||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
||||
input_name[2..].to_string(),
|
||||
))),
|
||||
#[cfg(feature = "http")]
|
||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||
input.parse().ok().map(ImageLocation::Remote)
|
||||
}
|
||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
})
|
||||
}
|
||||
Some(input_type) => {
|
||||
warn!(
|
||||
"{:?}",
|
||||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
fallback!()
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
||||
None if should_parse_desktop_file => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
||||
}) {
|
||||
location
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to fetch the image from the location
|
||||
/// and load it into the provided `GTK::Image` widget.
|
||||
pub fn load_into_image(&self, image: gtk::Image) -> Result<()> {
|
||||
// handle remote locations async to avoid blocking UI thread while downloading
|
||||
#[cfg(feature = "http")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
send!(tx, bytes);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let size = self.size;
|
||||
rx.attach(None, move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
);
|
||||
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
||||
{
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Continue(false)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.load_into_image_sync(&image)?;
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
self.load_into_image_sync(&image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||
let scale = image.scale_factor();
|
||||
|
||||
let pixbuf = match &self.location {
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
||||
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
||||
#[cfg(feature = "http")]
|
||||
_ => unreachable!(), // handled above
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image, scale)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image, scale: i32) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr =
|
||||
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||
let pixbuf =
|
||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||
let scaled_size = self.size * scale;
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||
Ok(pixbuf)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file,
|
||||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| {
|
||||
Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
|
||||
self.get_from_file(&path, scale)
|
||||
}
|
||||
|
||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||
#[cfg(feature = "http")]
|
||||
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
|
||||
let res = reqwest::get(url).await?;
|
||||
|
||||
let status = res.status();
|
||||
if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
|
||||
ImageLocation::Icon {
|
||||
name: "dialog-question-symbolic".to_string(),
|
||||
theme,
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ipc/client.rs
Normal file
28
src/ipc/client.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use super::Ipc;
|
||||
use crate::ipc::{Command, Response};
|
||||
use color_eyre::Result;
|
||||
use color_eyre::{Help, Report};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
impl Ipc {
|
||||
/// Sends a command to the IPC server.
|
||||
/// The server response is returned.
|
||||
pub async fn send(&self, command: Command) -> Result<Response> {
|
||||
let mut stream = match UnixStream::connect(&self.path).await {
|
||||
Ok(stream) => Ok(stream),
|
||||
Err(err) => Err(Report::new(err)
|
||||
.wrap_err("Failed to connect to Ironbar IPC server")
|
||||
.suggestion("Is Ironbar running?")),
|
||||
}?;
|
||||
|
||||
let write_buffer = serde_json::to_vec(&command)?;
|
||||
stream.write_all(&write_buffer).await?;
|
||||
|
||||
let mut read_buffer = vec![0; 1024];
|
||||
let bytes = stream.read(&mut read_buffer).await?;
|
||||
|
||||
let response = serde_json::from_slice(&read_buffer[..bytes])?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
79
src/ipc/commands.rs
Normal file
79
src/ipc/commands.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Command {
|
||||
/// Return "ok"
|
||||
Ping,
|
||||
|
||||
/// Open the GTK inspector
|
||||
Inspect,
|
||||
|
||||
/// Reload the config
|
||||
Reload,
|
||||
|
||||
/// Set an `ironvar` value.
|
||||
/// This creates it if it does not already exist, and updates it if it does.
|
||||
/// Any references to this variable are automatically and immediately updated.
|
||||
/// Keys and values can be any valid UTF-8 string.
|
||||
Set {
|
||||
/// Variable key. Can be any alphanumeric ASCII string.
|
||||
key: Box<str>,
|
||||
/// Variable value. Can be any valid UTF-8 string.
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Get the current value of an `ironvar`.
|
||||
Get {
|
||||
/// Variable key.
|
||||
key: Box<str>,
|
||||
},
|
||||
|
||||
/// Load an additional CSS stylesheet.
|
||||
/// The sheet is automatically hot-reloaded.
|
||||
LoadCss {
|
||||
/// The path to the sheet.
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Set the visibility of the bar with the given name.
|
||||
SetVisible {
|
||||
///Bar name to target.
|
||||
bar_name: String,
|
||||
/// The visibility status.
|
||||
#[arg(short, long)]
|
||||
visible: bool,
|
||||
},
|
||||
|
||||
/// Get the visibility of the bar with the given name.
|
||||
GetVisible {
|
||||
/// Bar name to target.
|
||||
bar_name: String,
|
||||
},
|
||||
|
||||
/// Toggle a popup open/closed.
|
||||
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
|
||||
TogglePopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
/// The name of the widget.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Open a popup, regardless of current state.
|
||||
OpenPopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
/// The name of the widget.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Close a popup, regardless of current state.
|
||||
ClosePopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user