Compare commits
415 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 |
144 changed files with 5137 additions and 622 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"
|
31
.travis.yml
31
.travis.yml
|
@ -1,31 +0,0 @@
|
|||
os: linux
|
||||
language: python
|
||||
env:
|
||||
global:
|
||||
- CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
|
||||
python:
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
before_script:
|
||||
- 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
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
libdbus-1-dev
|
||||
libgit2-dev
|
||||
libvirt-dev
|
||||
taskwarrior
|
||||
install:
|
||||
- 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 | cut -d ' ' -f 1 | sort -u)
|
||||
script:
|
||||
- coverage run --source=. -m pytest tests -v
|
||||
after_script:
|
||||
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
|
@ -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
|
||||
}
|
21
README.md
21
README.md
|
@ -1,13 +1,20 @@
|
|||
# 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.com/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.**
|
||||
|
@ -36,9 +43,7 @@ Supported FontAwesome version: 4 (free version of 5 doesn't include some of the
|
|||
---
|
||||
***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/)
|
||||
|
||||
---
|
||||
|
||||
|
@ -79,10 +84,14 @@ 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",
|
||||
)
|
||||
|
|
|
@ -68,13 +68,18 @@ def handle_commands(config, update_lock):
|
|||
|
||||
def handle_events(config, update_lock):
|
||||
while True:
|
||||
line = sys.stdin.readline().strip(",").strip()
|
||||
if line == "[": continue
|
||||
logging.info("input event: {}".format(line))
|
||||
process_event(line, config, update_lock)
|
||||
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():
|
||||
|
@ -97,6 +102,9 @@ 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()
|
||||
event_thread = threading.Thread(target=handle_events, args=(config, update_lock, ))
|
||||
event_thread.daemon = True
|
||||
|
@ -127,8 +135,6 @@ 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:
|
||||
|
@ -146,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
|
||||
|
||||
|
|
|
@ -240,11 +240,16 @@ 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"):
|
||||
|
@ -276,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
|
||||
|
@ -328,7 +342,7 @@ class Config(util.store.Store):
|
|||
"""
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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,8 +54,11 @@ 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_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_exclusive(event_id, lambda event: __execute(event, cmd, wait))
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import importlib
|
||||
import importlib.util
|
||||
import logging
|
||||
import threading
|
||||
|
||||
|
@ -27,10 +28,16 @@ def import_user(module_short, config, theme):
|
|||
return getattr(mod, "Module")(config, theme)
|
||||
else:
|
||||
log.debug("importing {} from user via importlib.util".format(module_short))
|
||||
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)
|
||||
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
|
||||
|
@ -89,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
|
||||
|
||||
|
@ -104,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.
|
||||
|
@ -120,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
|
||||
|
@ -285,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,12 +226,32 @@ 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():
|
||||
|
@ -223,9 +263,14 @@ class i3(object):
|
|||
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
|
||||
|
||||
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:
|
||||
|
@ -249,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": ","}
|
||||
|
|
|
@ -14,11 +14,20 @@ 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():
|
||||
|
|
|
@ -4,12 +4,15 @@ 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
|
||||
|
||||
|
@ -26,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
|
||||
|
@ -62,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":
|
||||
|
@ -79,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"):
|
||||
|
|
|
@ -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,6 +36,7 @@ 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
|
||||
)
|
||||
|
|
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
|
||||
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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")
|
||||
util.cli.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
|
|
@ -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
|
|
@ -24,12 +24,14 @@ 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)
|
||||
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):
|
||||
def toggle_state(self, event):
|
||||
util.cli.execute("dunstctl set-paused toggle", ignore_errors=True)
|
||||
|
||||
def state(self, widget):
|
||||
|
|
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
|
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
|
|
@ -3,7 +3,7 @@
|
|||
"""
|
||||
Displays the message that's received via unix socket.
|
||||
|
||||
Parameteres:
|
||||
Parameters:
|
||||
* messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock)
|
||||
|
||||
Example:
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
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
|
86
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal file
86
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal file
|
@ -6,12 +6,15 @@ Requires the following executable:
|
|||
* playerctl
|
||||
|
||||
Parameters:
|
||||
* playerctl.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {album}, {title}, {artist}, {trackNumber}
|
||||
* 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 inherited from `spotify` module, many thanks to its developers!
|
||||
Parameters are inspired by the `spotify` module, many thanks to its developers!
|
||||
|
||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||
"""
|
||||
|
@ -30,15 +33,17 @@ class Module(core.module.Module):
|
|||
|
||||
self.background = True
|
||||
|
||||
self.__hide = util.format.asbool(self.parameter("hide", "false"));
|
||||
self.__hidden = self.__hide
|
||||
|
||||
self.__layout = util.format.aslist(
|
||||
self.parameter(
|
||||
"layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next"
|
||||
)
|
||||
)
|
||||
|
||||
self.__song = ""
|
||||
self.__cmd = "playerctl "
|
||||
self.__format = self.parameter("format", "{artist} - {title}")
|
||||
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:
|
||||
|
@ -48,7 +53,6 @@ class Module(core.module.Module):
|
|||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "previous",
|
||||
}
|
||||
widget.set("state", "prev")
|
||||
elif widget_name == "playerctl.pause":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
|
@ -59,7 +63,6 @@ class Module(core.module.Module):
|
|||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": self.__cmd + "next",
|
||||
}
|
||||
widget.set("state", "next")
|
||||
elif widget_name == "playerctl.song":
|
||||
widget_map[widget] = [
|
||||
{
|
||||
|
@ -84,34 +87,49 @@ class Module(core.module.Module):
|
|||
if isinstance(callback_options, dict):
|
||||
core.input.register(widget, **callback_options)
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__get_song()
|
||||
def hidden(self):
|
||||
return self.__hidden
|
||||
|
||||
for widget in self.widgets():
|
||||
if widget.name == "playerctl.pause":
|
||||
playback_status = str(util.cli.execute(self.__cmd + "status")).strip()
|
||||
if playback_status != "":
|
||||
if playback_status == "Playing":
|
||||
widget.set("state", "playing")
|
||||
else:
|
||||
widget.set("state", "paused")
|
||||
elif widget.name == "playerctl.song":
|
||||
widget.set("state", "song")
|
||||
widget.full_text(self.__song)
|
||||
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)
|
||||
self.__song = ""
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
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 __get_song(self):
|
||||
album = str(util.cli.execute(self.__cmd + "metadata xesam:album")).strip()
|
||||
title = str(util.cli.execute(self.__cmd + "metadata xesam:title")).strip()
|
||||
artist = str(util.cli.execute(self.__cmd + "metadata xesam:albumArtist")).strip()
|
||||
track_number = str(util.cli.execute(self.__cmd + "metadata xesam:trackNumber")).strip()
|
||||
|
||||
self.__song = self.__format.format(
|
||||
album = album,
|
||||
title = title,
|
||||
artist = artist,
|
||||
trackNumber = track_number
|
||||
)
|
||||
try:
|
||||
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!
|
||||
"""
|
||||
|
|
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
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
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
|
||||
|
@ -20,6 +24,7 @@ class Module(core.module.Module):
|
|||
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):
|
||||
|
@ -27,10 +32,16 @@ class Module(core.module.Module):
|
|||
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
|
||||
client.sendall(bytes("num", "utf-8"))
|
||||
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")
|
||||
l = val.split('\n',2)
|
||||
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
|
||||
|
|
|
@ -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,6 +41,7 @@ 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:
|
||||
|
@ -52,6 +53,7 @@ class Module(core.module.Module):
|
|||
|
||||
def set_output(self, value):
|
||||
self.__output = value
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
|
||||
@core.decorators.scrollable
|
||||
def get_output(self, _):
|
||||
|
|
|
@ -9,7 +9,7 @@ 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
|
||||
|
|
|
@ -10,7 +10,7 @@ Requires the following executables:
|
|||
* smartctl
|
||||
|
||||
Parameters:
|
||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', '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.
|
||||
"""
|
||||
|
|
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
|
|
@ -16,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}}'
|
||||
|
|
|
@ -110,7 +110,8 @@ class Module(core.module.Module):
|
|||
def hidden(self):
|
||||
return self.string_song == ""
|
||||
|
||||
def __get_song(self):
|
||||
@core.decorators.scrollable
|
||||
def __get_song(self, widget):
|
||||
bus = self.__bus
|
||||
if self.__bus_name == "spotifyd":
|
||||
spotify = bus.get_object(
|
||||
|
@ -128,11 +129,10 @@ 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.__get_song()
|
||||
|
||||
if self.__bus_name == "spotifyd":
|
||||
bus = self.__bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
|
||||
|
@ -156,10 +156,9 @@ class Module(core.module.Module):
|
|||
widget.set("state", "paused")
|
||||
elif widget.name == "spotify.song":
|
||||
widget.set("state", "song")
|
||||
widget.full_text(self.__song)
|
||||
widget.full_text(self.__get_song(widget))
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.__song = ""
|
||||
|
||||
@property
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
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!
|
||||
|
@ -22,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)
|
||||
|
@ -29,41 +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"
|
||||
)
|
||||
try:
|
||||
return urllib.request.urlopen(url).read().strip()
|
||||
except urllib.request.URLError:
|
||||
logging.error("unable to open stock exchange url")
|
||||
return None
|
||||
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
|
||||
|
|
|
@ -8,8 +8,8 @@ Requires the following python packages:
|
|||
* 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)
|
||||
|
||||
|
@ -39,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))
|
||||
|
||||
|
@ -55,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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
|
||||
|
||||
|
|
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
|
|
@ -12,6 +12,7 @@ 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
|
||||
|
@ -20,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"
|
||||
)
|
||||
|
@ -34,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,7 +4,7 @@
|
|||
|
||||
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}%)')
|
||||
|
|
1
bumblebee_status/modules/core/layout.py
Symbolic link
1
bumblebee_status/modules/core/layout.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkb.py
|
|
@ -13,7 +13,9 @@ Parameters:
|
|||
* nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
|
||||
* nic.include: Comma-separated list of interfaces to include
|
||||
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
|
||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {strength}')
|
||||
* nic.strength_warning: Integer to set the threshold for warning state (defaults to 50)
|
||||
* nic.strength_critical: Integer to set the threshold for critical state (defaults to 30)
|
||||
"""
|
||||
|
||||
import re
|
||||
|
@ -23,12 +25,13 @@ import subprocess
|
|||
|
||||
import core.module
|
||||
import core.decorators
|
||||
import core.input
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=10)
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
widgets = []
|
||||
super().__init__(config, theme, widgets)
|
||||
|
@ -45,9 +48,19 @@ class Module(core.module.Module):
|
|||
self._states["exclude"].append(state[1:])
|
||||
else:
|
||||
self._states["include"].append(state)
|
||||
self._format = self.parameter("format", "{intf} {state} {ip} {ssid}")
|
||||
self._format = self.parameter("format", "{intf} {state} {ip} {ssid} {strength}")
|
||||
|
||||
self._strength_threshold_critical = self.parameter("strength_critical", 30)
|
||||
self._strength_threshold_warning = self.parameter("strength_warning", 50)
|
||||
|
||||
# Limits for the accepted dBm values of wifi strength
|
||||
self.__strength_dbm_lower_bound = -110
|
||||
self.__strength_dbm_upper_bound = -30
|
||||
|
||||
self.iw = shutil.which("iw")
|
||||
self._update_widgets(widgets)
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd='wifi-menu')
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd='nm-connection-editor')
|
||||
|
||||
def update(self):
|
||||
self._update_widgets(self.widgets())
|
||||
|
@ -64,15 +77,21 @@ class Module(core.module.Module):
|
|||
iftype = "wireless" if self._iswlan(intf) else "wired"
|
||||
iftype = "tunnel" if self._istunnel(intf) else iftype
|
||||
|
||||
# "strength" is none if interface type is not wlan
|
||||
strength = widget.get("strength")
|
||||
if self._iswlan(intf) and strength:
|
||||
if strength < self._strength_threshold_critical:
|
||||
states.append("critical")
|
||||
elif strength < self._strength_threshold_warning:
|
||||
states.append("warning")
|
||||
|
||||
states.append("{}-{}".format(iftype, widget.get("state")))
|
||||
|
||||
return states
|
||||
|
||||
def _iswlan(self, intf):
|
||||
# wifi, wlan, wlp, seems to work for me
|
||||
if intf.startswith("w"):
|
||||
return True
|
||||
return False
|
||||
return intf.startswith("w") and not intf.startswith("wwan")
|
||||
|
||||
def _istunnel(self, intf):
|
||||
return intf.startswith("tun") or intf.startswith("wg")
|
||||
|
@ -116,6 +135,9 @@ class Module(core.module.Module):
|
|||
):
|
||||
continue
|
||||
|
||||
strength_dbm = self.get_strength_dbm(intf)
|
||||
strength_percent = self.convert_strength_dbm_percent(strength_dbm)
|
||||
|
||||
widget = self.widget(intf)
|
||||
if not widget:
|
||||
widget = self.add_widget(name=intf)
|
||||
|
@ -126,12 +148,14 @@ class Module(core.module.Module):
|
|||
ip=", ".join(addr),
|
||||
intf=intf,
|
||||
state=state,
|
||||
strength=str(strength_percent) + "%" if strength_percent else "",
|
||||
ssid=self.get_ssid(intf),
|
||||
).split()
|
||||
)
|
||||
)
|
||||
widget.set("intf", intf)
|
||||
widget.set("state", state)
|
||||
widget.set("strength", strength_percent)
|
||||
|
||||
def get_ssid(self, intf):
|
||||
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
|
||||
|
@ -145,5 +169,23 @@ class Module(core.module.Module):
|
|||
|
||||
return ""
|
||||
|
||||
def get_strength_dbm(self, intf):
|
||||
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
|
||||
return None
|
||||
|
||||
with open("/proc/net/wireless", "r") as file:
|
||||
for line in file:
|
||||
if intf in line:
|
||||
# Remove trailing . by slicing it off ;)
|
||||
strength_dbm = line.split()[3][:-1]
|
||||
return util.format.asint(strength_dbm,
|
||||
minimum=self.__strength_dbm_lower_bound,
|
||||
maximum=self.__strength_dbm_upper_bound)
|
||||
|
||||
return None
|
||||
|
||||
def convert_strength_dbm_percent(self, signal):
|
||||
return int(100 * ((signal + 100) / 70.0)) if signal else None
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
"""Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol.
|
||||
|
||||
!!! This module will eventually be deprecated (since it has bad performance and high CPU load) and be replaced with "pulsectl", which is a much better drop-in replacement !!!
|
||||
|
||||
Aliases: pasink (use this to control output instead of input), pasource
|
||||
|
||||
Parameters:
|
||||
|
@ -11,6 +13,20 @@ Parameters:
|
|||
Note: If the left and right channels have different volumes, the limit might not be reached exactly.
|
||||
* pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango;
|
||||
0 for not showing volume bars (default)
|
||||
* pulseaudio.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown.
|
||||
Per default, the sink/source name returned by "pactl list sinks short" is used as display name.
|
||||
|
||||
As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"),
|
||||
its possible to map the name to more a user friendly name.
|
||||
|
||||
e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following
|
||||
bumblebee-status config entry: pulseaudio.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset
|
||||
|
||||
Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the
|
||||
"alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry:
|
||||
pulseaudio.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧
|
||||
* Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current
|
||||
default device add the following config entry to your bumblebee-status config: pulseaudio.left-click=select_default_device_popup
|
||||
|
||||
Requires the following executable:
|
||||
* pulseaudio
|
||||
|
@ -20,6 +36,7 @@ Requires the following executable:
|
|||
|
||||
import re
|
||||
import logging
|
||||
import functools
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
@ -29,10 +46,14 @@ import util.cli
|
|||
import util.graph
|
||||
import util.format
|
||||
|
||||
try:
|
||||
import util.popup
|
||||
except ImportError as e:
|
||||
logging.warning("Couldn't import util.popup: %s. Popups won't work!", e)
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme, channel):
|
||||
super().__init__(config, theme, core.widget.Widget(self.volume))
|
||||
super().__init__(config, theme, core.widget.Widget(self.display))
|
||||
|
||||
if util.format.asbool(self.parameter("autostart", False)):
|
||||
util.cli.execute("pulseaudio --start", ignore_errors=True)
|
||||
|
@ -48,7 +69,11 @@ class Module(core.module.Module):
|
|||
self._mute = False
|
||||
self._failed = False
|
||||
self._channel = channel
|
||||
self.__selected_default_device = None
|
||||
self._showbars = util.format.asbool(self.parameter("showbars", 0))
|
||||
self.__show_device_name = util.format.asbool(
|
||||
self.parameter("showdevicename", False)
|
||||
)
|
||||
|
||||
self._patterns = [
|
||||
{"expr": "Name:", "callback": (lambda line: False)},
|
||||
|
@ -123,9 +148,12 @@ class Module(core.module.Module):
|
|||
m = re.search(r"mono:.*\s*\/\s*(\d+)%", line)
|
||||
if m:
|
||||
self._mono = m.group(1)
|
||||
self._left = 0
|
||||
self._right = 0
|
||||
else:
|
||||
m = re.search(r"left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%", line)
|
||||
if m:
|
||||
self._mono = 0
|
||||
self._left = m.group(1)
|
||||
self._right = m.group(2)
|
||||
|
||||
|
@ -138,19 +166,19 @@ class Module(core.module.Module):
|
|||
logging.error("no pulseaudio device found")
|
||||
return "n/a"
|
||||
|
||||
def volume(self, widget):
|
||||
def display(self, widget):
|
||||
if self._failed == True:
|
||||
return "n/a"
|
||||
|
||||
vol = None
|
||||
if int(self._mono) > 0:
|
||||
vol = "{}%".format(self._mono)
|
||||
if self._showbars:
|
||||
vol = "{} {}".format(vol, util.graph.hbar(float(self._mono)))
|
||||
return vol
|
||||
elif self._left == self._right:
|
||||
vol = "{}%".format(self._left)
|
||||
if self._showbars:
|
||||
vol = "{} {}".format(vol, util.graph.hbar(float(self._left)))
|
||||
return vol
|
||||
else:
|
||||
vol = "{}%/{}%".format(self._left, self._right)
|
||||
if self._showbars:
|
||||
|
@ -159,19 +187,31 @@ class Module(core.module.Module):
|
|||
util.graph.hbar(float(self._left)),
|
||||
util.graph.hbar(float(self._right)),
|
||||
)
|
||||
return vol
|
||||
|
||||
output = vol
|
||||
if self.__show_device_name:
|
||||
friendly_name = self.parameter(
|
||||
self.__selected_default_device, self.__selected_default_device
|
||||
)
|
||||
icon = self.parameter("icon." + self.__selected_default_device, "")
|
||||
output = (
|
||||
icon + " " + friendly_name + " | " + vol
|
||||
if icon != ""
|
||||
else friendly_name + " | " + vol
|
||||
)
|
||||
return output
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self._failed = False
|
||||
channel = "sinks" if self._channel == "sink" else "sources"
|
||||
device = self._default_device()
|
||||
self.__selected_default_device = self._default_device()
|
||||
|
||||
result = util.cli.execute("pactl list {}".format(channel))
|
||||
found = False
|
||||
|
||||
for line in result.split("\n"):
|
||||
if "Name: {}".format(device) in line:
|
||||
if "Name: {}".format(self.__selected_default_device) in line:
|
||||
found = True
|
||||
continue
|
||||
if found is False:
|
||||
|
@ -189,11 +229,32 @@ class Module(core.module.Module):
|
|||
else:
|
||||
raise e
|
||||
|
||||
def __on_sink_selected(self, sink_name):
|
||||
util.cli.execute("pactl set-default-{} {}".format(self._channel, sink_name))
|
||||
|
||||
def select_default_device_popup(self, widget):
|
||||
channel = "sinks" if self._channel == "sink" else "sources"
|
||||
result = util.cli.execute("pactl list {} short".format(channel))
|
||||
|
||||
menu = util.popup.menu(self.__config)
|
||||
lines = result.splitlines()
|
||||
for line in lines:
|
||||
info = line.split("\t")
|
||||
try:
|
||||
friendly_name = self.parameter(info[1], info[1])
|
||||
menu.add_menuitem(
|
||||
friendly_name,
|
||||
callback=functools.partial(self.__on_sink_selected, info[1]),
|
||||
)
|
||||
except:
|
||||
logging.exception("Couldn't parse {}".format(channel))
|
||||
pass
|
||||
|
||||
menu.show(widget)
|
||||
|
||||
def state(self, widget):
|
||||
if self._mute:
|
||||
return ["warning", "muted"]
|
||||
if int(self._left) > int(100):
|
||||
return ["critical", "unmuted"]
|
||||
return ["unmuted"]
|
||||
|
||||
|
||||
|
|
207
bumblebee_status/modules/core/pulsectl.py
Normal file
207
bumblebee_status/modules/core/pulsectl.py
Normal file
|
@ -0,0 +1,207 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol.
|
||||
|
||||
**Please prefer this module over the "pulseaudio" module, which will eventually be deprecated
|
||||
|
||||
Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones)
|
||||
|
||||
NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein!
|
||||
NOTE2: For the parameter names below, please also use pulseout or pulsein, instead of pulsectl
|
||||
|
||||
Parameters:
|
||||
* pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running
|
||||
* pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%)
|
||||
* pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit')
|
||||
* pulsectl.popup-filter: Comma-separated list of device strings (if the device name contains it) to exclude
|
||||
from the default device popup menu (e.g. Monitor for sources)
|
||||
* pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango;
|
||||
'false' for not showing volume bars (default)
|
||||
* pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown.
|
||||
Per default, the sink/source name returned by "pactl list sinks short" is used as display name.
|
||||
|
||||
As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"),
|
||||
its possible to map the name to more a user friendly name.
|
||||
|
||||
e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following
|
||||
bumblebee-status config entry: pulsectl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset
|
||||
|
||||
Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the
|
||||
"alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry:
|
||||
pulsectl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧
|
||||
* Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current
|
||||
default device add the following config entry to your bumblebee-status config: pulsectl.left-click=select_default_device_popup
|
||||
|
||||
Requires the following Python module:
|
||||
* pulsectl
|
||||
"""
|
||||
|
||||
import pulsectl
|
||||
import logging
|
||||
import functools
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.event
|
||||
|
||||
import util.cli
|
||||
import util.graph
|
||||
import util.format
|
||||
|
||||
try:
|
||||
import util.popup
|
||||
except ImportError as e:
|
||||
logging.warning("Couldn't import util.popup: %s. Popups won't work!", e)
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme, type):
|
||||
super().__init__(config, theme, core.widget.Widget(self.display))
|
||||
self.background = True
|
||||
|
||||
self.__type = type
|
||||
self.__volume = 0
|
||||
self.__devicename = "n/a"
|
||||
self.__muted = False
|
||||
self.__showbars = util.format.asbool(self.parameter("showbars", False))
|
||||
self.__show_device_name = util.format.asbool(
|
||||
self.parameter("showdevicename", False)
|
||||
)
|
||||
|
||||
self.__change = util.format.asint(
|
||||
self.parameter("percent_change", "2%").strip("%"), 0, 100
|
||||
)
|
||||
self.__limit = util.format.asint(self.parameter("limit", "0%").strip("%"), 0)
|
||||
popup_filter_param = self.parameter("popup-filter", [])
|
||||
if popup_filter_param == '':
|
||||
self.__popup_filter = []
|
||||
else:
|
||||
self.__popup_filter = util.format.aslist(popup_filter_param)
|
||||
|
||||
events = [
|
||||
{
|
||||
"type": "mute",
|
||||
"action": self.toggle_mute,
|
||||
"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"])
|
||||
|
||||
if util.format.asbool(self.parameter("autostart", False)):
|
||||
util.cli.execute("pulseaudio --start", ignore_errors=True)
|
||||
|
||||
self.process(None)
|
||||
|
||||
def display(self, _):
|
||||
res = f"{int(self.__volume*100)}%"
|
||||
if self.__showbars:
|
||||
res = f"{res} {util.graph.hbar(self.__volume*100)}"
|
||||
|
||||
if self.__show_device_name:
|
||||
friendly_name = self.parameter(self.__devicename, self.__devicename)
|
||||
icon = self.parameter("icon." + self.__devicename, "")
|
||||
res = (
|
||||
icon + " " + friendly_name + " | " + res
|
||||
if icon != ""
|
||||
else friendly_name + " | " + res
|
||||
)
|
||||
return res
|
||||
|
||||
def toggle_mute(self, _):
|
||||
with pulsectl.Pulse(self.id + "vol") as pulse:
|
||||
dev = self.get_device(pulse)
|
||||
if not dev:
|
||||
return
|
||||
pulse.mute(dev, not self.__muted)
|
||||
|
||||
def change_volume(self, amount):
|
||||
with pulsectl.Pulse(self.id + "vol") as pulse:
|
||||
dev = self.get_device(pulse)
|
||||
if not dev:
|
||||
return
|
||||
vol = dev.volume
|
||||
vol.value_flat += amount
|
||||
if self.__limit > 0 and vol.value_flat > self.__limit/100:
|
||||
vol.value_flat = self.__limit/100
|
||||
pulse.volume_set(dev, vol)
|
||||
|
||||
def increase_volume(self, _):
|
||||
self.change_volume(self.__change/100.0)
|
||||
|
||||
def decrease_volume(self, _):
|
||||
self.change_volume(-self.__change/100.0)
|
||||
|
||||
def get_device(self, pulse):
|
||||
devs = pulse.sink_list() if self.__type == "sink" else pulse.source_list()
|
||||
default = pulse.server_info().default_sink_name if self.__type == "sink" else pulse.server_info().default_source_name
|
||||
|
||||
for dev in devs:
|
||||
if dev.name == default:
|
||||
return dev
|
||||
if len(devs) == 0:
|
||||
return None
|
||||
|
||||
return devs[0] # fallback
|
||||
|
||||
|
||||
def process(self, _):
|
||||
with pulsectl.Pulse(self.id + "proc") as pulse:
|
||||
dev = self.get_device(pulse)
|
||||
if not dev:
|
||||
self.__volume = 0
|
||||
self.__devicename = "n/a"
|
||||
else:
|
||||
self.__volume = dev.volume.value_flat
|
||||
self.__muted = dev.mute
|
||||
self.__devicename = dev.name
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
core.event.trigger("draw")
|
||||
|
||||
def update(self):
|
||||
with pulsectl.Pulse(self.id) as pulse:
|
||||
pulse.event_mask_set(self.__type)
|
||||
pulse.event_callback_set(self.process)
|
||||
pulse.event_listen()
|
||||
|
||||
def select_default_device_popup(self, widget):
|
||||
with pulsectl.Pulse(self.id) as pulse:
|
||||
if self.__type == "sink":
|
||||
devs = pulse.sink_list()
|
||||
else:
|
||||
devs = pulse.source_list()
|
||||
|
||||
devs = filter(lambda dev: not any(filter in dev.description for filter in self.__popup_filter), devs)
|
||||
menu = util.popup.menu(self.__config)
|
||||
for dev in devs:
|
||||
menu.add_menuitem(
|
||||
dev.description,
|
||||
callback=functools.partial(self.__on_default_changed, dev),
|
||||
)
|
||||
menu.show(widget)
|
||||
|
||||
def __on_default_changed(self, dev):
|
||||
with pulsectl.Pulse(self.id) as pulse:
|
||||
pulse.default_set(dev)
|
||||
|
||||
def state(self, _):
|
||||
if self.__muted:
|
||||
return ["warning", "muted"]
|
||||
if self.__volume >= .5:
|
||||
return ["unmuted", "unmuted-high"]
|
||||
if self.__volume >= .1:
|
||||
return ["unmuted", "unmuted-mid"]
|
||||
return ["unmuted", "unmuted-low"]
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
9
bumblebee_status/modules/core/pulsein.py
Normal file
9
bumblebee_status/modules/core/pulsein.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from .pulsectl import Module
|
||||
|
||||
|
||||
class Module(Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, "source")
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
9
bumblebee_status/modules/core/pulseout.py
Normal file
9
bumblebee_status/modules/core/pulseout.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from .pulsectl import Module
|
||||
|
||||
|
||||
class Module(Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, "sink")
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -12,6 +12,7 @@ Parameters:
|
|||
* redshift.lat : latitude if location is set to 'manual'
|
||||
* redshift.lon : longitude if location is set to 'manual'
|
||||
* redshift.show_transition: information about the transitions (x% day) defaults to True
|
||||
* redshift.adjust: set this to 'true' (defaults to false) to let bumblebee-status adjust color temperature, instead of just showing the current settings
|
||||
"""
|
||||
|
||||
import re
|
||||
|
@ -38,7 +39,13 @@ def get_redshift_value(module):
|
|||
if location == "manual" and (lat is None or lon is None):
|
||||
location = "geoclue2"
|
||||
|
||||
command = ["redshift", "-p"]
|
||||
command = ["redshift"]
|
||||
|
||||
if util.format.asbool(module.parameter("adjust", "false")) == True:
|
||||
command.extend(["-o", "-v"])
|
||||
else:
|
||||
command.append("-p")
|
||||
|
||||
if location == "manual":
|
||||
command.extend(["-l", "{}:{}".format(lat, lon)])
|
||||
if location == "geoclue2":
|
||||
|
@ -54,7 +61,7 @@ def get_redshift_value(module):
|
|||
for line in res.split("\n"):
|
||||
line = line.lower()
|
||||
if "temperature" in line:
|
||||
widget.set("temp", line.split(" ")[2])
|
||||
widget.set("temp", line.split(" ")[2].upper())
|
||||
if "period" in line:
|
||||
state = line.split(" ")[1]
|
||||
if "day" in state:
|
||||
|
|
53
bumblebee_status/modules/core/scroll.py
Normal file
53
bumblebee_status/modules/core/scroll.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays two widgets that can be used to scroll the whole status bar
|
||||
|
||||
Parameters:
|
||||
* scroll.width: Width (in number of widgets) to display
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.event
|
||||
|
||||
import util.format
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
self.__offset = 0
|
||||
self.__widgetcount = 0
|
||||
w = self.add_widget(full_text = "<")
|
||||
core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_left)
|
||||
w = self.add_widget(full_text = ">")
|
||||
core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_right)
|
||||
self.__width = util.format.asint(self.parameter("width"))
|
||||
config.set("output.width", self.__width)
|
||||
core.event.register("output.done", self.update_done)
|
||||
|
||||
|
||||
def scroll_left(self, _):
|
||||
if self.__offset > 0:
|
||||
core.event.trigger("output.scroll-left")
|
||||
|
||||
def scroll_right(self, _):
|
||||
if self.__offset + self.__width < self.__widgetcount:
|
||||
core.event.trigger("output.scroll-right")
|
||||
|
||||
def update_done(self, offset, widgetcount):
|
||||
self.__offset = offset
|
||||
self.__widgetcount = widgetcount
|
||||
|
||||
def scroll(self):
|
||||
return False
|
||||
|
||||
def state(self, widget):
|
||||
if widget.id == self.widgets()[0].id:
|
||||
if self.__offset == 0:
|
||||
return ["warning"]
|
||||
elif self.__offset + self.__width >= self.__widgetcount:
|
||||
return ["warning"]
|
||||
return []
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -11,7 +11,7 @@ Parameters:
|
|||
* sensors2.showother: Enable or display 'other' sensor readings (default: false)
|
||||
* sensors2.showname: Enable or disable show of sensor name (default: false)
|
||||
* sensors2.chip_include: Comma-separated list of chip to include (defaults to '' will include all by default, example: 'coretemp,bat')
|
||||
* sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exlude none by default)
|
||||
* sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exclude none by default)
|
||||
* sensors2.field_include: Comma separated list of chip to include (defaults to '' will include all by default, example: 'temp,fan')
|
||||
* sensors2.field_exclude: Comma separated list of chip to exclude (defaults to '' will exclude none by default)
|
||||
* sensors2.chip_field_exclude: Comma separated list of chip field to exclude (defaults to '' will exclude none by default, example: 'coretemp-isa-0000.temp1,coretemp-isa-0000.fan1')
|
||||
|
|
|
@ -9,7 +9,7 @@ Parameters:
|
|||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import core.input
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
|
@ -20,5 +20,8 @@ class Module(core.module.Module):
|
|||
def text(self, _):
|
||||
return self.__text
|
||||
|
||||
def update_text(self, event):
|
||||
self.__text = core.input.button_name(event["button"])
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -52,7 +52,7 @@ def build_menu(parent, current_directory, callback):
|
|||
)
|
||||
|
||||
else:
|
||||
submenu = util.popup.menu(parent, leave=False)
|
||||
submenu = util.popup.menu(self.__config, parent, leave=False)
|
||||
build_menu(
|
||||
submenu, os.path.join(current_directory, entry.name), callback
|
||||
)
|
||||
|
@ -73,7 +73,7 @@ class Module(core.module.Module):
|
|||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu(leave=False)
|
||||
menu = util.popup.menu(self.__config, leave=False)
|
||||
|
||||
build_menu(menu, self.__path, self.__callback)
|
||||
menu.show(widget, offset_x=self.__offx, offset_y=self.__offy)
|
||||
|
|
|
@ -52,7 +52,19 @@ def execute(
|
|||
raise RuntimeError("{} not found".format(cmd))
|
||||
|
||||
if wait:
|
||||
out, _ = proc.communicate()
|
||||
timeout = 60
|
||||
try:
|
||||
out, _ = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
logging.warning(
|
||||
f"""
|
||||
Communication with process pid={proc.pid} hangs for more
|
||||
than {timeout} seconds.
|
||||
If this is not expected, the process is stale, or
|
||||
you might have run in stdout / stderr deadlock.
|
||||
"""
|
||||
)
|
||||
out, _ = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
err = "{} exited with code {}".format(cmd, proc.returncode)
|
||||
logging.warning(err)
|
||||
|
|
|
@ -3,8 +3,10 @@ service and caches it for 12h (retries are done every
|
|||
30m in case of problems)
|
||||
|
||||
Right now, it uses (in order of preference):
|
||||
- http://free.ipwhois.io/
|
||||
- http://ipapi.co/
|
||||
- http://free.ipwhois.io/ - 10k free requests/month
|
||||
- http://ipapi.co/ - 30k free requests/month
|
||||
- http://ip-api.com/ - ~2m free requests/month
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
@ -16,21 +18,36 @@ __document = None
|
|||
__data = {}
|
||||
__next = 0
|
||||
__sources = [
|
||||
{
|
||||
"url": "http://ipapi.co/json",
|
||||
"mapping": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"country_name": "country",
|
||||
"ip": "public_ip",
|
||||
},
|
||||
},
|
||||
{
|
||||
"url": "http://free.ipwhois.io/json/",
|
||||
"mapping": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"country": "country",
|
||||
"country": "country_name",
|
||||
"country_code": "country_code",
|
||||
"city": "city_name",
|
||||
"ip": "public_ip",
|
||||
},
|
||||
},
|
||||
{
|
||||
"url": "http://ip-api.com/json",
|
||||
"mapping": {
|
||||
"lat": "latitude",
|
||||
"lon": "longitude",
|
||||
"country": "country_name",
|
||||
"countryCode": "country_code",
|
||||
"city": "city_name",
|
||||
"query": "public_ip",
|
||||
},
|
||||
},
|
||||
{
|
||||
"url": "http://ipapi.co/json",
|
||||
"mapping": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"country_name": "country_name",
|
||||
"country_code": "country_code",
|
||||
"city": "city_name",
|
||||
"ip": "public_ip",
|
||||
},
|
||||
}
|
||||
|
@ -59,17 +76,22 @@ def __load():
|
|||
__next = time.time() + 60 * 30 # error - try again every 30m
|
||||
|
||||
|
||||
def __get(name, default=None):
|
||||
def __get(name):
|
||||
global __data
|
||||
if not __data or __expired():
|
||||
__load()
|
||||
return __data.get(name, default)
|
||||
if name in __data:
|
||||
return __data[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def reset():
|
||||
"""Resets the location library, ensuring that a new query will be started
|
||||
"""
|
||||
"""Resets the location library, ensuring that a new query will be started"""
|
||||
global __next
|
||||
global __data
|
||||
|
||||
__data = None
|
||||
__next = 0
|
||||
|
||||
|
||||
|
@ -88,7 +110,25 @@ def country():
|
|||
:return: country name
|
||||
:rtype: string
|
||||
"""
|
||||
return __get("country")
|
||||
return __get("country_name")
|
||||
|
||||
|
||||
def country_code():
|
||||
"""Returns the current country code
|
||||
|
||||
:return: country code
|
||||
:rtype: string
|
||||
"""
|
||||
return __get("country_code")
|
||||
|
||||
|
||||
def city_name():
|
||||
"""Returns the current city name
|
||||
|
||||
:return: city name
|
||||
:rtype: string
|
||||
"""
|
||||
return __get("city_name")
|
||||
|
||||
|
||||
def public_ip():
|
||||
|
@ -100,4 +140,20 @@ def public_ip():
|
|||
return __get("public_ip")
|
||||
|
||||
|
||||
def location_info():
|
||||
"""Returns the current location information
|
||||
|
||||
:return: public IP, country name, country code, city name & coordinates
|
||||
:rtype: dictionary
|
||||
"""
|
||||
return {
|
||||
"public_ip": __get("public_ip"),
|
||||
"country": __get("country_name"),
|
||||
"country_code": __get("country_code"),
|
||||
"city_name": __get("city_name"),
|
||||
"latitude": __get("latitude"),
|
||||
"longitude": __get("longitude"),
|
||||
}
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import logging
|
||||
|
||||
import tkinter as tk
|
||||
import tkinter.font as tkFont
|
||||
|
||||
import functools
|
||||
|
||||
|
@ -10,11 +11,12 @@ import functools
|
|||
class menu(object):
|
||||
"""Draws a hierarchical popup menu
|
||||
|
||||
:param config: Global config singleton, passed on from modules
|
||||
:param parent: If given, this menu is a leave of the "parent" menu
|
||||
:param leave: If set to True, close this menu when mouse leaves the area (defaults to True)
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, leave=True):
|
||||
def __init__(self, config, parent=None, leave=True):
|
||||
self.running = True
|
||||
|
||||
self.parent = parent
|
||||
|
@ -23,6 +25,7 @@ class menu(object):
|
|||
self._root.withdraw()
|
||||
self._menu = tk.Menu(self._root, tearoff=0)
|
||||
self._menu.bind("<FocusOut>", self.__on_focus_out)
|
||||
self._font_size = tkFont.Font(size=config.popup_font_size())
|
||||
|
||||
if leave:
|
||||
self._menu.bind("<Leave>", self.__on_focus_out)
|
||||
|
@ -49,6 +52,7 @@ class menu(object):
|
|||
return self._menu
|
||||
|
||||
def __on_focus_out(self, event=None):
|
||||
self.running = False
|
||||
self._root.destroy()
|
||||
|
||||
def __on_click(self, callback):
|
||||
|
@ -67,7 +71,7 @@ class menu(object):
|
|||
"""
|
||||
|
||||
def add_cascade(self, menuitem, submenu):
|
||||
self._menu.add_cascade(label=menuitem, menu=submenu.menu())
|
||||
self._menu.add_cascade(label=menuitem, menu=submenu.menu(), font=self._font_size)
|
||||
|
||||
"""Adds an item to the current menu
|
||||
|
||||
|
@ -77,7 +81,7 @@ class menu(object):
|
|||
|
||||
def add_menuitem(self, menuitem, callback):
|
||||
self._menu.add_command(
|
||||
label=menuitem, command=functools.partial(self.__on_click, callback)
|
||||
label=menuitem, command=functools.partial(self.__on_click, callback), font=self._font_size,
|
||||
)
|
||||
|
||||
"""Adds a separator to the menu in the current location"""
|
||||
|
|
28
create-pkgbuild.py
Normal file
28
create-pkgbuild.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
rv = requests.request(
|
||||
"GET",
|
||||
"https://api.github.com/repos/tobi-wan-kenobi/bumblebee-status/releases/latest",
|
||||
)
|
||||
|
||||
if rv.status_code != 200:
|
||||
sys.exit(1)
|
||||
|
||||
release = json.loads(rv.text)
|
||||
|
||||
tar = requests.get(f"https://github.com/tobi-wan-kenobi/bumblebee-status/archive/{release['name']}.tar.gz")
|
||||
checksum = hashlib.sha512(tar.content).hexdigest()
|
||||
|
||||
template = ""
|
||||
with open("./PKGBUILD.template") as f:
|
||||
template = f.read()
|
||||
|
||||
template = template.replace("<PKGVERSION>", release["name"].lstrip("v"))
|
||||
template = template.replace("<SHA512SUM>", checksum)
|
||||
|
||||
print(template)
|
17
docs/FAQ.rst
17
docs/FAQ.rst
|
@ -29,9 +29,9 @@ didn’t have background color support for the status bar.
|
|||
Some of the icons don’t render correctly
|
||||
----------------------------------------
|
||||
|
||||
Please check that you have |Font Awesome| installed (version 4).
|
||||
Please check that you have `Font Awesome`_ installed (version 4).
|
||||
|
||||
.. note:: The |Font Awesome| is required for all themes that
|
||||
.. note:: The `Font Awesome`_ is required for all themes that
|
||||
contain icons (because that is the font that includes these icons).
|
||||
Please refer to your distribution’s package management on how to install
|
||||
them, or get them from their website directly. Also, please note that
|
||||
|
@ -52,4 +52,15 @@ Please check that you have |Font Awesome| installed (version 4).
|
|||
# Other
|
||||
# see https://github.com/gabrielelana/awesome-terminal-fonts
|
||||
|
||||
.. |Font Awesome| image:: https://fontawesome.com/
|
||||
You might also need to add it to the `font` directive in your i3 configuration, for example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
bar {
|
||||
font pango:FontAwesome, Fira mono 10
|
||||
status_command bumblebee-status -m title pasink pasource cpu memory battery datetime --iconset awesome-fonts
|
||||
}
|
||||
|
||||
If you are unsure about how the font is named, you can use the ``pango-list`` command line tool to look at the fonts installed on your computer. Also note how you can specify multiple fonts, separated by commas, in the above example.
|
||||
|
||||
.. _Font Awesome: https://fontawesome.com/
|
||||
|
|
|
@ -49,6 +49,8 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_logo = "logo.png"
|
||||
html_favicon = "favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
|
|
|
@ -250,5 +250,5 @@ module using ``self.set()`` or via the CLI using the ``--parameter`` flag:
|
|||
|
||||
- ``scrolling.width``: Integer, defaults to 30, determines the minimum width of the widgets, if ``makewide`` is specified
|
||||
- ``scrolling.makewide``: Boolean, defaults to true, determines whether the widgets should be expanded to their minwidth
|
||||
``scrolling.bounce``: Boolean, defaults to true, determines whether the content should change directions when a scroll is completed, or just marquee through
|
||||
- ``scrolling.bounce``: Boolean, defaults to true, determines whether the content should change directions when a scroll is completed, or just marquee through
|
||||
|
||||
|
|
BIN
docs/favicon.ico
Normal file
BIN
docs/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -160,6 +160,7 @@ Configuration files have the following format:
|
|||
|
||||
[core]
|
||||
modules = <comma-separated list of modules to load>
|
||||
autohide = <comma-separated list of modules to hide, unless in warning/error state>
|
||||
theme = <theme to use by default>
|
||||
|
||||
[module-parameters]
|
||||
|
|
|
@ -9,6 +9,8 @@ Welcome to bumblebee-status's documentation!
|
|||
bumblebee-status is a modular, theme-able status line generator for the
|
||||
`i3 window manager <https://i3wm.org/>`__.
|
||||
|
||||
Logo courtesy of [kellya](https://github.com/kellya) - thank you!
|
||||
|
||||
Focus is on:
|
||||
|
||||
- ease of use, sane defaults (no mandatory configuration file)
|
||||
|
|
|
@ -44,6 +44,14 @@ like this:
|
|||
-t <theme>
|
||||
}
|
||||
|
||||
Line continuations (breaking a single line into multiple lines) is allowed in
|
||||
the i3 configuration, but please ensure that all lines except the final one need to have a trailing
|
||||
"\".
|
||||
This is explained in detail here:
|
||||
[i3 user guide: line continuation](https://i3wm.org/docs/userguide.html#line_continuation)
|
||||
|
||||
|
||||
|
||||
You can retrieve a list of modules (and their parameters) and themes by
|
||||
entering:
|
||||
|
||||
|
|
BIN
docs/logo.png
Normal file
BIN
docs/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
493
docs/modules.rst
493
docs/modules.rst
|
@ -22,6 +22,7 @@ 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)
|
||||
|
||||
.. image:: ../screenshots/cpu.png
|
||||
|
||||
|
@ -59,7 +60,7 @@ Shows free diskspace, total diskspace and the percentage of free disk space.
|
|||
|
||||
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}%)')
|
||||
|
@ -84,6 +85,30 @@ Requires:
|
|||
|
||||
.. image:: ../screenshots/git.png
|
||||
|
||||
keys
|
||||
~~~~
|
||||
|
||||
Shows when a key is pressed
|
||||
|
||||
Parameters:
|
||||
* keys.keys: Comma-separated list of keys to monitor (defaults to "")
|
||||
|
||||
layout
|
||||
~~~~~~
|
||||
|
||||
Displays the current keyboard layout using libX11
|
||||
|
||||
Requires the following library:
|
||||
* libX11.so.6
|
||||
and python module:
|
||||
* xkbgroup
|
||||
|
||||
Parameters:
|
||||
* layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed)
|
||||
* layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true.
|
||||
|
||||
.. image:: ../screenshots/layout.png
|
||||
|
||||
layout-xkb
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -162,7 +187,9 @@ Parameters:
|
|||
* nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
|
||||
* nic.include: Comma-separated list of interfaces to include
|
||||
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
|
||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {strength}')
|
||||
* nic.strength_warning: Integer to set the threshold for warning state (defaults to 50)
|
||||
* nic.strength_critical: Integer to set the threshold for critical state (defaults to 30)
|
||||
|
||||
.. image:: ../screenshots/nic.png
|
||||
|
||||
|
@ -188,6 +215,8 @@ pulseaudio
|
|||
|
||||
Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol.
|
||||
|
||||
!!! This module will eventually be deprecated (since it has bad performance and high CPU load) and be replaced with "pulsectl", which is a much better drop-in replacement !!!
|
||||
|
||||
Aliases: pasink (use this to control output instead of input), pasource
|
||||
|
||||
Parameters:
|
||||
|
@ -197,6 +226,20 @@ Parameters:
|
|||
Note: If the left and right channels have different volumes, the limit might not be reached exactly.
|
||||
* pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango;
|
||||
0 for not showing volume bars (default)
|
||||
* pulseaudio.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown.
|
||||
Per default, the sink/source name returned by "pactl list sinks short" is used as display name.
|
||||
|
||||
As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"),
|
||||
its possible to map the name to more a user friendly name.
|
||||
|
||||
e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following
|
||||
bumblebee-status config entry: pulseaudio.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset
|
||||
|
||||
Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the
|
||||
"alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry:
|
||||
pulseaudio.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧
|
||||
* Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current
|
||||
default device add the following config entry to your bumblebee-status config: pulseaudio.left-click=select_default_device_popup
|
||||
|
||||
Requires the following executable:
|
||||
* pulseaudio
|
||||
|
@ -205,6 +248,44 @@ Requires the following executable:
|
|||
|
||||
.. image:: ../screenshots/pulseaudio.png
|
||||
|
||||
pulsectl
|
||||
~~~~~~~~
|
||||
|
||||
Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol.
|
||||
|
||||
**Please prefer this module over the "pulseaudio" module, which will eventually be deprecated
|
||||
|
||||
Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones)
|
||||
|
||||
NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein!
|
||||
NOTE2: For the parameter names below, please also use pulseout or pulsein, instead of pulsectl
|
||||
|
||||
Parameters:
|
||||
* pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running
|
||||
* pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%)
|
||||
* pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit')
|
||||
* pulsectl.popup-filter: Comma-separated list of device strings (if the device name contains it) to exclude
|
||||
from the default device popup menu (e.g. Monitor for sources)
|
||||
* pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango;
|
||||
'false' for not showing volume bars (default)
|
||||
* pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown.
|
||||
Per default, the sink/source name returned by "pactl list sinks short" is used as display name.
|
||||
|
||||
As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"),
|
||||
its possible to map the name to more a user friendly name.
|
||||
|
||||
e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following
|
||||
bumblebee-status config entry: pulsectl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset
|
||||
|
||||
Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the
|
||||
"alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry:
|
||||
pulsectl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧
|
||||
* Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current
|
||||
default device add the following config entry to your bumblebee-status config: pulsectl.left-click=select_default_device_popup
|
||||
|
||||
Requires the following Python module:
|
||||
* pulsectl
|
||||
|
||||
redshift
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -220,9 +301,18 @@ Parameters:
|
|||
* redshift.lat : latitude if location is set to 'manual'
|
||||
* redshift.lon : longitude if location is set to 'manual'
|
||||
* redshift.show_transition: information about the transitions (x% day) defaults to True
|
||||
* redshift.adjust: set this to 'true' (defaults to false) to let bumblebee-status adjust color temperature, instead of just showing the current settings
|
||||
|
||||
.. image:: ../screenshots/redshift.png
|
||||
|
||||
scroll
|
||||
~~~~~~
|
||||
|
||||
Displays two widgets that can be used to scroll the whole status bar
|
||||
|
||||
Parameters:
|
||||
* scroll.width: Width (in number of widgets) to display
|
||||
|
||||
sensors2
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -237,7 +327,7 @@ Parameters:
|
|||
* sensors2.showother: Enable or display 'other' sensor readings (default: false)
|
||||
* sensors2.showname: Enable or disable show of sensor name (default: false)
|
||||
* sensors2.chip_include: Comma-separated list of chip to include (defaults to '' will include all by default, example: 'coretemp,bat')
|
||||
* sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exlude none by default)
|
||||
* sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exclude none by default)
|
||||
* sensors2.field_include: Comma separated list of chip to include (defaults to '' will include all by default, example: 'temp,fan')
|
||||
* sensors2.field_exclude: Comma separated list of chip to exclude (defaults to '' will exclude none by default)
|
||||
* sensors2.chip_field_exclude: Comma separated list of chip field to exclude (defaults to '' will exclude none by default, example: 'coretemp-isa-0000.temp1,coretemp-isa-0000.fan1')
|
||||
|
@ -336,6 +426,7 @@ 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%)
|
||||
|
||||
|
@ -343,6 +434,8 @@ 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!
|
||||
|
||||
.. image:: ../screenshots/amixer.png
|
||||
|
||||
apt
|
||||
|
@ -366,6 +459,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
|
||||
|
@ -382,6 +478,8 @@ Requires the following executable:
|
|||
|
||||
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
||||
|
||||
.. image:: ../screenshots/arch-update.png
|
||||
|
||||
arch_update
|
||||
~~~~~~~~~~~
|
||||
|
||||
|
@ -392,6 +490,18 @@ Requires the following executable:
|
|||
|
||||
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
||||
|
||||
aur-update
|
||||
~~~~~~~~~~
|
||||
|
||||
Check updates for AUR.
|
||||
|
||||
Requires the following executable:
|
||||
* yay (https://github.com/Jguer/yay)
|
||||
|
||||
contributed by `ishaanbhimwal <https://github.com/ishaanbhimwal>`_ - many thanks!
|
||||
|
||||
.. image:: ../screenshots/aur-update.png
|
||||
|
||||
battery
|
||||
~~~~~~~
|
||||
|
||||
|
@ -463,6 +573,26 @@ Parameters:
|
|||
|
||||
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||
|
||||
blugon
|
||||
~~~~~~
|
||||
|
||||
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>`
|
||||
|
||||
brightness
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -551,15 +681,58 @@ 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.
|
||||
|
||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||
|
||||
cpu3
|
||||
~~~~
|
||||
|
||||
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>`
|
||||
|
||||
currency
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -711,11 +884,56 @@ be running. Scripts will be executed when dunst gets unpaused.
|
|||
Requires:
|
||||
* dunst v1.5.0+
|
||||
|
||||
Parameters:
|
||||
* dunstctl.disabled(Boolean): dunst state on start
|
||||
|
||||
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
|
||||
contributed by `joachimmathes <https://github.com/joachimmathes>`_ - many thanks!
|
||||
|
||||
.. image:: ../screenshots/dunstctl.png
|
||||
|
||||
emerge_status
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
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
|
||||
|
||||
.. image:: ../screenshots/emerge_status.png
|
||||
|
||||
gcalendar
|
||||
~~~~~~~~~
|
||||
|
||||
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
|
||||
|
||||
getcrypto
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -758,6 +976,29 @@ contributed by:
|
|||
|
||||
.. image:: ../screenshots/github.png
|
||||
|
||||
gitlab
|
||||
~~~~~~
|
||||
|
||||
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)
|
||||
|
||||
.. image:: ../screenshots/gitlab.png
|
||||
|
||||
gpmdp
|
||||
~~~~~
|
||||
|
||||
|
@ -822,18 +1063,6 @@ contributed by `pierre87 <https://github.com/pierre87>`_ - many thanks!
|
|||
|
||||
.. image:: ../screenshots/kernel.png
|
||||
|
||||
layout
|
||||
~~~~~~
|
||||
|
||||
Displays and changes the current keyboard layout
|
||||
|
||||
Requires the following executable:
|
||||
* setxkbmap
|
||||
|
||||
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
||||
|
||||
.. image:: ../screenshots/layout.png
|
||||
|
||||
layout-xkbswitch
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -869,7 +1098,7 @@ messagereceiver
|
|||
|
||||
Displays the message that's received via unix socket.
|
||||
|
||||
Parameteres:
|
||||
Parameters:
|
||||
* messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock)
|
||||
|
||||
Example:
|
||||
|
@ -954,12 +1183,22 @@ 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!
|
||||
|
||||
.. image:: ../screenshots/mpd.png
|
||||
|
||||
network
|
||||
~~~~~~~
|
||||
|
||||
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!
|
||||
|
||||
network_traffic
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -992,12 +1231,16 @@ Displays GPU name, temperature and memory usage.
|
|||
|
||||
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.
|
||||
|
||||
octoprint
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -1015,13 +1258,21 @@ Parameters:
|
|||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
|
||||
optman
|
||||
~~~~~~
|
||||
|
||||
Displays currently active gpu by optimus-manager
|
||||
Requires the following packages:
|
||||
|
||||
* optimus-manager
|
||||
|
||||
pacman
|
||||
~~~~~~
|
||||
|
||||
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
|
||||
|
@ -1031,6 +1282,31 @@ contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
|||
|
||||
.. image:: ../screenshots/pacman.png
|
||||
|
||||
pamixer
|
||||
~~~~~~~
|
||||
|
||||
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
|
||||
|
||||
persian_date
|
||||
~~~~~~~~~~~~
|
||||
|
||||
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"
|
||||
|
||||
pihole
|
||||
~~~~~~
|
||||
|
||||
|
@ -1038,10 +1314,30 @@ Displays the pi-hole status (up/down) together with the number of ads that were
|
|||
|
||||
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!
|
||||
|
||||
pipewire
|
||||
~~~~~~~~
|
||||
|
||||
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
|
||||
|
||||
playerctl
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -1051,12 +1347,15 @@ Requires the following executable:
|
|||
* playerctl
|
||||
|
||||
Parameters:
|
||||
* playerctl.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {album}, {title}, {artist}, {trackNumber}
|
||||
* 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 inherited from `spotify` module, many thanks to its developers!
|
||||
Parameters are inspired by the `spotify` module, many thanks to its developers!
|
||||
|
||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||
|
||||
|
@ -1078,7 +1377,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!
|
||||
|
||||
|
@ -1142,7 +1441,29 @@ contributed by `remi-dupre <https://github.com/remi-dupre>`_ - many thanks!
|
|||
publicip
|
||||
~~~~~~~~
|
||||
|
||||
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!
|
||||
|
||||
rofication
|
||||
~~~~~~~~~~
|
||||
|
@ -1155,6 +1476,9 @@ 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>.
|
||||
|
||||
rotation
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -1184,6 +1508,7 @@ sensors
|
|||
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
|
||||
|
@ -1237,7 +1562,7 @@ 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
|
||||
|
@ -1259,10 +1584,20 @@ Requires the following executables:
|
|||
* smartctl
|
||||
|
||||
Parameters:
|
||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', '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.
|
||||
|
||||
solaar
|
||||
~~~~~~
|
||||
|
||||
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!
|
||||
|
||||
spaceapi
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -1279,7 +1614,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}}'
|
||||
|
@ -1320,12 +1655,11 @@ stock
|
|||
|
||||
Display a stock quote from finance.yahoo.com
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
|
||||
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!
|
||||
|
@ -1343,8 +1677,8 @@ Requires the following python packages:
|
|||
* 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)
|
||||
|
||||
|
@ -1360,11 +1694,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')
|
||||
|
@ -1373,6 +1707,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!
|
||||
|
||||
taskwarrior
|
||||
|
@ -1385,6 +1722,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!
|
||||
|
@ -1430,6 +1768,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!
|
||||
|
@ -1458,6 +1797,27 @@ Parameters:
|
|||
* 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>`
|
||||
|
||||
todoist
|
||||
~~~~~~~
|
||||
|
||||
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)"
|
||||
|
||||
.. image:: ../screenshots/todoist.png
|
||||
|
||||
traffic
|
||||
~~~~~~~
|
||||
|
||||
|
@ -1469,7 +1829,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
|
||||
|
||||
|
@ -1496,6 +1856,27 @@ contributed by `ccoors <https://github.com/ccoors>`_ - many thanks!
|
|||
|
||||
.. image:: ../screenshots/uptime.png
|
||||
|
||||
usage
|
||||
~~~~~
|
||||
|
||||
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)
|
||||
|
||||
vpn
|
||||
~~~
|
||||
|
||||
|
@ -1515,6 +1896,34 @@ Displays the VPN profile that is currently in use.
|
|||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
|
||||
wakatime
|
||||
~~~~~~~~
|
||||
|
||||
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)
|
||||
|
||||
.. image:: ../screenshots/wakatime.png
|
||||
|
||||
watson
|
||||
~~~~~~
|
||||
|
||||
|
@ -1523,6 +1932,10 @@ Displays the status of watson (time-tracking tool)
|
|||
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!
|
||||
|
||||
weather
|
||||
|
@ -1540,7 +1953,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!
|
||||
|
|
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
docutils<0.18
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue