Compare commits
637 commits
Author | SHA1 | Date | |
---|---|---|---|
676bbebf4c | |||
617a12f96d | |||
5053bb0f1b | |||
21060a10a0 | |||
a56b86100a | |||
fd4b940d58 | |||
2bbac991db | |||
255794cd1c | |||
2e81eed830 | |||
60bfb19378 | |||
d7d4603855 | |||
cbd0c58b4a | |||
bbc26c263c | |||
53de1b524a | |||
c3d4fce74c | |||
8cc7c9de9b | |||
3ed26c62a5 | |||
900a0710c5 | |||
2e18d71284 | |||
61123eb7a0 | |||
27be30263f | |||
559517f345 | |||
b45ff330c9 | |||
|
bfafd93643 | ||
|
c14ed1166d | ||
|
9f6c9cc7d2 | ||
|
025d3fcb51 | ||
|
05622f985a | ||
|
c4a3f488aa | ||
|
68de299763 | ||
|
85760926d7 | ||
|
3bc3c75ff4 | ||
|
d30e5c694e | ||
|
b217ac9c9e | ||
|
fcda13cac0 | ||
|
0c2123cfe4 | ||
|
9251217bb3 | ||
|
9170785ed5 | ||
|
2e7e75a27c | ||
|
d303b794f3 | ||
|
b42323013d | ||
|
8583b5123e | ||
|
b762132037 | ||
|
fded39fa81 | ||
|
9c5e30ac61 | ||
|
9e6e656fa8 | ||
|
b9e45ca994 | ||
|
37b5646d65 | ||
|
d03e6307f5 | ||
|
1471d8824b | ||
|
04a222f3c8 | ||
|
868fdbedd3 | ||
|
e1f50f4782 | ||
|
f855d5c235 | ||
|
2546dbae2e | ||
|
d27986a316 | ||
|
4df5272164 | ||
|
839d79e68f | ||
|
ee796f6589 | ||
|
9b4944c53f | ||
|
8178919e2c | ||
|
305f9cf491 | ||
|
b866ab25b6 | ||
|
775210db08 | ||
|
2ef6f84df3 | ||
|
a97e6f2f7d | ||
|
0b4ff04be5 | ||
|
7cda35c1df | ||
|
b3007dd042 | ||
|
8967eec44b | ||
|
14f19c897a | ||
|
e9696b2150 | ||
|
c0526f2775 | ||
|
6b4898017f | ||
|
bdfc4fdab4 | ||
|
a6de61b751 | ||
|
1dd39a4e43 | ||
|
592d08c082 | ||
|
cad45ecd2c | ||
|
79081ebb4f | ||
|
1b0478edd4 | ||
|
2b4e2b2c82 | ||
|
42e041ce03 | ||
|
e58afff48a | ||
|
f34e02d824 | ||
|
2e1289f778 | ||
|
b750d96a72 | ||
|
61e38c6094 | ||
|
ad8b1802f5 | ||
|
99bd2a81b6 | ||
|
93f3da1e08 | ||
|
7161ef211c | ||
|
f77f5552ae | ||
|
be332005fa | ||
|
cc883d1723 | ||
|
30362cb124 | ||
|
098f03ac52 | ||
|
ae29c1b79f | ||
|
b327162f3b | ||
|
8eb2545eed | ||
|
f0ce6a1f7f | ||
|
a6f2e6fc5e | ||
|
87a2890b48 | ||
|
6a93238bda | ||
|
79ce2167b0 | ||
|
1fef60b32c | ||
|
0bc2c6b8e1 | ||
|
07e2364f78 | ||
|
5412591a0e | ||
|
697c3310a0 | ||
|
1682a47554 | ||
|
cace02909e | ||
|
605b749e22 | ||
|
61fe7f6d3e | ||
|
e70402e92c | ||
|
88f24100ff | ||
|
a7979e7d66 | ||
|
7ae95ad6b6 | ||
|
ccf2fb3fd0 | ||
|
7ec3adfa47 | ||
|
1c19250fe5 | ||
|
38d3a6d4c4 | ||
|
0151d20451 | ||
|
acb387a685 | ||
|
e5f36053af | ||
|
d94ee9416a | ||
|
0807bfb5c3 | ||
|
c637392bd0 | ||
|
4337575557 | ||
|
a5e0b01e3b | ||
|
97a022e452 | ||
|
7a1022de46 | ||
|
01cf02c560 | ||
|
71d65fafc6 | ||
|
a6388aea49 | ||
|
46b379815b | ||
|
7e6ae7c7be | ||
|
5ddc0d84a3 | ||
|
245cacac42 | ||
|
d158fbccba | ||
|
aa86ac931f | ||
|
700f977c87 | ||
|
84a3ebf47c | ||
|
9fe4e6c347 | ||
|
a07f40d051 | ||
|
578f806504 | ||
|
5b864e4924 | ||
|
96c9989ad5 | ||
|
54d5e83909 | ||
|
e76a6e0ba3 | ||
|
754707379a | ||
|
c40f59f7be | ||
|
3f97ea6a39 | ||
|
21cbbe685d | ||
|
910b9a8963 | ||
|
cd46b9c6a6 | ||
|
2287dcab48 | ||
|
a97f46c087 | ||
|
eb11c279f6 | ||
|
025b1ec2f2 | ||
|
20bc4b3fa6 | ||
|
ca6bf2e189 | ||
|
003a6efc8e | ||
|
a1d94d4355 | ||
|
cc910f1198 | ||
|
f4bd0fba0b | ||
|
28601cf2b7 | ||
|
72a888748e | ||
|
d57ef9364a | ||
|
87764dccf0 | ||
|
3c37b666f5 | ||
|
8d0f8a4177 | ||
|
ee9885a601 | ||
|
ee81f6198e | ||
|
40041d6080 | ||
|
b90346424b | ||
|
978519e130 | ||
|
1983408e58 | ||
|
0f74b690ca | ||
|
05f0e08493 | ||
|
8d2cdebbaf | ||
|
ae04cc9897 | ||
|
b1fd18b9af | ||
|
d5d0d6a56c | ||
|
7ee9645437 | ||
|
82e55ec517 | ||
|
fed7a067ba | ||
|
2b3b9c0ca0 | ||
|
bb36f98aaa | ||
|
80efa64614 | ||
|
84dfd85396 | ||
|
f9cfede0d0 | ||
|
55f3085c90 | ||
|
124f13075d | ||
|
2ab14d9cd3 | ||
|
470f05150d | ||
|
6af47dc506 | ||
|
7a47e9e13d | ||
|
a7dba79664 | ||
|
062506f467 | ||
|
b752eb8934 | ||
|
38a5f38b43 | ||
|
f59da4c2d8 | ||
|
42ef6a3f02 | ||
|
921ddb64f0 | ||
|
a78403d3e8 | ||
|
61ebc3aea6 | ||
|
6bac2b6e34 | ||
|
6980d5e5d0 | ||
|
db13bd94a1 | ||
|
e8c493607f | ||
|
00b7929df5 | ||
|
184762ac57 | ||
|
7635fec36f | ||
|
a8a4a86350 | ||
|
a1c4b3c65e | ||
|
b5535fcdc1 | ||
|
8e85eaa018 | ||
|
e2aa039276 | ||
|
a5fbc73c44 | ||
|
39f7ad9a75 | ||
|
2fa7931783 | ||
|
b594fd4263 | ||
|
284662a0ba | ||
|
9a6e61173f | ||
|
05c28c52c7 | ||
|
a97a7fe507 | ||
|
6f137c4927 | ||
|
218bfa2235 | ||
|
6f4f163a7d | ||
|
326e2f9318 | ||
|
f462102439 | ||
|
eabf167c1f | ||
|
df9890690a | ||
|
16c4ce2ee6 | ||
|
e6f1939857 | ||
|
2d6041be5d | ||
|
b46c295827 | ||
|
9072249d5c | ||
|
3764e758a2 | ||
|
7b63efee36 | ||
|
4afb8d8636 | ||
|
7dd5914e3f | ||
|
c001f031a1 | ||
|
7fc712862c | ||
|
5c166beebf | ||
|
6b2b5217ed | ||
|
91b1b5e037 | ||
|
2e8495d5ff | ||
|
f01179290b | ||
|
221ea0d22f | ||
|
a6d2ccc666 | ||
|
b7dd47c834 | ||
|
3da0f08fcb | ||
|
c011cfb6c1 | ||
|
d20dacb2dc | ||
|
d3de79e6b4 | ||
|
771e7482d7 | ||
|
d1ae8f277f | ||
|
83d910a7ef | ||
|
f64f71b6e2 | ||
|
a48ddbb2c8 | ||
|
8501c406af | ||
|
2383cadadc | ||
|
4d3de3be04 | ||
|
82fa347f2c | ||
|
7932af712e | ||
|
d446a44f06 | ||
|
9b61eee725 | ||
|
1fc4139b7c | ||
|
69edfc42ae | ||
|
f513582d44 | ||
|
2d99bce987 | ||
|
879744e19c | ||
|
a8fabce14e | ||
|
c228ca3b12 | ||
|
82ca97c65f | ||
|
fd1eb6e790 | ||
|
b31dea19cc | ||
|
d52f713063 | ||
|
0bf91c2f15 | ||
|
3de6f9f4b9 | ||
|
e5cdabcc0f | ||
|
fbe70607be | ||
|
9cbc39e462 | ||
|
bf7aac4e6e | ||
|
7d33171749 | ||
|
eb51a3c1c7 | ||
|
c57daf65ce | ||
|
9a2e7637c9 | ||
|
07200c466b | ||
|
33d22c2637 | ||
|
950931e1b9 | ||
|
a17356ee9d | ||
|
928f8258aa | ||
|
03731136b6 | ||
|
8897c1bde5 | ||
|
283d47ff65 | ||
|
3aadab5628 | ||
|
2a77e3a85c | ||
|
b1f49f6a1e | ||
|
4784be4076 | ||
|
a1ae6d4f34 | ||
|
5c390be25c | ||
|
4f9553f7ea | ||
|
8458eef1e6 | ||
|
4c08cd812e | ||
|
80493d3bea | ||
|
c40a174463 | ||
|
8867f4f188 | ||
|
08b5386140 | ||
|
8bde6378d4 | ||
|
1089792bc6 | ||
|
30dd0f2efb | ||
|
c019c4f382 | ||
|
5d1059ba63 | ||
|
a89bc096ef | ||
|
f4ca5eaa3b | ||
|
8a50eb6f81 | ||
|
441d4f0275 | ||
|
51c3805f7f | ||
|
d430f90434 | ||
|
4b7a6a18d5 | ||
|
8991bba90e | ||
|
e590a3cf3f | ||
|
973dd6117e | ||
|
6ce761695a | ||
|
a84b4f9a65 | ||
|
26e4bdd7eb | ||
|
5ad211f862 | ||
|
6a3e4761bf | ||
|
74ecbb6ca8 | ||
|
cbd989309d | ||
|
f0ab3ef03a | ||
|
c7f58ae2a4 | ||
|
4bbe25d195 | ||
|
d94d12897d | ||
|
2cb72fcc30 | ||
|
dced20bf89 | ||
|
fdc9b78967 | ||
|
d8216a5e2c | ||
|
0fe89e13a0 | ||
|
7542a47dbc | ||
|
d89c6b1bc1 | ||
|
99bb5e99aa | ||
|
876774ce40 | ||
|
e6bb787e01 | ||
|
6b31cdb698 | ||
|
9106ec9c8f | ||
|
0dc6a95ac2 | ||
|
618a22c122 | ||
|
4007517e45 | ||
|
d67232d8cf | ||
|
1580951474 | ||
|
40de07ba2e | ||
|
b5395fe764 | ||
|
c96d119b0e | ||
|
ed5a4e61e4 | ||
|
9c463fc2b7 | ||
|
d4339f6e43 | ||
|
27194a92c8 | ||
|
05f76c0d9a | ||
|
5a1addec7f | ||
|
439b140916 | ||
|
8be9f1a05c | ||
|
b00c0ae47c | ||
|
473d2fbd14 | ||
|
5c65a8fef4 | ||
|
f98053371e | ||
|
f6dd17b383 | ||
|
98c92bb78f | ||
|
4d422ffc84 | ||
|
5a2dfc226b | ||
|
a678241a70 | ||
|
5d80a5a1a0 | ||
|
48501fa534 | ||
|
f9017c3a38 | ||
|
2100a7cfdb | ||
|
3f524ab371 | ||
|
911230c659 | ||
|
c7df1926dc | ||
|
448ab6de83 | ||
|
4987c7d3e2 | ||
|
f141b95d8f | ||
|
1232c4d960 | ||
|
80663bdbc8 | ||
|
4485b65722 | ||
|
3ff2e49e5f | ||
|
37ccbd7f4a | ||
|
447d094fe2 | ||
|
e5007a5729 | ||
|
ec71d7fbbe | ||
|
c4046d0cd2 | ||
|
51f68addcd | ||
|
4b6b4b9052 | ||
|
dfd23a44de | ||
|
902288f30d | ||
|
7f03c9ce2d | ||
|
9e20b48cee | ||
|
046b950b8a | ||
|
afeb30e40e | ||
|
9553bec7db | ||
|
1e13798c95 | ||
|
028932a560 | ||
|
fb6be007e5 | ||
|
10c169af8a | ||
|
8001ed3ada | ||
|
4a6be622a8 | ||
|
0410ac9c6b | ||
|
527d1706c2 | ||
|
7f2ce7d76e | ||
|
abcf861fcb | ||
|
10c9321c24 | ||
|
3d809eb590 | ||
|
7756eaaa31 | ||
|
e560649531 | ||
|
4187bddad6 | ||
|
65da1e2246 | ||
|
38613495f2 | ||
|
9f89e3a657 | ||
|
868502d62e | ||
|
8d88b23947 | ||
|
7d0d1455c8 | ||
|
6d1536ca80 | ||
|
0ff49ac7d5 | ||
|
32eef6b204 | ||
|
da7734d81f | ||
|
6d7934f0fe | ||
|
d4425039b9 | ||
|
618ebbeccc | ||
|
31f1f99102 | ||
|
6e3caa6f14 | ||
|
0734c970b0 | ||
|
406eadeac7 | ||
|
a27c284869 | ||
|
beca26c2bf | ||
|
45c0a382c9 | ||
|
21ded8f640 | ||
|
413abdcae7 | ||
|
5e790b7496 | ||
|
0e37d6cbf2 | ||
|
b74ebce702 | ||
|
436cea8f37 | ||
|
af95b2e276 | ||
|
960792b2e5 | ||
|
73b071edb0 | ||
|
7b1659a1b5 | ||
|
7fd4f710a1 | ||
|
a94114dd94 | ||
|
64355e5314 | ||
|
601b2115ce | ||
|
3644acce76 | ||
|
fa66873582 | ||
|
13b06793da | ||
|
9e9dff7658 | ||
|
e872897346 | ||
|
a018343af6 | ||
|
d0e309ad0f | ||
|
b598869450 | ||
|
5ff6263986 | ||
|
3937e73e7d | ||
|
e0fc98bfb0 | ||
|
47a640e610 | ||
|
a58454de97 | ||
|
21268d7d86 | ||
|
071a69b2d8 | ||
|
659a0147da | ||
|
38c4b46351 | ||
|
cd851340e2 | ||
|
1d8b261057 | ||
|
efebc8d049 | ||
|
3c08eafa4a | ||
|
2de39be731 | ||
|
a8d1254e06 | ||
|
5fb19b66da | ||
|
de01d96b91 | ||
|
f5e6bc12db | ||
|
d0ee1b06e4 | ||
|
a8b28cd0bf | ||
|
681bba4f12 | ||
|
6c08336154 | ||
|
02465ea0c2 | ||
|
08ef42834e | ||
|
0fc1782e0b | ||
|
a811c9c886 | ||
|
c5f13746b3 | ||
|
5e96b63c60 | ||
|
d1e9e7e8bb | ||
|
9b82e736a0 | ||
|
45125f39af | ||
|
3c0499ba56 | ||
|
68bd6f8ef8 | ||
|
1a7ae9ecc6 | ||
|
49edf97b89 | ||
|
1912f3053d | ||
|
bfbd94232d | ||
|
04a2ea438b | ||
|
270b3e13bf | ||
|
180a87e0c3 | ||
|
f7feb3e674 | ||
|
0a7a4150e0 | ||
|
1751e4afa2 | ||
|
284ef6b260 | ||
|
fbe5764313 | ||
|
823a57d261 | ||
|
5ec4b04a64 | ||
|
130bfc0f0b | ||
|
f2153b95a5 | ||
|
57f669a5e7 | ||
|
a253e70328 | ||
|
4df495601a | ||
|
96f8e92822 | ||
|
1bdaedb481 | ||
|
7d85ba87d5 | ||
|
9e80d4d907 | ||
|
1759c33cf3 | ||
|
806b97895e | ||
|
8a4fc40947 | ||
|
e42f09b27e | ||
|
aa6238a5c6 | ||
|
fcbb89db90 | ||
|
f37eb31f94 | ||
|
4621de95b5 | ||
|
2390dfa946 | ||
|
070fe865dd | ||
|
b79c19b616 | ||
|
3a5365a2c3 | ||
|
965e7b2453 | ||
|
7a4d4d5ab6 | ||
|
f180c7cc58 | ||
|
100568206a | ||
|
d568ef3622 | ||
|
ecf1825c00 | ||
|
e7b853c667 | ||
|
710a2cef98 | ||
|
7e7327c374 | ||
|
6c0930cfae | ||
|
1638180b6d | ||
|
7b57299cdb | ||
|
04f6a6a08f | ||
|
213c28992c | ||
|
032a651efa | ||
|
1b66e17780 | ||
|
6db8470c2b | ||
|
07f0b7e34a | ||
|
b1501a8159 | ||
|
0776592da6 | ||
|
f22095e978 | ||
|
ebeb084da0 | ||
|
0c8d682d62 | ||
|
49de0e520b | ||
|
6f6f3cedd9 | ||
|
820598b1b8 | ||
|
780c5bf3d0 | ||
|
945838dc74 | ||
|
9d5035fa10 | ||
|
c339a16365 | ||
|
58bbcf2ac5 | ||
|
22ddcf42bd | ||
|
dff187252a | ||
|
2c8dafec70 | ||
|
905f71fa52 | ||
|
362d1a5f6f | ||
|
db41792afb | ||
|
38e54a7c81 | ||
|
9b5477675c | ||
|
738a846853 | ||
|
d759ed5051 | ||
|
ad91b1c025 | ||
|
9e04e0a27b | ||
|
c77f3aa3bc | ||
|
b368cb49da | ||
|
5959d73cde | ||
|
f8d035c079 | ||
|
5de616ff89 | ||
|
72d255f0ae | ||
|
969378846f | ||
|
13a851a636 | ||
|
06afb03807 | ||
|
06d6739da4 | ||
|
3fd8c5aaa0 | ||
|
b1adc382aa | ||
|
647ad44b31 | ||
|
22d85d6d1d | ||
|
d05ff39f02 | ||
|
b14eae4d6c | ||
|
5874850bd5 | ||
|
8f57bb952d | ||
|
5f2857ad9a | ||
|
4aff0499f0 | ||
|
d358790c6a | ||
|
b39dba8867 | ||
|
c692a776b6 | ||
|
bee2586ed4 | ||
|
632eb0c450 | ||
|
6870c3ba84 | ||
|
993be61eec | ||
|
84f2fdd419 | ||
|
b7083aacce | ||
|
6c7737cdd5 | ||
|
550b594c86 | ||
|
4fe56fc00d | ||
|
6c9e89627a | ||
|
bf1cae2399 | ||
|
463bc9665b | ||
|
a008ce3e58 | ||
|
54a2fc3a41 | ||
|
548ccc5e94 | ||
|
9be1331e1b | ||
|
dc70527797 | ||
|
6025fcd2da | ||
|
7c8ddc9c87 | ||
|
72966ee37d | ||
|
df29627983 | ||
|
83a4be3bc0 | ||
|
088309df23 | ||
|
b1bb0fe690 | ||
|
0785202860 | ||
|
5ea2708f45 | ||
|
a4a622252b | ||
|
2b888325a6 | ||
|
2d6cedd8ac | ||
|
01cde70e14 | ||
|
adbba9bf9a | ||
|
0067ce83f0 | ||
|
72045b2318 | ||
|
a9f50f1b51 | ||
|
057f894d52 | ||
|
5c207df6ae | ||
|
aac2316d74 | ||
|
6b63b87c42 | ||
|
edb8eaf410 | ||
|
b4fc88b57e | ||
|
8f0fdd6b39 | ||
|
6d9d325eca | ||
|
7579a08615 | ||
|
f75f1a9f26 |
286 changed files with 8610 additions and 972 deletions
29
.github/workflows/aurpublish.yml
vendored
Normal file
29
.github/workflows/aurpublish.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: Upload AUR Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
aur-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
- name: Create PKGBUILD
|
||||
run: |
|
||||
python ./create-pkgbuild.py > ./PKGBUILD
|
||||
- name: Publish AUR package
|
||||
uses: KSXGitHub/github-actions-deploy-aur@v2.5.0
|
||||
with:
|
||||
pkgname: bumblebee-status
|
||||
pkgbuild: ./PKGBUILD
|
||||
commit_username: ${{ secrets.AUR_USERNAME }}
|
||||
commit_email: ${{ secrets.AUR_EMAIL }}
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_message: Update AUR package
|
||||
ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
|
45
.github/workflows/autotest.yml
vendored
Normal file
45
.github/workflows/autotest.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened, edited ]
|
||||
push:
|
||||
|
||||
env:
|
||||
CC_TEST_REPORTER_ID: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
- name: Install Ubuntu dependencies
|
||||
run: sudo apt-get install -y libdbus-1-dev libgit2-dev libvirt-dev taskwarrior
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U coverage pytest pytest-mock freezegun
|
||||
pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true
|
||||
pip install $(cat requirements/modules/*.txt | grep -v power | cut -d ' ' -f 1 | sort -u)
|
||||
- name: Install Code Climate dependency
|
||||
run: |
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||
chmod +x ./cc-test-reporter
|
||||
./cc-test-reporter before-build
|
||||
- name: Run tests
|
||||
run: |
|
||||
coverage run --source=. -m pytest tests -v
|
||||
- name: Report coverage
|
||||
uses: paambaati/codeclimate-action@v3.2.0
|
||||
with:
|
||||
coverageCommand: coverage3 xml
|
||||
debug: true
|
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '31 0 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
*.o
|
||||
|
||||
# Vim swap files
|
||||
*swp
|
||||
*~
|
||||
|
|
9
.readthedocs.yaml
Normal file
9
.readthedocs.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
version: 2
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3"
|
18
.travis.yml
18
.travis.yml
|
@ -1,18 +0,0 @@
|
|||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
before_install:
|
||||
- sudo apt-get -qq update
|
||||
install:
|
||||
- pip install -U coverage==4.3 pytest pytest-mock
|
||||
- pip install codeclimate-test-reporter
|
||||
script:
|
||||
- coverage run --source=. -m pytest tests -v
|
||||
- CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter
|
||||
addons:
|
||||
code_climate:
|
||||
repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
|
|
@ -12,6 +12,5 @@ But even if you can't provide those, any indicator that something is not working
|
|||
|
||||
### Adding a new module or theme
|
||||
If you want to add a new module, please have a look at [how to write a new module](docs/development/module.rst) and [how to write a new theme](docs/development/theme.rst). Then simply create a Pull Request and I will review the changes as soon as possible.
|
||||
If you want to do me a *big* favour, check the Travis status for any failing unit tests. Oh - and if you happen to add unit tests, that's also something I am very grateful for!
|
||||
|
||||
Thanks for reading until here! :)
|
||||
|
|
45
PKGBUILD.template
Normal file
45
PKGBUILD.template
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Maintainer: Tobias Witek <tobi@tobi-wan-kenobi.at>
|
||||
# Contributor: Daniel M. Capella <polycitizen@gmail.com>
|
||||
# Contributor: spookykidmm <https://github.com/spookykidmm>
|
||||
|
||||
pkgname=bumblebee-status
|
||||
pkgver=<PKGVERSION>
|
||||
pkgrel=1
|
||||
pkgdesc='Modular, theme-able status line generator for the i3 window manager'
|
||||
arch=('any')
|
||||
url=https://github.com/tobi-wan-kenobi/bumblebee-status
|
||||
license=('MIT')
|
||||
depends=('python' 'python-netifaces' 'python-psutil' 'python-requests')
|
||||
optdepends=('xorg-xbacklight: to display a displays brightness'
|
||||
'xorg-xset: enable/disable automatic screen locking'
|
||||
'libnotify: enable/disable automatic screen locking'
|
||||
'dnf: display DNF package update information'
|
||||
'xorg-setxkbmap: display/change the current keyboard layout'
|
||||
'redshift: display the redshifts current color'
|
||||
'pulseaudio: control pulseaudio sink/sources'
|
||||
'xorg-xrandr: enable/disable screen outputs'
|
||||
'pacman: display current status of pacman'
|
||||
'iputils: display a ping'
|
||||
'python-i3ipc: display titlebar'
|
||||
'fakeroot: dependency of the pacman module'
|
||||
'python-pytz: timezone conversion for datetimetz module'
|
||||
'python-tzlocal: retrieve system timezone for datetimetz module'
|
||||
)
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
||||
sha512sums=('<SHA512SUM>')
|
||||
|
||||
package() {
|
||||
install -d "$pkgdir"/usr/bin \
|
||||
"$pkgdir"/usr/share/$pkgname/bumblebee_status/{core,util} \
|
||||
"$pkgdir"/usr/share/$pkgname/bumblebee_status/modules/{core,contrib} \
|
||||
"$pkgdir"/usr/share/$pkgname/themes/icons
|
||||
ln -s /usr/share/$pkgname/$pkgname "$pkgdir"/usr/bin/$pkgname
|
||||
ln -s /usr/share/$pkgname/bumblebee-ctl "$pkgdir"/usr/bin/bumblebee-ctl
|
||||
|
||||
cd $pkgname-$pkgver
|
||||
cp -a --parents $pkgname bumblebee_status/{,core/,util/,modules/core/,modules/contrib/}*.py \
|
||||
themes/{,icons/}*.json $pkgdir/usr/share/$pkgname
|
||||
cp -r bin $pkgdir/usr/share/$pkgname/
|
||||
|
||||
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||
}
|
28
README.md
28
README.md
|
@ -1,13 +1,21 @@
|
|||
# bumblebee-status
|
||||
<img src="https://github.com/kellya/bumblebee-status-icon/blob/main/img/bumblebee_status_rtl.svg" width="50" style="display:inline-block">bumblebee-status
|
||||
=====================================================
|
||||
|
||||
logo courtesy of [kellya](https://github.com/kellya) - thank you!
|
||||
|
||||
[](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status)
|
||||
[](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
|
||||

|
||||

|
||||

|
||||
[](https://badge.fury.io/py/bumblebee-status)
|
||||

|
||||

|
||||
[](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml)
|
||||
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||
[](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml)
|
||||

|
||||
|
||||
**Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.**
|
||||
|
||||
|
@ -28,16 +36,14 @@ Thanks a lot!
|
|||
|
||||
Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors)
|
||||
|
||||
Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8
|
||||
Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
|
||||
|
||||
Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons)
|
||||
|
||||
---
|
||||
**NOTE**
|
||||
***NOTE***
|
||||
|
||||
The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`!
|
||||
|
||||
If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
|
||||
The default branch for this project is `main`. If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
|
||||
|
||||
---
|
||||
|
||||
|
@ -76,10 +82,16 @@ makepkg -sicr
|
|||
pip install --user bumblebee-status
|
||||
```
|
||||
|
||||
There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)!
|
||||
|
||||
An ebuild, for Gentoo Linux, is available on [gallifrey overlay](https://github.com/fedeliallalinea/gallifrey/tree/master/x11-misc/bumblebee-status). Instructions for adding the overlay can be found [here](https://github.com/fedeliallalinea/gallifrey/blob/master/README.md).
|
||||
|
||||
# Dependencies
|
||||
[Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables)
|
||||
for each module. If you are not using a module, you don't need the dependencies.
|
||||
|
||||
Some themes (e.g. all ‘powerline’ themes) require Font Awesome http://fontawesome.io/ and a powerline-compatible font (powerline-fonts) https://github.com/powerline/fonts
|
||||
|
||||
# Usage
|
||||
## Normal usage
|
||||
In your i3wm configuration, modify the *status_command* for your i3bar like this:
|
||||
|
|
|
@ -12,6 +12,7 @@ button = {
|
|||
"right-mouse": 3,
|
||||
"wheel-up": 4,
|
||||
"wheel-down": 5,
|
||||
"update": -1,
|
||||
}
|
||||
|
||||
|
||||
|
@ -20,7 +21,7 @@ def main():
|
|||
parser.add_argument(
|
||||
"-b",
|
||||
"--button",
|
||||
choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down"],
|
||||
choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"],
|
||||
help="button to emulate",
|
||||
default="left-mouse",
|
||||
)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import signal
|
||||
import socket
|
||||
import select
|
||||
import logging
|
||||
import threading
|
||||
|
||||
|
@ -38,44 +38,48 @@ class CommandSocket(object):
|
|||
self.__socket.close()
|
||||
os.unlink(self.__name)
|
||||
|
||||
def process_event(event_line, config, update_lock):
|
||||
modules = {}
|
||||
try:
|
||||
event = json.loads(event_line)
|
||||
core.input.trigger(event)
|
||||
if "name" in event:
|
||||
modules[event["name"]] = True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def handle_input(output, update_lock):
|
||||
delay = float(config.get("engine.input_delay", 0.0))
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
if update_lock.acquire(blocking=False) == True:
|
||||
core.event.trigger("update", modules.keys(), force=True)
|
||||
core.event.trigger("draw")
|
||||
update_lock.release()
|
||||
|
||||
def handle_commands(config, update_lock):
|
||||
with CommandSocket() as cmdsocket:
|
||||
poll = select.poll()
|
||||
poll.register(sys.stdin.fileno(), select.POLLIN)
|
||||
poll.register(cmdsocket, select.POLLIN)
|
||||
|
||||
while True:
|
||||
events = poll.poll()
|
||||
tmp, _ = cmdsocket.accept()
|
||||
line = tmp.recv(4096).decode()
|
||||
tmp.close()
|
||||
logging.debug("socket event {}".format(line))
|
||||
process_event(line, config, update_lock)
|
||||
|
||||
modules = {}
|
||||
for fileno, event in events:
|
||||
if fileno == cmdsocket.fileno():
|
||||
tmp, _ = cmdsocket.accept()
|
||||
line = tmp.recv(4096).decode()
|
||||
tmp.close()
|
||||
logging.debug("socket event {}".format(line))
|
||||
else:
|
||||
line = "["
|
||||
while line.startswith("["):
|
||||
line = sys.stdin.readline().strip(",").strip()
|
||||
logging.info("input event: {}".format(line))
|
||||
try:
|
||||
event = json.loads(line)
|
||||
core.input.trigger(event)
|
||||
if "name" in event:
|
||||
modules[event["name"]] = True
|
||||
except ValueError:
|
||||
pass
|
||||
update_lock.acquire()
|
||||
core.event.trigger("update", modules.keys())
|
||||
core.event.trigger("draw")
|
||||
update_lock.release()
|
||||
|
||||
poll.unregister(sys.stdin.fileno())
|
||||
def handle_events(config, update_lock):
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline().strip(",").strip()
|
||||
if line == "[": continue
|
||||
logging.info("input event: {}".format(line))
|
||||
process_event(line, config, update_lock)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
|
||||
def main():
|
||||
global started
|
||||
|
||||
config = core.config.Config(sys.argv[1:])
|
||||
level = logging.DEBUG if config.debug() else logging.ERROR
|
||||
if config.logfile():
|
||||
|
@ -98,10 +102,23 @@ def main():
|
|||
core.input.register(None, core.input.WHEEL_UP, "i3-msg workspace prev_on_output")
|
||||
core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output")
|
||||
|
||||
core.event.trigger("start")
|
||||
started = True
|
||||
|
||||
update_lock = threading.Lock()
|
||||
input_thread = threading.Thread(target=handle_input, args=(output, update_lock, ))
|
||||
input_thread.daemon = True
|
||||
input_thread.start()
|
||||
event_thread = threading.Thread(target=handle_events, args=(config, update_lock, ))
|
||||
event_thread.daemon = True
|
||||
event_thread.start()
|
||||
|
||||
cmd_thread = threading.Thread(target=handle_commands, args=(config, update_lock, ))
|
||||
cmd_thread.daemon = True
|
||||
cmd_thread.start()
|
||||
|
||||
def sig_USR1_handler(signum,stack):
|
||||
if update_lock.acquire(blocking=False) == True:
|
||||
core.event.trigger("update", force=True)
|
||||
core.event.trigger("draw")
|
||||
update_lock.release()
|
||||
|
||||
if config.debug():
|
||||
modules.append(core.module.load("debug", config, theme))
|
||||
|
@ -118,8 +135,7 @@ def main():
|
|||
if util.format.asbool(config.get("engine.collapsible", True)) == True:
|
||||
core.input.register(None, core.input.MIDDLE_MOUSE, output.toggle_minimize)
|
||||
|
||||
core.event.trigger("start")
|
||||
started = True
|
||||
signal.signal(10, sig_USR1_handler)
|
||||
while True:
|
||||
if update_lock.acquire(blocking=False) == True:
|
||||
core.event.trigger("update")
|
||||
|
@ -136,6 +152,7 @@ if __name__ == "__main__":
|
|||
main()
|
||||
except Exception as e:
|
||||
# really basic errors -> make sure these are shown in the status bar by minimal config
|
||||
logging.exception(e)
|
||||
if not started:
|
||||
print("{\"version\":1}")
|
||||
print("[")
|
||||
|
|
|
@ -292,7 +292,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
|||
# TAG-NUM-gHEX
|
||||
mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
|
||||
if not mo:
|
||||
# unparseable. Maybe git-describe is misbehaving?
|
||||
# unparsable. Maybe git-describe is misbehaving?
|
||||
pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
|
||||
return pieces
|
||||
|
||||
|
|
|
@ -71,6 +71,11 @@ class print_usage(argparse.Action):
|
|||
)
|
||||
|
||||
rst = {}
|
||||
|
||||
if self._format == "rst":
|
||||
print(".. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY")
|
||||
print(".. To change this document, please update the docstrings in the individual modules")
|
||||
|
||||
for m in all_modules():
|
||||
try:
|
||||
module_type = "core"
|
||||
|
@ -142,6 +147,13 @@ class Config(util.store.Store):
|
|||
parser = argparse.ArgumentParser(
|
||||
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config-file",
|
||||
action="store",
|
||||
default=None,
|
||||
help="Specify a configuration file to use"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
|
||||
)
|
||||
|
@ -153,7 +165,7 @@ class Config(util.store.Store):
|
|||
default=[],
|
||||
help=PARAMETER_HELP,
|
||||
)
|
||||
parser.add_argument("-t", "--theme", default="default", help=THEME_HELP)
|
||||
parser.add_argument("-t", "--theme", default=None, help=THEME_HELP)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--iconset",
|
||||
|
@ -167,6 +179,13 @@ class Config(util.store.Store):
|
|||
default=[],
|
||||
help="Specify a list of modules to hide when not in warning/error state",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--errorhide",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Specify a list of modules that are hidden when in state error"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--debug", action="store_true", help="Add debug fields to i3 output"
|
||||
)
|
||||
|
@ -191,13 +210,18 @@ class Config(util.store.Store):
|
|||
|
||||
self.__args = parser.parse_args(args)
|
||||
|
||||
for cfg in [
|
||||
"~/.bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status/config",
|
||||
]:
|
||||
if self.__args.config_file:
|
||||
cfg = self.__args.config_file
|
||||
cfg = os.path.expanduser(cfg)
|
||||
self.load_config(cfg)
|
||||
else:
|
||||
for cfg in [
|
||||
"~/.bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status/config",
|
||||
]:
|
||||
cfg = os.path.expanduser(cfg)
|
||||
self.load_config(cfg)
|
||||
|
||||
parameters = [item for sub in self.__args.parameters for item in sub]
|
||||
for param in parameters:
|
||||
|
@ -216,15 +240,24 @@ class Config(util.store.Store):
|
|||
:param filename: path to the file to load
|
||||
"""
|
||||
|
||||
def load_config(self, filename):
|
||||
if os.path.exists(filename):
|
||||
def load_config(self, filename, content=None):
|
||||
if os.path.exists(filename) or content != None:
|
||||
log.info("loading {}".format(filename))
|
||||
tmp = RawConfigParser()
|
||||
tmp.read(u"{}".format(filename))
|
||||
tmp.optionxform = str
|
||||
|
||||
if content:
|
||||
tmp.read_string(content)
|
||||
else:
|
||||
tmp.read(u"{}".format(filename))
|
||||
|
||||
if tmp.has_section("module-parameters"):
|
||||
for key, value in tmp.items("module-parameters"):
|
||||
self.set(key, value)
|
||||
if tmp.has_section("core"):
|
||||
for key, value in tmp.items("core"):
|
||||
self.set(key, value)
|
||||
|
||||
|
||||
"""Returns a list of configured modules
|
||||
|
||||
|
@ -233,7 +266,11 @@ class Config(util.store.Store):
|
|||
"""
|
||||
|
||||
def modules(self):
|
||||
return [item for sub in self.__args.modules for item in sub]
|
||||
list_of_modules = [item for sub in self.__args.modules for item in sub]
|
||||
|
||||
if list_of_modules == []:
|
||||
list_of_modules = util.format.aslist(self.get('modules', []))
|
||||
return list_of_modules
|
||||
|
||||
"""Returns the global update interval
|
||||
|
||||
|
@ -244,6 +281,15 @@ class Config(util.store.Store):
|
|||
def interval(self, default=1):
|
||||
return util.format.seconds(self.get("interval", default))
|
||||
|
||||
"""Returns the global popup menu font size
|
||||
|
||||
:return: popup menu font size
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
def popup_font_size(self, default=12):
|
||||
return util.format.asint(self.get("popup_font_size", default))
|
||||
|
||||
"""Returns whether debug mode is enabled
|
||||
|
||||
:return: True if debug is enabled, False otherwise
|
||||
|
@ -278,7 +324,7 @@ class Config(util.store.Store):
|
|||
"""
|
||||
|
||||
def theme(self):
|
||||
return self.__args.theme
|
||||
return self.__args.theme or self.get("theme") or "default"
|
||||
|
||||
"""Returns the configured iconset name
|
||||
|
||||
|
@ -289,14 +335,21 @@ class Config(util.store.Store):
|
|||
def iconset(self):
|
||||
return self.__args.iconset
|
||||
|
||||
"""Returns which modules should be hidden if their state is not warning/critical
|
||||
"""Returns whether a module should be hidden if their state is not warning/critical
|
||||
|
||||
:return: list of modules to hide automatically
|
||||
:rtype: list of strings
|
||||
:return: True if module should be hidden automatically, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
def autohide(self, name):
|
||||
return name in self.__args.autohide
|
||||
return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", []))
|
||||
|
||||
"""Returns which modules should be hidden if they are in state error
|
||||
|
||||
:return: returns True if name should be hidden, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
def errorhide(self, name):
|
||||
return name in self.__args.errorhide
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -8,6 +8,13 @@ def register(event, callback, *args, **kwargs):
|
|||
|
||||
__callbacks.setdefault(event, []).append(cb)
|
||||
|
||||
def register_exclusive(event, callback, *args, **kwargs):
|
||||
cb = callback
|
||||
if args or kwargs:
|
||||
cb = lambda: callback(*args, **kwargs)
|
||||
|
||||
__callbacks[event] = [cb]
|
||||
|
||||
def unregister(event):
|
||||
if event in __callbacks:
|
||||
del __callbacks[event]
|
||||
|
|
|
@ -10,6 +10,7 @@ MIDDLE_MOUSE = 2
|
|||
RIGHT_MOUSE = 3
|
||||
WHEEL_UP = 4
|
||||
WHEEL_DOWN = 5
|
||||
UPDATE = -1
|
||||
|
||||
|
||||
def button_name(button):
|
||||
|
@ -23,6 +24,8 @@ def button_name(button):
|
|||
return "wheel-up"
|
||||
if button == WHEEL_DOWN:
|
||||
return "wheel-down"
|
||||
if button == UPDATE:
|
||||
return "update"
|
||||
return "n/a"
|
||||
|
||||
|
||||
|
@ -51,10 +54,13 @@ def register(obj, button=None, cmd=None, wait=False):
|
|||
event_id = __event_id(obj.id if obj is not None else "", button)
|
||||
logging.debug("registering callback {}".format(event_id))
|
||||
core.event.unregister(event_id) # make sure there's always only one input event
|
||||
|
||||
if callable(cmd):
|
||||
core.event.register(event_id, cmd)
|
||||
core.event.register_exclusive(event_id, cmd)
|
||||
elif obj and hasattr(obj, cmd) and callable(getattr(obj, cmd)):
|
||||
core.event.register_exclusive(event_id, lambda event: getattr(obj, cmd)(event))
|
||||
else:
|
||||
core.event.register(event_id, lambda event: __execute(event, cmd, wait))
|
||||
core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait))
|
||||
|
||||
|
||||
def trigger(event):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import importlib
|
||||
import importlib.util
|
||||
import logging
|
||||
import threading
|
||||
|
||||
|
@ -17,6 +18,27 @@ except Exception as e:
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def import_user(module_short, config, theme):
|
||||
usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short))
|
||||
if os.path.exists(usermod):
|
||||
if hasattr(importlib, "machinery"):
|
||||
log.debug("importing {} from user via machinery".format(module_short))
|
||||
mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short),
|
||||
os.path.expanduser(usermod)).load_module()
|
||||
return getattr(mod, "Module")(config, theme)
|
||||
else:
|
||||
log.debug("importing {} from user via importlib.util".format(module_short))
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.Module(config, theme)
|
||||
except Exception as e:
|
||||
spec = importlib.util.find_spec("modules.{}".format(module_short), usermod)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.Module(config, theme)
|
||||
raise ImportError("not found")
|
||||
|
||||
"""Loads a module by name
|
||||
|
||||
|
@ -33,20 +55,25 @@ def load(module_name, config=core.config.Config([]), theme=None):
|
|||
error = None
|
||||
module_short, alias = (module_name.split(":") + [module_name])[0:2]
|
||||
config.set("__alias__", alias)
|
||||
for namespace in ["core", "contrib"]:
|
||||
|
||||
try:
|
||||
mod = importlib.import_module("modules.core.{}".format(module_short))
|
||||
log.debug("importing {} from core".format(module_short))
|
||||
return getattr(mod, "Module")(config, theme)
|
||||
except ImportError as e:
|
||||
try:
|
||||
mod = importlib.import_module(
|
||||
"modules.{}.{}".format(namespace, module_short)
|
||||
)
|
||||
log.debug(
|
||||
"importing {} from {}.{}".format(module_short, namespace, module_short)
|
||||
)
|
||||
log.warning("failed to import {} from core: {}".format(module_short, e))
|
||||
mod = importlib.import_module("modules.contrib.{}".format(module_short))
|
||||
log.debug("importing {} from contrib".format(module_short))
|
||||
return getattr(mod, "Module")(config, theme)
|
||||
except ImportError as e:
|
||||
log.debug("failed to import {}: {}".format(module_name, e))
|
||||
error = e
|
||||
log.fatal("failed to import {}: {}".format(module_name, error))
|
||||
return Error(config=config, module=module_name, error=error)
|
||||
try:
|
||||
log.warning("failed to import {} from system: {}".format(module_short, e))
|
||||
return import_user(module_short, config, theme)
|
||||
except ImportError as e:
|
||||
log.fatal("import failed: {}".format(e))
|
||||
log.fatal("failed to import {}".format(module_short))
|
||||
return Error(config=config, module=module_name, error="unable to load module")
|
||||
|
||||
|
||||
class Module(core.input.Object):
|
||||
|
@ -69,6 +96,8 @@ class Module(core.input.Object):
|
|||
self.alias = self.__config.get("__alias__", None)
|
||||
self.id = self.alias if self.alias else self.name
|
||||
self.next_update = None
|
||||
self.minimized = False
|
||||
self.minimized = self.parameter("start-minimized", False)
|
||||
|
||||
self.theme = theme
|
||||
|
||||
|
@ -84,6 +113,15 @@ class Module(core.input.Object):
|
|||
def hidden(self):
|
||||
return False
|
||||
|
||||
"""Override this to show the module even if it normally would be scrolled away
|
||||
|
||||
:return: True if the module should be hidden, False otherwise
|
||||
:rtype: boolean
|
||||
"""
|
||||
|
||||
def scroll(self):
|
||||
return True
|
||||
|
||||
"""Retrieve CLI/configuration parameters for this module. For example, if
|
||||
the module is called "test" and the user specifies "-p test.x=123" on the
|
||||
commandline, using self.parameter("x") retrieves the value 123.
|
||||
|
@ -100,6 +138,8 @@ class Module(core.input.Object):
|
|||
|
||||
for prefix in [self.name, self.module_name, self.alias]:
|
||||
value = self.__config.get("{}.{}".format(prefix, key), value)
|
||||
if self.minimized:
|
||||
value = self.__config.get("{}.minimized.{}".format(prefix, key), value)
|
||||
return value
|
||||
|
||||
"""Set a parameter for this module
|
||||
|
@ -123,7 +163,7 @@ class Module(core.input.Object):
|
|||
|
||||
def update_wrapper(self):
|
||||
if self.background == True:
|
||||
if self.__thread and self.__thread.isAlive():
|
||||
if self.__thread and self.__thread.is_alive():
|
||||
return # skip this update interval
|
||||
self.__thread = threading.Thread(target=self.internal_update, args=(True,))
|
||||
self.__thread.start()
|
||||
|
@ -170,9 +210,9 @@ class Module(core.input.Object):
|
|||
:rtype: bumblebee_status.widget.Widget
|
||||
"""
|
||||
|
||||
def add_widget(self, full_text="", name=None):
|
||||
def add_widget(self, full_text="", name=None, hidden=False):
|
||||
widget_id = "{}::{}".format(self.name, len(self.widgets()))
|
||||
widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id)
|
||||
widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id, hidden=hidden)
|
||||
self.widgets().append(widget)
|
||||
widget.module = self
|
||||
return widget
|
||||
|
@ -265,7 +305,7 @@ class Error(Module):
|
|||
def full_text(self, widget):
|
||||
return "{}: {}".format(self.__module, self.__error)
|
||||
|
||||
"""Overriden state, always returns critical (it *is* an error, after all"""
|
||||
"""Overridden state, always returns critical (it *is* an error, after all"""
|
||||
|
||||
def state(self, widget):
|
||||
return ["critical"]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import sys
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
import core.theme
|
||||
import core.event
|
||||
|
@ -57,6 +58,9 @@ class block(object):
|
|||
def set(self, key, value):
|
||||
self.__attributes[key] = value
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.__attributes.get(key, default)
|
||||
|
||||
def is_pango(self, attr):
|
||||
if isinstance(attr, dict) and "pango" in attr:
|
||||
return True
|
||||
|
@ -91,9 +95,17 @@ class block(object):
|
|||
assign(self.__attributes, result, "background", "bg")
|
||||
|
||||
if "full_text" in self.__attributes:
|
||||
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
|
||||
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
|
||||
self.set("_prefix", prefix)
|
||||
self.set("_suffix", suffix)
|
||||
self.set("_raw", self.get("full_text"))
|
||||
result["full_text"] = self.pangoize(result["full_text"])
|
||||
result["full_text"] = self.__format(self.__attributes["full_text"])
|
||||
|
||||
if "min-width" in self.__attributes and "padding" in self.__attributes:
|
||||
self.set("min-width", self.__format(self.get("min-width")))
|
||||
|
||||
for k in [
|
||||
"name",
|
||||
"instance",
|
||||
|
@ -123,11 +135,8 @@ class block(object):
|
|||
def __format(self, text):
|
||||
if text is None:
|
||||
return None
|
||||
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
|
||||
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
|
||||
self.set("_prefix", prefix)
|
||||
self.set("_suffix", suffix)
|
||||
self.set("_raw", text)
|
||||
prefix = self.get("_prefix")
|
||||
suffix = self.get("_suffix")
|
||||
return "{}{}{}".format(prefix, text, suffix)
|
||||
|
||||
|
||||
|
@ -137,10 +146,14 @@ class i3(object):
|
|||
self.__content = {}
|
||||
self.__theme = theme
|
||||
self.__config = config
|
||||
self.__offset = 0
|
||||
self.__lock = threading.Lock()
|
||||
core.event.register("update", self.update)
|
||||
core.event.register("start", self.draw, "start")
|
||||
core.event.register("draw", self.draw, "statusline")
|
||||
core.event.register("stop", self.draw, "stop")
|
||||
core.event.register("output.scroll-left", self.scroll_left)
|
||||
core.event.register("output.scroll-right", self.scroll_right)
|
||||
|
||||
def content(self):
|
||||
return self.__content
|
||||
|
@ -158,18 +171,25 @@ class i3(object):
|
|||
def toggle_minimize(self, event):
|
||||
widget_id = event["instance"]
|
||||
|
||||
for module in self.__modules:
|
||||
if module.widget(widget_id=widget_id) and util.format.asbool(module.parameter("minimize", False)) == True:
|
||||
# this module can customly minimize
|
||||
module.minimized = not module.minimized
|
||||
return
|
||||
|
||||
if widget_id in self.__content:
|
||||
self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"]
|
||||
|
||||
def draw(self, what, args=None):
|
||||
cb = getattr(self, what)
|
||||
data = cb(args) if args else cb()
|
||||
if "blocks" in data:
|
||||
sys.stdout.write(json.dumps(data["blocks"], default=dump_json))
|
||||
if "suffix" in data:
|
||||
sys.stdout.write(data["suffix"])
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
with self.__lock:
|
||||
cb = getattr(self, what)
|
||||
data = cb(args) if args else cb()
|
||||
if "blocks" in data:
|
||||
sys.stdout.write(json.dumps(data["blocks"], default=dump_json))
|
||||
if "suffix" in data:
|
||||
sys.stdout.write(data["suffix"])
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def start(self):
|
||||
return {
|
||||
|
@ -206,29 +226,57 @@ class i3(object):
|
|||
blk.set("__state", state)
|
||||
return blk
|
||||
|
||||
def scroll_left(self):
|
||||
if self.__offset > 0:
|
||||
self.__offset -= 1
|
||||
|
||||
def scroll_right(self):
|
||||
self.__offset += 1
|
||||
|
||||
def blocks(self, module):
|
||||
blocks = []
|
||||
if module.minimized:
|
||||
blocks.extend(self.separator_block(module, module.widgets()[0]))
|
||||
blocks.append(self.__content_block(module, module.widgets()[0]))
|
||||
self.__widgetcount += 1
|
||||
return blocks
|
||||
|
||||
width = self.__config.get("output.width", 0)
|
||||
for widget in module.widgets():
|
||||
if module.scroll() == True and width > 0:
|
||||
self.__widgetcount += 1
|
||||
if self.__widgetcount-1 < self.__offset:
|
||||
continue
|
||||
if self.__widgetcount-1 >= self.__offset + width:
|
||||
continue
|
||||
if widget.module and self.__config.autohide(widget.module.name):
|
||||
if not any(
|
||||
state in widget.state() for state in ["warning", "critical"]
|
||||
state in widget.state() for state in ["warning", "critical", "no-autohide"]
|
||||
):
|
||||
continue
|
||||
if module.hidden():
|
||||
continue
|
||||
if widget.hidden:
|
||||
continue
|
||||
if "critical" in widget.state() and self.__config.errorhide(widget.module.name):
|
||||
continue
|
||||
blocks.extend(self.separator_block(module, widget))
|
||||
blocks.append(self.__content_block(module, widget))
|
||||
core.event.trigger("next-widget")
|
||||
core.event.trigger("output.done", self.__offset, self.__widgetcount)
|
||||
return blocks
|
||||
|
||||
# TODO: only updates full text, not the state!?
|
||||
def update(self, affected_modules=None, redraw_only=False):
|
||||
def update(self, affected_modules=None, redraw_only=False, force=False):
|
||||
with self.__lock:
|
||||
self.update2(affected_modules, redraw_only, force)
|
||||
|
||||
def update2(self, affected_modules=None, redraw_only=False, force=False):
|
||||
now = time.time()
|
||||
for module in self.__modules:
|
||||
if affected_modules and not module.id in affected_modules:
|
||||
continue
|
||||
if not affected_modules and module.next_update:
|
||||
if now < module.next_update:
|
||||
if now < module.next_update and not force:
|
||||
continue
|
||||
|
||||
if not redraw_only:
|
||||
|
@ -246,6 +294,7 @@ class i3(object):
|
|||
|
||||
def statusline(self):
|
||||
blocks = []
|
||||
self.__widgetcount = 0
|
||||
for module in self.__modules:
|
||||
blocks.extend(self.blocks(module))
|
||||
return {"blocks": blocks, "suffix": ","}
|
||||
|
|
|
@ -7,16 +7,27 @@ import glob
|
|||
|
||||
import core.event
|
||||
import util.algorithm
|
||||
import util.xresources
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
THEME_BASE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
PATHS = [
|
||||
".",
|
||||
os.path.join(THEME_BASE_DIR, "../../themes"),
|
||||
os.path.join(THEME_BASE_DIR, "../../themes")
|
||||
]
|
||||
|
||||
if os.environ.get("XDG_DATA_DIRS"):
|
||||
PATHS.extend([
|
||||
os.path.join(p, "bumblebee-status/themes") for p in os.environ["XDG_DATA_DIRS"].split(":")
|
||||
])
|
||||
|
||||
PATHS.extend([
|
||||
os.path.expanduser("~/.config/bumblebee-status/themes"),
|
||||
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
|
||||
]
|
||||
os.path.expanduser("~/.local/pipx/venvs/bumblebee-status/share/bumblebee-status/themes"), # PIPX
|
||||
"/usr/share/bumblebee-status/themes",
|
||||
])
|
||||
|
||||
|
||||
def themes():
|
||||
|
@ -89,13 +100,21 @@ class Theme(object):
|
|||
try:
|
||||
if isinstance(name, dict):
|
||||
return name
|
||||
|
||||
result = {}
|
||||
if name.lower() == "wal":
|
||||
wal = self.__load_json("~/.cache/wal/colors.json")
|
||||
result = {}
|
||||
for field in ["special", "colors"]:
|
||||
for key in wal.get(field, {}):
|
||||
result[key] = wal[field][key]
|
||||
return result
|
||||
if name.lower() == "xresources":
|
||||
for key in ("background", "foreground"):
|
||||
result[key] = xresources.query(key)
|
||||
for i in range(16):
|
||||
key = color + str(i)
|
||||
result[key] = xresources.query(key)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
log.error("failed to load colors: {}", e)
|
||||
|
||||
|
|
|
@ -10,12 +10,13 @@ log = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Widget(util.store.Store, core.input.Object):
|
||||
def __init__(self, full_text="", name=None, widget_id=None):
|
||||
def __init__(self, full_text="", name=None, widget_id=None, hidden=False):
|
||||
super(Widget, self).__init__()
|
||||
self.__full_text = full_text
|
||||
self.module = None
|
||||
self.name = name
|
||||
self.id = widget_id or self.id
|
||||
self.hidden = hidden
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
"""get volume level or control it
|
||||
|
||||
Requires the following executable:
|
||||
* amixer
|
||||
|
||||
Parameters:
|
||||
* amixer.card: Sound Card to use (default is 0)
|
||||
* amixer.device: Device to use (default is Master,0)
|
||||
* amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%)
|
||||
|
||||
contributed by `zetxx <https://github.com/zetxx>`_ - many thanks!
|
||||
|
||||
input handling contributed by `ardadem <https://github.com/ardadem>`_ - many thanks!
|
||||
|
||||
multiple audio cards contributed by `hugoeustaquio <https://github.com/hugoeustaquio>`_ - many thanks!
|
||||
"""
|
||||
import re
|
||||
|
||||
|
@ -23,6 +29,7 @@ class Module(core.module.Module):
|
|||
|
||||
self.__level = "n/a"
|
||||
self.__muted = True
|
||||
self.__card = self.parameter("card", "0")
|
||||
self.__device = self.parameter("device", "Master,0")
|
||||
self.__change = util.format.asint(
|
||||
self.parameter("percent_change", "4%").strip("%"), 0, 100
|
||||
|
@ -59,7 +66,7 @@ class Module(core.module.Module):
|
|||
self.set_parameter("{}%-".format(self.__change))
|
||||
|
||||
def set_parameter(self, parameter):
|
||||
util.cli.execute("amixer -q set {} {}".format(self.__device, parameter))
|
||||
util.cli.execute("amixer -c {} -q set {} {}".format(self.__card, self.__device, parameter))
|
||||
|
||||
def volume(self, widget):
|
||||
if self.__level == "n/a":
|
||||
|
@ -76,7 +83,7 @@ class Module(core.module.Module):
|
|||
def update(self):
|
||||
try:
|
||||
self.__level = util.cli.execute(
|
||||
"amixer get {}".format(self.__device)
|
||||
"amixer -c {} get {}".format(self.__card, self.__device)
|
||||
)
|
||||
except Exception as e:
|
||||
self.__level = "n/a"
|
||||
|
|
|
@ -14,6 +14,7 @@ import threading
|
|||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
@ -56,6 +57,8 @@ class Module(core.module.Module):
|
|||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||
self.__thread = None
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE,
|
||||
cmd=self.updates)
|
||||
|
||||
def updates(self, widget):
|
||||
if widget.get("error"):
|
||||
|
@ -65,7 +68,7 @@ class Module(core.module.Module):
|
|||
)
|
||||
|
||||
def update(self):
|
||||
if self.__thread and self.__thread.isAlive():
|
||||
if self.__thread and self.__thread.is_alive():
|
||||
return
|
||||
|
||||
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))
|
||||
|
|
|
@ -8,6 +8,9 @@ saved screen layout as well as toggle on/off individual connected displays.
|
|||
Parameters:
|
||||
* No configuration parameters
|
||||
|
||||
Requires the following python modules:
|
||||
* tkinter
|
||||
|
||||
Requires the following executable:
|
||||
* arandr
|
||||
* xrandr
|
||||
|
@ -54,7 +57,7 @@ class Module(core.module.Module):
|
|||
def activate_layout(layout_path):
|
||||
log.debug("activating layout")
|
||||
log.debug(layout_path)
|
||||
execute(layout_path)
|
||||
execute(layout_path, ignore_errors=True)
|
||||
|
||||
def popup(self, widget):
|
||||
"""Create Popup that allows the user to control their displays in one
|
||||
|
@ -64,7 +67,7 @@ class Module(core.module.Module):
|
|||
menu = popup.menu()
|
||||
menu.add_menuitem(
|
||||
"arandr",
|
||||
callback=partial(execute, self.manager)
|
||||
callback=partial(execute, self.manager, ignore_errors=True)
|
||||
)
|
||||
menu.add_separator()
|
||||
|
||||
|
@ -105,11 +108,12 @@ class Module(core.module.Module):
|
|||
if count_on == 1:
|
||||
log.info("attempted to turn off last display")
|
||||
return
|
||||
execute("{} --output {} --off".format(self.toggle_cmd, display))
|
||||
execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True)
|
||||
else:
|
||||
log.debug("toggling on {}".format(display))
|
||||
execute(
|
||||
"{} --output {} --auto".format(self.toggle_cmd, display)
|
||||
"{} --output {} --auto".format(self.toggle_cmd, display),
|
||||
ignore_errors=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -120,7 +124,7 @@ class Module(core.module.Module):
|
|||
connected).
|
||||
"""
|
||||
displays = {}
|
||||
for line in execute("xrandr -q").split("\n"):
|
||||
for line in execute("xrandr -q", ignore_errors=True).split("\n"):
|
||||
if "connected" not in line:
|
||||
continue
|
||||
is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line))
|
||||
|
@ -136,16 +140,19 @@ class Module(core.module.Module):
|
|||
def _get_layouts():
|
||||
"""Loads and parses the arandr screen layout scripts."""
|
||||
layouts = {}
|
||||
for filename in os.listdir(__screenlayout_dir__):
|
||||
if fnmatch.fnmatch(filename, '*.sh'):
|
||||
fullpath = os.path.join(__screenlayout_dir__, filename)
|
||||
with open(fullpath, "r") as file:
|
||||
for line in file:
|
||||
s_line = line.strip()
|
||||
if "xrandr" not in s_line:
|
||||
continue
|
||||
displays_in_file = Module._parse_layout(line)
|
||||
layouts[filename] = displays_in_file
|
||||
try:
|
||||
for filename in os.listdir(__screenlayout_dir__):
|
||||
if fnmatch.fnmatch(filename, '*.sh'):
|
||||
fullpath = os.path.join(__screenlayout_dir__, filename)
|
||||
with open(fullpath, "r") as file:
|
||||
for line in file:
|
||||
s_line = line.strip()
|
||||
if "xrandr" not in s_line:
|
||||
continue
|
||||
displays_in_file = Module._parse_layout(line)
|
||||
layouts[filename] = displays_in_file
|
||||
except Exception as e:
|
||||
log.error(str(e))
|
||||
return layouts
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -7,6 +7,7 @@ contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
|||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
@ -35,12 +36,13 @@ class Module(core.module.Module):
|
|||
|
||||
def update(self):
|
||||
self.__error = False
|
||||
sleep(1)
|
||||
code, result = util.cli.execute(
|
||||
"checkupdates", ignore_errors=True, return_exitcode=True
|
||||
)
|
||||
|
||||
if code == 0:
|
||||
self.__packages = len(result.split("\n"))
|
||||
self.__packages = len(result.strip().split("\n"))
|
||||
elif code == 2:
|
||||
self.__packages = 0
|
||||
else:
|
||||
|
|
1
bumblebee_status/modules/contrib/arch_update.py
Symbolic link
1
bumblebee_status/modules/contrib/arch_update.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
arch-update.py
|
57
bumblebee_status/modules/contrib/aur-update.py
Normal file
57
bumblebee_status/modules/contrib/aur-update.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""Check updates for AUR.
|
||||
|
||||
Requires the following executable:
|
||||
* yay (https://github.com/Jguer/yay)
|
||||
|
||||
contributed by `ishaanbhimwal <https://github.com/ishaanbhimwal>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
||||
self.background = True
|
||||
self.__packages = 0
|
||||
self.__error = False
|
||||
|
||||
@property
|
||||
def __format(self):
|
||||
return self.parameter("format", "Update AUR: {}")
|
||||
|
||||
def utilization(self, widget):
|
||||
return self.__format.format(self.__packages)
|
||||
|
||||
def hidden(self):
|
||||
return self.__packages == 0
|
||||
|
||||
def update(self):
|
||||
self.__error = False
|
||||
code, result = util.cli.execute(
|
||||
"yay -Qum", ignore_errors=True, return_exitcode=True
|
||||
)
|
||||
|
||||
if code == 0:
|
||||
if result == "":
|
||||
self.__packages = 0
|
||||
else:
|
||||
self.__packages = len(result.strip().split("\n"))
|
||||
else:
|
||||
self.__error = True
|
||||
logging.error("aur-update exited with {}: {}".format(code, result))
|
||||
|
||||
def state(self, widget):
|
||||
if self.__error:
|
||||
return "warning"
|
||||
return self.threshold_state(self.__packages, 1, 100)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -68,7 +68,7 @@ class UPowerManager:
|
|||
isPresent = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "IsPresent"
|
||||
)
|
||||
isRechargable = battery_proxy_interface.Get(
|
||||
isRechargeable = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "IsRechargeable"
|
||||
)
|
||||
online = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Online")
|
||||
|
@ -128,7 +128,7 @@ class UPowerManager:
|
|||
"HasHistory": hasHistory,
|
||||
"HasStatistics": hasStatistics,
|
||||
"IsPresent": isPresent,
|
||||
"IsRechargeable": isRechargable,
|
||||
"IsRechargeable": isRechargeable,
|
||||
"Online": online,
|
||||
"PowerSupply": powersupply,
|
||||
"Capacity": capacity,
|
||||
|
@ -207,6 +207,14 @@ class UPowerManager:
|
|||
data = upower_interface.GetTotal()
|
||||
return data
|
||||
|
||||
def is_battery_present(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
return bool(
|
||||
battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "IsPresent")
|
||||
)
|
||||
|
||||
def is_loading(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
@ -259,6 +267,11 @@ class Module(core.module.Module):
|
|||
widget.set("capacity", -1)
|
||||
widget.set("ac", False)
|
||||
output = "n/a"
|
||||
if not self.power.is_battery_present(self.device):
|
||||
widget.set("ac", True)
|
||||
widget.set("capacity", 100)
|
||||
output = "ac"
|
||||
return output
|
||||
try:
|
||||
capacity = int(self.power.get_device_percentage(self.device))
|
||||
capacity = capacity if capacity < 100 else 100
|
||||
|
@ -298,11 +311,6 @@ class Module(core.module.Module):
|
|||
if capacity < 0:
|
||||
return ["critical", "unknown"]
|
||||
|
||||
if capacity < int(self.parameter("critical", 10)):
|
||||
state.append("critical")
|
||||
elif capacity < int(self.parameter("warning", 20)):
|
||||
state.append("warning")
|
||||
|
||||
if widget.get("ac"):
|
||||
state.append("AC")
|
||||
else:
|
||||
|
@ -328,6 +336,16 @@ class Module(core.module.Module):
|
|||
state.append("charged")
|
||||
else:
|
||||
state.append("charging")
|
||||
if (
|
||||
capacity < int(self.parameter("critical", 10))
|
||||
and self.power.get_state(self.device) == "Discharging"
|
||||
):
|
||||
state.append("critical")
|
||||
elif (
|
||||
capacity < int(self.parameter("warning", 20))
|
||||
and self.power.get_state(self.device) == "Discharging"
|
||||
):
|
||||
state.append("warning")
|
||||
return state
|
||||
|
||||
|
||||
|
|
|
@ -130,10 +130,19 @@ class Module(core.module.Module):
|
|||
log.debug("adding new widget for {}".format(battery))
|
||||
widget = self.add_widget(full_text=self.capacity, name=battery)
|
||||
|
||||
for w in self.widgets():
|
||||
if util.format.asbool(self.parameter("decorate", True)) == False:
|
||||
try:
|
||||
with open("/sys/class/power_supply/{}/model_name".format(battery)) as f:
|
||||
widget.set("pen", ("Pen" in f.read().strip()))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if util.format.asbool(self.parameter("decorate", True)) == False:
|
||||
for widget in self.widgets():
|
||||
widget.set("theme.exclude", "suffix")
|
||||
|
||||
def hidden(self):
|
||||
return len(self._batteries) == 0
|
||||
|
||||
def ac(self, widget):
|
||||
return "ac"
|
||||
|
||||
|
@ -144,15 +153,16 @@ class Module(core.module.Module):
|
|||
capacity = self.__manager.capacity(widget.name)
|
||||
widget.set("capacity", capacity)
|
||||
widget.set("ac", self.__manager.isac_any(self._batteries))
|
||||
widget.set("theme.minwidth", "100%")
|
||||
|
||||
# Read power conumption
|
||||
if util.format.asbool(self.parameter("showpowerconsumption", False)):
|
||||
output = "{}% ({})".format(
|
||||
capacity, self.__manager.consumption(widget.name)
|
||||
)
|
||||
else:
|
||||
elif capacity < 100:
|
||||
output = "{}%".format(capacity)
|
||||
else:
|
||||
output = ""
|
||||
|
||||
if (
|
||||
util.format.asbool(self.parameter("showremaining", True))
|
||||
|
@ -164,6 +174,16 @@ class Module(core.module.Module):
|
|||
output, util.format.duration(remaining, compact=True, unit=True)
|
||||
)
|
||||
|
||||
# if bumblebee.util.asbool(self.parameter("rate", True)):
|
||||
# try:
|
||||
# with open("{}/power_now".format(widget.name)) as f:
|
||||
# rate = (float(f.read())/1000000)
|
||||
# if rate > 0:
|
||||
# output = "{} {:.2f}w".format(output, rate)
|
||||
# except Exception:
|
||||
# pass
|
||||
|
||||
|
||||
if util.format.asbool(self.parameter("showdevice", False)):
|
||||
output = "{} ({})".format(output, widget.name)
|
||||
|
||||
|
@ -173,15 +193,13 @@ class Module(core.module.Module):
|
|||
state = []
|
||||
capacity = widget.get("capacity")
|
||||
|
||||
if widget.get("pen"):
|
||||
state.append("PEN")
|
||||
|
||||
if capacity < 0:
|
||||
log.debug("battery state: {}".format(state))
|
||||
return ["critical", "unknown"]
|
||||
|
||||
if capacity < int(self.parameter("critical", 10)):
|
||||
state.append("critical")
|
||||
elif capacity < int(self.parameter("warning", 20)):
|
||||
state.append("warning")
|
||||
|
||||
if widget.get("ac"):
|
||||
state.append("AC")
|
||||
else:
|
||||
|
@ -189,16 +207,10 @@ class Module(core.module.Module):
|
|||
charge = self.__manager.charge_any(self._batteries)
|
||||
else:
|
||||
charge = self.__manager.charge(widget.name)
|
||||
if charge == "Discharging":
|
||||
if charge in ["Discharging", "Unknown"]:
|
||||
state.append(
|
||||
"discharging-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
elif charge == "Unknown":
|
||||
state.append(
|
||||
"unknown-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
min([5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
@ -206,6 +218,18 @@ class Module(core.module.Module):
|
|||
state.append("charged")
|
||||
else:
|
||||
state.append("charging")
|
||||
|
||||
if (
|
||||
capacity < int(self.parameter("critical", 10))
|
||||
and self.__manager.charge_any(self._batteries) == "Discharging"
|
||||
):
|
||||
state.append("critical")
|
||||
elif (
|
||||
capacity < int(self.parameter("warning", 20))
|
||||
and self.__manager.charge_any(self._batteries) == "Discharging"
|
||||
):
|
||||
state.append("warning")
|
||||
|
||||
return state
|
||||
|
||||
|
||||
|
|
1
bumblebee_status/modules/contrib/battery_upower.py
Symbolic link
1
bumblebee_status/modules/contrib/battery_upower.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
battery-upower.py
|
|
@ -1,4 +1,4 @@
|
|||
"""Displays bluetooth status (Bluez). Left mouse click launches manager app,
|
||||
"""Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`,
|
||||
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state.
|
||||
|
||||
Parameters:
|
||||
|
@ -75,19 +75,15 @@ class Module(core.module.Module):
|
|||
|
||||
def popup(self, widget):
|
||||
"""Show a popup menu."""
|
||||
menu = util.popup.PopupMenu()
|
||||
menu = util.popup.menu(self.__config)
|
||||
if self._status == "On":
|
||||
menu.add_menuitem("Disable Bluetooth")
|
||||
menu.add_menuitem("Disable Bluetooth", callback=self._toggle)
|
||||
elif self._status == "Off":
|
||||
menu.add_menuitem("Enable Bluetooth")
|
||||
menu.add_menuitem("Enable Bluetooth", callback=self._toggle)
|
||||
else:
|
||||
return
|
||||
|
||||
# show menu and get return code
|
||||
ret = menu.show(widget)
|
||||
if ret == 0:
|
||||
# first (and only) item selected.
|
||||
self._toggle()
|
||||
menu.show(widget)
|
||||
|
||||
def _toggle(self, widget=None):
|
||||
"""Toggle bluetooth state."""
|
||||
|
@ -106,7 +102,7 @@ class Module(core.module.Module):
|
|||
)
|
||||
|
||||
logging.debug("bt: toggling bluetooth")
|
||||
util.cli.execute(cmd)
|
||||
util.cli.execute(cmd, ignore_errors=True)
|
||||
|
||||
def state(self, widget):
|
||||
"""Get current state."""
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""Displays bluetooth status. Left mouse click launches manager app,
|
||||
"""Displays bluetooth status. Left mouse click launches manager app `blueman-manager`,
|
||||
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and
|
||||
python-dbus to count the number of connections
|
||||
|
||||
|
@ -8,7 +8,6 @@ Parameters:
|
|||
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
@ -22,7 +21,6 @@ import core.input
|
|||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.status))
|
||||
|
@ -37,7 +35,7 @@ class Module(core.module.Module):
|
|||
|
||||
def status(self, widget):
|
||||
"""Get status."""
|
||||
return self._status
|
||||
return self._status if self._status.isdigit() and int(self._status) > 1 else ""
|
||||
|
||||
def update(self):
|
||||
"""Update current state."""
|
||||
|
@ -46,7 +44,7 @@ class Module(core.module.Module):
|
|||
)
|
||||
if state > 0:
|
||||
connected_devices = self.get_connected_devices()
|
||||
self._status = "On - {}".format(connected_devices)
|
||||
self._status = "{}".format(connected_devices)
|
||||
else:
|
||||
self._status = "Off"
|
||||
adapters_cmd = "rfkill list | grep Bluetooth"
|
||||
|
@ -58,31 +56,23 @@ class Module(core.module.Module):
|
|||
|
||||
def _toggle(self, widget=None):
|
||||
"""Toggle bluetooth state."""
|
||||
if "On" in self._status:
|
||||
state = "false"
|
||||
else:
|
||||
state = "true"
|
||||
|
||||
cmd = (
|
||||
"dbus-send --system --print-reply --dest=org.blueman.Mechanism /org/blueman/mechanism org.blueman.Mechanism.SetRfkillState boolean:%s"
|
||||
% state
|
||||
)
|
||||
|
||||
logging.debug("bt: toggling bluetooth")
|
||||
core.util.execute(cmd)
|
||||
|
||||
SetRfkillState = self._bus.get_object("org.blueman.Mechanism", "/org/blueman/mechanism").get_dbus_method("SetRfkillState", dbus_interface="org.blueman.Mechanism")
|
||||
SetRfkillState(self._status == "Off")
|
||||
|
||||
def state(self, widget):
|
||||
"""Get current state."""
|
||||
state = []
|
||||
|
||||
if self._status == "No Adapter Found":
|
||||
if self._status in [ "No Adapter Found", "Off" ]:
|
||||
state.append("critical")
|
||||
elif self._status == "On - 0":
|
||||
state.append("warning")
|
||||
elif "On" in self._status and not (self._status == "On - 0"):
|
||||
state.append("ON")
|
||||
elif self._status == "0":
|
||||
state.append("enabled")
|
||||
else:
|
||||
state.append("critical")
|
||||
state.append("connected")
|
||||
state.append("good")
|
||||
|
||||
return state
|
||||
|
||||
def get_connected_devices(self):
|
||||
|
@ -92,12 +82,8 @@ class Module(core.module.Module):
|
|||
).GetManagedObjects()
|
||||
for path, interfaces in objects.items():
|
||||
if "org.bluez.Device1" in interfaces:
|
||||
if dbus.Interface(
|
||||
self._bus.get_object("org.bluez", path),
|
||||
"org.freedesktop.DBus.Properties",
|
||||
).Get("org.bluez.Device1", "Connected"):
|
||||
if dbus.Interface(self._bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties", ).Get("org.bluez.Device1", "Connected"):
|
||||
devices += 1
|
||||
return devices
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
97
bumblebee_status/modules/contrib/blugon.py
Normal file
97
bumblebee_status/modules/contrib/blugon.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
"""Displays temperature of blugon and Controls it.
|
||||
|
||||
Use wheel up and down to change temperature, middle click to toggle and right click to reset temperature.
|
||||
|
||||
Default Values:
|
||||
* Minimum temperature: 1000 (red)
|
||||
* Maximum temperature: 20000 (blue)
|
||||
* Default temperature: 6600
|
||||
|
||||
Requires the following executable:
|
||||
* blugon
|
||||
|
||||
Parameters:
|
||||
* blugon.step: The amount of increase/decrease on scroll (default: 200)
|
||||
|
||||
contributed by `DTan13 <https://github.com/DTan13>`
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
self.__state = True
|
||||
self.__default = 6600
|
||||
self.__step = (
|
||||
util.format.asint(self.parameter("step")) if self.parameter("step") else 200
|
||||
)
|
||||
self.__max, self.__min = 20000, 1000
|
||||
|
||||
file = open(os.path.expanduser("~/.config/blugon/current"))
|
||||
self.__current = int(float(file.read()))
|
||||
|
||||
events = [
|
||||
{
|
||||
"type": "toggle",
|
||||
"action": self.toggle,
|
||||
"button": core.input.MIDDLE_MOUSE,
|
||||
},
|
||||
{
|
||||
"type": "blue",
|
||||
"action": self.blue,
|
||||
"button": core.input.WHEEL_UP,
|
||||
},
|
||||
{
|
||||
"type": "red",
|
||||
"action": self.red,
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
},
|
||||
{
|
||||
"type": "reset",
|
||||
"action": self.reset,
|
||||
"button": core.input.RIGHT_MOUSE,
|
||||
},
|
||||
]
|
||||
|
||||
for event in events:
|
||||
core.input.register(self, button=event["button"], cmd=event["action"])
|
||||
|
||||
def set_temp(self):
|
||||
temp = self.__current if self.__state else self.__default
|
||||
util.cli.execute("blugon --setcurrent={}".format(temp))
|
||||
|
||||
def full_text(self, widget):
|
||||
return self.__current if self.__state else self.__default
|
||||
|
||||
def state(self, widget):
|
||||
if not self.__state:
|
||||
return ["critical"]
|
||||
|
||||
def toggle(self, event):
|
||||
self.__state = not self.__state
|
||||
self.set_temp()
|
||||
|
||||
def reset(self, event):
|
||||
self.__current = 6600
|
||||
self.set_temp()
|
||||
|
||||
def blue(self, event):
|
||||
if self.__state and (self.__current < self.__max):
|
||||
self.__current += self.__step
|
||||
self.set_temp()
|
||||
|
||||
def red(self, event):
|
||||
if self.__state and (self.__current > self.__min):
|
||||
self.__current -= self.__step
|
||||
self.set_temp()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
"""Displays the brightness of a display
|
||||
|
||||
The following executables can be used if `use_acpi` is not enabled:
|
||||
* brightnessctl
|
||||
* light
|
||||
* xbacklight
|
||||
|
||||
Parameters:
|
||||
* brightness.step: The amount of increase/decrease on scroll in % (defaults to 2)
|
||||
* brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true
|
||||
|
|
|
@ -24,9 +24,9 @@ Parameters:
|
|||
* cpu2.fanspeed
|
||||
* cpu2.colored: 1 for colored per core load graph, 0 for mono (default)
|
||||
* cpu2.temp_pattern: pattern to look for in the output of 'sensors -u';
|
||||
required if cpu2.temp widged is used
|
||||
required if cpu2.temp widget is used
|
||||
* cpu2.fan_pattern: pattern to look for in the output of 'sensors -u';
|
||||
required if cpu2.fanspeed widged is used
|
||||
required if cpu2.fanspeed widget is used
|
||||
|
||||
Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're
|
||||
lacking the aforementioned pattern settings or they have wrong values.
|
||||
|
|
158
bumblebee_status/modules/contrib/cpu3.py
Normal file
158
bumblebee_status/modules/contrib/cpu3.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
"""Multiwidget CPU module
|
||||
|
||||
Can display any combination of:
|
||||
|
||||
* max CPU frequency
|
||||
* total CPU load in percents (integer value)
|
||||
* per-core CPU load as graph - either mono or colored
|
||||
* CPU temperature (in Celsius degrees)
|
||||
* CPU fan speed
|
||||
|
||||
Requirements:
|
||||
|
||||
* the psutil Python module for the first three items from the list above
|
||||
* sensors executable for the rest
|
||||
|
||||
Parameters:
|
||||
* cpu3.layout: Space-separated list of widgets to add.
|
||||
Possible widgets are:
|
||||
|
||||
* cpu3.maxfreq
|
||||
* cpu3.cpuload
|
||||
* cpu3.coresload
|
||||
* cpu3.temp
|
||||
* cpu3.fanspeed
|
||||
* cpu3.colored: 1 for colored per core load graph, 0 for mono (default)
|
||||
* cpu3.temp_json: json path to look for in the output of 'sensors -j';
|
||||
required if cpu3.temp widget is used
|
||||
* cpu3.fan_json: json path to look for in the output of 'sensors -j';
|
||||
required if cpu3.fanspeed widget is used
|
||||
|
||||
Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're
|
||||
lacking the aforementioned json path settings or they have wrong values.
|
||||
|
||||
Example json paths:
|
||||
* `cpu3.temp_json="coretemp-isa-0000.Package id 0.temp1_input"`
|
||||
* `cpu3.fan_json="thinkpad-isa-0000.fan1.fan1_input"`
|
||||
|
||||
contributed by `SuperQ <https://github.com/SuperQ>`
|
||||
based on cpu2 by `<somospocos <https://github.com/somospocos>`
|
||||
"""
|
||||
|
||||
import json
|
||||
import psutil
|
||||
|
||||
import core.module
|
||||
|
||||
import util.cli
|
||||
import util.graph
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__layout = self.parameter(
|
||||
"layout", "cpu3.maxfreq cpu3.cpuload cpu3.coresload cpu3.temp cpu3.fanspeed"
|
||||
)
|
||||
self.__widget_names = self.__layout.split()
|
||||
self.__colored = util.format.asbool(self.parameter("colored", False))
|
||||
for widget_name in self.__widget_names:
|
||||
if widget_name == "cpu3.maxfreq":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.maxfreq)
|
||||
widget.set("type", "freq")
|
||||
elif widget_name == "cpu3.cpuload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.cpuload)
|
||||
widget.set("type", "load")
|
||||
elif widget_name == "cpu3.coresload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.coresload)
|
||||
widget.set("type", "loads")
|
||||
elif widget_name == "cpu3.temp":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.temp)
|
||||
widget.set("type", "temp")
|
||||
elif widget_name == "cpu3.fanspeed":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.fanspeed)
|
||||
widget.set("type", "fan")
|
||||
if self.__colored:
|
||||
widget.set("pango", True)
|
||||
self.__temp_json = self.parameter("temp_json")
|
||||
if self.__temp_json is None:
|
||||
self.__temp = "n/a"
|
||||
self.__fan_json = self.parameter("fan_json")
|
||||
if self.__fan_json is None:
|
||||
self.__fan = "n/a"
|
||||
# maxfreq is loaded only once at startup
|
||||
if "cpu3.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
|
||||
def maxfreq(self, _):
|
||||
return "{:.2f}GHz".format(self.__maxfreq)
|
||||
|
||||
def cpuload(self, _):
|
||||
return "{:>3}%".format(self.__cpuload)
|
||||
|
||||
def add_color(self, bar):
|
||||
"""add color as pango markup to a bar"""
|
||||
if bar in ["▁", "▂"]:
|
||||
color = self.theme.color("green", "green")
|
||||
elif bar in ["▃", "▄"]:
|
||||
color = self.theme.color("yellow", "yellow")
|
||||
elif bar in ["▅", "▆"]:
|
||||
color = self.theme.color("orange", "orange")
|
||||
elif bar in ["▇", "█"]:
|
||||
color = self.theme.color("red", "red")
|
||||
colored_bar = '<span foreground="{}">{}</span>'.format(color, bar)
|
||||
return colored_bar
|
||||
|
||||
def coresload(self, _):
|
||||
mono_bars = [util.graph.hbar(x) for x in self.__coresload]
|
||||
if not self.__colored:
|
||||
return "".join(mono_bars)
|
||||
colored_bars = [self.add_color(x) for x in mono_bars]
|
||||
return "".join(colored_bars)
|
||||
|
||||
def temp(self, _):
|
||||
if self.__temp == "n/a" or self.__temp == 0:
|
||||
return "n/a"
|
||||
return "{}°C".format(self.__temp)
|
||||
|
||||
def fanspeed(self, _):
|
||||
if self.__fanspeed == "n/a":
|
||||
return "n/a"
|
||||
return "{}RPM".format(self.__fanspeed)
|
||||
|
||||
def _parse_sensors_output(self):
|
||||
output = util.cli.execute("sensors -j")
|
||||
json_data = json.loads(output)
|
||||
|
||||
temp = "n/a"
|
||||
fan = "n/a"
|
||||
temp_json = json_data
|
||||
fan_json = json_data
|
||||
for path in self.__temp_json.split('.'):
|
||||
temp_json = temp_json[path]
|
||||
for path in self.__fan_json.split('.'):
|
||||
fan_json = fan_json[path]
|
||||
if temp_json is not None:
|
||||
temp = float(temp_json)
|
||||
if fan_json is not None:
|
||||
fan = int(fan_json)
|
||||
return temp, fan
|
||||
|
||||
def update(self):
|
||||
if "cpu3.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
if "cpu3.cpuload" in self.__widget_names:
|
||||
self.__cpuload = round(psutil.cpu_percent(percpu=False))
|
||||
if "cpu3.coresload" in self.__widget_names:
|
||||
self.__coresload = psutil.cpu_percent(percpu=True)
|
||||
if "cpu3.temp" in self.__widget_names or "cpu3.fanspeed" in self.__widget_names:
|
||||
self.__temp, self.__fanspeed = self._parse_sensors_output()
|
||||
|
||||
def state(self, widget):
|
||||
"""for having per-widget icons"""
|
||||
return [widget.get("type", "")]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
"""Displays the current date and time with timezone options.
|
||||
|
||||
Requires the following python packages:
|
||||
* tzlocal
|
||||
* pytz
|
||||
|
||||
Parameters:
|
||||
* datetimetz.format : strftime()-compatible formatting string
|
||||
* datetimetz.timezone : IANA timezone name
|
||||
|
|
|
@ -5,8 +5,6 @@ some media control bindings.
|
|||
Left click toggles pause, scroll up skips the current song, scroll
|
||||
down returns to the previous song.
|
||||
|
||||
Requires the following library:
|
||||
* subprocess
|
||||
Parameters:
|
||||
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {artist}, {title}, {album}, {length},
|
||||
|
@ -114,7 +112,7 @@ class Module(core.module.Module):
|
|||
self._song = ""
|
||||
return
|
||||
## perform the actual query -- these can be much more sophisticated
|
||||
data = util.cli.execute(self.now_playing_tf + self._tf_format)
|
||||
data = util.cli.execute(self.now_playing_tf + '"'+self._tf_format+'"')
|
||||
self._song = data
|
||||
|
||||
def update_standard(self, widgets):
|
||||
|
|
|
@ -5,13 +5,8 @@
|
|||
Requires the following executable:
|
||||
* dnf
|
||||
|
||||
Parameters:
|
||||
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
|
||||
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import core.event
|
||||
import core.module
|
||||
import core.widget
|
||||
|
@ -20,46 +15,13 @@ import core.decorators
|
|||
import util.cli
|
||||
|
||||
|
||||
def get_dnf_info(widget):
|
||||
res = util.cli.execute("dnf updateinfo", ignore_errors=True)
|
||||
|
||||
security = 0
|
||||
bugfixes = 0
|
||||
enhancements = 0
|
||||
other = 0
|
||||
for line in res.split("\n"):
|
||||
if not line.startswith(" "):
|
||||
continue
|
||||
elif "ecurity" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
security += int(s)
|
||||
elif "ugfix" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
bugfixes += int(s)
|
||||
elif "hancement" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
enhancements += int(s)
|
||||
else:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
other += int(s)
|
||||
|
||||
widget.set("security", security)
|
||||
widget.set("bugfixes", bugfixes)
|
||||
widget.set("enhancements", enhancements)
|
||||
widget.set("other", other)
|
||||
|
||||
core.event.trigger("update", [widget.module.id], redraw_only=True)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||
|
||||
self.background = True
|
||||
|
||||
def updates(self, widget):
|
||||
result = []
|
||||
for t in ["security", "bugfixes", "enhancements", "other"]:
|
||||
|
@ -67,8 +29,38 @@ class Module(core.module.Module):
|
|||
return "/".join(result)
|
||||
|
||||
def update(self):
|
||||
thread = threading.Thread(target=get_dnf_info, args=(self.widget(),))
|
||||
thread.start()
|
||||
widget = self.widget()
|
||||
res = util.cli.execute("dnf updateinfo", ignore_errors=True)
|
||||
|
||||
security = 0
|
||||
bugfixes = 0
|
||||
enhancements = 0
|
||||
other = 0
|
||||
for line in res.split("\n"):
|
||||
if not line.startswith(" "):
|
||||
continue
|
||||
elif "ecurity" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
security += int(s)
|
||||
elif "ugfix" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
bugfixes += int(s)
|
||||
elif "hancement" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
enhancements += int(s)
|
||||
else:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
other += int(s)
|
||||
|
||||
widget.set("security", security)
|
||||
widget.set("bugfixes", bugfixes)
|
||||
widget.set("enhancements", enhancements)
|
||||
widget.set("other", other)
|
||||
|
||||
|
||||
def state(self, widget):
|
||||
cnt = 0
|
||||
|
|
44
bumblebee_status/modules/contrib/dunstctl.py
Normal file
44
bumblebee_status/modules/contrib/dunstctl.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Toggle dunst notifications using dunstctl.
|
||||
|
||||
When notifications are paused using this module dunst doesn't get killed and
|
||||
you'll keep getting notifications on the background that will be displayed when
|
||||
unpausing. This is specially useful if you're using dunst's scripting
|
||||
(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to
|
||||
be running. Scripts will be executed when dunst gets unpaused.
|
||||
|
||||
Requires:
|
||||
* dunst v1.5.0+
|
||||
|
||||
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
|
||||
contributed by `joachimmathes <https://github.com/joachimmathes>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(""))
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_state)
|
||||
self.__states = {"unknown": ["unknown", "critical"],
|
||||
"true": ["muted", "warning"],
|
||||
"false": ["unmuted"]}
|
||||
if util.format.asbool(self.parameter("disabled", False)):
|
||||
util.cli.execute("dunstctl set-paused true", ignore_errors=True)
|
||||
|
||||
def toggle_state(self, event):
|
||||
util.cli.execute("dunstctl set-paused toggle", ignore_errors=True)
|
||||
|
||||
def state(self, widget):
|
||||
return self.__states[self.__is_dunst_paused()]
|
||||
|
||||
def __is_dunst_paused(self):
|
||||
result = util.cli.execute("dunstctl is-paused",
|
||||
return_exitcode=True,
|
||||
ignore_errors=True)
|
||||
return result[1].rstrip() if result[0] == 0 else "unknown"
|
113
bumblebee_status/modules/contrib/emerge_status.py
Normal file
113
bumblebee_status/modules/contrib/emerge_status.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
"""Display information about the currently running emerge process.
|
||||
|
||||
Requires the following executable:
|
||||
* emerge
|
||||
|
||||
Parameters:
|
||||
* emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}')
|
||||
|
||||
This code is based on emerge_status module from p3status [1] original created by AnwariasEu.
|
||||
|
||||
[1] https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py
|
||||
"""
|
||||
|
||||
import re
|
||||
import copy
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=10)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
self.__format = self.parameter(
|
||||
"format", "{current}/{total} {action} {category}/{pkg}"
|
||||
)
|
||||
self.__ret_default = {
|
||||
"action": "",
|
||||
"category": "",
|
||||
"current": 0,
|
||||
"pkg": "",
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
response = {}
|
||||
ret = copy.deepcopy(self.__ret_default)
|
||||
if self.__emerge_running():
|
||||
ret = self.__get_progress()
|
||||
|
||||
widget = self.widget("status")
|
||||
if not widget:
|
||||
widget = self.add_widget(name="status")
|
||||
|
||||
if ret["total"] == 0:
|
||||
widget.full_text("emrg calculating...")
|
||||
else:
|
||||
widget.full_text(
|
||||
" ".join(
|
||||
self.__format.format(
|
||||
current=ret["current"],
|
||||
total=ret["total"],
|
||||
action=ret["action"],
|
||||
category=ret["category"],
|
||||
pkg=ret["pkg"],
|
||||
).split()
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.clear_widgets()
|
||||
|
||||
def __emerge_running(self):
|
||||
"""
|
||||
Check if emerge is running.
|
||||
Returns true if at least one instance of emerge is running.
|
||||
"""
|
||||
try:
|
||||
util.cli.execute("pgrep emerge")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def __get_progress(self):
|
||||
"""
|
||||
Get current progress of emerge.
|
||||
Returns a dict containing current and total value.
|
||||
"""
|
||||
input_data = []
|
||||
ret = {}
|
||||
|
||||
# traverse emerge.log from bottom up to get latest information
|
||||
last_lines = util.cli.execute("tail -50 /var/log/emerge.log")
|
||||
input_data = last_lines.split("\n")
|
||||
input_data.reverse()
|
||||
|
||||
for line in input_data:
|
||||
if "*** terminating." in line:
|
||||
# copy content of ret_default, not only the references
|
||||
ret = copy.deepcopy(self.__ret_default)
|
||||
break
|
||||
else:
|
||||
status_re = re.compile(
|
||||
r"\((?P<cu>[\d]+) of (?P<t>[\d]+)\) "
|
||||
r"(?P<a>[a-zA-Z/]+( [a-zA-Z]+)?) "
|
||||
r"\((?P<ca>[\w\-]+)/(?P<p>[\w.]+)"
|
||||
)
|
||||
res = status_re.search(line)
|
||||
if res is not None:
|
||||
ret["action"] = res.group("a").lower()
|
||||
ret["category"] = res.group("ca")
|
||||
ret["current"] = res.group("cu")
|
||||
ret["pkg"] = res.group("p")
|
||||
ret["total"] = res.group("t")
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
171
bumblebee_status/modules/contrib/gcalendar.py
Normal file
171
bumblebee_status/modules/contrib/gcalendar.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
"""Displays first upcoming event in google calendar.
|
||||
|
||||
Events that are set as 'all-day' will not be shown.
|
||||
|
||||
Requires credentials.json from a google api application where the google calendar api is installed.
|
||||
On first time run the browser will open and google will ask for permission for this app to access
|
||||
the google calendar and then save a .gcalendar_token.json file to the credentials_path directory
|
||||
which stores this permission.
|
||||
|
||||
A refresh is done every 15 minutes.
|
||||
|
||||
Parameters:
|
||||
* gcalendar.time_format: Format time output. Defaults to "%H:%M".
|
||||
* gcalendar.date_format: Format date output. Defaults to "%d.%m.%y".
|
||||
* gcalendar.credentials_path: Path to credentials.json. Defaults to "~/".
|
||||
* gcalendar.locale: locale to use rather than the system default.
|
||||
|
||||
Requires these pip packages:
|
||||
* google-api-python-client >= 1.8.0
|
||||
* google-auth-httplib2
|
||||
* google-auth-oauthlib
|
||||
"""
|
||||
|
||||
# This import belongs to the google code
|
||||
from __future__ import print_function
|
||||
|
||||
from dateutil.parser import parse as dtparse
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import util.format
|
||||
|
||||
import datetime
|
||||
import os.path
|
||||
import locale
|
||||
import time
|
||||
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
# Minutes
|
||||
update_every = 15
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=update_every)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [core.widget.Widget(self.__datetime), core.widget.Widget(self.__summary)])
|
||||
self.__error = False
|
||||
self.__time_format = self.parameter("time_format", "%H:%M")
|
||||
self.__date_format = self.parameter("date_format", "%d.%m.%y")
|
||||
self.__credentials_path = os.path.expanduser(
|
||||
self.parameter("credentials_path", "~/")
|
||||
)
|
||||
self.__credentials = os.path.join(self.__credentials_path, "credentials.json")
|
||||
self.__token = os.path.join(self.__credentials_path, ".gcalendar_token.json")
|
||||
|
||||
l = locale.getdefaultlocale()
|
||||
if not l or l == (None, None):
|
||||
l = ("en_US", "UTF-8")
|
||||
lcl = self.parameter("locale", ".".join(l))
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, lcl.split("."))
|
||||
except Exception:
|
||||
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
|
||||
|
||||
self.__last_update = time.time()
|
||||
self.__gcalendar_date, self.__gcalendar_summary = self.__fetch_from_calendar()
|
||||
|
||||
def hidden(self):
|
||||
return self.__error
|
||||
|
||||
def __datetime(self, _):
|
||||
return self.__gcalendar_date
|
||||
|
||||
@core.decorators.scrollable
|
||||
def __summary(self, _):
|
||||
return self.__gcalendar_summary
|
||||
|
||||
|
||||
def __fetch_from_calendar(self):
|
||||
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
|
||||
|
||||
creds = None
|
||||
|
||||
try:
|
||||
# The file token.json stores the user's access and refresh tokens, and is
|
||||
# created automatically when the authorization flow completes for the first
|
||||
# time.
|
||||
if os.path.exists(self.__token):
|
||||
creds = Credentials.from_authorized_user_file(self.__token, SCOPES)
|
||||
# If there are no (valid) credentials available, let the user log in.
|
||||
if not creds or not creds.valid:
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
creds.refresh(Request())
|
||||
else:
|
||||
flow = InstalledAppFlow.from_client_secrets_file(
|
||||
self.__credentials, SCOPES
|
||||
)
|
||||
creds = flow.run_local_server(port=0)
|
||||
# Save the credentials for the next run
|
||||
with open(self.__token, "w") as token:
|
||||
token.write(creds.to_json())
|
||||
|
||||
service = build("calendar", "v3", credentials=creds)
|
||||
|
||||
# Call the Calendar API
|
||||
now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time
|
||||
end = (
|
||||
datetime.datetime.utcnow() + datetime.timedelta(days=7)
|
||||
).isoformat() + "Z" # 'Z' indicates UTC time
|
||||
# Get all calendars
|
||||
calendar_list = service.calendarList().list().execute()
|
||||
event_list = []
|
||||
for calendar_list_entry in calendar_list["items"]:
|
||||
calendar_id = calendar_list_entry["id"]
|
||||
events_result = (
|
||||
service.events()
|
||||
.list(
|
||||
calendarId=calendar_id,
|
||||
timeMin=now,
|
||||
timeMax=end,
|
||||
singleEvents=True,
|
||||
orderBy="startTime",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
events = events_result.get("items", [])
|
||||
|
||||
for event in events:
|
||||
start = dtparse(
|
||||
event["start"].get("dateTime", event["start"].get("date"))
|
||||
)
|
||||
# Only add to list if not an whole day event
|
||||
if start.tzinfo:
|
||||
event_list.append(
|
||||
{
|
||||
"date": start,
|
||||
"summary": event["summary"],
|
||||
"type": event["eventType"],
|
||||
}
|
||||
)
|
||||
sorted_list = sorted(event_list, key=lambda t: t["date"])
|
||||
next_event = sorted_list[0]
|
||||
|
||||
if next_event["date"] >= datetime.datetime.now(datetime.timezone.utc):
|
||||
if next_event["date"].date() == datetime.datetime.utcnow().date():
|
||||
dt = next_event["date"].astimezone()\
|
||||
.strftime(f"{self.__time_format}")
|
||||
else:
|
||||
dt = next_event["date"].astimezone()\
|
||||
.strftime(f"{self.__date_format} {self.__time_format}")
|
||||
return (dt, next_event["summary"])
|
||||
|
||||
return (None, "No upcoming events.")
|
||||
except:
|
||||
self.__error = True
|
||||
|
||||
def update(self):
|
||||
# Since scrolling runs the update command and therefore negates the
|
||||
# every decorator, this need to be stopped
|
||||
# to not break the API rules of google.
|
||||
if self.__last_update+(update_every*60) < time.time():
|
||||
self.__last_update = time.time()
|
||||
self.__gcalendar_date, self.__gcalendar_summary = self.__fetch_from_calendar()
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -5,6 +5,8 @@ Displays the unread GitHub notifications count for a GitHub user using the follo
|
|||
|
||||
* https://developer.github.com/v3/activity/notifications/#notification-reasons
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
|
@ -81,7 +83,6 @@ class Module(core.module.Module):
|
|||
self.__label += "/".join(counts)
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.__label = "n/a"
|
||||
|
||||
def __getUnreadNotificationsCountByReason(self, notifications, reason):
|
||||
|
|
87
bumblebee_status/modules/contrib/gitlab.py
Normal file
87
bumblebee_status/modules/contrib/gitlab.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the GitLab todo count:
|
||||
|
||||
* https://docs.gitlab.com/ee/user/todos.html
|
||||
* https://docs.gitlab.com/ee/api/todos.html
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the GitLab todo query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* gitlab.token: GitLab personal access token, the token needs to have the "read_api" scope.
|
||||
* gitlab.host: Host of the GitLab instance, default is "gitlab.com".
|
||||
* gitlab.actions: Comma separated actions to be parsed (e.g.: gitlab.actions=assigned,approval_required)
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
import util
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.gitlab))
|
||||
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
self.__host = self.parameter("host", "gitlab.com")
|
||||
|
||||
self.__actions = []
|
||||
actions = self.parameter("actions", "")
|
||||
if actions:
|
||||
self.__actions = util.format.aslist(actions)
|
||||
|
||||
self.__requests = requests.Session()
|
||||
self.__requests.headers.update({"PRIVATE-TOKEN": self.parameter("token", "")})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd="{cmd} https:/{host}//dashboard/todos".format(
|
||||
cmd=cmd, host=self.__host
|
||||
),
|
||||
)
|
||||
|
||||
def gitlab(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
url = "https://{host}/api/v4/todos".format(host=self.__host)
|
||||
response = self.__requests.get(url)
|
||||
todos = response.json()
|
||||
if self.__actions:
|
||||
todos = [t for t in todos if t["action_name"] in self.__actions]
|
||||
self.__label = str(len(todos))
|
||||
except Exception as e:
|
||||
self.__label = "n/a"
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
|
||||
try:
|
||||
if int(self.__label) > 0:
|
||||
state.append("warning")
|
||||
except ValueError:
|
||||
pass
|
||||
return state
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Fetch hard drive temeperature data from a hddtemp daemon
|
||||
"""Fetch hard drive temperature data from a hddtemp daemon
|
||||
that runs on localhost and default port (7634)
|
||||
|
||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
"""Displays the indicator status, for numlock, scrolllock and capslock
|
||||
|
||||
Requires the following executable:
|
||||
* xset
|
||||
|
||||
Parameters:
|
||||
* indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock')
|
||||
* indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning')
|
||||
|
|
|
@ -19,13 +19,13 @@ class Module(core.module.Module):
|
|||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.current_layout))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_keymap)
|
||||
self.__current_layout = self.__get_current_layout()
|
||||
|
||||
def current_layout(self, _):
|
||||
return self.__current_layout
|
||||
|
||||
def __next_keymap(self, event):
|
||||
def next_keymap(self, event):
|
||||
util.cli.execute("xkb-switch -n", ignore_errors=True)
|
||||
|
||||
def __get_current_layout(self):
|
||||
|
|
1
bumblebee_status/modules/contrib/layout_xkbswitch.py
Symbolic link
1
bumblebee_status/modules/contrib/layout_xkbswitch.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkbswitch.py
|
85
bumblebee_status/modules/contrib/messagereceiver.py
Normal file
85
bumblebee_status/modules/contrib/messagereceiver.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the message that's received via unix socket.
|
||||
|
||||
Parameters:
|
||||
* messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock)
|
||||
|
||||
Example:
|
||||
The following examples assume that /tmp/bumblebee_messagereceiver.sock is used as unix socket address.
|
||||
|
||||
In order to send the string "I bumblebee-status" to your status bar, use the following command:
|
||||
echo -e '{"message":"I bumblebee-status", "state": ""}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO
|
||||
|
||||
In order to highlight the text, the state variable can be used:
|
||||
echo -e '{"message":"I bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import socket
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.never
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.message))
|
||||
|
||||
self.background = True
|
||||
|
||||
self.__unix_socket_address = self.parameter("address", "")
|
||||
|
||||
self.__message = ""
|
||||
self.__state = []
|
||||
|
||||
def message(self, widget):
|
||||
return self.__message
|
||||
|
||||
def __read_data_from_socket(self):
|
||||
while True:
|
||||
try:
|
||||
os.unlink(self.__unix_socket_address)
|
||||
except OSError:
|
||||
if os.path.exists(self.__unix_socket_address):
|
||||
logging.exception(
|
||||
"Couldn't bind to unix socket %s", self.__unix_socket_address
|
||||
)
|
||||
raise
|
||||
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
|
||||
s.bind(self.__unix_socket_address)
|
||||
s.listen()
|
||||
|
||||
conn, _ = s.accept()
|
||||
with conn:
|
||||
while True:
|
||||
data = conn.recv(1024)
|
||||
if not data:
|
||||
break
|
||||
yield data.decode("utf-8")
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
for received_data in self.__read_data_from_socket():
|
||||
parsed_data = json.loads(received_data)
|
||||
self.__message = parsed_data["message"]
|
||||
self.__state = parsed_data["state"]
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
except json.JSONDecodeError:
|
||||
logging.exception("Couldn't parse message")
|
||||
except Exception:
|
||||
logging.exception("Unexpected exception while reading from socket")
|
||||
|
||||
def state(self, widget):
|
||||
return self.__state
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -42,6 +42,7 @@ Parameters:
|
|||
if {file} = '/foo/bar.baz', then {file2} = 'bar'
|
||||
|
||||
* mpd.host: MPD host to connect to. (mpc behaviour by default)
|
||||
* mpd.port: MPD port to connect to. (mpc behaviour by default)
|
||||
* mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main.
|
||||
|
||||
contributed by `alrayyes <https://github.com/alrayyes>`_ - many thanks!
|
||||
|
@ -73,10 +74,12 @@ class Module(core.module.Module):
|
|||
self._repeat = False
|
||||
self._tags = defaultdict(lambda: "")
|
||||
|
||||
if not self.parameter("host"):
|
||||
self._hostcmd = ""
|
||||
else:
|
||||
self._hostcmd = " -h " + self.parameter("host")
|
||||
self._hostcmd = ""
|
||||
if self.parameter("host"):
|
||||
self._hostcmd = " -h {}".format(self.parameter("host"))
|
||||
if self.parameter("port"):
|
||||
self._hostcmd += " -p {}".format(self.parameter("port"))
|
||||
|
||||
|
||||
# Create widgets
|
||||
widget_map = {}
|
||||
|
@ -94,6 +97,12 @@ class Module(core.module.Module):
|
|||
"cmd": "mpc toggle" + self._hostcmd,
|
||||
}
|
||||
widget.full_text(self.description)
|
||||
elif widget_name == "mpd.toggle":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc toggle" + self._hostcmd,
|
||||
}
|
||||
widget.full_text(self.toggle)
|
||||
elif widget_name == "mpd.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
|
@ -127,6 +136,9 @@ class Module(core.module.Module):
|
|||
def description(self, widget):
|
||||
return string.Formatter().vformat(self._fmt, (), self._tags)
|
||||
|
||||
def toggle(self, widget):
|
||||
return str(util.cli.execute("mpc status %currenttime%/%totaltime%", ignore_errors=True)).strip()
|
||||
|
||||
def update(self):
|
||||
self._load_song()
|
||||
|
||||
|
|
128
bumblebee_status/modules/contrib/network.py
Normal file
128
bumblebee_status/modules/contrib/network.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
"""
|
||||
A module to show the currently active network connection (ethernet or wifi) and connection strength if the connection is wireless.
|
||||
|
||||
Requires the Python netifaces package and iw installed on Linux.
|
||||
|
||||
A simpler take on nic and network_traffic. No extra config necessary!
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import netifaces
|
||||
import socket
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.network))
|
||||
self.__is_wireless = False
|
||||
self.__is_connected = False
|
||||
self.__interface = None
|
||||
self.__message = None
|
||||
self.__signal = -110
|
||||
|
||||
# Get network information to display to the user
|
||||
def network(self, widgets):
|
||||
# Determine whether there is an internet connection
|
||||
self.__is_connected = self.__attempt_connection()
|
||||
|
||||
# Attempt to extract a valid network interface device
|
||||
try:
|
||||
self.__interface = netifaces.gateways()["default"][netifaces.AF_INET][1]
|
||||
except Exception:
|
||||
self.__interface = None
|
||||
|
||||
# Check to see if the interface (if connected to the internet) is wireless
|
||||
if self.__is_connected and self.__interface:
|
||||
self.__is_wireless = self.__interface_is_wireless(self.__interface)
|
||||
|
||||
# setup message to send to the user
|
||||
if not self.__is_connected or not self.__interface:
|
||||
self.__message = "No connection"
|
||||
elif not self.__is_wireless:
|
||||
# Assuming that if user is connected via non-wireless means that it will be ethernet
|
||||
self.__signal = -30
|
||||
self.__message = "Ethernet"
|
||||
else:
|
||||
# We have a wireless connection
|
||||
iw_dat = util.cli.execute("iwgetid")
|
||||
has_ssid = "ESSID" in iw_dat
|
||||
signal = self.__compute_signal(self.__interface)
|
||||
|
||||
# If signal is None, that means that we can't compute the default interface's signal strength
|
||||
self.__signal = (
|
||||
util.format.asint(signal, minimum=-110, maximum=-30) if signal else None
|
||||
)
|
||||
|
||||
ssid = (
|
||||
iw_dat[iw_dat.index(":") + 1 :].replace('"', "").strip()
|
||||
if has_ssid
|
||||
else "Unknown"
|
||||
)
|
||||
self.__message = self.__generate_wireles_message(ssid, self.__signal)
|
||||
|
||||
return self.__message
|
||||
|
||||
# State determined by signal strength
|
||||
def state(self, widget):
|
||||
if self.__compute_strength(self.__signal) < 50:
|
||||
return "critical"
|
||||
if self.__compute_strength(self.__signal) < 75:
|
||||
return "warning"
|
||||
|
||||
return None
|
||||
|
||||
# manually done for better granularity / ease of parsing strength data
|
||||
def __generate_wireles_message(self, ssid, signal):
|
||||
computed_strength = self.__compute_strength(signal)
|
||||
strength_str = str(computed_strength) if computed_strength else "?"
|
||||
|
||||
return "{} {}%".format(ssid, strength_str)
|
||||
|
||||
def __compute_strength(self, signal):
|
||||
return int(100 * ((signal + 100) / 70.0)) if signal else None
|
||||
|
||||
# get signal strength in decibels/milliwat
|
||||
def __compute_signal(self, interface):
|
||||
# Get connection strength
|
||||
cmd = "iwconfig {}".format(interface)
|
||||
config_dat = " ".join(util.cli.execute(cmd).split())
|
||||
config_tokens = config_dat.replace("=", " ").split()
|
||||
|
||||
# handle weird output
|
||||
try:
|
||||
signal = config_tokens[config_tokens.index("level") + 1]
|
||||
except Exception:
|
||||
signal = None
|
||||
|
||||
return signal
|
||||
|
||||
def __attempt_connection(self):
|
||||
can_connect = False
|
||||
try:
|
||||
socket.create_connection(("1.1.1.1", 53))
|
||||
can_connect = True
|
||||
except Exception:
|
||||
can_connect = False
|
||||
|
||||
return can_connect
|
||||
|
||||
def __interface_is_wireless(self, interface):
|
||||
is_wireless = False
|
||||
try:
|
||||
with open("/proc/net/wireless", "r") as f:
|
||||
is_wireless = interface in f.read()
|
||||
f.close()
|
||||
except Exception:
|
||||
is_wireless = False
|
||||
|
||||
return is_wireless
|
||||
|
|
@ -38,8 +38,8 @@ class Module(core.module.Module):
|
|||
try:
|
||||
self._bandwidth = BandwidthInfo()
|
||||
|
||||
self._rate_recv = "?"
|
||||
self._rate_sent = "?"
|
||||
self._rate_recv = 0
|
||||
self._rate_sent = 0
|
||||
self._bytes_recv = self._bandwidth.bytes_recv()
|
||||
self._bytes_sent = self._bandwidth.bytes_sent()
|
||||
except Exception:
|
||||
|
@ -97,9 +97,6 @@ class BandwidthInfo(object):
|
|||
"""Return default active network adapter"""
|
||||
gateway = netifaces.gateways()["default"]
|
||||
|
||||
if not gateway:
|
||||
raise "No default gateway found"
|
||||
|
||||
return gateway[netifaces.AF_INET][1]
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -4,11 +4,15 @@
|
|||
|
||||
Parameters:
|
||||
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
|
||||
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem}
|
||||
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct}
|
||||
|
||||
Requires nvidia-smi
|
||||
|
||||
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
|
||||
|
||||
Note: mem_io_pct is (from `man nvidia-smi`):
|
||||
> Percent of time over the past sample period during which global (device)
|
||||
> memory was being read or written.
|
||||
"""
|
||||
|
||||
import core.module
|
||||
|
@ -41,6 +45,9 @@ class Module(core.module.Module):
|
|||
clockMem = ""
|
||||
clockGpu = ""
|
||||
fanspeed = ""
|
||||
gpuUsagePct = ""
|
||||
memIoPct = ""
|
||||
memUsage = "not found"
|
||||
for item in sp.split("\n"):
|
||||
try:
|
||||
key, val = item.split(":")
|
||||
|
@ -61,10 +68,18 @@ class Module(core.module.Module):
|
|||
name = val
|
||||
elif key == "Fan Speed":
|
||||
fanspeed = val.split(" ")[0]
|
||||
elif title == "Utilization":
|
||||
if key == "Gpu":
|
||||
gpuUsagePct = val.split(" ")[0]
|
||||
elif key == "Memory":
|
||||
memIoPct = val.split(" ")[0]
|
||||
|
||||
except:
|
||||
title = item.strip()
|
||||
|
||||
if totalMem and usedMem:
|
||||
memUsage = int(int(usedMem) / int(totalMem) * 100)
|
||||
|
||||
str_format = self.parameter(
|
||||
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
|
||||
)
|
||||
|
@ -76,6 +91,9 @@ class Module(core.module.Module):
|
|||
clock_gpu=clockGpu,
|
||||
clock_mem=clockMem,
|
||||
fanspeed=fanspeed,
|
||||
gpu_usage_pct=gpuUsagePct,
|
||||
mem_io_pct=memIoPct,
|
||||
mem_usage_pct=memUsage,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar.
|
||||
"""Displays the Octorrint status and the printer's bed/tools temperature in the status bar.
|
||||
|
||||
Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled).
|
||||
|
||||
Prerequisites:
|
||||
* tk python library (usually python-tk or python3-tk, depending on your distribution)
|
||||
|
||||
Parameters:
|
||||
* octoprint.address : Octoprint address (e.q: http://192.168.1.3)
|
||||
* octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface)
|
||||
|
@ -82,8 +85,15 @@ class Module(core.module.Module):
|
|||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
|
||||
|
||||
def octoprint_status(self, widget):
|
||||
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown":
|
||||
return self.__octoprint_state
|
||||
if (
|
||||
self.__octoprint_state.startswith("Offline")
|
||||
or self.__octoprint_state == "Unknown"
|
||||
):
|
||||
return (
|
||||
(self.__octoprint_state[:25] + "...")
|
||||
if len(self.__octoprint_state) > 25
|
||||
else self.__octoprint_state
|
||||
)
|
||||
return (
|
||||
self.__octoprint_state
|
||||
+ " | B: "
|
||||
|
|
30
bumblebee_status/modules/contrib/optman.py
Normal file
30
bumblebee_status/modules/contrib/optman.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""Displays currently active gpu by optimus-manager
|
||||
Requires the following packages:
|
||||
|
||||
* optimus-manager
|
||||
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__gpumode = ""
|
||||
|
||||
def output(self, _):
|
||||
return "GPU: {}".format(self.__gpumode)
|
||||
|
||||
def update(self):
|
||||
cmd = "optimus-manager --print-mode"
|
||||
output = util.cli.execute(cmd).strip()
|
||||
|
||||
if "intel" in output:
|
||||
self.__gpumode = "Intel"
|
||||
elif "nvidia" in output:
|
||||
self.__gpumode = "Nvidia"
|
||||
elif "amd" in output:
|
||||
self.__gpumode = "AMD"
|
|
@ -3,7 +3,7 @@
|
|||
"""Displays update information per repository for pacman.
|
||||
|
||||
Parameters:
|
||||
* pacman.sum: If you prefere displaying updates with a single digit (defaults to 'False')
|
||||
* pacman.sum: If you prefer displaying updates with a single digit (defaults to 'False')
|
||||
|
||||
Requires the following executables:
|
||||
* fakeroot
|
||||
|
|
88
bumblebee_status/modules/contrib/pamixer.py
Normal file
88
bumblebee_status/modules/contrib/pamixer.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
"""get volume level or control it
|
||||
|
||||
Requires the following executable:
|
||||
* pamixer
|
||||
|
||||
Parameters:
|
||||
* pamixer.percent_change: How much to change volume by when scrolling on the module (default is 4%)
|
||||
|
||||
heavily based on amixer module
|
||||
"""
|
||||
import re
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.volume))
|
||||
|
||||
self.__level = "volume 0%"
|
||||
self.__muted = True
|
||||
self.__change = util.format.asint(
|
||||
self.parameter("percent_change", "4%").strip("%"), 0, 200
|
||||
)
|
||||
|
||||
events = [
|
||||
{
|
||||
"type": "mute",
|
||||
"action": self.toggle,
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.increase_volume,
|
||||
"button": core.input.WHEEL_UP,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.decrease_volume,
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
},
|
||||
]
|
||||
|
||||
for event in events:
|
||||
core.input.register(self, button=event["button"], cmd=event["action"])
|
||||
|
||||
def toggle(self, event):
|
||||
self.set_parameter("--toggle-mute")
|
||||
|
||||
def increase_volume(self, event):
|
||||
self.set_parameter("--increase {}".format(self.__change))
|
||||
|
||||
def decrease_volume(self, event):
|
||||
self.set_parameter("--decrease {}".format(self.__change))
|
||||
|
||||
def set_parameter(self, parameter):
|
||||
util.cli.execute("pamixer {}".format(parameter))
|
||||
|
||||
def volume(self, widget):
|
||||
if self.__level == "volume 0%":
|
||||
self.__muted = True
|
||||
return self.__level
|
||||
m = re.search(r"([\d]+)\%", self.__level)
|
||||
if m:
|
||||
if m.group(1) != "0%" in self.__level:
|
||||
self.__muted = False
|
||||
return "volume {}%".format(m.group(1))
|
||||
else:
|
||||
return "volume 0%"
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
volume = util.cli.execute("pamixer --get-volume-human".format())
|
||||
self.__level = volume
|
||||
self.__muted = False
|
||||
except Exception as e:
|
||||
self.__level = "volume 0%"
|
||||
|
||||
def state(self, widget):
|
||||
if self.__muted:
|
||||
return ["warning", "muted"]
|
||||
return ["unmuted"]
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
31
bumblebee_status/modules/contrib/persian_date.py
Normal file
31
bumblebee_status/modules/contrib/persian_date.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current date and time in Persian(Jalali) Calendar.
|
||||
|
||||
Requires the following python packages:
|
||||
* jdatetime
|
||||
|
||||
Parameters:
|
||||
* datetime.format: strftime()-compatible formatting string. default: "%A %d %B" e.g., "جمعه ۱۳ اسفند"
|
||||
* datetime.locale: locale to use. default: "fa_IR"
|
||||
"""
|
||||
|
||||
import jdatetime
|
||||
|
||||
import core.decorators
|
||||
from modules.core.datetime import Module as dtmodule
|
||||
|
||||
|
||||
class Module(dtmodule):
|
||||
@core.decorators.every(minutes=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, dtlibrary=jdatetime)
|
||||
|
||||
def default_format(self):
|
||||
return "%A %d %B"
|
||||
|
||||
def default_locale(self):
|
||||
return ("fa_IR", "UTF-8")
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -4,13 +4,20 @@
|
|||
|
||||
Parameters:
|
||||
* pihole.address : pi-hole address (e.q: http://192.168.1.3)
|
||||
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
|
||||
|
||||
|
||||
* pihole.apitoken : pi-hole API token (can be obtained in the pi-hole webinterface (Settings -> API)
|
||||
|
||||
OR (deprecated!)
|
||||
|
||||
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
|
||||
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
import logging
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
@ -22,7 +29,18 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.pihole_status))
|
||||
|
||||
self._pihole_address = self.parameter("address", "")
|
||||
self._pihole_pw_hash = self.parameter("pwhash", "")
|
||||
pihole_pw_hash = self.parameter("pwhash", "")
|
||||
pihole_api_token = self.parameter("apitoken", "")
|
||||
|
||||
self._pihole_secret = (
|
||||
pihole_api_token if pihole_api_token != "" else pihole_pw_hash
|
||||
)
|
||||
|
||||
if pihole_pw_hash != "":
|
||||
logging.warn(
|
||||
"pihole: The 'pwhash' parameter is deprecated - consider using the 'apitoken' parameter instead!"
|
||||
)
|
||||
|
||||
self._pihole_status = None
|
||||
self._ads_blocked_today = "-"
|
||||
self.update_pihole_status()
|
||||
|
@ -42,7 +60,11 @@ class Module(core.module.Module):
|
|||
|
||||
def update_pihole_status(self):
|
||||
try:
|
||||
data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
|
||||
data = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?summary&auth="
|
||||
+ self._pihole_secret
|
||||
).json()
|
||||
self._pihole_status = True if data["status"] == "enabled" else False
|
||||
self._ads_blocked_today = data["ads_blocked_today"]
|
||||
except Exception as e:
|
||||
|
@ -56,13 +78,13 @@ class Module(core.module.Module):
|
|||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?disable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
+ self._pihole_secret
|
||||
)
|
||||
else:
|
||||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?enable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
+ self._pihole_secret
|
||||
)
|
||||
if req is not None:
|
||||
if req.status_code == 200:
|
||||
|
|
90
bumblebee_status/modules/contrib/pipewire.py
Normal file
90
bumblebee_status/modules/contrib/pipewire.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
"""get volume level or control it
|
||||
|
||||
Requires the following executable:
|
||||
* wpctl
|
||||
|
||||
Parameters:
|
||||
* wpctl.percent_change: How much to change volume by when scrolling on the module (default is 4%)
|
||||
|
||||
heavily based on amixer module
|
||||
"""
|
||||
import re
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.volume))
|
||||
|
||||
self.__level = "N/A"
|
||||
self.__muted = True
|
||||
self.__change = (
|
||||
util.format.asint(self.parameter("percent_change", "4%").strip("%"), 0, 200)
|
||||
/ 100.0
|
||||
) # divide by 100 because wpctl represents 100% volume as 1.00, 50% as 0.50, etc
|
||||
|
||||
self.__id = self.parameter("sink_id") or "@DEFAULT_AUDIO_SINK@"
|
||||
|
||||
events = [
|
||||
{
|
||||
"type": "mute",
|
||||
"action": self.toggle,
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.increase_volume,
|
||||
"button": core.input.WHEEL_UP,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.decrease_volume,
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
},
|
||||
]
|
||||
|
||||
for event in events:
|
||||
core.input.register(self, button=event["button"], cmd=event["action"])
|
||||
|
||||
def toggle(self, event):
|
||||
util.cli.execute("wpctl set-mute {} toggle".format(self.__id))
|
||||
|
||||
def increase_volume(self, event):
|
||||
util.cli.execute(
|
||||
"wpctl set-volume --limit 1.0 {} {}+".format(self.__id, self.__change)
|
||||
)
|
||||
|
||||
def decrease_volume(self, event):
|
||||
util.cli.execute(
|
||||
"wpctl set-volume --limit 1.0 {} {}-".format(self.__id, self.__change)
|
||||
)
|
||||
|
||||
def volume(self, widget):
|
||||
if self.__level == "N/A":
|
||||
return self.__level
|
||||
return "{}%".format(int(float(self.__level) * 100))
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
# `wpctl get-volume` will return a string like "Volume: n.nn" or "Volume: n.nn [MUTED]"
|
||||
volume = util.cli.execute("wpctl get-volume {}".format(self.__id))
|
||||
v = re.search("\d\.\d+", volume)
|
||||
m = re.search("MUTED", volume)
|
||||
self.__level = v.group()
|
||||
self.__muted = True if m else False
|
||||
except Exception:
|
||||
self.__level = "N/A"
|
||||
|
||||
def state(self, widget):
|
||||
if self.__muted:
|
||||
return ["warning", "muted"]
|
||||
return ["unmuted"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
150
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal file
150
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal file
|
@ -5,57 +5,131 @@
|
|||
Requires the following executable:
|
||||
* playerctl
|
||||
|
||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||
Parameters:
|
||||
* playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}').
|
||||
The format string is passed to 'playerctl -f' as an argument. Read `the README <https://github.com/altdesktop/playerctl#printing-properties-and-metadata>`_ for more information.
|
||||
* playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||
Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next
|
||||
* playerctl.args: The arguments added to playerctl.
|
||||
You can check 'playerctl --help' or `its README <https://github.com/altdesktop/playerctl#using-the-cli>`_. For example, it could be '-p vlc,%any'.
|
||||
* playerctl.hide: Hide the widgets when no players are found. Defaults to "false".
|
||||
|
||||
Parameters are inspired by the `spotify` module, many thanks to its developers!
|
||||
|
||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
import logging
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self,config , theme):
|
||||
widgets = [
|
||||
core.widget.Widget(name="playerctl.prev"),
|
||||
core.widget.Widget(name="playerctl.main", full_text=self.description),
|
||||
core.widget.Widget(name="playerctl.next"),
|
||||
]
|
||||
super(Module, self).__init__(config, theme , widgets)
|
||||
def __init__(self, config, theme):
|
||||
super(Module, self).__init__(config, theme, [])
|
||||
|
||||
core.input.register(widgets[0], button=core.input.LEFT_MOUSE,
|
||||
cmd="playerctl previous")
|
||||
core.input.register(widgets[1], button=core.input.LEFT_MOUSE,
|
||||
cmd="playerctl play-pause")
|
||||
core.input.register(widgets[2], button=core.input.LEFT_MOUSE,
|
||||
cmd="playerctl next")
|
||||
self.background = True
|
||||
|
||||
self._status = None
|
||||
self._tags = None
|
||||
self.__hide = util.format.asbool(self.parameter("hide", "false"));
|
||||
self.__hidden = self.__hide
|
||||
|
||||
def description(self, widget):
|
||||
return self._tags if self._tags else "..."
|
||||
self.__layout = util.format.aslist(
|
||||
self.parameter(
|
||||
"layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next"
|
||||
)
|
||||
)
|
||||
|
||||
self.__cmd = "playerctl " + self.parameter("args", "") + " "
|
||||
self.__format = self.parameter("format", "{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}")
|
||||
|
||||
widget_map = {}
|
||||
for widget_name in self.__layout:
|
||||
widget = self.add_widget(name=widget_name)
|
||||
if widget_name == "playerctl.prev":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "previous",
|
||||
}
|
||||
elif widget_name == "playerctl.pause":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "play-pause",
|
||||
}
|
||||
elif widget_name == "playerctl.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "next",
|
||||
}
|
||||
elif widget_name == "playerctl.song":
|
||||
widget_map[widget] = [
|
||||
{
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "play-pause",
|
||||
}, {
|
||||
"button": core.input.WHEEL_UP,
|
||||
"cmd": self.__cmd + "next",
|
||||
}, {
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
"cmd": self.__cmd + "previous",
|
||||
}
|
||||
]
|
||||
else:
|
||||
raise KeyError(
|
||||
"The playerctl module does not have a {widget_name!r} widget".format(
|
||||
widget_name=widget_name
|
||||
)
|
||||
)
|
||||
|
||||
for widget, callback_options in widget_map.items():
|
||||
if isinstance(callback_options, dict):
|
||||
core.input.register(widget, **callback_options)
|
||||
|
||||
def hidden(self):
|
||||
return self.__hidden
|
||||
|
||||
def status(self):
|
||||
try:
|
||||
playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip()
|
||||
if playback_status == "No players found":
|
||||
return None
|
||||
return playback_status
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
self._load_song()
|
||||
playback_status = self.status()
|
||||
if not playback_status:
|
||||
self.__hidden = self.__hide
|
||||
else:
|
||||
self.__hidden = False
|
||||
for widget in self.widgets():
|
||||
if playback_status:
|
||||
if widget.name == "playerctl.pause":
|
||||
if playback_status == "Playing":
|
||||
widget.set("state", "playing")
|
||||
elif playback_status == "Paused":
|
||||
widget.set("state", "paused")
|
||||
elif playback_status == "Stopped":
|
||||
widget.set("state", "stopped")
|
||||
else:
|
||||
widget.set("state", "")
|
||||
elif widget.name == "playerctl.next":
|
||||
widget.set("state", "next")
|
||||
elif widget.name == "playerctl.prev":
|
||||
widget.set("state", "prev")
|
||||
elif widget.name == "playerctl.song":
|
||||
widget.full_text(self.__get_song())
|
||||
else:
|
||||
widget.set("state", "")
|
||||
widget.full_text(" ")
|
||||
|
||||
def state(self, widget):
|
||||
if widget.name == "playerctl.prev":
|
||||
return "prev"
|
||||
if widget.name == "playerctl.next":
|
||||
return "next"
|
||||
return self._status
|
||||
|
||||
def _load_song(self):
|
||||
info = ""
|
||||
def __get_song(self):
|
||||
try:
|
||||
status = util.cli.execute("playerctl status").lower()
|
||||
info = util.cli.execute("playerctl metadata xesam:title")
|
||||
except :
|
||||
self._status = None
|
||||
self._tags = None
|
||||
return
|
||||
self._status = status.split("\n")[0].lower()
|
||||
self._tags = info.split("\n")[0][:20]
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return " "
|
||||
|
|
|
@ -13,7 +13,7 @@ Parameters:
|
|||
Example: 'notify-send 'Time up!''. If you want to chain multiple commands,
|
||||
please use an external wrapper script and invoke that. The module itself does
|
||||
not support command chaining (see https://github.com/tobi-wan-kenobi/bumblebee-status/issues/532
|
||||
for a detailled explanation)
|
||||
for a detailed explanation)
|
||||
|
||||
contributed by `martindoublem <https://github.com/martindoublem>`_, inspired by `karthink <https://github.com/karthink>`_ - many thanks!
|
||||
"""
|
||||
|
|
72
bumblebee_status/modules/contrib/portage_status.py
Normal file
72
bumblebee_status/modules/contrib/portage_status.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
"""Displays the status of Gentoo portage operations.
|
||||
|
||||
Parameters:
|
||||
* portage_status.logfile: logfile for portage (default is /var/log/emerge.log)
|
||||
|
||||
contributed by `andrewreisner <https://github.com/andrewreisner>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__logfile = self.parameter("logfile", "/var/log/emerge.log")
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
self.__action = ""
|
||||
self.__package = ""
|
||||
self.__status = ""
|
||||
|
||||
def output(self, widget):
|
||||
return " ".join(
|
||||
[
|
||||
atom
|
||||
for atom in (self.__action, self.__package, self.__status)
|
||||
if atom != ""
|
||||
]
|
||||
)
|
||||
|
||||
def state(self, widgets):
|
||||
if self.__action == "":
|
||||
return "idle"
|
||||
return "active"
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
with open(self.__logfile, "rb") as f:
|
||||
f.seek(-2, os.SEEK_END)
|
||||
while f.read(1) != b"\n":
|
||||
f.seek(-2, os.SEEK_CUR)
|
||||
last_line = f.readline().decode()
|
||||
if "===" in last_line:
|
||||
if "Unmerging..." in last_line:
|
||||
self.__action = "Unmerging"
|
||||
package_beg = last_line.find("(") + 1
|
||||
package_end = last_line.find("-", last_line.find("/")) - 1
|
||||
self.__package = last_line[package_beg : package_end + 1]
|
||||
else: # merging
|
||||
status_beg = last_line.find("(")
|
||||
status_end = last_line.find(")")
|
||||
self.__status = last_line[status_beg : status_end + 1]
|
||||
package_beg = last_line.find("(", status_end) + 1
|
||||
package_end = (
|
||||
package_beg
|
||||
+ last_line[package_beg:].find(
|
||||
"-", last_line[package_beg:].find("/")
|
||||
)
|
||||
- 1
|
||||
)
|
||||
self.__package = last_line[package_beg : package_end + 1]
|
||||
action_beg = status_end + 2
|
||||
action_end = package_beg - 3
|
||||
self.__action = last_line[action_beg : action_end + 1]
|
||||
else:
|
||||
self.clear()
|
||||
except Exception:
|
||||
self.clear()
|
99
bumblebee_status/modules/contrib/power-profile.py
Normal file
99
bumblebee_status/modules/contrib/power-profile.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
"""
|
||||
Displays the current Power-Profile active
|
||||
|
||||
|
||||
Left-Click or Right-Click as well as Scrolling up / down changes the active Power-Profile
|
||||
|
||||
Prerequisites:
|
||||
* dbus-python
|
||||
* power-profiles-daemon
|
||||
"""
|
||||
|
||||
import dbus
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class PowerProfileManager:
|
||||
def __init__(self):
|
||||
self.POWER_PROFILES_NAME = "net.hadess.PowerProfiles"
|
||||
self.POWER_PROFILES_PATH = "/net/hadess/PowerProfiles"
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE = "ActiveProfile"
|
||||
self.PP_PROPERTIES_ALL_POWER_PROFILES = "Profiles"
|
||||
|
||||
self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties"
|
||||
bus = dbus.SystemBus()
|
||||
pp_proxy = bus.get_object(self.POWER_PROFILES_NAME, self.POWER_PROFILES_PATH)
|
||||
self.pp_interface = dbus.Interface(pp_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
def get_current_power_profile(self):
|
||||
return self.pp_interface.Get(
|
||||
self.POWER_PROFILES_NAME, self.PP_PROPERTIES_CURRENT_POWER_PROFILE
|
||||
)
|
||||
|
||||
def __get_all_power_profile_names(self):
|
||||
power_profiles = self.pp_interface.Get(
|
||||
self.POWER_PROFILES_NAME, self.PP_PROPERTIES_ALL_POWER_PROFILES
|
||||
)
|
||||
power_profiles_names = []
|
||||
for pp in power_profiles:
|
||||
power_profiles_names.append(pp["Profile"])
|
||||
|
||||
return power_profiles_names
|
||||
|
||||
def next_power_profile(self, event):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp_index = self.__get_current_pp_index()
|
||||
next_index = 0
|
||||
if current_pp_index != (len(all_pp_names) - 1):
|
||||
next_index = current_pp_index + 1
|
||||
|
||||
self.pp_interface.Set(
|
||||
self.POWER_PROFILES_NAME,
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE,
|
||||
all_pp_names[next_index],
|
||||
)
|
||||
|
||||
def prev_power_profile(self, event):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp_index = self.__get_current_pp_index()
|
||||
last_index = len(all_pp_names) - 1
|
||||
if current_pp_index is not 0:
|
||||
last_index = current_pp_index - 1
|
||||
|
||||
self.pp_interface.Set(
|
||||
self.POWER_PROFILES_NAME,
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE,
|
||||
all_pp_names[last_index],
|
||||
)
|
||||
|
||||
def __get_current_pp_index(self):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp = self.get_current_power_profile()
|
||||
return all_pp_names.index(current_pp)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
self.pp_manager = PowerProfileManager()
|
||||
core.input.register(
|
||||
self, button=core.input.WHEEL_UP, cmd=self.pp_manager.next_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.WHEEL_DOWN, cmd=self.pp_manager.prev_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd=self.pp_manager.next_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.RIGHT_MOUSE, cmd=self.pp_manager.prev_power_profile
|
||||
)
|
||||
|
||||
def full_text(self, widgets):
|
||||
return self.pp_manager.get_current_power_profile()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -20,7 +20,8 @@ Parameters:
|
|||
* prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel')
|
||||
* prime.intelstring: String to use when intel is selected (defaults to 'intel')
|
||||
|
||||
Requires the following executable:
|
||||
Requires the following executables:
|
||||
* sudo
|
||||
* prime-select
|
||||
|
||||
contributed by `jeffeb3 <https://github.com/jeffeb3>`_ - many thanks!
|
||||
|
|
|
@ -29,6 +29,9 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.get_progress_text))
|
||||
self.__active = False
|
||||
|
||||
def hidden(self):
|
||||
return not self.__active
|
||||
|
||||
def get_progress_text(self, widget):
|
||||
if self.update_progress_info(widget):
|
||||
width = util.format.asint(self.parameter("barwidth", 8))
|
||||
|
@ -53,7 +56,7 @@ class Module(core.module.Module):
|
|||
return self.parameter("placeholder", "n/a")
|
||||
|
||||
def update_progress_info(self, widget):
|
||||
"""Update widget's informations about the copy"""
|
||||
"""Update widget's information about the copy"""
|
||||
if not self.__active:
|
||||
return
|
||||
|
||||
|
@ -61,8 +64,8 @@ class Module(core.module.Module):
|
|||
# 1. pid
|
||||
# 2. command
|
||||
# 3. arguments
|
||||
# 4. progress (xx.x formated)
|
||||
# 5. quantity (.. unit / .. unit formated)
|
||||
# 4. progress (xx.x formatted)
|
||||
# 5. quantity (.. unit / .. unit formatted)
|
||||
# 6. speed
|
||||
# 7. time remaining
|
||||
extract_nospeed = re.compile(
|
||||
|
@ -77,7 +80,7 @@ class Module(core.module.Module):
|
|||
result = extract_wtspeed.match(raw)
|
||||
|
||||
if not result:
|
||||
# Abord speed measures
|
||||
# Abort speed measures
|
||||
raw = util.cli.execute("progress -q")
|
||||
result = extract_nospeed.match(raw)
|
||||
|
||||
|
@ -101,7 +104,7 @@ class Module(core.module.Module):
|
|||
|
||||
def state(self, widget):
|
||||
if self.__active:
|
||||
return "copying"
|
||||
return ["copying", "no-autohide"]
|
||||
return "pending"
|
||||
|
||||
|
||||
|
|
|
@ -1,28 +1,155 @@
|
|||
"""Displays public IP address
|
||||
"""
|
||||
Displays information about the public IP address associated with the default route:
|
||||
* Public IP address
|
||||
* Country Name
|
||||
* Country Code
|
||||
* City Name
|
||||
* Geographic Coordinates
|
||||
|
||||
Left mouse click on the widget forces immediate update.
|
||||
Any change to the default route will cause the widget to update.
|
||||
|
||||
Requirements:
|
||||
* netifaces
|
||||
|
||||
Parameters:
|
||||
* publicip.format: Format string (defaults to ‘{ip} ({country_code})’)
|
||||
* Available format strings - ip, country_name, country_code, city_name, coordinates
|
||||
|
||||
Examples:
|
||||
* bumblebee-status -m publicip -p publicip.format="{ip} ({country_code})"
|
||||
* bumblebee-status -m publicip -p publicip.format="{ip} which is in {city_name}"
|
||||
* bumblebee-status -m publicip -p publicip.format="Your packets are right here: {coordinates}"
|
||||
|
||||
contributed by `tfwiii <https://github.com/tfwiii>` - many thanks!
|
||||
"""
|
||||
|
||||
import re
|
||||
import threading
|
||||
import netifaces
|
||||
import time
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.format
|
||||
import util.location
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.public_ip))
|
||||
super().__init__(config, theme, core.widget.Widget(self.publicip))
|
||||
|
||||
self.__ip = ""
|
||||
self.__previous_default_route = None
|
||||
self.__current_default_route = None
|
||||
self.background = True
|
||||
|
||||
def public_ip(self, widget):
|
||||
return self.__ip
|
||||
# Immediate update (override default) when left click on widget
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__click_update)
|
||||
|
||||
# By default show: <ip> (<2 letter country code>)
|
||||
self._format = self.parameter("format", "{ip} ({country_code})")
|
||||
|
||||
self.__monitor = threading.Thread(target=self.monitor, args=())
|
||||
self.__monitor.start()
|
||||
|
||||
def monitor(self):
|
||||
__previous_ips = set()
|
||||
__current_ips = set()
|
||||
# Initially set to True to force an info update on first pass
|
||||
__information_changed = True
|
||||
|
||||
self.update()
|
||||
|
||||
while threading.main_thread().is_alive():
|
||||
__current_ips.clear()
|
||||
# Look for any changes to IP addresses
|
||||
try:
|
||||
for interface in netifaces.interfaces():
|
||||
try:
|
||||
__current_ips.add(netifaces.ifaddresses(interface)[2][0]['addr'])
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
# If not ip address information found clear __current_ips
|
||||
__current_ips.clear()
|
||||
|
||||
# If a change of any interfaces' IP then flag change
|
||||
if __current_ips.symmetric_difference(__previous_ips):
|
||||
__previous_ips = __current_ips.copy()
|
||||
__information_changed = True
|
||||
|
||||
# Update if change is flagged
|
||||
if __information_changed:
|
||||
__information_changed = False
|
||||
self.update()
|
||||
|
||||
# Throttle the calls to netifaces
|
||||
time.sleep(1)
|
||||
|
||||
def publicip(self, widget):
|
||||
if widget.get("public_ip") is None:
|
||||
return "n/a"
|
||||
return self._format.format(
|
||||
ip = widget.get("public_ip", "-"),
|
||||
country_name = widget.get("country_name", "-"),
|
||||
country_code = widget.get("country_code", "-"),
|
||||
city_name = widget.get("city_name", "-"),
|
||||
coordinates = widget.get("coordinates", "-"),
|
||||
)
|
||||
|
||||
def __click_update(self, event):
|
||||
util.location.reset()
|
||||
|
||||
def update(self):
|
||||
widget = self.widget()
|
||||
|
||||
try:
|
||||
self.__ip = util.location.public_ip()
|
||||
except Exception:
|
||||
self.__ip = "n/a"
|
||||
util.location.reset()
|
||||
time.sleep(5) # wait for reset to complete before querying results
|
||||
|
||||
# Fetch fresh location information
|
||||
__info = util.location.location_info()
|
||||
__raw_lat = __info["latitude"]
|
||||
__raw_lon = __info["longitude"]
|
||||
|
||||
# Contstruct coordinates string if util.location has provided required info
|
||||
if isinstance(__raw_lat, float) and isinstance(__raw_lon, float):
|
||||
__lat = float("{:.2f}".format(__raw_lat))
|
||||
__lon = float("{:.2f}".format(__raw_lon))
|
||||
if __lat < 0:
|
||||
__coords = str(__lat) + "°S"
|
||||
else:
|
||||
__coords = str(__lat) + "°N"
|
||||
__coords += ","
|
||||
if __lon < 0:
|
||||
__coords += str(__lon) + "°W"
|
||||
else:
|
||||
__coords += str(__lon) + "°E"
|
||||
else:
|
||||
__coords = "Unknown"
|
||||
|
||||
# Set widget values
|
||||
widget.set("public_ip", __info["public_ip"])
|
||||
widget.set("country_name", __info["country"])
|
||||
widget.set("country_code", __info["country_code"])
|
||||
widget.set("city_name", __info["city_name"])
|
||||
widget.set("coordinates", __coords)
|
||||
|
||||
# Update widget values
|
||||
core.event.trigger("update", [widget.module.id], redraw_only=True)
|
||||
except Exception as ex:
|
||||
widget.set("public_ip", None)
|
||||
logging.error(str(ex))
|
||||
|
||||
def state(self, widget):
|
||||
return widget.get("state", None)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
59
bumblebee_status/modules/contrib/rofication.py
Normal file
59
bumblebee_status/modules/contrib/rofication.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
"""Rofication indicator
|
||||
|
||||
https://github.com/DaveDavenport/Rofication
|
||||
simple module to show an icon + the number of notifications stored in rofication
|
||||
module will have normal highlighting if there are zero notifications,
|
||||
"warning" highlighting if there are nonzero notifications,
|
||||
"critical" highlighting if there are any critical notifications
|
||||
|
||||
Parameters:
|
||||
* rofication.regolith: Switch to regolith fork of rofication, see <https://github.com/regolith-linux/regolith-rofication>.
|
||||
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import sys
|
||||
import socket
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
self.__critical = False
|
||||
self.__numnotifications = 0
|
||||
self.__regolith = self.parameter("regolith", False)
|
||||
|
||||
|
||||
def full_text(self, widgets):
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.connect("/tmp/rofi_notification_daemon")
|
||||
# below code will fetch two numbers in a list, e.g. ['22', '1']
|
||||
# first is total number of notifications, second is number of critical notifications
|
||||
if self.__regolith:
|
||||
client.sendall(bytes("num\n", "utf-8"))
|
||||
else:
|
||||
client.sendall(bytes("num", "utf-8"))
|
||||
val = client.recv(512)
|
||||
val = val.decode("utf-8")
|
||||
if self.__regolith:
|
||||
l = val.split(',',2)
|
||||
else:
|
||||
l = val.split('\n',2)
|
||||
self.__numnotifications = int(l[0])
|
||||
self.__critical = bool(int(l[1]))
|
||||
return self.__numnotifications
|
||||
|
||||
def state(self, widget):
|
||||
# rofication doesn't really support the idea of seen vs unseen notifications
|
||||
# marking a message as "seen" actually just sets its urgency to normal
|
||||
# so, doing highlighting if any notifications are present
|
||||
if self.__critical:
|
||||
return ["critical"]
|
||||
elif self.__numnotifications:
|
||||
return ["warning"]
|
||||
return []
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -31,14 +31,13 @@ class Module(core.module.Module):
|
|||
orientation = curr_orient
|
||||
break
|
||||
|
||||
widget = self.widget(display)
|
||||
widget = self.widget(name=display)
|
||||
if not widget:
|
||||
widget = self.add_widget(full_text=display, name=display)
|
||||
core.input.register(
|
||||
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
|
||||
)
|
||||
widget.set("orientation", orientation)
|
||||
widgets.append(widget)
|
||||
|
||||
def state(self, widget):
|
||||
return widget.get("orientation", "normal")
|
||||
|
|
|
@ -55,7 +55,7 @@ class Module(core.module.Module):
|
|||
|
||||
self._state = []
|
||||
|
||||
self._newspaper_filename = tempfile.mktemp(".html")
|
||||
self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html")
|
||||
|
||||
self._last_refresh = 0
|
||||
self._last_update = 0
|
||||
|
@ -308,10 +308,11 @@ class Module(core.module.Module):
|
|||
|
||||
while newspaper_items:
|
||||
content += self._create_news_section(newspaper_items)
|
||||
open(self._newspaper_filename, "w").write(
|
||||
self._newspaper_file.write(
|
||||
HTML_TEMPLATE.replace("[[CONTENT]]", content)
|
||||
)
|
||||
webbrowser.open("file://" + self._newspaper_filename)
|
||||
self._newspaper_file.flush()
|
||||
webbrowser.open("file://" + self._newspaper_file.name)
|
||||
self._update_history("newspaper")
|
||||
self._save_history()
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"""Displays sensor temperature
|
||||
|
||||
Parameters:
|
||||
* sensors.use_sensors: whether to use the sensors command
|
||||
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
||||
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
||||
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
||||
|
@ -18,6 +19,7 @@ contributed by `mijoharas <https://github.com/mijoharas>`_ - many thanks!
|
|||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
@ -46,22 +48,25 @@ class Module(core.module.Module):
|
|||
self._json = util.format.asbool(self.parameter("json", False))
|
||||
self._freq = util.format.asbool(self.parameter("show_freq", True))
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
|
||||
self.determine_method()
|
||||
self.use_sensors = self.determine_method()
|
||||
|
||||
def determine_method(self):
|
||||
if util.format.asbool(self.parameter("use_sensors")) == True:
|
||||
return True
|
||||
if util.format.asbool(self.parameter("use_sensors")) == False:
|
||||
return False
|
||||
if self.parameter("path") != None and self._json == False:
|
||||
self.use_sensors = False # use thermal zone
|
||||
else:
|
||||
# try to use output of sensors -u
|
||||
try:
|
||||
output = util.cli.execute("sensors -u")
|
||||
self.use_sensors = True
|
||||
log.debug("Sensors command available")
|
||||
except FileNotFoundError as e:
|
||||
log.info(
|
||||
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
|
||||
)
|
||||
self.use_sensors = False
|
||||
return False
|
||||
# try to use output of sensors -u
|
||||
try:
|
||||
_ = util.cli.execute("sensors -u")
|
||||
log.debug("Sensors command available")
|
||||
return True
|
||||
except FileNotFoundError as e:
|
||||
log.info(
|
||||
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
|
||||
)
|
||||
return False
|
||||
|
||||
def _get_temp_from_sensors(self):
|
||||
if self._json == True:
|
||||
|
@ -92,22 +97,31 @@ class Module(core.module.Module):
|
|||
|
||||
def get_temp(self):
|
||||
if self.use_sensors:
|
||||
temperature = self._get_temp_from_sensors()
|
||||
log.debug("Retrieve temperature from sensors -u")
|
||||
else:
|
||||
try:
|
||||
temperature = open(
|
||||
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp")
|
||||
).read()[:2]
|
||||
log.debug("retrieved temperature from /sys/class/")
|
||||
# TODO: Iterate through all thermal zones to determine the correct one and use its value
|
||||
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal
|
||||
|
||||
except IOError:
|
||||
temperature = "unknown"
|
||||
log.info("Can not determine temperature, please install lm-sensors")
|
||||
|
||||
return temperature
|
||||
return self._get_temp_from_sensors()
|
||||
try:
|
||||
path = None
|
||||
# use path provided by the user
|
||||
if self.parameter("path") is not None:
|
||||
path = self.parameter("path")
|
||||
# find the thermal zone that provides cpu temperature
|
||||
else:
|
||||
for zone in os.listdir("/sys/class/thermal"):
|
||||
if not zone.startswith("thermal_zone"):
|
||||
continue
|
||||
if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp":
|
||||
continue
|
||||
path = f"/sys/class/thermal/{zone}/temp"
|
||||
# use zone 0 as fallback
|
||||
if path is None:
|
||||
log.info("Can not determine temperature path, using thermal_zone0")
|
||||
path = "/sys/class/thermal/thermal_zone0/temp"
|
||||
log.debug(f"retrieving temperature from {path}")
|
||||
# the values are t°C * 1000, so divide by 1000
|
||||
return str(int(open(path).read()) / 1000)
|
||||
except IOError:
|
||||
log.info("Can not determine temperature, please install lm-sensors")
|
||||
return "unknown"
|
||||
|
||||
def get_mhz(self):
|
||||
mhz = None
|
||||
|
|
|
@ -41,19 +41,21 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.get_output))
|
||||
|
||||
self.__command = self.parameter("command", 'echo "no command configured"')
|
||||
self.__command = os.path.expanduser(self.__command)
|
||||
self.__async = util.format.asbool(self.parameter("async"))
|
||||
|
||||
if self.__async:
|
||||
self.__output = "please wait..."
|
||||
self.__current_thread = threading.Thread()
|
||||
|
||||
# LMB and RMB will update output regardless of timer
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
|
||||
if self.parameter("scrolling.makewide") is None:
|
||||
self.set("scrolling.makewide", False)
|
||||
|
||||
def set_output(self, value):
|
||||
self.__output = value
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
|
||||
@core.decorators.scrollable
|
||||
def get_output(self, _):
|
||||
return self.__output
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
when clicking on it.
|
||||
|
||||
For more than one shortcut, the commands and labels are strings separated by
|
||||
a demiliter (; semicolon by default).
|
||||
a delimiter (; semicolon by default).
|
||||
|
||||
For example in order to create two shortcuts labeled A and B with commands
|
||||
cmdA and cmdB you could do:
|
||||
|
||||
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B'
|
||||
./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)'
|
||||
|
||||
Parameters:
|
||||
* shortcut.cmds : List of commands to execute
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
|
||||
"""Displays HDD smart status of different drives or all drives
|
||||
|
||||
Requires the following executables:
|
||||
* sudo
|
||||
* smartctl
|
||||
|
||||
Parameters:
|
||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles')
|
||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'separate' or 'singles')
|
||||
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
||||
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
||||
"""
|
||||
|
@ -34,7 +38,7 @@ class Module(core.module.Module):
|
|||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
if self.display == "combined":
|
||||
if self.display == "combined" or self.display == "combined_singles":
|
||||
widget = self.add_widget()
|
||||
widget.set("device", "combined")
|
||||
widget.set("assessment", self.combined())
|
||||
|
@ -77,6 +81,8 @@ class Module(core.module.Module):
|
|||
|
||||
def combined(self):
|
||||
for device in self.devices:
|
||||
if self.display == "combined_singles" and device not in self.drives:
|
||||
continue
|
||||
result = self.smart(device)
|
||||
if result == "Fail":
|
||||
return "Fail"
|
||||
|
|
58
bumblebee_status/modules/contrib/solaar.py
Normal file
58
bumblebee_status/modules/contrib/solaar.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
"""Shows status and load percentage of logitech's unifying device
|
||||
|
||||
Requires the following executable:
|
||||
* solaar (from community)
|
||||
|
||||
contributed by `cambid <https://github.com/cambid>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
||||
self.__battery = self.parameter("device", "")
|
||||
self.background = True
|
||||
self.__battery_status = ""
|
||||
self.__error = False
|
||||
if self.__battery != "":
|
||||
self.__cmd = f"solaar show '{self.__battery}'"
|
||||
else:
|
||||
self.__cmd = "solaar show"
|
||||
|
||||
@property
|
||||
def __format(self):
|
||||
return self.parameter("format", "{}")
|
||||
|
||||
def utilization(self, widget):
|
||||
return self.__format.format(self.__battery_status)
|
||||
|
||||
def update(self):
|
||||
self.__error = False
|
||||
code, result = util.cli.execute(
|
||||
self.__cmd, ignore_errors=True, return_exitcode=True
|
||||
)
|
||||
|
||||
if code == 0:
|
||||
for line in result.split('\n'):
|
||||
if line.count('Battery') > 0:
|
||||
self.__battery_status = line.split(':')[1].strip()
|
||||
else:
|
||||
self.__error = True
|
||||
logging.error(f"solaar exited with {code}: {result}")
|
||||
|
||||
def state(self, widget):
|
||||
if self.__error:
|
||||
return "warning"
|
||||
return "okay"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -9,7 +9,6 @@ an example.
|
|||
|
||||
Requires the following libraries:
|
||||
* requests
|
||||
* regex
|
||||
|
||||
Parameters:
|
||||
* spaceapi.url: String representation of the api endpoint
|
||||
|
@ -17,7 +16,7 @@ Parameters:
|
|||
|
||||
Format Strings:
|
||||
* Format strings are indicated by double %%
|
||||
* They represent a leaf in the JSON tree, layers seperated by '.'
|
||||
* They represent a leaf in the JSON tree, layers separated by '.'
|
||||
* Boolean values can be overwritten by appending '%true%false'
|
||||
in the format string
|
||||
* Example: to reference 'open' in '{'state':{'open': true}}'
|
||||
|
|
|
@ -8,10 +8,16 @@ Parameters:
|
|||
Available values are: {album}, {title}, {artist}, {trackNumber}
|
||||
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
|
||||
* spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget.
|
||||
Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous.
|
||||
* spotify.bus_name: String (defaults to `spotify`)
|
||||
Available values: spotify, spotifyd
|
||||
|
||||
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
||||
|
||||
added controls by `LtPeriwinkle <https://github.com/LtPeriwinkle>`_ - many thanks!
|
||||
|
||||
fixed icons and layout parameter by `gkeep <https://github.com/gkeep>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
@ -23,31 +29,98 @@ import core.input
|
|||
import core.decorators
|
||||
import util.format
|
||||
|
||||
import logging
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__layout = self.parameter(
|
||||
"layout",
|
||||
util.format.aslist("spotify.song,spotify.prev,spotify.pause,spotify.next"),
|
||||
self.background = True
|
||||
|
||||
self.__bus_name = self.parameter("bus_name", "spotify")
|
||||
|
||||
self.__layout = util.format.aslist(
|
||||
self.parameter(
|
||||
"layout", "spotify.song,spotify.prev,spotify.pause,spotify.next",
|
||||
)
|
||||
)
|
||||
|
||||
self.__bus = dbus.SessionBus()
|
||||
self.__song = ""
|
||||
self.__pause = ""
|
||||
self.__format = self.parameter("format", "{artist} - {title}")
|
||||
|
||||
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
|
||||
if self.__bus_name == "spotifyd":
|
||||
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotifyd \
|
||||
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
||||
else:
|
||||
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
|
||||
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
||||
|
||||
widget_map = {}
|
||||
for widget_name in self.__layout:
|
||||
widget = self.add_widget(name=widget_name)
|
||||
if widget_name == "spotify.prev":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "Previous",
|
||||
}
|
||||
widget.set("state", "prev")
|
||||
elif widget_name == "spotify.pause":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "PlayPause",
|
||||
}
|
||||
elif widget_name == "spotify.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "Next",
|
||||
}
|
||||
widget.set("state", "next")
|
||||
elif widget_name == "spotify.song":
|
||||
if util.format.asbool(self.parameter("concise_controls", "false")):
|
||||
widget_map[widget] = [
|
||||
{
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "PlayPause",
|
||||
}, {
|
||||
"button": core.input.WHEEL_UP,
|
||||
"cmd": self.__cmd + "Next",
|
||||
}, {
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
"cmd": self.__cmd + "Previous",
|
||||
}
|
||||
]
|
||||
else:
|
||||
raise KeyError(
|
||||
"The spotify module does not have a {widget_name!r} widget".format(
|
||||
widget_name=widget_name
|
||||
)
|
||||
)
|
||||
# is there any reason the inputs can't be directly registered above?
|
||||
for widget, callback_options in widget_map.items():
|
||||
if isinstance(callback_options, dict):
|
||||
core.input.register(widget, **callback_options)
|
||||
|
||||
elif isinstance(callback_options, list): # used by concise_controls
|
||||
for opts in callback_options:
|
||||
core.input.register(widget, **opts)
|
||||
|
||||
|
||||
def hidden(self):
|
||||
return self.string_song == ""
|
||||
|
||||
def __get_song(self):
|
||||
bus = dbus.SessionBus()
|
||||
spotify = bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
@core.decorators.scrollable
|
||||
def __get_song(self, widget):
|
||||
bus = self.__bus
|
||||
if self.__bus_name == "spotifyd":
|
||||
spotify = bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
else:
|
||||
spotify = bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties")
|
||||
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
|
||||
self.__song = self.__format.format(
|
||||
|
@ -56,54 +129,36 @@ class Module(core.module.Module):
|
|||
artist=",".join(props.get("xesam:artist")),
|
||||
trackNumber=str(props.get("xesam:trackNumber")),
|
||||
)
|
||||
return self.__song
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.clear_widgets()
|
||||
self.__get_song()
|
||||
if self.__bus_name == "spotifyd":
|
||||
bus = self.__bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
else:
|
||||
bus = self.__bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
|
||||
widget_map = {}
|
||||
for widget_name in self.__layout:
|
||||
widget = self.add_widget(name=widget_name)
|
||||
if widget_name == "spotify.prev":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "Previous",
|
||||
}
|
||||
widget.set("state", "prev")
|
||||
elif widget_name == "spotify.pause":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "PlayPause",
|
||||
}
|
||||
for widget in self.widgets():
|
||||
if widget.name == "spotify.pause":
|
||||
playback_status = str(
|
||||
dbus.Interface(dbus.SessionBus().get_object(
|
||||
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties")
|
||||
.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
||||
dbus.Interface(
|
||||
bus,
|
||||
"org.freedesktop.DBus.Properties",
|
||||
).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
||||
)
|
||||
if playback_status == "Playing":
|
||||
widget.set("state", "playing")
|
||||
else:
|
||||
widget.set("state", "paused")
|
||||
elif widget_name == "spotify.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "Next",
|
||||
}
|
||||
widget.set("state", "next")
|
||||
elif widget_name == "spotify.song":
|
||||
elif widget.name == "spotify.song":
|
||||
widget.set("state", "song")
|
||||
widget.full_text(self.__song)
|
||||
else:
|
||||
raise KeyError(
|
||||
"The spotify module does not have a {widget_name!r} widget".format(
|
||||
widget_name=widget_name
|
||||
)
|
||||
)
|
||||
for widget, callback_options in widget_map.items():
|
||||
core.input.register(widget, **callback_options)
|
||||
widget.full_text(self.__get_song(widget))
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
self.__song = ""
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Display a stock quote from worldtradingdata.com
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
"""Display a stock quote from finance.yahoo.com
|
||||
|
||||
Parameters:
|
||||
* stock.symbols : Comma-separated list of symbols to fetch
|
||||
* stock.change : Should we fetch change in stock value (defaults to True)
|
||||
* stock.apikey : API key created on https://alphavantage.co
|
||||
* stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}"
|
||||
* stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent"
|
||||
|
||||
|
||||
contributed by `msoulier <https://github.com/msoulier>`_ - many thanks!
|
||||
|
@ -25,6 +24,12 @@ import core.decorators
|
|||
|
||||
import util.format
|
||||
|
||||
def flatten(d, result):
|
||||
for k, v in d.items():
|
||||
if type(v) is dict:
|
||||
flatten(v, result)
|
||||
else:
|
||||
result[k] = v
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(hours=1)
|
||||
|
@ -32,37 +37,41 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.value))
|
||||
|
||||
self.__symbols = self.parameter("symbols", "")
|
||||
self.__apikey = self.parameter("apikey", None)
|
||||
self.__fields = self.parameter("fields", "01. symbol,05. price,10. change percent").split(",")
|
||||
self.__url = self.parameter("url", "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}")
|
||||
self.__change = util.format.asbool(self.parameter("change", True))
|
||||
self.__value = None
|
||||
self.__values = []
|
||||
|
||||
|
||||
def value(self, widget):
|
||||
results = []
|
||||
if not self.__value:
|
||||
return "n/a"
|
||||
data = json.loads(self.__value)
|
||||
result = ""
|
||||
|
||||
for symbol in data["quoteResponse"]["result"]:
|
||||
valkey = "regularMarketChange" if self.__change else "regularMarketPrice"
|
||||
sym = symbol.get("symbol", "n/a")
|
||||
currency = symbol.get("currency", "USD")
|
||||
val = "n/a" if not valkey in symbol else "{:.2f}".format(symbol[valkey])
|
||||
results.append("{} {} {}".format(sym, val, currency))
|
||||
return " ".join(results)
|
||||
for value in self.__values:
|
||||
res = {}
|
||||
flatten(value, res)
|
||||
for field in self.__fields:
|
||||
result += res.get(field, "n/a") + " "
|
||||
result = result[:-1]
|
||||
return result
|
||||
|
||||
def fetch(self):
|
||||
results = []
|
||||
if self.__symbols:
|
||||
url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols="
|
||||
url += (
|
||||
self.__symbols
|
||||
+ "&fields=regularMarketPrice,currency,regularMarketChange"
|
||||
)
|
||||
return urllib.request.urlopen(url).read().strip()
|
||||
for symbol in self.__symbols.split(","):
|
||||
url = self.__url.format(symbol=symbol, apikey=self.__apikey)
|
||||
try:
|
||||
results.append(json.loads(urllib.request.urlopen(url).read().strip()))
|
||||
except urllib.request.URLError:
|
||||
logging.error("unable to open stock exchange url")
|
||||
return []
|
||||
else:
|
||||
logging.error("unable to retrieve stock exchange rate")
|
||||
return None
|
||||
return []
|
||||
return results
|
||||
|
||||
def update(self):
|
||||
self.__value = self.fetch()
|
||||
self.__values = self.fetch()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
Requires the following python packages:
|
||||
* requests
|
||||
* suntime
|
||||
* python-dateutil
|
||||
|
||||
Parameters:
|
||||
* cpu.lat : Latitude of your location
|
||||
* cpu.lon : Longitude of your location
|
||||
* sun.lat : Latitude of your location
|
||||
* sun.lon : Longitude of your location
|
||||
|
||||
(if none of those are set, location is determined automatically via location APIs)
|
||||
|
||||
|
@ -38,7 +39,11 @@ class Module(core.module.Module):
|
|||
self.__sun = None
|
||||
|
||||
if not lat or not lon:
|
||||
lat, lon = util.location.coordinates()
|
||||
try:
|
||||
lat, lon = util.location.coordinates()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if lat and lon:
|
||||
self.__sun = Sun(float(lat), float(lon))
|
||||
|
||||
|
@ -54,6 +59,10 @@ class Module(core.module.Module):
|
|||
return "n/a"
|
||||
|
||||
def __calculate_times(self):
|
||||
if not self.__sun:
|
||||
self.__sunset = self.__sunrise = None
|
||||
return
|
||||
|
||||
self.__isup = False
|
||||
|
||||
order_matters = True
|
||||
|
|
|
@ -8,11 +8,11 @@ adds the possibility to
|
|||
* reboot
|
||||
|
||||
the system.
|
||||
|
||||
|
||||
Per default a confirmation dialog is shown before the actual action is performed.
|
||||
|
||||
|
||||
Parameters:
|
||||
* system.confirm: show confirmation dialog before performing any action (default: true)
|
||||
* system.confirm: show confirmation dialog before performing any action (default: true)
|
||||
* system.reboot: specify a reboot command (defaults to 'reboot')
|
||||
* system.shutdown: specify a shutdown command (defaults to 'shutdown -h now')
|
||||
* system.logout: specify a logout command (defaults to 'i3exit logout')
|
||||
|
@ -21,6 +21,9 @@ Parameters:
|
|||
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
||||
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
||||
|
||||
Requirements:
|
||||
tkinter (python3-tk package on debian based systems either you can install it as python package)
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
@ -69,7 +72,12 @@ class Module(core.module.Module):
|
|||
util.cli.execute(command)
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu()
|
||||
popupcmd = self.parameter("popupcmd", "");
|
||||
if (popupcmd != ""):
|
||||
util.cli.execute(popupcmd)
|
||||
return
|
||||
|
||||
menu = util.popup.menu(self.__config)
|
||||
reboot_cmd = self.parameter("reboot", "reboot")
|
||||
shutdown_cmd = self.parameter("shutdown", "shutdown -h now")
|
||||
logout_cmd = self.parameter("logout", "i3exit logout")
|
||||
|
@ -93,7 +101,7 @@ class Module(core.module.Module):
|
|||
menu.add_menuitem(
|
||||
"log out",
|
||||
callback=functools.partial(
|
||||
self.__on_command, "Log out", "Log out?", "i3exit logout"
|
||||
self.__on_command, "Log out", "Log out?", logout_cmd
|
||||
),
|
||||
)
|
||||
# don't ask for these
|
||||
|
|
|
@ -5,6 +5,7 @@ Requires the following library:
|
|||
|
||||
Parameters:
|
||||
* taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc)
|
||||
* taskwarrior.show_active: true/false(default) to show the active task ID and description when one is active, otherwise show the total number pending.
|
||||
|
||||
|
||||
contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
|
||||
|
@ -22,20 +23,45 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__pending_tasks = "0"
|
||||
self.__status = "stopped"
|
||||
|
||||
def update(self):
|
||||
"""Return a string with the number of pending tasks from TaskWarrior."""
|
||||
"""Return a string with the number of pending tasks from TaskWarrior
|
||||
or the descripton of an active task.
|
||||
|
||||
if show.active is set in the config, show the description of the
|
||||
current active task, otherwise the number of pending tasks will be displayed.
|
||||
"""
|
||||
try:
|
||||
taskrc = self.parameter("taskrc", "~/.taskrc")
|
||||
show_active = self.parameter("show_active", False)
|
||||
w = TaskWarrior(config_filename=taskrc)
|
||||
pending_tasks = w.filter_tasks({"status": "pending"})
|
||||
self.__pending_tasks = str(len(pending_tasks))
|
||||
active_tasks = (
|
||||
w.filter_tasks({"start.any": "", "status": "pending"}) or None
|
||||
)
|
||||
if show_active and active_tasks:
|
||||
# this is using the first element of the list, if there happen
|
||||
# to be other active tasks, they won't be displayed.
|
||||
reporting_tasks = (
|
||||
f"{active_tasks[0]['id']} - {active_tasks[0]['description']}"
|
||||
)
|
||||
self.__status = "active"
|
||||
else:
|
||||
reporting_tasks = len(w.filter_tasks({"status": "pending"}))
|
||||
self.__status = "stopped"
|
||||
self.__pending_tasks = reporting_tasks
|
||||
except:
|
||||
self.__pending_tasks = "n/a"
|
||||
self.__status = "stopped"
|
||||
|
||||
@core.decorators.scrollable
|
||||
def output(self, _):
|
||||
"""Format the task counter to output in bumblebee."""
|
||||
return "{}".format(self.__pending_tasks)
|
||||
|
||||
def state(self, widget):
|
||||
"""Return the set status to reflect state"""
|
||||
return self.__status
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
89
bumblebee_status/modules/contrib/thunderbird.py
Normal file
89
bumblebee_status/modules/contrib/thunderbird.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the unread emails count for one or more Thunderbird inboxes
|
||||
|
||||
Parameters:
|
||||
* thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird)
|
||||
* thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf)
|
||||
|
||||
Tips:
|
||||
* You can run the following command in order to list all your Thunderbird inboxes
|
||||
|
||||
find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}'
|
||||
|
||||
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.thunderbird))
|
||||
|
||||
self.__total = 0
|
||||
self.__label = ""
|
||||
self.__inboxes = []
|
||||
|
||||
self.__home = self.parameter("home", "")
|
||||
inboxes = self.parameter("inboxes", "")
|
||||
if inboxes:
|
||||
self.__inboxes = util.format.aslist(inboxes)
|
||||
|
||||
def thunderbird(self, _):
|
||||
return str(self.__label)
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__total = 0
|
||||
self.__label = ""
|
||||
|
||||
stream = self.__getThunderbirdStream()
|
||||
unread = self.__getUnreadMessagesByInbox(stream)
|
||||
|
||||
counts = []
|
||||
for inbox in self.__inboxes:
|
||||
count = unread[inbox]
|
||||
self.__total += int(count)
|
||||
counts.append(count)
|
||||
|
||||
self.__label = "/".join(counts)
|
||||
|
||||
except Exception as err:
|
||||
self.__label = err
|
||||
|
||||
def __getThunderbirdStream(self):
|
||||
cmd = (
|
||||
"find "
|
||||
+ self.__home
|
||||
+ " -name '*.msf' -exec grep -REo 'A2=[0-9]' {} + | grep"
|
||||
)
|
||||
for inbox in self.__inboxes:
|
||||
cmd += " -e {}".format(inbox)
|
||||
cmd += "| awk -F / '{print $(NF-1)\"/\"$(NF)}'"
|
||||
|
||||
return util.cli.execute(cmd, shell=True).strip().split("\n")
|
||||
|
||||
def __getUnreadMessagesByInbox(self, stream):
|
||||
unread = {}
|
||||
for line in stream:
|
||||
entry = line.split(":A2=")
|
||||
inbox = entry[0]
|
||||
count = entry[1]
|
||||
unread[inbox] = count
|
||||
|
||||
return unread
|
||||
|
||||
def state(self, widget):
|
||||
if self.__total > 0:
|
||||
return ["warning"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -9,6 +9,7 @@ Parameters:
|
|||
* title.max : Maximum character length for title before truncating. Defaults to 64.
|
||||
* title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'.
|
||||
* title.scroll : Boolean flag for scrolling title. Defaults to False
|
||||
* title.short : Boolean flag for short title. Defaults to False
|
||||
|
||||
|
||||
contributed by `UltimatePancake <https://github.com/UltimatePancake>`_ - many thanks!
|
||||
|
@ -35,6 +36,7 @@ class Module(core.module.Module):
|
|||
|
||||
# parsing of parameters
|
||||
self.__scroll = util.format.asbool(self.parameter("scroll", False))
|
||||
self.__short = util.format.asbool(self.parameter("short", False))
|
||||
self.__max = int(self.parameter("max", 64))
|
||||
self.__placeholder = self.parameter("placeholder", "...")
|
||||
self.__title = ""
|
||||
|
@ -48,8 +50,9 @@ class Module(core.module.Module):
|
|||
|
||||
# create a connection with i3ipc
|
||||
self.__i3 = i3ipc.Connection()
|
||||
# event is called both on focus change and title change
|
||||
# event is called both on focus change and title change, and on workspace change
|
||||
self.__i3.on("window", lambda __p_i3, __p_e: self.__pollTitle())
|
||||
self.__i3.on("workspace", lambda __p_i3, __p_e: self.__pollTitle())
|
||||
# begin listening for events
|
||||
threading.Thread(target=self.__i3.main).start()
|
||||
|
||||
|
@ -66,7 +69,9 @@ class Module(core.module.Module):
|
|||
def __pollTitle(self):
|
||||
"""Updating current title."""
|
||||
try:
|
||||
self.__full_title = self.__i3.get_tree().find_focused().name
|
||||
focused = self.__i3.get_tree().find_focused().name
|
||||
self.__full_title = focused.split(
|
||||
"-")[-1].strip() if self.__short else focused
|
||||
except:
|
||||
self.__full_title = no_title
|
||||
if self.__full_title is None:
|
||||
|
|
|
@ -21,9 +21,10 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
|
||||
self.__editor = self.parameter("editor", "xdg-open")
|
||||
self.__todos = self.count_items()
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc)
|
||||
self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc)
|
||||
)
|
||||
|
||||
def output(self, widget):
|
||||
|
@ -39,11 +40,12 @@ class Module(core.module.Module):
|
|||
|
||||
def count_items(self):
|
||||
try:
|
||||
i = -1
|
||||
i = 0
|
||||
with open(self.__doc) as f:
|
||||
for i, l in enumerate(f):
|
||||
pass
|
||||
return i + 1
|
||||
for l in f.readlines():
|
||||
if l.strip() != '':
|
||||
i += 1
|
||||
return i
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
|
57
bumblebee_status/modules/contrib/todo_org.py
Normal file
57
bumblebee_status/modules/contrib/todo_org.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
"""Displays the number of todo items from an org-mode file
|
||||
Parameters:
|
||||
* todo_org.file: File to read TODOs from (defaults to ~/org/todo.org)
|
||||
* todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed)
|
||||
Based on the todo module by `codingo <https://github.com/codingo>`
|
||||
"""
|
||||
|
||||
import re
|
||||
import os.path
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
from util.format import asbool
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__todo_regex = re.compile("^\\s*\\*+\\s*TODO")
|
||||
self.__done_regex = re.compile("^\\s*\\*+\\s*DONE")
|
||||
|
||||
self.__doc = os.path.expanduser(
|
||||
self.parameter("file", "~/org/todo.org")
|
||||
)
|
||||
self.__remaining = asbool(self.parameter("remaining", "False"))
|
||||
self.__todo, self.__total = self.count_items()
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd="emacs {}".format(self.__doc)
|
||||
)
|
||||
|
||||
def output(self, widget):
|
||||
if self.__remaining:
|
||||
return "TODO: {}/{}".format(self.__todo, self.__total)
|
||||
return "TODO: {}/{}".format(self.__total-self.__todo, self.__total)
|
||||
|
||||
def update(self):
|
||||
self.__todo, self.__total = self.count_items()
|
||||
|
||||
def count_items(self):
|
||||
todo, total = 0, 0
|
||||
try:
|
||||
with open(self.__doc, "r") as f:
|
||||
for line in f:
|
||||
if self.__todo_regex.match(line.upper()) is not None:
|
||||
todo += 1
|
||||
total += 1
|
||||
elif self.__done_regex.match(line.upper()) is not None:
|
||||
total += 1
|
||||
return todo, total
|
||||
except OSError:
|
||||
return -1, -1
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
76
bumblebee_status/modules/contrib/todoist.py
Normal file
76
bumblebee_status/modules/contrib/todoist.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the nº of Todoist tasks that are due:
|
||||
|
||||
* https://developer.todoist.com/rest/v2/#get-active-tasks
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the Todoist get active tasks query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* todoist.token: Todoist api token, you can get it in https://todoist.com/app/settings/integrations/developer.
|
||||
* todoist.filter: a filter statement defined by Todoist (https://todoist.com/help/articles/introduction-to-filters), eg: "!assigned to: others & (Overdue | due: today)"
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
HOST_API = "https://api.todoist.com"
|
||||
HOST_WEBSITE = "https://todoist.com/app/today"
|
||||
|
||||
TASKS_URL = f"{HOST_API}/rest/v2/tasks"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.todoist))
|
||||
|
||||
self.__user_id = None
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
|
||||
token = self.parameter("token", "")
|
||||
self.__filter = self.parameter("filter", "")
|
||||
|
||||
self.__requests = requests.Session()
|
||||
self.__requests.headers.update({"Authorization": f"Bearer {token}"})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd=f"{cmd} {HOST_WEBSITE}",
|
||||
)
|
||||
|
||||
def todoist(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__label = self.__get_pending_tasks()
|
||||
except Exception:
|
||||
self.__label = "n/a"
|
||||
|
||||
def __get_pending_tasks(self) -> str:
|
||||
params = {"filter": self.__filter} if self.__filter else None
|
||||
|
||||
response = self.__requests.get(TASKS_URL, params=params)
|
||||
data = response.json()
|
||||
|
||||
return str(len(data))
|
|
@ -8,7 +8,7 @@ Parameters:
|
|||
* traffic.showname: If set to False, hide network interface name (defaults to True)
|
||||
* traffic.format: Format string for download/upload speeds.
|
||||
Defaults to '{:.2f}'
|
||||
* traffic.graphlen: Graph lenth in seconds. Positive even integer. Each
|
||||
* traffic.graphlen: Graph length in seconds. Positive even integer. Each
|
||||
char shows 2 seconds. If set, enables up/down traffic
|
||||
graphs
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
"""Toggle twmn notifications.
|
||||
|
||||
Requires the following executable:
|
||||
* systemctl
|
||||
|
||||
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
|
78
bumblebee_status/modules/contrib/usage.py
Normal file
78
bumblebee_status/modules/contrib/usage.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Module for ActivityWatch (https://activitywatch.net/)
|
||||
Displays the amount of time the system was used actively.
|
||||
|
||||
Requirements:
|
||||
* sqlite3 module for python
|
||||
* ActivityWatch
|
||||
|
||||
Errors:
|
||||
* when you get 'error: unable to open database file', modify the parameter 'database' to your ActivityWatch database file
|
||||
-> often found by running 'locate aw-server/peewee-sqlite.v2.db'
|
||||
|
||||
Parameters:
|
||||
* usage.database: path to your database file
|
||||
* usage.format: Specify what gets printed to the bar
|
||||
-> use 'HH', 'MM' or 'SS', they will get replaced by the number of hours, minutes and seconds, respectively
|
||||
|
||||
contributed by lasnikr (https://github.com/lasnikr)
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__usage = ""
|
||||
|
||||
def output(self, _):
|
||||
return "{}".format(self.__usage)
|
||||
|
||||
def update(self):
|
||||
database_loc = self.parameter(
|
||||
"database", "~/.local/share/activitywatch/aw-server/peewee-sqlite.v2.db"
|
||||
)
|
||||
home = os.path.expanduser("~")
|
||||
|
||||
database = sqlite3.connect(database_loc.replace("~", home))
|
||||
cursor = database.cursor()
|
||||
|
||||
cursor.execute("SELECT key, id FROM bucketmodel")
|
||||
|
||||
bucket_id = 1
|
||||
|
||||
for tuple in cursor.fetchall():
|
||||
if "aw-watcher-afk" in tuple[1]:
|
||||
bucket_id = tuple[0]
|
||||
|
||||
cursor.execute(
|
||||
f"SELECT duration, datastr FROM eventmodel WHERE bucket_id = {bucket_id} "
|
||||
+ 'AND strftime("%Y,%m,%d", timestamp) = strftime("%Y,%m,%d", "now")'
|
||||
)
|
||||
|
||||
duration = 0
|
||||
|
||||
for tuple in cursor.fetchall():
|
||||
if '{"status": "not-afk"}' in tuple[1]:
|
||||
duration += tuple[0]
|
||||
|
||||
hours = "%.0f" % (duration // 3600)
|
||||
minutes = "%.0f" % ((duration % 3600) // 60)
|
||||
seconds = "%.0f" % (duration % 60)
|
||||
|
||||
formatting = self.parameter("format", "HHh, MMmin")
|
||||
self.__usage = (
|
||||
formatting.replace("HH", hours)
|
||||
.replace("MM", minutes)
|
||||
.replace("SS", seconds)
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -1,4 +1,5 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Displays the VPN profile that is currently in use.
|
||||
|
||||
|
@ -68,7 +69,7 @@ class Module(core.module.Module):
|
|||
|
||||
def vpn_status(self, widget):
|
||||
if self.__connected_vpn_profile is None:
|
||||
return "off"
|
||||
return ""
|
||||
return self.__connected_vpn_profile
|
||||
|
||||
def __on_vpndisconnect(self):
|
||||
|
@ -93,7 +94,7 @@ class Module(core.module.Module):
|
|||
self.__connected_vpn_profile = None
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu()
|
||||
menu = util.popup.menu(self.__config)
|
||||
|
||||
if self.__connected_vpn_profile is not None:
|
||||
menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect)
|
||||
|
|
94
bumblebee_status/modules/contrib/wakatime.py
Normal file
94
bumblebee_status/modules/contrib/wakatime.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the WakaTime daily/weekly/monthly times:
|
||||
|
||||
* https://wakatime.com/developers#stats
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the Wakatime status query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* wakatime.token: Wakatime secret api key, you can get it in https://wakatime.com/settings/account.
|
||||
* wakatime.range: Range of the output, default is "Today". Can be one of “Today”, “Yesterday”, “Last 7 Days”, “Last 7 Days from Yesterday”, “Last 14 Days”, “Last 30 Days”, “This Week”, “Last Week”, “This Month”, or “Last Month”.
|
||||
* wakatime.format: Format of the output, default is "digital"
|
||||
Valid inputs are:
|
||||
* "decimal" -> 1.37
|
||||
* "digital" -> 1:22
|
||||
* "seconds" -> 4931.29
|
||||
* "text" -> 1 hr 22 mins
|
||||
* "%H:%M:%S" -> 01:22:31 (or any other valid format)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
HOST_API = "https://wakatime.com"
|
||||
SUMMARIES_URL = f"{HOST_API}/api/v1/users/current/summaries"
|
||||
UTF8 = "utf-8"
|
||||
FORMAT_PARAMETERS = ["decimal", "digital", "seconds", "text"]
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.wakatime))
|
||||
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
|
||||
self.__output_format = self.parameter("format", "digital")
|
||||
self.__range = self.parameter("range", "Today")
|
||||
|
||||
self.__requests = requests.Session()
|
||||
|
||||
token = self.__encode_to_base_64(self.parameter("token", ""))
|
||||
self.__requests.headers.update({"Authorization": f"Basic {token}"})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd=f"{cmd} {HOST_API}/dashboard",
|
||||
)
|
||||
|
||||
def wakatime(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__label = self.__get_waka_time(self.__range)
|
||||
except Exception:
|
||||
self.__label = "n/a"
|
||||
|
||||
def __get_waka_time(self, since_date: str) -> str:
|
||||
response = self.__requests.get(f"{SUMMARIES_URL}?range={since_date}")
|
||||
|
||||
data = response.json()
|
||||
grand_total = data["cumulative_total"]
|
||||
|
||||
if self.__output_format in FORMAT_PARAMETERS:
|
||||
return str(grand_total[self.__output_format])
|
||||
else:
|
||||
total_seconds = int(grand_total["seconds"])
|
||||
return time.strftime(self.__output_format, time.gmtime(total_seconds))
|
||||
|
||||
@staticmethod
|
||||
def __encode_to_base_64(s: str) -> str:
|
||||
return base64.b64encode(s.encode(UTF8)).decode(UTF8)
|
|
@ -5,6 +5,10 @@
|
|||
Requires the following executable:
|
||||
* watson
|
||||
|
||||
Parameters:
|
||||
* watson.format: Output format, defaults to "{project} [{tags}]"
|
||||
Supported fields are: {project}, {tags}, {relative_start}, {absolute_start}
|
||||
|
||||
contributed by `bendardenne <https://github.com/bendardenne>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
@ -26,11 +30,11 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.text))
|
||||
|
||||
self.__tracking = False
|
||||
self.__project = ""
|
||||
self.__info = {}
|
||||
self.__format = self.parameter("format", "{project} [{tags}]")
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle)
|
||||
|
||||
def toggle(self, widget):
|
||||
self.__project = "hit"
|
||||
if self.__tracking:
|
||||
util.cli.execute("watson stop")
|
||||
else:
|
||||
|
@ -39,20 +43,27 @@ class Module(core.module.Module):
|
|||
|
||||
def text(self, widget):
|
||||
if self.__tracking:
|
||||
return self.__project
|
||||
return self.__format.format(**self.__info)
|
||||
else:
|
||||
return "Paused"
|
||||
|
||||
def update(self):
|
||||
output = util.cli.execute("watson status")
|
||||
if re.match(r"No project started", output):
|
||||
|
||||
m = re.search(r"Project ([^\[\]]+)(?: \[(.+)\])? started (.+) \((.+)\)", output)
|
||||
|
||||
if m:
|
||||
self.__tracking = True
|
||||
self.__info = {
|
||||
"project": m.group(1),
|
||||
"tags": m.group(2) or "",
|
||||
"relative_start": m.group(3),
|
||||
"absolute_start": m.group(4),
|
||||
}
|
||||
else:
|
||||
self.__tracking = False
|
||||
return
|
||||
|
||||
self.__tracking = True
|
||||
m = re.search(r"Project (.+) started", output)
|
||||
self.__project = m.group(1)
|
||||
|
||||
def state(self, widget):
|
||||
return "on" if self.__tracking else "off"
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Parameters:
|
|||
* weather.unit: metric (default), kelvin, imperial
|
||||
* weather.showcity: If set to true, show location information, otherwise hide it (defaults to true)
|
||||
* weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false)
|
||||
* weather.apikey: API key from http://api.openweathermap.org
|
||||
* weather.apikey: API key from https://api.openweathermap.org
|
||||
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
|
@ -116,7 +116,7 @@ class Module(core.module.Module):
|
|||
|
||||
def update(self):
|
||||
try:
|
||||
weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format(
|
||||
weather_url = "https://api.openweathermap.org/data/2.5/weather?appid={}".format(
|
||||
self.__apikey
|
||||
)
|
||||
weather_url = "{}&units={}".format(weather_url, self.__unit)
|
||||
|
|
126
bumblebee_status/modules/contrib/wlrotation.py
Normal file
126
bumblebee_status/modules/contrib/wlrotation.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Shows a widget for each connected screen and allows the user to loop through different orientations.
|
||||
|
||||
Parameters:
|
||||
* wlrotation.display : Name of the output display that will be rotated
|
||||
+ wlrotation.auto : Boolean value if the display should be rotatet automatic by default
|
||||
|
||||
Requires the following executable:
|
||||
* swaymsg
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
import util.cli
|
||||
|
||||
import iio
|
||||
import json
|
||||
from math import degrees, atan2, sqrt
|
||||
from os import environ, path
|
||||
|
||||
possible_orientations = ["normal", "90", "180", "270"]
|
||||
|
||||
class iioValue:
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
self.scale = self.read('scale')
|
||||
self.offset = self.read('offset')
|
||||
|
||||
def read(self, attr):
|
||||
return float(self.channel.attrs[attr].value)
|
||||
|
||||
def value(self):
|
||||
return (self.read('raw') + self.offset) * self.scale
|
||||
|
||||
class iioAccelDevice:
|
||||
def __init__(self):
|
||||
self.ctx = iio.Context() # store ctx pointer
|
||||
d = self.ctx.find_device('accel_3d')
|
||||
self.x = iioValue(d.find_channel('accel_x'))
|
||||
self.y = iioValue(d.find_channel('accel_y'))
|
||||
self.z = iioValue(d.find_channel('accel_z'))
|
||||
|
||||
def orientation(self):
|
||||
"""
|
||||
returns tuple of `[success, value]` where `success` indicates, if an accurate value could be meassured and `value` the sway output api compatible value or `normal` if success is `False`
|
||||
"""
|
||||
x_deg, y_deg, z_deg = self._deg()
|
||||
if abs(z_deg) < 70: # checks if device is angled too shallow
|
||||
if x_deg >= 70: return True, "270"
|
||||
if x_deg <= -70: return True, "90"
|
||||
if abs(x_deg) <= 20:
|
||||
if y_deg < 0: return True, "normal"
|
||||
if y_deg > 0: return True, "180"
|
||||
return False, "normal"
|
||||
|
||||
def _deg(self):
|
||||
gravity = 9.81
|
||||
x, y, z = self.x.value() / gravity, self.y.value() / gravity, self.z.value() / gravity
|
||||
return degrees(atan2(x, sqrt(pow(y, 2) + pow(z, 2)))), degrees(atan2(y, sqrt(pow(z, 2) + pow(x, 2)))), degrees(atan2(z, sqrt(pow(x, 2) + pow(y, 2))))
|
||||
|
||||
class Display():
|
||||
def __init__(self, name, widget, display_data, auto=False):
|
||||
self.name = name
|
||||
self.widget = widget
|
||||
self.accelDevice = iioAccelDevice()
|
||||
self._lock_auto_rotation(not auto)
|
||||
|
||||
self.widget.set("orientation", display_data['transform'])
|
||||
|
||||
core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=self.rotate_90deg)
|
||||
core.input.register(widget, button=core.input.RIGHT_MOUSE, cmd=self.toggle)
|
||||
|
||||
def rotate_90deg(self, event):
|
||||
# compute new orientation based on current orientation
|
||||
current = self.widget.get("orientation")
|
||||
self._set_rotation(possible_orientations[(possible_orientations.index(current) + 1) % len(possible_orientations)])
|
||||
# disable auto rotation
|
||||
self._lock_auto_rotation(True)
|
||||
|
||||
def toggle(self, event):
|
||||
self._lock_auto_rotation(not self.locked)
|
||||
|
||||
def auto_rotate(self):
|
||||
# automagically rotate the display based on sensor values
|
||||
# this is only called if rotation lock is disabled
|
||||
success, value = self.accelDevice.orientation()
|
||||
if success:
|
||||
self._set_rotation(value)
|
||||
|
||||
def _set_rotation(self, new_orientation):
|
||||
self.widget.set("orientation", new_orientation)
|
||||
util.cli.execute("swaymsg 'output {} transform {}'".format(self.name, new_orientation))
|
||||
|
||||
def _lock_auto_rotation(self, locked):
|
||||
self.locked = locked
|
||||
self.widget.set("locked", self.locked)
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.display = None
|
||||
display_filter = self.parameter("display", None)
|
||||
for display in json.loads(util.cli.execute("swaymsg -t get_outputs -r")):
|
||||
name = display['name']
|
||||
if display_filter == None or display_filter == name:
|
||||
self.display = Display(name, self.add_widget(name=name), display, auto=util.format.asbool(self.parameter("auto", False)))
|
||||
break # I assume that it makes only sense to rotate a single screen
|
||||
|
||||
def update(self):
|
||||
if self.display == None:
|
||||
return
|
||||
if self.display.locked:
|
||||
return
|
||||
|
||||
self.display.auto_rotate()
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
state.append("locked" if widget.get("locked", True) else "auto")
|
||||
return state
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -1,5 +1,8 @@
|
|||
"""Displays info about zpools present on the system
|
||||
|
||||
Requires the following executable:
|
||||
* sudo (if `zpool.sudo` is explicitly set to `true`)
|
||||
|
||||
Parameters:
|
||||
* zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools
|
||||
is displayed. (Default: '')
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
|
||||
"""Displays CPU utilization across all CPUs.
|
||||
|
||||
By default, opens `gnome-system-monitor` on left mouse click.
|
||||
|
||||
Requirements:
|
||||
* the psutil Python module for the first three items from the list above
|
||||
* gnome-system-monitor for default mouse click action
|
||||
|
||||
Parameters:
|
||||
* cpu.warning : Warning threshold in % of CPU usage (defaults to 70%)
|
||||
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
|
||||
* cpu.format : Format string (defaults to '{:.01f}%')
|
||||
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
|
||||
"""
|
||||
|
||||
import psutil
|
||||
|
@ -17,12 +21,19 @@ import core.module
|
|||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
||||
self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20))
|
||||
self._utilization = psutil.cpu_percent(percpu=False)
|
||||
super().__init__(config, theme, [])
|
||||
self._percpu = util.format.asbool(self.parameter("percpu", False))
|
||||
|
||||
for idx, cpu_perc in enumerate(self.cpu_utilization()):
|
||||
widget = self.add_widget(name="cpu#{}".format(idx), full_text=self.utilization)
|
||||
widget.set("utilization", cpu_perc)
|
||||
widget.set("theme.minwidth", self._format.format(100.0 - 10e-20))
|
||||
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
|
||||
)
|
||||
|
@ -31,14 +42,19 @@ class Module(core.module.Module):
|
|||
def _format(self):
|
||||
return self.parameter("format", "{:.01f}%")
|
||||
|
||||
def utilization(self, _):
|
||||
return self._format.format(self._utilization)
|
||||
def utilization(self, widget):
|
||||
return self._format.format(widget.get("utilization", 0.0))
|
||||
|
||||
def cpu_utilization(self):
|
||||
tmp = psutil.cpu_percent(percpu=self._percpu)
|
||||
return tmp if self._percpu else [tmp]
|
||||
|
||||
def update(self):
|
||||
self._utilization = psutil.cpu_percent(percpu=False)
|
||||
for idx, cpu_perc in enumerate(self.cpu_utilization()):
|
||||
self.widgets()[idx].set("utilization", cpu_perc)
|
||||
|
||||
def state(self, _):
|
||||
return self.threshold_state(self._utilization, 70, 80)
|
||||
def state(self, widget):
|
||||
return self.threshold_state(widget.get("utilization", 0.0), 70, 80)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -17,26 +17,33 @@ import core.input
|
|||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
def __init__(self, config, theme, dtlibrary=None):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
|
||||
self._fmt = self.parameter("format", self.default_format())
|
||||
l = locale.getdefaultlocale()
|
||||
self.dtlibrary = dtlibrary or datetime
|
||||
|
||||
def set_locale(self):
|
||||
l = self.default_locale()
|
||||
if not l or l == (None, None):
|
||||
l = ("en_US", "UTF-8")
|
||||
lcl = self.parameter("locale", ".".join(l))
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, lcl.split("."))
|
||||
locale.setlocale(locale.LC_ALL, lcl.split("."))
|
||||
except Exception as e:
|
||||
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
|
||||
locale.setlocale(locale.LC_ALL, ("en_US", "UTF-8"))
|
||||
|
||||
def default_format(self):
|
||||
return "%x %X"
|
||||
|
||||
def default_locale(self):
|
||||
return locale.getdefaultlocale()
|
||||
|
||||
def full_text(self, widget):
|
||||
self.set_locale()
|
||||
enc = locale.getpreferredencoding()
|
||||
retval = datetime.datetime.now().strftime(self._fmt)
|
||||
fmt = self.parameter("format", self.default_format())
|
||||
retval = self.dtlibrary.datetime.now().strftime(fmt)
|
||||
if hasattr(retval, "decode"):
|
||||
return retval.decode(enc)
|
||||
return retval
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
Parameters:
|
||||
* disk.warning: Warning threshold in % of disk space (defaults to 80%)
|
||||
* disk.critical: Critical threshold in % of disk space (defaults ot 90%)
|
||||
* disk.critical: Critical threshold in % of disk space (defaults to 90%)
|
||||
* disk.path: Path to calculate disk usage from (defaults to /)
|
||||
* disk.open: Which application / file manager to launch (default xdg-open)
|
||||
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
|
||||
* disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
|
||||
"""
|
||||
|
||||
import os
|
||||
|
@ -25,6 +26,7 @@ class Module(core.module.Module):
|
|||
|
||||
self._path = self.parameter("path", "/")
|
||||
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
|
||||
self._system = self.parameter("system", "IEC")
|
||||
|
||||
self._used = 0
|
||||
self._left = 0
|
||||
|
@ -38,9 +40,9 @@ class Module(core.module.Module):
|
|||
)
|
||||
|
||||
def diskspace(self, widget):
|
||||
used_str = util.format.byte(self._used)
|
||||
size_str = util.format.byte(self._size)
|
||||
left_str = util.format.byte(self._left)
|
||||
used_str = util.format.byte(self._used, sys=self._system)
|
||||
size_str = util.format.byte(self._size, sys=self._system)
|
||||
left_str = util.format.byte(self._left, sys=self._system)
|
||||
percent_str = self._percent
|
||||
|
||||
return self._format.format(
|
||||
|
|
56
bumblebee_status/modules/core/keys.py
Normal file
56
bumblebee_status/modules/core/keys.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Shows when a key is pressed
|
||||
|
||||
Parameters:
|
||||
* keys.keys: Comma-separated list of keys to monitor (defaults to "")
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import core.event
|
||||
|
||||
import util.format
|
||||
|
||||
from pynput.keyboard import Listener
|
||||
|
||||
NAMES = {
|
||||
"Key.cmd": "cmd",
|
||||
"Key.ctrl": "ctrl",
|
||||
"Key.shift": "shift",
|
||||
"Key.alt": "alt",
|
||||
}
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.never
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self._listener = Listener(on_press=self._key_press, on_release=self._key_release)
|
||||
|
||||
self._keys = util.format.aslist(self.parameter("keys", "Key.cmd,Key.ctrl,Key.alt,Key.shift"))
|
||||
|
||||
for k in self._keys:
|
||||
self.add_widget(name=k, full_text=self._display_name(k), hidden=True)
|
||||
self._listener.start()
|
||||
|
||||
def _display_name(self, key):
|
||||
return NAMES.get(key, key)
|
||||
|
||||
def _key_press(self, key):
|
||||
key = str(key)
|
||||
if not key in self._keys: return
|
||||
self.widget(key).hidden = False
|
||||
core.event.trigger("update", [self.id], redraw_only=False)
|
||||
|
||||
def _key_release(self, key):
|
||||
key = str(key)
|
||||
if not key in self._keys: return
|
||||
self.widget(key).hidden = True
|
||||
core.event.trigger("update", [self.id], redraw_only=False)
|
||||
|
||||
def state(self, widget):
|
||||
return widget.name
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -53,7 +53,7 @@ class Module(core.module.Module):
|
|||
log.debug("group num: {}".format(xkb.group_num))
|
||||
name = (
|
||||
xkb.group_name
|
||||
if util.format.asbool(self.parameter("showname"), False)
|
||||
if util.format.asbool(self.parameter("showname", False))
|
||||
else xkb.group_symbol
|
||||
)
|
||||
if self.__show_variant:
|
||||
|
|
1
bumblebee_status/modules/core/layout.py
Symbolic link
1
bumblebee_status/modules/core/layout.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkb.py
|
1
bumblebee_status/modules/core/layout_xkb.py
Symbolic link
1
bumblebee_status/modules/core/layout_xkb.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkb.py
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue