Compare commits

...

127 commits
v2.1.6 ... main

Author SHA1 Message Date
676bbebf4c
bluetooth2: states and styling 2024-04-17 23:05:16 +02:00
617a12f96d
Merge branch 'fix/bluetooth' 2024-04-17 22:53:03 +02:00
5053bb0f1b
themes/awesome-fonts: fix bluetooth icons 2024-04-17 22:47:45 +02:00
21060a10a0
bluetooth2: only show connection count in cases of excessive numbers of connections 2024-04-17 22:46:18 +02:00
a56b86100a
fix updated nerdfonts 2023-12-06 22:17:49 +01:00
fd4b940d58
nic.py: adapting wifi-menu path 2023-12-06 22:01:20 +01:00
2bbac991db
battery: updating nerd font icons 2023-11-10 21:03:43 +01:00
255794cd1c
vpn: updating nerd font icon 2023-11-10 20:51:31 +01:00
2e81eed830
pulsein: replacing muted nerd font icon with slash in the other direction, just to align it with bluetooth2 2023-11-10 20:47:35 +01:00
60bfb19378
pulseout: adding nerd font icons for low, mid, heigh, muted 2023-11-10 20:44:16 +01:00
d7d4603855
wlrotation: adding awesome-fonts icons 2023-11-10 20:03:44 +01:00
cbd0c58b4a
wlrotation: refactored the entire module and integrated external services 2023-11-10 19:44:33 +01:00
bbc26c263c
wlrotation: fix nerd font icon 2023-11-10 16:51:07 +01:00
53de1b524a
bluetooth2: using dbus api, shortening output, some refactoring 2023-11-10 16:50:16 +01:00
c3d4fce74c
nic: fix syntax 2023-11-10 16:48:05 +01:00
8cc7c9de9b
icons: fix nerd font battery/ac 2023-11-10 16:46:38 +01:00
3ed26c62a5
vpn: shorter default label 2023-11-08 22:59:56 +01:00
900a0710c5
nic: add click handler 2023-11-08 22:56:23 +01:00
2e18d71284
battery: better support for pen battery and some formatting 2023-11-08 22:46:58 +01:00
61123eb7a0
fix wlrotation: migrated to new api 2023-11-08 21:52:08 +01:00
27be30263f
Merge branch 'feature/wlrotation' into merge 2023-11-08 20:51:27 +01:00
559517f345
update golor theme and icons 2023-11-08 20:28:49 +01:00
b45ff330c9
wlrotation: init 2023-11-08 00:38:39 +01:00
tobi-wan-kenobi
bfafd93643 fix: remove power from tests installation as a quickfix 2023-10-26 09:19:46 +02:00
tobi-wan-kenobi
c14ed1166d fix: autotest - try to pin down pip versions 2023-10-26 09:11:39 +02:00
tobi-wan-kenobi
9f6c9cc7d2 doc: update readthedocs.yaml 2023-10-26 09:08:43 +02:00
tobi-wan-kenobi
025d3fcb51 fix(module/shell): synchonous invocation was broken
For some (unknown) reason, redrawing while in the update loop breaks the
update (this probably warrants a closer look).

As a quickfix to restore functionality, remove the unnecessary redraw
call and move it into the async codepath, where it is actually needed.

fixes #1001
2023-10-26 09:00:09 +02:00
tobi-wan-kenobi
05622f985a fix(module/shell): expand user directory in command spec
Make sure that ~/ is expanded to the user's home directory in the
command specified for the shell module.

fixes #1002
2023-10-26 08:59:14 +02:00
tobi-wan-kenobi
c4a3f488aa
Merge pull request #1000 from Sigggii/main
Add power-profile module
2023-10-04 06:25:02 +02:00
siggi
68de299763 Merge remote-tracking branch 'origin/main' 2023-10-03 22:36:23 +02:00
siggi
85760926d7 Add Power-Profile module 2023-10-03 22:28:28 +02:00
tobi-wan-kenobi
3bc3c75ff4
Merge pull request #997 from zetxx/patch-2
Update modules.rst
2023-10-02 14:38:16 +02:00
Elin Angelov
d30e5c694e
Update modules.rst 2023-10-02 12:31:15 +03:00
tobi-wan-kenobi
b217ac9c9e docs: update module documentation 2023-10-01 10:07:03 +02:00
tobi-wan-kenobi
fcda13cac0
Merge pull request #994 from zetxx/patch-1
feat: add `disabled` parameter
2023-09-20 14:28:53 +02:00
Elin Angelov
0c2123cfe4
fix: identation 2023-09-20 15:11:24 +03:00
Elin Angelov
9251217bb3
feat: add disabled parameter
it pauses dunst notification on startup
2023-09-20 14:59:24 +03:00
tobi-wan-kenobi
9170785ed5 fix(module-shell): fix syntax error 2023-09-15 15:21:38 +02:00
tobi-wan-kenobi
2e7e75a27c feat(shell): add timeout warning and faster update
see #990
2023-09-15 15:15:31 +02:00
tobi-wan-kenobi
d303b794f3 feat(docs): clarify line continuation
fixes #987
2023-09-15 15:11:23 +02:00
tobi-wan-kenobi
b42323013d fix(config): make config file keys case sensitive
Since the configparser library by default parses keys case insensitive
(all lowercase), certain mappings, especially in the pulseaudio modules,
could fail ("internal" pulseaudio device names are matched against
entries in the configuration).

fixes #992
2023-09-15 15:06:57 +02:00
tobi-wan-kenobi
8583b5123e
Merge pull request #991 from LokiLuciferase/feature/title-change-on-workspace-switch
poll window title also on workspace change (not only window events)
2023-09-13 21:25:16 +02:00
Lukas Lüftinger
b762132037 poll window title also on workspace change (not only window events) 2023-09-13 18:02:26 +02:00
tobi-wan-kenobi
fded39fa81 [modules/pulsectl] make device names case sensitive
A previous change accidentially changed the "pretty" device name mapping
to be required to be in lowercase (rather than the exact name of the
devices. Restore previous functionality.

fixes #989
2023-09-11 09:36:47 +02:00
tobi-wan-kenobi
9c5e30ac61
Merge pull request #988 from TheEdgeOfRage/pulsectl-filter
Add device filter support to pulsectl popup menu
2023-09-07 10:32:54 +02:00
Pavle Portic
9e6e656fa8
Add device filter support to pulsectl popup menu 2023-09-06 22:34:38 +02:00
tobi-wan-kenobi
b9e45ca994
Merge pull request #986 from TheEdgeOfRage/pulsectl-font-size
Allow adjusting the fontsize of the pulsectl default device popup
2023-09-06 16:00:46 +02:00
Pavle Portic
37b5646d65
Allow adjusting the font size of tk popups 2023-09-06 12:11:57 +02:00
tobi-wan-kenobi
d03e6307f5 [contrib/stock] change API to alphavantage.co
Changed the stock module to work again (with an API key), and make it
easier to change the data provider by the user.

fixes #971
2023-07-21 14:18:17 +02:00
tobi-wan-kenobi
1471d8824b
Merge pull request #980 from sazk07/main
Fix: pipewire.py module
2023-07-20 00:35:40 +02:00
Shahan Arshad
04a222f3c8
Fix: pipewire.py module
mousewheel up and down events were initially not working on my bumblebee status bar. After verifying on the command line, it seems that wpctl set-volume command requires sink ID first and percentage change as the second arg.

steps to verify, assuming wireplumber is installed and sink ID is 32

```sh
wpctl set-volume 32 50%
```
will set the volume to 50%

```sh
wpctl set-volume 50% 32
```
will result in `Object '52' not found
2023-07-20 02:16:24 +05:00
tobi-wan-kenobi
868fdbedd3
Merge pull request #979 from sazk07/patch-1
Add: missing mention of pipewire module on website
2023-07-19 20:05:50 +02:00
Shahan Arshad
e1f50f4782
Add: missing mention of pipewire module on website
I was reading the docs on the website(https://bumblebee-status.readthedocs.io/en/latest/modules.html#contrib) and there was no mention of the pipewire module which exists among the contrib modules. This edit will inform users about the pipewire module
2023-07-19 22:10:05 +05:00
tobi-wan-kenobi
f855d5c235 [modules/aur-update] hide if no packages
fixes #978
2023-07-17 12:58:48 +02:00
tobi-wan-kenobi
2546dbae2e [tests/amixer] fix tests 2023-07-17 12:55:06 +02:00
tobi-wan-kenobi
d27986a316 [requirements] remove json requirement - which is a builtin 2023-07-17 12:43:06 +02:00
tobi-wan-kenobi
4df5272164 [workflows] update python versions 2023-07-17 12:36:32 +02:00
tobi-wan-kenobi
839d79e68f
Merge pull request #977 from jebaum/main
allow setting sink ID in pipewire module
2023-07-17 01:41:01 +02:00
James Baumgarten
ee796f6589 allow setting sink ID in pipewire module 2023-07-16 15:06:31 -06:00
tobi-wan-kenobi
9b4944c53f
Merge pull request #976 from lasnikr/main
[modules/usage] A module for "ActivityWatch"
2023-07-10 22:03:02 +02:00
Lasnik
8178919e2c add description 2023-07-10 16:52:22 +02:00
Lasnik
305f9cf491 formatting guidelines 2023-07-10 15:51:00 +02:00
Lasnik
b866ab25b6 create module 2023-07-10 15:42:11 +02:00
tobi-wan-kenobi
775210db08
Merge pull request #974 from hugoeustaquio/main
Adding suport for multiple sound cards, not only devices.
2023-06-30 06:15:10 +02:00
Hugo Eustáquio
2ef6f84df3 Adding suport for multiple sound cards, not only devices. 2023-06-29 14:38:54 -03:00
tobi-wan-kenobi
a97e6f2f7d
Merge pull request #973 from SuperQ/cpu3
[modules/cpu3] Add new CPU module
2023-06-19 14:46:32 +02:00
SuperQ
0b4ff04be5
[modules/cpu3] Add new CPU module
Based on cpu2 module, but use `sensors -j` and some json path walking to
better parse CPU temp and fan speeds. The output of `sensors -u` can
contain many duplicates of the same sensor name, but from different
sensor device paths.

Signed-off-by: SuperQ <superq@gmail.com>
2023-06-19 14:08:16 +02:00
tobi-wan-kenobi
7cda35c1df
Merge pull request #972 from bbernhard/fix_pihole
use API token instead of password hash in pihole module
2023-05-28 18:08:38 +02:00
Bernhard B
b3007dd042 use API token instead of password hash in pihole module
* with newer versions of pi-hole, it is not possible
  anymore to use the password hash for the API authentication.
  Instead, one needs to use the dedicated API token for that.
  In order to stay backwards compatible, and not break existing
  bumblebee_status setups, the 'pwhash' parameter is still supported
  (in case someone runs an outdated pi-hole version).

* the new pi-hole API endpoints do not allow to access the summary
  endpoint without an API token. So, therefore '&auth=<api token>' was
  added.
2023-05-28 17:13:52 +02:00
tobi-wan-kenobi
8967eec44b [module/watson] Add formatting string
Make it possible to customize the watson message in the widget

see #963
2023-05-11 14:28:32 +02:00
tobi-wan-kenobi
14f19c897a [modules/pulsectl] add default device selection
re-enable functionality to add a popup that allows the user to select
the default source/sink.

fixes #965
2023-05-11 08:45:03 +02:00
tobi-wan-kenobi
e9696b2150 [readthedocs] explicitly specify build OS
fixes #970
2023-05-11 08:30:59 +02:00
tobi-wan-kenobi
c0526f2775
Merge pull request #969 from Duarte-Figueiredo/fix-build
Fixed typo in 'today' that is currently breaking the tests
2023-05-06 12:45:19 +02:00
Duarte Figueiredo
6b4898017f Fixed typo in 'today' that is currently breaking the tests 2023-05-06 11:20:45 +01:00
tobi-wan-kenobi
bdfc4fdab4
Merge pull request #968 from Duarte-Figueiredo/main
Updated gitlab module to have state of warning when there is at least 1 notification, just like the github module
2023-05-06 12:01:24 +02:00
Duarte Figueiredo
a6de61b751 Updated gitlab module to have state of warning when there is at least 1 notification, just like the github module 2023-05-06 10:56:17 +01:00
tobi-wan-kenobi
1dd39a4e43
Merge pull request #967 from Duarte-Figueiredo/main
[modules/todoist] - New module that connects to https://api.todoist.com
2023-04-19 14:20:59 +02:00
Duarte Figueiredo
592d08c082 removed Final import because of python3.7 backwards compatibility 2023-04-19 12:01:07 +01:00
Duarte Figueiredo
cad45ecd2c [modules/todoist] - New module that connects to https://api.todoist.com and displays number of tasks due 2023-04-19 11:50:25 +01:00
tobi-wan-kenobi
79081ebb4f
Merge pull request #966 from Duarte-Figueiredo/main
[modules/wakatime] - New module that connects to https://wakatime.com api
2023-04-16 15:24:50 +02:00
Duarte Figueiredo
1b0478edd4 changed icon from normal w to font-awesome clock 2023-04-16 11:38:13 +01:00
Duarte Figueiredo
2b4e2b2c82 rename mock_summaries_api_response test function 2023-04-16 11:29:31 +01:00
Duarte Figueiredo
42e041ce03 [modules/wakatime] - New module that connects to https://wakatime.com and displays coding duration stats 2023-04-16 11:24:51 +01:00
tobi-wan-kenobi
e58afff48a
Merge pull request #964 from dmturner/weather
Change OpenWeatherMap request url from HTTP to HTTPS
2023-04-13 14:26:12 +02:00
dmturner
f34e02d824 Change OpenWeatherMap request url from HTTP to HTTPS 2023-04-13 12:49:39 +01:00
tobi-wan-kenobi
2e1289f778 [core] fix importlib.util error
add explicit import of importlib.util

fixes #962
2023-04-11 12:42:55 +02:00
tobi-wan-kenobi
b750d96a72
Merge pull request #959 from chedge/pipx_compatibility
Added path for themes directory when installed via pipx
2023-03-26 01:30:32 +01:00
C H
61e38c6094 Added path for themes directory when installed via pipx 2023-03-25 15:27:42 -07:00
tobi-wan-kenobi
ad8b1802f5
Merge pull request #957 from LokiLuciferase/fix/playerctl-calls
remove unnecessary `playerctl` subprocess call to determine whether widget should be hidden
2023-03-15 20:03:14 +01:00
Lukas Lüftinger
99bd2a81b6 remove unnecessary playerctl calls to determine whether widgets should be hidden 2023-03-15 19:01:48 +01:00
tobi-wan-kenobi
93f3da1e08
Merge pull request #951 from beckcl/gitlab-module
Add GitLab module
2023-02-19 08:04:37 +01:00
Clemens Beck
7161ef211c [modules/gitlab] add module 2023-02-19 03:43:14 +01:00
tobi-wan-kenobi
f77f5552ae
Merge pull request #950 from jebaum/main
fix bug in pipewire module
2023-02-11 08:22:02 +01:00
James Baumgarten
be332005fa fix bug in pipewire module 2023-02-10 08:52:59 -07:00
tobi-wan-kenobi
cc883d1723
Merge pull request #949 from jebaum/main
add pipewire module
2023-02-04 07:58:45 +01:00
James Baumgarten
30362cb124 add pipewire module 2023-02-03 21:23:34 -07:00
tobi-wan-kenobi
098f03ac52
Merge pull request #947 from arivarton/gcalendar_fixes
Added a max_chars parameter to be able to control the widget width.
2023-01-29 17:04:24 +01:00
arivarton
ae29c1b79f Divided date/time and summary into two widgets and made the summary
widget scrollable.
2023-01-29 13:05:13 +01:00
arivarton
b327162f3b Added a max_chars parameter to be able to control the widget width.
Also moved the try block a bit further up to catch network errors.
2023-01-04 21:34:21 +01:00
tobi-wan-kenobi
8eb2545eed
Merge pull request #943 from pvutov/main
Documentation: Fix the default format string for nvidiagpu
2022-11-30 20:07:58 +01:00
pvutov
f0ce6a1f7f
Documentation: Fix the default format string for nvidiagpu 2022-11-30 21:04:33 +02:00
tobi-wan-kenobi
a6f2e6fc5e [modules/mpd] make mpd port configurable
fixes #941
2022-11-27 17:41:48 +01:00
tobi-wan-kenobi
87a2890b48 [modules/pulsectl] fix case when no devices are available
no devices lead to an exception that completely stopped bumblebee-status
from processing data.

handle this case more gracefully by defaulting to a volume of 0%. if
this proves to be an issue, we can still add error indicators later.

see #940
2022-11-27 12:03:35 +01:00
tobi-wan-kenobi
6a93238bda [core] log exceptions
to enable error investigation, log exceptions.

see #940
2022-11-27 09:46:55 +01:00
tobi-wan-kenobi
79ce2167b0 [autotest] update codeclimate action 2022-11-26 12:09:04 +01:00
tobi-wan-kenobi
1fef60b32c [tests] fix location tests 2022-11-26 12:05:42 +01:00
tobi-wan-kenobi
0bc2c6b8e1 [tests] remove unsupported python version 2022-11-26 10:17:09 +01:00
tobi-wan-kenobi
07e2364f78 [main] fix i3 protocol buf on error messages ("could not parse JSON")
Errors during startup currently cause bumblebee-status to mistakenly
output the first line of output (the "version" line) of the i3 protocol
twice, causing an error message that says "could not parse JSON")

see #940
2022-11-26 10:11:51 +01:00
tobi-wan-kenobi
5412591a0e
Merge pull request #934 from tfwiii/main
publicip - Bug Fix
2022-10-12 09:03:14 +02:00
tfwiii
697c3310a0 publicip - Bug Fix - IP address changes wer being missed if an interface was present but did not have an IPv4 address associated with it. Added exception handling to mitigate this. 2022-10-12 13:52:55 +07:00
tobi-wan-kenobi
1682a47554
Merge pull request #933 from tfwiii/main
publicip module - Fixed bug and minor improvements in output
2022-10-08 06:08:41 +02:00
tfwiii
cace02909e Bug fix improvements to publicip and util.location
Fixed publicip bug arising from last PR review
Simplified ip change detection code
Added pause after location.reset() call to allow completion before query
util.location - change order of information providers as default was not returning geo coords
2022-10-08 10:42:12 +07:00
tfwiii
605b749e22 Removed debugging prints 2022-10-06 14:21:43 +07:00
tfwiii
61fe7f6d3e Handled fail where core.location does not provide values for latitude and longitude. Added handling for coordinates N, S, E, W. 2022-10-06 13:49:37 +07:00
tobi-wan-kenobi
e70402e92c
Merge pull request #932 from ramonsaraiva/add-moonlight-theme
Add moonlight theme (powerline)
2022-09-27 17:05:00 +02:00
Ramon Saraiva
88f24100ff [themes] add moonlight theme (powerline) 2022-09-27 11:10:33 -03:00
tobi-wan-kenobi
a7979e7d66
Merge pull request #930 from benthetechguy/man-fix
Don't install manpages to /usr/usr
2022-09-21 06:32:43 +02:00
Ben Westover
7ae95ad6b6
Don't install manpages to /usr/usr
`data_files` shouldn't have `usr/` in it; this causes the manpages to be installed to `/usr/usr/share/man/man1` instead of `/usr/share/man/man1`.
2022-09-20 23:15:38 -04:00
tobi-wan-kenobi
ccf2fb3fd0
Merge pull request #929 from alonsomoya/docs/network_trafic_dependency
contrib/network_traffic dependency in docs
2022-09-20 15:10:12 +02:00
Jose Javier ALONSO MOYA
7ec3adfa47 contrib/network_traffic dependency in docs 2022-09-20 15:00:14 +02:00
tobi-wan-kenobi
1c19250fe5 [core/output] fix broken output 2022-09-18 16:50:43 +02:00
tobi-wan-kenobi
38d3a6d4c4 [doc] rearrange badges 2022-09-18 09:04:39 +02:00
tobi-wan-kenobi
0151d20451 [doc] update badges 2022-09-18 09:04:08 +02:00
tobi-wan-kenobi
acb387a685 Merge branch '921-scrolling-status-bar' 2022-09-17 17:04:12 +02:00
tobi-wan-kenobi
c40f59f7be [modules/scroll] edge case error 2022-09-11 16:00:35 +02:00
tobi-wan-kenobi
3f97ea6a39 [doc] add scroll menu
see #921
2022-09-11 13:16:06 +02:00
tobi-wan-kenobi
21cbbe685d [modules/scroll] add preliminary version of scrolling module
add a scrolling module that can be used to scroll the whole bar to an
arbitrary number of widgets.

its parameter is "width", which determines the number of widgets to
display.

see #921
2022-09-11 13:12:51 +02:00
65 changed files with 1850 additions and 430 deletions

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v3
@ -24,12 +24,12 @@ jobs:
cache: 'pip'
- name: Install Ubuntu dependencies
run: sudo apt-get install -y libdbus-1-dev libgit2-dev libvirt-dev taskwarrior
- name: Install Python dependencies
- 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 | cut -d ' ' -f 1 | sort -u)
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
@ -38,8 +38,8 @@ jobs:
- name: Run tests
run: |
coverage run --source=. -m pytest tests -v
- name: Report coverage
uses: paambaati/codeclimate-action@v3.0.0
- name: Report coverage
uses: paambaati/codeclimate-action@v3.2.0
with:
coverageCommand: coverage3 xml
debug: true

View file

@ -3,4 +3,7 @@ version: 2
python:
install:
- requirements: docs/requirements.txt
build:
os: ubuntu-22.04
tools:
python: "3"

View file

@ -4,10 +4,13 @@
logo courtesy of [kellya](https://github.com/kellya) - thank you!
[![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
![Commits since release](https://img.shields.io/github/commits-since/tobi-wan-kenobi/bumblebee-status/latest)
![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status)
![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git)
[![PyPI version](https://badge.fury.io/py/bumblebee-status.svg)](https://badge.fury.io/py/bumblebee-status)
![PyPI version](https://img.shields.io/pypi/v/bumblebee-status)
![Contributors](https://img.shields.io/github/contributors-anon/tobi-wan-kenobi/bumblebee-status)
[![Tests](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml)
[![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
[![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
[![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)

View file

@ -68,10 +68,13 @@ 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():
@ -100,6 +103,7 @@ def main():
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, ))
@ -131,7 +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)
started = True
signal.signal(10, sig_USR1_handler)
while True:
if update_lock.acquire(blocking=False) == True:
@ -149,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("[")

View file

@ -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

View file

@ -1,5 +1,6 @@
import os
import importlib
import importlib.util
import logging
import threading
@ -112,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.

View file

@ -146,11 +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
@ -223,13 +226,29 @@ 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", "no-autohide"]
@ -244,6 +263,7 @@ 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):
@ -274,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": ","}

View file

@ -25,6 +25,7 @@ if os.environ.get("XDG_DATA_DIRS"):
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",
])

View file

@ -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"

View file

@ -31,7 +31,7 @@ class Module(core.module.Module):
return self.__format.format(self.__packages)
def hidden(self):
return self.__packages == 0 and not self.__error
return self.__packages == 0
def update(self):
self.__error = False

View file

@ -130,8 +130,14 @@ 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):
@ -147,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))
@ -167,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)
@ -176,6 +193,9 @@ 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"]
@ -187,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:

View file

@ -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."""

View file

@ -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, ignore_errors=True)
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

View 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

View file

@ -28,6 +28,8 @@ class Module(core.module.Module):
self.__states = {"unknown": ["unknown", "critical"],
"true": ["muted", "warning"],
"false": ["unmuted"]}
if util.format.asbool(self.parameter("disabled", False)):
util.cli.execute("dunstctl set-paused true", ignore_errors=True)
def toggle_state(self, event):
util.cli.execute("dunstctl set-paused toggle", ignore_errors=True)

View file

@ -3,7 +3,9 @@
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.
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.
@ -15,7 +17,7 @@ Parameters:
Requires these pip packages:
* google-api-python-client >= 1.8.0
* google-auth-httplib2
* google-auth-httplib2
* google-auth-oauthlib
"""
@ -27,10 +29,12 @@ 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
@ -38,11 +42,15 @@ 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=15)
@core.decorators.every(minutes=update_every)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.first_event))
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(
@ -60,32 +68,44 @@ class Module(core.module.Module):
except Exception:
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
def first_event(self, widget):
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"]
"""Shows basic usage of the Google Calendar API.
Prints the start and name of the next 10 events on the user's calendar.
"""
creds = None
# 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())
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
@ -125,33 +145,27 @@ class Module(core.module.Module):
}
)
sorted_list = sorted(event_list, key=lambda t: t["date"])
next_event = sorted_list[0]
for gevent in sorted_list:
if gevent["date"] >= datetime.datetime.now(datetime.timezone.utc):
if gevent["date"].date() == datetime.datetime.utcnow().date():
return str(
"%s %s"
% (
gevent["date"]
.astimezone()
.strftime(f"{self.__time_format}"),
gevent["summary"],
)
)
else:
return str(
"%s %s"
% (
gevent["date"]
.astimezone()
.strftime(f"{self.__date_format} {self.__time_format}"),
gevent["summary"],
)
)
return "No upcoming events found."
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:
return None
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

View 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

View file

@ -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 = {}

View file

@ -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:

View 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

7
bumblebee_status/modules/contrib/playerctl.py Executable file → Normal file
View file

@ -34,6 +34,7 @@ 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(
@ -87,7 +88,7 @@ class Module(core.module.Module):
core.input.register(widget, **callback_options)
def hidden(self):
return self.__hide and self.status() == None
return self.__hidden
def status(self):
try:
@ -101,6 +102,10 @@ class Module(core.module.Module):
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":

View 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

View file

@ -60,43 +60,36 @@ class Module(core.module.Module):
self.__monitor.start()
def monitor(self):
default_route = None
interfaces = None
__previous_ips = set()
__current_ips = set()
# Initially set to True to force an info update on first pass
information_changed = True
__information_changed = True
self.update()
while threading.main_thread().is_alive():
# Look for any changes in the netifaces default route information
__current_ips.clear()
# Look for any changes to IP addresses
try:
current_default_route = netifaces.gateways()["default"][2]
for interface in netifaces.interfaces():
try:
__current_ips.add(netifaces.ifaddresses(interface)[2][0]['addr'])
except:
pass
except:
# error reading out default gw -> assume none exists
current_default_route = None
if current_default_route != default_route:
default_route = current_default_route
information_changed = True
# 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
# netifaces does not check ALL routing tables which might lead to false negatives
# (ref: http://linux-ip.net/html/routing-tables.html) so additionally... look for
# any changes in the netifaces interfaces information which might also be an inticator
# of a change of route/external IP
if not information_changed: # Only check if no routing table change found
try:
current_interfaces = netifaces.interfaces()
except:
# error reading interfaces information -> assume none exists
current_interfaces = None
if current_interfaces != interfaces:
interfaces = current_interfaces
information_changed = True
# Update either routing or interface information has changed
if information_changed:
information_changed = False
# Update if change is flagged
if __information_changed:
__information_changed = False
self.update()
# Throttle the calls to netifaces
time.sleep(1)
@ -104,11 +97,11 @@ class Module(core.module.Module):
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", "-"),
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):
@ -119,14 +112,28 @@ class Module(core.module.Module):
try:
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
__lat = "{:.2f}".format(__info["latitude"])
__lon = "{:.2f}".format(__info["longitude"])
__coords = __lat + "°N" + "," + " " + __lon + "°E"
# 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"])

View file

@ -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, _):

View file

@ -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

View file

@ -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')
@ -77,7 +77,7 @@ class Module(core.module.Module):
util.cli.execute(popupcmd)
return
menu = util.popup.menu()
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")

View file

@ -50,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()

View file

@ -0,0 +1,76 @@
# pylint: disable=C0111,R0903
"""
Displays the 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))

View 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

View file

@ -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)

View 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)

View file

@ -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"

View file

@ -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)

View 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

View file

@ -25,6 +25,7 @@ import subprocess
import core.module
import core.decorators
import core.input
import util.cli
import util.format
@ -58,6 +59,8 @@ class Module(core.module.Module):
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())
@ -88,9 +91,7 @@ class Module(core.module.Module):
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")

View file

@ -236,7 +236,7 @@ class Module(core.module.Module):
channel = "sinks" if self._channel == "sink" else "sources"
result = util.cli.execute("pactl list {} short".format(channel))
menu = util.popup.menu()
menu = util.popup.menu(self.__config)
lines = result.splitlines()
for line in lines:
info = line.split("\t")

View file

@ -13,6 +13,8 @@ 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.
@ -35,6 +37,8 @@ Requires the following Python module:
"""
import pulsectl
import logging
import functools
import core.module
import core.widget
@ -45,13 +49,18 @@ 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 = "n/a"
self.__volume = 0
self.__devicename = "n/a"
self.__muted = False
self.__showbars = util.format.asbool(self.parameter("showbars", False))
@ -63,6 +72,11 @@ class Module(core.module.Module):
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 = [
{
@ -108,11 +122,15 @@ class Module(core.module.Module):
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:
@ -132,16 +150,22 @@ class Module(core.module.Module):
for dev in devs:
if dev.name == default:
return dev
return devs[0] # fallback
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)
self.__volume = dev.volume.value_flat
self.__muted = dev.mute
self.__devicename = dev.name
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")
@ -151,9 +175,33 @@ class Module(core.module.Module):
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"]
return ["unmuted"]
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

View 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

View file

@ -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)

View file

@ -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)

View file

@ -18,17 +18,6 @@ __document = None
__data = {}
__next = 0
__sources = [
{
"url": "http://ipapi.co/json",
"mapping": {
"latitude": "latitude",
"longitude": "longitude",
"country_name": "country_name",
"country_code": "country_code",
"city": "city_name",
"ip": "public_ip",
},
},
{
"url": "http://free.ipwhois.io/json/",
"mapping": {
@ -43,14 +32,25 @@ __sources = [
{
"url": "http://ip-api.com/json",
"mapping": {
"latitude": "lat",
"longitude": "lon",
"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",
},
}
]

View file

@ -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)
@ -68,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
@ -78,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"""

View 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:

View file

@ -264,6 +264,8 @@ 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.
@ -303,6 +305,14 @@ Parameters:
.. 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
~~~~~~~~
@ -416,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%)
@ -423,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
@ -677,6 +690,49 @@ 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
~~~~~~~~
@ -828,6 +884,9 @@ 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!
@ -858,7 +917,9 @@ 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.
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.
@ -870,7 +931,7 @@ Parameters:
Requires these pip packages:
* google-api-python-client >= 1.8.0
* google-auth-httplib2
* google-auth-httplib2
* google-auth-oauthlib
getcrypto
@ -915,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
~~~~~
@ -1099,6 +1183,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!
@ -1229,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
~~~~~~~~~
@ -1552,7 +1657,9 @@ Display a stock quote from finance.yahoo.com
Parameters:
* stock.symbols : Comma-separated list of symbols to fetch
* stock.change : Should we fetch change in stock value (defaults to True)
* stock.apikey : API key created on https://alphavantage.co
* stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}"
* stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent"
contributed by `msoulier <https://github.com/msoulier>`_ - many thanks!
@ -1587,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')
@ -1690,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
~~~~~~~
@ -1728,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
~~~
@ -1747,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
~~~~~~
@ -1755,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
@ -1772,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!

View file

@ -97,3 +97,8 @@ List of available themes
:alt: Default
Default (nothing or -t default)
.. figure:: ../screenshots/themes/moonlight-powerline.png
:alt: Moonlight Powerline
Moonlight Powerline (-t moonlight-powerline) (contributed by `Ramon Saraiva <https://github.com/ramonsaraiva>`__)

View file

@ -0,0 +1 @@
psutil

View file

@ -0,0 +1 @@
requests

View file

@ -0,0 +1,2 @@
dbus-python
power-profiles-daemon

BIN
screenshots/gitlab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screenshots/todoist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
screenshots/wakatime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -56,7 +56,7 @@ setup(
("share/bumblebee-status/themes", glob.glob("themes/*.json")),
("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")),
("share/bumblebee-status/utility", glob.glob("bin/*")),
("usr/share/man/man1", glob.glob("man/*.1")),
("share/man/man1", glob.glob("man/*.1")),
],
packages=find_packages(exclude=["tests", "tests.*"])
)

View file

@ -113,6 +113,12 @@ def test_missing_parameter():
assert cfg.get("test.key") == None
assert cfg.get("test.key", "no-value-set") == "no-value-set"
def test_file_case_sensitivity():
cfg = core.config.Config([])
cfg.load_config("", content="[module-parameters]\ntest.key = VaLuE\ntest.KeY2 = value")
assert cfg.get("test.key") == "VaLuE"
assert cfg.get("test.KeY2") == "value"
#
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -117,29 +117,29 @@ def test_toggle(module_mock, mocker):
command = mocker.patch('util.cli.execute')
module = module_mock()
module.toggle(False)
command.assert_called_once_with('amixer -q set Master,0 toggle')
command.assert_called_once_with('amixer -c 0 -q set Master,0 toggle')
def test_default_volume(module_mock, mocker):
module = module_mock()
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set Master,0 4%+')
command.assert_called_once_with('amixer -c 0 -q set Master,0 4%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set Master,0 4%-')
command.assert_called_once_with('amixer -c 0 -q set Master,0 4%-')
def test_custom_volume(module_mock, mocker):
module = module_mock(['-p', 'amixer.percent_change=25'])
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set Master,0 25%+')
command.assert_called_once_with('amixer -c 0 -q set Master,0 25%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set Master,0 25%-')
command.assert_called_once_with('amixer -c 0 -q set Master,0 25%-')
def test_custom_device(module_mock, mocker):
mocker.patch('util.cli.execute')
@ -147,13 +147,13 @@ def test_custom_device(module_mock, mocker):
command = mocker.patch('util.cli.execute')
module.toggle(False)
command.assert_called_once_with('amixer -q set CustomMaster toggle')
command.assert_called_once_with('amixer -c 0 -q set CustomMaster toggle')
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set CustomMaster 4%+')
command.assert_called_once_with('amixer -c 0 -q set CustomMaster 4%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set CustomMaster 4%-')
command.assert_called_once_with('amixer -c 0 -q set CustomMaster 4%-')

View file

@ -0,0 +1,6 @@
import pytest
pytest.importorskip("psutil")
def test_load_module():
__import__("modules.contrib.cpu3")

View file

@ -0,0 +1,70 @@
from unittest import TestCase, mock
import pytest
from requests import Session
import core.config
import core.widget
import modules.contrib.gitlab
pytest.importorskip("requests")
def build_gitlab_module(actions=""):
config = core.config.Config(["-p", "gitlab.actions={}".format(actions)])
return modules.contrib.gitlab.Module(config=config, theme=None)
def mock_todo_api_response():
res = mock.Mock()
res.json = lambda: [
{"action_name": "assigned"},
{"action_name": "assigned"},
{"action_name": "approval_required"},
]
res.status_code = 200
return res
class TestGitlabUnit(TestCase):
def test_load_module(self):
__import__("modules.contrib.gitlab")
@mock.patch.object(Session, "get", return_value=mock_todo_api_response())
def test_unfiltered(self, _):
module = build_gitlab_module()
module.update()
assert module.widgets()[0].full_text() == "3"
@mock.patch.object(Session, "get", return_value=mock_todo_api_response())
def test_filtered(self, _):
module = build_gitlab_module(actions="approval_required")
module.update()
assert module.widgets()[0].full_text() == "1"
@mock.patch.object(Session, "get", return_value=mock_todo_api_response())
def test_state_warning(self, _):
module = build_gitlab_module(actions="approval_required")
module.update()
assert module.state(None) == ["warning"]
@mock.patch.object(Session, "get", return_value=mock_todo_api_response())
def test_state_normal(self, _):
module = build_gitlab_module(actions="empty_filter")
module.update()
assert module.state(None) == []
@mock.patch.object(Session, "get", return_value=mock_todo_api_response())
def test_state_normal_before_update(self, _):
module = build_gitlab_module(actions="approval_required")
assert module.state(None) == []
@mock.patch.object(Session, "get", side_effect=Exception("Something went wrong"))
def test_state_normal_if_na(self, _):
module = build_gitlab_module(actions="approval_required")
module.update()
assert module.state(None) == []

View file

@ -0,0 +1,32 @@
from unittest.mock import patch, MagicMock
import unittest
import pytest
import core.config
import modules.contrib.power_profile
pytest.importorskip("dbus")
def build_powerprofile_module():
config = core.config.Config([])
return modules.contrib.power_profile.Module(config=config, theme=None)
class TestPowerProfileUnit(unittest.TestCase):
def __get_mock_dbus_get_method(self, mock_system_bus):
return (
mock_system_bus.return_value.get_object.return_value.get_dbus_method.return_value
)
def test_load_module(self):
__import__("modules.contrib.power-profile")
@patch("dbus.SystemBus")
def test_full_text(self, mock_system_bus):
mock_get = self.__get_mock_dbus_get_method(mock_system_bus)
mock_get.return_value = "balanced"
module = build_powerprofile_module()
module.update()
assert module.widgets()[0].full_text() == "balanced"

View file

@ -0,0 +1,58 @@
from unittest import TestCase, mock
import pytest
from requests import Session
import core.config
import core.widget
import modules.contrib.todoist
pytest.importorskip("requests")
def build_todoist_module(todoist_filter=None):
config = core.config.Config([
"-p",
f"todoist.filter={todoist_filter}" if todoist_filter else ""
])
return modules.contrib.todoist.Module(config=config, theme=None)
def mock_tasks_api_response():
res = mock.Mock()
res.json = lambda: [
{
"id": "-1",
"project_id": "-1"
},
{
"id": "-2",
"project_id": "-2"
}
]
res.status_code = 200
return res
class TestTodoistUnit(TestCase):
def test_load_module(self):
__import__("modules.contrib.todoist")
@mock.patch.object(Session, "get", return_value=mock_tasks_api_response())
def test_default_values(self, mock_get):
module = build_todoist_module()
module.update()
assert module.widgets()[0].full_text() == "2"
mock_get.assert_called_with('https://api.todoist.com/rest/v2/tasks', params=None)
@mock.patch.object(Session, "get", return_value=mock_tasks_api_response())
def test_custom_filter(self, mock_get):
module = build_todoist_module(todoist_filter="!assigned to: others & (Overdue | due: today)")
module.update()
assert module.widgets()[0].full_text() == "2"
mock_get.assert_called_with('https://api.todoist.com/rest/v2/tasks',
params={'filter': '!assigned to: others & (Overdue | due: today)'})

View file

@ -0,0 +1,56 @@
from unittest import TestCase, mock
import pytest
from requests import Session
import core.config
import core.widget
import modules.contrib.wakatime
pytest.importorskip("requests")
def build_wakatime_module(waka_format=None, waka_range=None):
config = core.config.Config([
"-p",
f"wakatime.format={waka_format}" if waka_format else "",
f"wakatime.range={waka_range}" if waka_range else ""
])
return modules.contrib.wakatime.Module(config=config, theme=None)
def mock_summaries_api_response():
res = mock.Mock()
res.json = lambda: {
"cumulative_total": {
"text": "3 hrs 2 mins",
"seconds": 10996,
"digital": "3:02",
"decimal": "3.03"
},
}
res.status_code = 200
return res
class TestWakatimeUnit(TestCase):
def test_load_module(self):
__import__("modules.contrib.wakatime")
@mock.patch.object(Session, "get", return_value=mock_summaries_api_response())
def test_default_values(self, mock_get):
module = build_wakatime_module()
module.update()
assert module.widgets()[0].full_text() == "3:02"
mock_get.assert_called_with('https://wakatime.com/api/v1/users/current/summaries?range=Today')
@mock.patch.object(Session, "get", return_value=mock_summaries_api_response())
def test_custom_configs(self, mock_get):
module = build_wakatime_module(waka_format="text", waka_range="last 7 days")
module.update()
assert module.widgets()[0].full_text() == "3 hrs 2 mins"
mock_get.assert_called_with('https://wakatime.com/api/v1/users/current/summaries?range=last 7 days')

View file

@ -14,16 +14,16 @@ def urllib_req(mocker):
def secondaryLocation():
return {
"country": "Middle Earth",
"longitude": "10.0",
"latitude": "20.5",
"ip": "127.0.0.1",
"lon": "10.0",
"lat": "20.5",
"query": "127.0.0.1",
}
@pytest.fixture
def primaryLocation():
return {
"country_name": "Rivia",
"country": "Rivia",
"longitude": "-10.0",
"latitude": "-23",
"ip": "127.0.0.6",
@ -33,7 +33,7 @@ def primaryLocation():
def test_primary_provider(urllib_req, primaryLocation):
urllib_req.urlopen.return_value.read.return_value = json.dumps(primaryLocation)
assert util.location.country() == primaryLocation["country_name"]
assert util.location.country() == primaryLocation["country"]
assert util.location.coordinates() == (
primaryLocation["latitude"],
primaryLocation["longitude"],
@ -48,10 +48,10 @@ def test_secondary_provider(mocker, urllib_req, secondaryLocation):
assert util.location.country() == secondaryLocation["country"]
assert util.location.coordinates() == (
secondaryLocation["latitude"],
secondaryLocation["longitude"],
secondaryLocation["lat"],
secondaryLocation["lon"],
)
assert util.location.public_ip() == secondaryLocation["ip"]
assert util.location.public_ip() == secondaryLocation["query"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -9,6 +9,10 @@
"fg": "#fbf1c7",
"bg": "#cc241d"
},
"good": {
"fg": "#1d2021",
"bg": "#b8bb26"
},
"default-separators": false,
"separator-block-width": 0
},
@ -34,16 +38,6 @@
"bg": "#859900"
}
},
"battery": {
"charged": {
"fg": "#1d2021",
"bg": "#b8bb26"
},
"AC": {
"fg": "#1d2021",
"bg": "#b8bb26"
}
},
"bluetooth": {
"ON": {
"fg": "#1d2021",

View file

@ -309,6 +309,9 @@
"github": {
"prefix": "github"
},
"gitlab": {
"prefix": "gitlab"
},
"deezer": {
"prefix": ""
},
@ -407,5 +410,8 @@
"speedtest": {
"running": { "prefix": [".", "..", "...", ".."] },
"not-running": { "prefix": "[start]" }
},
"power-profile": {
"prefix": "profile"
}
}

View file

@ -199,11 +199,14 @@
},
"pulseout": {
"muted": {
"prefix": ""
"prefix": "󰝟"
},
"unmuted": {
"prefix": ""
}
},
"unmuted-low": { "prefix": "󰕿" },
"unmuted-mid": { "prefix": "󰖀" },
"unmuted-high": { "prefix": "󰕾" }
},
"amixer": {
"muted": {
@ -223,56 +226,40 @@
},
"pulsein": {
"muted": {
"prefix": ""
"prefix": "󰍭"
},
"unmuted": {
"prefix": ""
}
},
"pipewire": {
"muted": {
"prefix": ""
},
"unmuted": {
"prefix": ""
}
},
"kernel": {
"prefix": "\uf17c"
},
"nic": {
"wireless-up": {
"prefix": ""
},
"wireless-down": {
"prefix": ""
},
"wired-up": {
"prefix": ""
},
"wired-down": {
"prefix": ""
},
"tunnel-up": {
"prefix": ""
},
"tunnel-down": {
"prefix": ""
}
"wireless-up": { "prefix": "" },
"wireless-down": { "prefix": "睊" },
"wired-up": { "prefix": "" },
"wired-down": { "prefix": "" },
"tunnel-up": { "prefix": "嬨" },
"tunnel-down": { "prefix": "嬨" }
},
"bluetooth": {
"ON": {
"prefix": ""
},
"OFF": {
"prefix": ""
},
"?": {
"prefix": ""
}
"ON": { "prefix": "󰂯" },
"OFF": { "prefix": "󰂲" },
"?": { "prefix": "󰂱" }
},
"bluetooth2": {
"ON": {
"prefix": ""
},
"warning": {
"prefix": ""
},
"critical": {
"prefix": ""
}
"connected": { "prefix": "󰂱" },
"enabled": { "prefix": "󰂯" },
"critical": { "prefix": "󰂲" }
},
"battery-upower": {
"charged": {
@ -341,136 +328,46 @@
}
},
"battery": {
"charged": {
"prefix": "",
"suffix": ""
},
"AC": {
"suffix": ""
},
"charged": { "prefix": "󰂄" },
"AC": { "suffix": "󱐥" },
"PEN": { "suffix": "󰏪" },
"charging": {
"prefix": [
"",
"",
"",
"",
""
],
"suffix": ""
"prefix": [ "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅" ],
"suffix": ""
},
"discharging-10": {
"prefix": "",
"suffix": ""
},
"discharging-25": {
"prefix": "",
"suffix": ""
},
"discharging-50": {
"prefix": "",
"suffix": ""
},
"discharging-80": {
"prefix": "",
"suffix": ""
},
"discharging-100": {
"prefix": "",
"suffix": ""
},
"unlimited": {
"prefix": "",
"suffix": ""
},
"estimate": {
"prefix": ""
},
"unknown-10": {
"prefix": "",
"suffix": ""
},
"unknown-25": {
"prefix": "",
"suffix": ""
},
"unknown-50": {
"prefix": "",
"suffix": ""
},
"unknown-80": {
"prefix": "",
"suffix": ""
},
"unknown-100": {
"prefix": "",
"suffix": ""
}
"discharging-05": { "prefix": "󰂎", "suffix": "" },
"discharging-10": { "prefix": "󰁺", "suffix": "" },
"discharging-20": { "prefix": "󰁻", "suffix": "" },
"discharging-30": { "prefix": "󰁼", "suffix": "" },
"discharging-40": { "prefix": "󰁽", "suffix": "" },
"discharging-50": { "prefix": "󰁾", "suffix": "" },
"discharging-60": { "prefix": "󰁿", "suffix": "" },
"discharging-70": { "prefix": "󰂀", "suffix": "" },
"discharging-80": { "prefix": "󰂁", "suffix": "" },
"discharging-90": { "prefix": "󰂂", "suffix": "" },
"discharging-100": { "prefix": "󰁹" },
"unlimited": { "prefix": "", "suffix": "" },
"estimate": { "prefix": "" }
},
"battery_all": {
"charged": {
"prefix": "",
"suffix": ""
},
"AC": {
"suffix": ""
},
"charged": { "prefix": "", "suffix": "" },
"AC": { "suffix": "" },
"charging": {
"prefix": [
"",
"",
"",
"",
""
],
"prefix": [ "", "", "", "", "" ],
"suffix": ""
},
"discharging-10": {
"prefix": "",
"suffix": ""
},
"discharging-25": {
"prefix": "",
"suffix": ""
},
"discharging-50": {
"prefix": "",
"suffix": ""
},
"discharging-80": {
"prefix": "",
"suffix": ""
},
"discharging-100": {
"prefix": "",
"suffix": ""
},
"unlimited": {
"prefix": "",
"suffix": ""
},
"estimate": {
"prefix": ""
},
"unknown-10": {
"prefix": "",
"suffix": ""
},
"unknown-25": {
"prefix": "",
"suffix": ""
},
"unknown-50": {
"prefix": "",
"suffix": ""
},
"unknown-80": {
"prefix": "",
"suffix": ""
},
"unknown-100": {
"prefix": "",
"suffix": ""
}
"discharging-10": { "prefix": "", "suffix": "" },
"discharging-25": { "prefix": "", "suffix": "" },
"discharging-50": { "prefix": "", "suffix": "" },
"discharging-80": { "prefix": "", "suffix": "" },
"discharging-100": { "prefix": "", "suffix": "" },
"unlimited": { "prefix": "", "suffix": "" },
"estimate": { "prefix": "" },
"unknown-10": { "prefix": "", "suffix": "" },
"unknown-25": { "prefix": "", "suffix": "" },
"unknown-50": { "prefix": "", "suffix": "" },
"unknown-80": { "prefix": "", "suffix": "" },
"unknown-100": { "prefix": "", "suffix": "" }
},
"caffeine": {
"activated": {
@ -573,6 +470,15 @@
"github": {
"prefix": "  "
},
"gitlab": {
"prefix": ""
},
"wakatime": {
"prefix": "\uF017"
},
"todoist": {
"prefix": "\uF14A"
},
"deezer": {
"prefix": "  "
},
@ -674,7 +580,7 @@
}
},
"vpn": {
"prefix": ""
"prefix": "󰖂"
},
"system": {
"prefix": "  "
@ -722,5 +628,12 @@
},
"thunderbird": {
"prefix": ""
},
"power-profile": {
"prefix": "\uF2C1"
},
"wlrotation": {
"auto": {"prefix": "󰑵"},
"locked": {"prefix": "󰑸"}
}
}

View file

@ -0,0 +1,24 @@
{
"icons": ["awesome-fonts"],
"defaults": {
"separator-block-width": 0,
"warning": {
"fg": "#e4f3fa",
"bg": "#fc7b7b"
},
"critical": {
"fg": "#e4f3fa",
"bg": "#ff5370"
}
},
"cycle": [
{
"fg": "#e4f3fa",
"bg": "#403c64"
},
{
"fg": "#e4f3fa",
"bg": "#212337"
}
]
}