Compare commits
127 commits
Author | SHA1 | Date | |
---|---|---|---|
676bbebf4c | |||
617a12f96d | |||
5053bb0f1b | |||
21060a10a0 | |||
a56b86100a | |||
fd4b940d58 | |||
2bbac991db | |||
255794cd1c | |||
2e81eed830 | |||
60bfb19378 | |||
d7d4603855 | |||
cbd0c58b4a | |||
bbc26c263c | |||
53de1b524a | |||
c3d4fce74c | |||
8cc7c9de9b | |||
3ed26c62a5 | |||
900a0710c5 | |||
2e18d71284 | |||
61123eb7a0 | |||
27be30263f | |||
559517f345 | |||
b45ff330c9 | |||
|
bfafd93643 | ||
|
c14ed1166d | ||
|
9f6c9cc7d2 | ||
|
025d3fcb51 | ||
|
05622f985a | ||
|
c4a3f488aa | ||
|
68de299763 | ||
|
85760926d7 | ||
|
3bc3c75ff4 | ||
|
d30e5c694e | ||
|
b217ac9c9e | ||
|
fcda13cac0 | ||
|
0c2123cfe4 | ||
|
9251217bb3 | ||
|
9170785ed5 | ||
|
2e7e75a27c | ||
|
d303b794f3 | ||
|
b42323013d | ||
|
8583b5123e | ||
|
b762132037 | ||
|
fded39fa81 | ||
|
9c5e30ac61 | ||
|
9e6e656fa8 | ||
|
b9e45ca994 | ||
|
37b5646d65 | ||
|
d03e6307f5 | ||
|
1471d8824b | ||
|
04a222f3c8 | ||
|
868fdbedd3 | ||
|
e1f50f4782 | ||
|
f855d5c235 | ||
|
2546dbae2e | ||
|
d27986a316 | ||
|
4df5272164 | ||
|
839d79e68f | ||
|
ee796f6589 | ||
|
9b4944c53f | ||
|
8178919e2c | ||
|
305f9cf491 | ||
|
b866ab25b6 | ||
|
775210db08 | ||
|
2ef6f84df3 | ||
|
a97e6f2f7d | ||
|
0b4ff04be5 | ||
|
7cda35c1df | ||
|
b3007dd042 | ||
|
8967eec44b | ||
|
14f19c897a | ||
|
e9696b2150 | ||
|
c0526f2775 | ||
|
6b4898017f | ||
|
bdfc4fdab4 | ||
|
a6de61b751 | ||
|
1dd39a4e43 | ||
|
592d08c082 | ||
|
cad45ecd2c | ||
|
79081ebb4f | ||
|
1b0478edd4 | ||
|
2b4e2b2c82 | ||
|
42e041ce03 | ||
|
e58afff48a | ||
|
f34e02d824 | ||
|
2e1289f778 | ||
|
b750d96a72 | ||
|
61e38c6094 | ||
|
ad8b1802f5 | ||
|
99bd2a81b6 | ||
|
93f3da1e08 | ||
|
7161ef211c | ||
|
f77f5552ae | ||
|
be332005fa | ||
|
cc883d1723 | ||
|
30362cb124 | ||
|
098f03ac52 | ||
|
ae29c1b79f | ||
|
b327162f3b | ||
|
8eb2545eed | ||
|
f0ce6a1f7f | ||
|
a6f2e6fc5e | ||
|
87a2890b48 | ||
|
6a93238bda | ||
|
79ce2167b0 | ||
|
1fef60b32c | ||
|
0bc2c6b8e1 | ||
|
07e2364f78 | ||
|
5412591a0e | ||
|
697c3310a0 | ||
|
1682a47554 | ||
|
cace02909e | ||
|
605b749e22 | ||
|
61fe7f6d3e | ||
|
e70402e92c | ||
|
88f24100ff | ||
|
a7979e7d66 | ||
|
7ae95ad6b6 | ||
|
ccf2fb3fd0 | ||
|
7ec3adfa47 | ||
|
1c19250fe5 | ||
|
38d3a6d4c4 | ||
|
0151d20451 | ||
|
acb387a685 | ||
|
c40f59f7be | ||
|
3f97ea6a39 | ||
|
21cbbe685d |
65 changed files with 1850 additions and 430 deletions
10
.github/workflows/autotest.yml
vendored
10
.github/workflows/autotest.yml
vendored
|
@ -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
|
||||
|
|
|
@ -3,4 +3,7 @@ version: 2
|
|||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3"
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
logo courtesy of [kellya](https://github.com/kellya) - thank you!
|
||||
|
||||
[](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
|
||||

|
||||

|
||||

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

|
||||

|
||||
[](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml)
|
||||
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
|
||||
[](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||
|
|
|
@ -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("[")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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": ","}
|
||||
|
|
|
@ -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",
|
||||
])
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
158
bumblebee_status/modules/contrib/cpu3.py
Normal file
158
bumblebee_status/modules/contrib/cpu3.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
"""Multiwidget CPU module
|
||||
|
||||
Can display any combination of:
|
||||
|
||||
* max CPU frequency
|
||||
* total CPU load in percents (integer value)
|
||||
* per-core CPU load as graph - either mono or colored
|
||||
* CPU temperature (in Celsius degrees)
|
||||
* CPU fan speed
|
||||
|
||||
Requirements:
|
||||
|
||||
* the psutil Python module for the first three items from the list above
|
||||
* sensors executable for the rest
|
||||
|
||||
Parameters:
|
||||
* cpu3.layout: Space-separated list of widgets to add.
|
||||
Possible widgets are:
|
||||
|
||||
* cpu3.maxfreq
|
||||
* cpu3.cpuload
|
||||
* cpu3.coresload
|
||||
* cpu3.temp
|
||||
* cpu3.fanspeed
|
||||
* cpu3.colored: 1 for colored per core load graph, 0 for mono (default)
|
||||
* cpu3.temp_json: json path to look for in the output of 'sensors -j';
|
||||
required if cpu3.temp widget is used
|
||||
* cpu3.fan_json: json path to look for in the output of 'sensors -j';
|
||||
required if cpu3.fanspeed widget is used
|
||||
|
||||
Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're
|
||||
lacking the aforementioned json path settings or they have wrong values.
|
||||
|
||||
Example json paths:
|
||||
* `cpu3.temp_json="coretemp-isa-0000.Package id 0.temp1_input"`
|
||||
* `cpu3.fan_json="thinkpad-isa-0000.fan1.fan1_input"`
|
||||
|
||||
contributed by `SuperQ <https://github.com/SuperQ>`
|
||||
based on cpu2 by `<somospocos <https://github.com/somospocos>`
|
||||
"""
|
||||
|
||||
import json
|
||||
import psutil
|
||||
|
||||
import core.module
|
||||
|
||||
import util.cli
|
||||
import util.graph
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__layout = self.parameter(
|
||||
"layout", "cpu3.maxfreq cpu3.cpuload cpu3.coresload cpu3.temp cpu3.fanspeed"
|
||||
)
|
||||
self.__widget_names = self.__layout.split()
|
||||
self.__colored = util.format.asbool(self.parameter("colored", False))
|
||||
for widget_name in self.__widget_names:
|
||||
if widget_name == "cpu3.maxfreq":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.maxfreq)
|
||||
widget.set("type", "freq")
|
||||
elif widget_name == "cpu3.cpuload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.cpuload)
|
||||
widget.set("type", "load")
|
||||
elif widget_name == "cpu3.coresload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.coresload)
|
||||
widget.set("type", "loads")
|
||||
elif widget_name == "cpu3.temp":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.temp)
|
||||
widget.set("type", "temp")
|
||||
elif widget_name == "cpu3.fanspeed":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.fanspeed)
|
||||
widget.set("type", "fan")
|
||||
if self.__colored:
|
||||
widget.set("pango", True)
|
||||
self.__temp_json = self.parameter("temp_json")
|
||||
if self.__temp_json is None:
|
||||
self.__temp = "n/a"
|
||||
self.__fan_json = self.parameter("fan_json")
|
||||
if self.__fan_json is None:
|
||||
self.__fan = "n/a"
|
||||
# maxfreq is loaded only once at startup
|
||||
if "cpu3.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
|
||||
def maxfreq(self, _):
|
||||
return "{:.2f}GHz".format(self.__maxfreq)
|
||||
|
||||
def cpuload(self, _):
|
||||
return "{:>3}%".format(self.__cpuload)
|
||||
|
||||
def add_color(self, bar):
|
||||
"""add color as pango markup to a bar"""
|
||||
if bar in ["▁", "▂"]:
|
||||
color = self.theme.color("green", "green")
|
||||
elif bar in ["▃", "▄"]:
|
||||
color = self.theme.color("yellow", "yellow")
|
||||
elif bar in ["▅", "▆"]:
|
||||
color = self.theme.color("orange", "orange")
|
||||
elif bar in ["▇", "█"]:
|
||||
color = self.theme.color("red", "red")
|
||||
colored_bar = '<span foreground="{}">{}</span>'.format(color, bar)
|
||||
return colored_bar
|
||||
|
||||
def coresload(self, _):
|
||||
mono_bars = [util.graph.hbar(x) for x in self.__coresload]
|
||||
if not self.__colored:
|
||||
return "".join(mono_bars)
|
||||
colored_bars = [self.add_color(x) for x in mono_bars]
|
||||
return "".join(colored_bars)
|
||||
|
||||
def temp(self, _):
|
||||
if self.__temp == "n/a" or self.__temp == 0:
|
||||
return "n/a"
|
||||
return "{}°C".format(self.__temp)
|
||||
|
||||
def fanspeed(self, _):
|
||||
if self.__fanspeed == "n/a":
|
||||
return "n/a"
|
||||
return "{}RPM".format(self.__fanspeed)
|
||||
|
||||
def _parse_sensors_output(self):
|
||||
output = util.cli.execute("sensors -j")
|
||||
json_data = json.loads(output)
|
||||
|
||||
temp = "n/a"
|
||||
fan = "n/a"
|
||||
temp_json = json_data
|
||||
fan_json = json_data
|
||||
for path in self.__temp_json.split('.'):
|
||||
temp_json = temp_json[path]
|
||||
for path in self.__fan_json.split('.'):
|
||||
fan_json = fan_json[path]
|
||||
if temp_json is not None:
|
||||
temp = float(temp_json)
|
||||
if fan_json is not None:
|
||||
fan = int(fan_json)
|
||||
return temp, fan
|
||||
|
||||
def update(self):
|
||||
if "cpu3.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
if "cpu3.cpuload" in self.__widget_names:
|
||||
self.__cpuload = round(psutil.cpu_percent(percpu=False))
|
||||
if "cpu3.coresload" in self.__widget_names:
|
||||
self.__coresload = psutil.cpu_percent(percpu=True)
|
||||
if "cpu3.temp" in self.__widget_names or "cpu3.fanspeed" in self.__widget_names:
|
||||
self.__temp, self.__fanspeed = self._parse_sensors_output()
|
||||
|
||||
def state(self, widget):
|
||||
"""for having per-widget icons"""
|
||||
return [widget.get("type", "")]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
87
bumblebee_status/modules/contrib/gitlab.py
Normal file
87
bumblebee_status/modules/contrib/gitlab.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the GitLab todo count:
|
||||
|
||||
* https://docs.gitlab.com/ee/user/todos.html
|
||||
* https://docs.gitlab.com/ee/api/todos.html
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the GitLab todo query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* gitlab.token: GitLab personal access token, the token needs to have the "read_api" scope.
|
||||
* gitlab.host: Host of the GitLab instance, default is "gitlab.com".
|
||||
* gitlab.actions: Comma separated actions to be parsed (e.g.: gitlab.actions=assigned,approval_required)
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
import util
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.gitlab))
|
||||
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
self.__host = self.parameter("host", "gitlab.com")
|
||||
|
||||
self.__actions = []
|
||||
actions = self.parameter("actions", "")
|
||||
if actions:
|
||||
self.__actions = util.format.aslist(actions)
|
||||
|
||||
self.__requests = requests.Session()
|
||||
self.__requests.headers.update({"PRIVATE-TOKEN": self.parameter("token", "")})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd="{cmd} https:/{host}//dashboard/todos".format(
|
||||
cmd=cmd, host=self.__host
|
||||
),
|
||||
)
|
||||
|
||||
def gitlab(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
url = "https://{host}/api/v4/todos".format(host=self.__host)
|
||||
response = self.__requests.get(url)
|
||||
todos = response.json()
|
||||
if self.__actions:
|
||||
todos = [t for t in todos if t["action_name"] in self.__actions]
|
||||
self.__label = str(len(todos))
|
||||
except Exception as e:
|
||||
self.__label = "n/a"
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
|
||||
try:
|
||||
if int(self.__label) > 0:
|
||||
state.append("warning")
|
||||
except ValueError:
|
||||
pass
|
||||
return state
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -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 = {}
|
||||
|
|
|
@ -4,13 +4,20 @@
|
|||
|
||||
Parameters:
|
||||
* pihole.address : pi-hole address (e.q: http://192.168.1.3)
|
||||
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
|
||||
|
||||
|
||||
* pihole.apitoken : pi-hole API token (can be obtained in the pi-hole webinterface (Settings -> API)
|
||||
|
||||
OR (deprecated!)
|
||||
|
||||
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
|
||||
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
import logging
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
@ -22,7 +29,18 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.pihole_status))
|
||||
|
||||
self._pihole_address = self.parameter("address", "")
|
||||
self._pihole_pw_hash = self.parameter("pwhash", "")
|
||||
pihole_pw_hash = self.parameter("pwhash", "")
|
||||
pihole_api_token = self.parameter("apitoken", "")
|
||||
|
||||
self._pihole_secret = (
|
||||
pihole_api_token if pihole_api_token != "" else pihole_pw_hash
|
||||
)
|
||||
|
||||
if pihole_pw_hash != "":
|
||||
logging.warn(
|
||||
"pihole: The 'pwhash' parameter is deprecated - consider using the 'apitoken' parameter instead!"
|
||||
)
|
||||
|
||||
self._pihole_status = None
|
||||
self._ads_blocked_today = "-"
|
||||
self.update_pihole_status()
|
||||
|
@ -42,7 +60,11 @@ class Module(core.module.Module):
|
|||
|
||||
def update_pihole_status(self):
|
||||
try:
|
||||
data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
|
||||
data = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?summary&auth="
|
||||
+ self._pihole_secret
|
||||
).json()
|
||||
self._pihole_status = True if data["status"] == "enabled" else False
|
||||
self._ads_blocked_today = data["ads_blocked_today"]
|
||||
except Exception as e:
|
||||
|
@ -56,13 +78,13 @@ class Module(core.module.Module):
|
|||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?disable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
+ self._pihole_secret
|
||||
)
|
||||
else:
|
||||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?enable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
+ self._pihole_secret
|
||||
)
|
||||
if req is not None:
|
||||
if req.status_code == 200:
|
||||
|
|
90
bumblebee_status/modules/contrib/pipewire.py
Normal file
90
bumblebee_status/modules/contrib/pipewire.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
"""get volume level or control it
|
||||
|
||||
Requires the following executable:
|
||||
* wpctl
|
||||
|
||||
Parameters:
|
||||
* wpctl.percent_change: How much to change volume by when scrolling on the module (default is 4%)
|
||||
|
||||
heavily based on amixer module
|
||||
"""
|
||||
import re
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.volume))
|
||||
|
||||
self.__level = "N/A"
|
||||
self.__muted = True
|
||||
self.__change = (
|
||||
util.format.asint(self.parameter("percent_change", "4%").strip("%"), 0, 200)
|
||||
/ 100.0
|
||||
) # divide by 100 because wpctl represents 100% volume as 1.00, 50% as 0.50, etc
|
||||
|
||||
self.__id = self.parameter("sink_id") or "@DEFAULT_AUDIO_SINK@"
|
||||
|
||||
events = [
|
||||
{
|
||||
"type": "mute",
|
||||
"action": self.toggle,
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.increase_volume,
|
||||
"button": core.input.WHEEL_UP,
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"action": self.decrease_volume,
|
||||
"button": core.input.WHEEL_DOWN,
|
||||
},
|
||||
]
|
||||
|
||||
for event in events:
|
||||
core.input.register(self, button=event["button"], cmd=event["action"])
|
||||
|
||||
def toggle(self, event):
|
||||
util.cli.execute("wpctl set-mute {} toggle".format(self.__id))
|
||||
|
||||
def increase_volume(self, event):
|
||||
util.cli.execute(
|
||||
"wpctl set-volume --limit 1.0 {} {}+".format(self.__id, self.__change)
|
||||
)
|
||||
|
||||
def decrease_volume(self, event):
|
||||
util.cli.execute(
|
||||
"wpctl set-volume --limit 1.0 {} {}-".format(self.__id, self.__change)
|
||||
)
|
||||
|
||||
def volume(self, widget):
|
||||
if self.__level == "N/A":
|
||||
return self.__level
|
||||
return "{}%".format(int(float(self.__level) * 100))
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
# `wpctl get-volume` will return a string like "Volume: n.nn" or "Volume: n.nn [MUTED]"
|
||||
volume = util.cli.execute("wpctl get-volume {}".format(self.__id))
|
||||
v = re.search("\d\.\d+", volume)
|
||||
m = re.search("MUTED", volume)
|
||||
self.__level = v.group()
|
||||
self.__muted = True if m else False
|
||||
except Exception:
|
||||
self.__level = "N/A"
|
||||
|
||||
def state(self, widget):
|
||||
if self.__muted:
|
||||
return ["warning", "muted"]
|
||||
return ["unmuted"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
7
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal file
7
bumblebee_status/modules/contrib/playerctl.py
Executable file → Normal 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":
|
||||
|
|
99
bumblebee_status/modules/contrib/power-profile.py
Normal file
99
bumblebee_status/modules/contrib/power-profile.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
"""
|
||||
Displays the current Power-Profile active
|
||||
|
||||
|
||||
Left-Click or Right-Click as well as Scrolling up / down changes the active Power-Profile
|
||||
|
||||
Prerequisites:
|
||||
* dbus-python
|
||||
* power-profiles-daemon
|
||||
"""
|
||||
|
||||
import dbus
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class PowerProfileManager:
|
||||
def __init__(self):
|
||||
self.POWER_PROFILES_NAME = "net.hadess.PowerProfiles"
|
||||
self.POWER_PROFILES_PATH = "/net/hadess/PowerProfiles"
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE = "ActiveProfile"
|
||||
self.PP_PROPERTIES_ALL_POWER_PROFILES = "Profiles"
|
||||
|
||||
self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties"
|
||||
bus = dbus.SystemBus()
|
||||
pp_proxy = bus.get_object(self.POWER_PROFILES_NAME, self.POWER_PROFILES_PATH)
|
||||
self.pp_interface = dbus.Interface(pp_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
def get_current_power_profile(self):
|
||||
return self.pp_interface.Get(
|
||||
self.POWER_PROFILES_NAME, self.PP_PROPERTIES_CURRENT_POWER_PROFILE
|
||||
)
|
||||
|
||||
def __get_all_power_profile_names(self):
|
||||
power_profiles = self.pp_interface.Get(
|
||||
self.POWER_PROFILES_NAME, self.PP_PROPERTIES_ALL_POWER_PROFILES
|
||||
)
|
||||
power_profiles_names = []
|
||||
for pp in power_profiles:
|
||||
power_profiles_names.append(pp["Profile"])
|
||||
|
||||
return power_profiles_names
|
||||
|
||||
def next_power_profile(self, event):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp_index = self.__get_current_pp_index()
|
||||
next_index = 0
|
||||
if current_pp_index != (len(all_pp_names) - 1):
|
||||
next_index = current_pp_index + 1
|
||||
|
||||
self.pp_interface.Set(
|
||||
self.POWER_PROFILES_NAME,
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE,
|
||||
all_pp_names[next_index],
|
||||
)
|
||||
|
||||
def prev_power_profile(self, event):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp_index = self.__get_current_pp_index()
|
||||
last_index = len(all_pp_names) - 1
|
||||
if current_pp_index is not 0:
|
||||
last_index = current_pp_index - 1
|
||||
|
||||
self.pp_interface.Set(
|
||||
self.POWER_PROFILES_NAME,
|
||||
self.PP_PROPERTIES_CURRENT_POWER_PROFILE,
|
||||
all_pp_names[last_index],
|
||||
)
|
||||
|
||||
def __get_current_pp_index(self):
|
||||
all_pp_names = self.__get_all_power_profile_names()
|
||||
current_pp = self.get_current_power_profile()
|
||||
return all_pp_names.index(current_pp)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
self.pp_manager = PowerProfileManager()
|
||||
core.input.register(
|
||||
self, button=core.input.WHEEL_UP, cmd=self.pp_manager.next_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.WHEEL_DOWN, cmd=self.pp_manager.prev_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd=self.pp_manager.next_power_profile
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.RIGHT_MOUSE, cmd=self.pp_manager.prev_power_profile
|
||||
)
|
||||
|
||||
def full_text(self, widgets):
|
||||
return self.pp_manager.get_current_power_profile()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -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"])
|
||||
|
|
|
@ -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, _):
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
Parameters:
|
||||
* stock.symbols : Comma-separated list of symbols to fetch
|
||||
* stock.change : Should we fetch change in stock value (defaults to True)
|
||||
* stock.apikey : API key created on https://alphavantage.co
|
||||
* stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}"
|
||||
* stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent"
|
||||
|
||||
|
||||
contributed by `msoulier <https://github.com/msoulier>`_ - many thanks!
|
||||
|
@ -22,6 +24,12 @@ import core.decorators
|
|||
|
||||
import util.format
|
||||
|
||||
def flatten(d, result):
|
||||
for k, v in d.items():
|
||||
if type(v) is dict:
|
||||
flatten(v, result)
|
||||
else:
|
||||
result[k] = v
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(hours=1)
|
||||
|
@ -29,41 +37,41 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.value))
|
||||
|
||||
self.__symbols = self.parameter("symbols", "")
|
||||
self.__apikey = self.parameter("apikey", None)
|
||||
self.__fields = self.parameter("fields", "01. symbol,05. price,10. change percent").split(",")
|
||||
self.__url = self.parameter("url", "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}")
|
||||
self.__change = util.format.asbool(self.parameter("change", True))
|
||||
self.__value = None
|
||||
self.__values = []
|
||||
|
||||
|
||||
def value(self, widget):
|
||||
results = []
|
||||
if not self.__value:
|
||||
return "n/a"
|
||||
data = json.loads(self.__value)
|
||||
result = ""
|
||||
|
||||
for symbol in data["quoteResponse"]["result"]:
|
||||
valkey = "regularMarketChange" if self.__change else "regularMarketPrice"
|
||||
sym = symbol.get("symbol", "n/a")
|
||||
currency = symbol.get("currency", "USD")
|
||||
val = "n/a" if not valkey in symbol else "{:.2f}".format(symbol[valkey])
|
||||
results.append("{} {} {}".format(sym, val, currency))
|
||||
return " ".join(results)
|
||||
for value in self.__values:
|
||||
res = {}
|
||||
flatten(value, res)
|
||||
for field in self.__fields:
|
||||
result += res.get(field, "n/a") + " "
|
||||
result = result[:-1]
|
||||
return result
|
||||
|
||||
def fetch(self):
|
||||
results = []
|
||||
if self.__symbols:
|
||||
url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols="
|
||||
url += (
|
||||
self.__symbols
|
||||
+ "&fields=regularMarketPrice,currency,regularMarketChange"
|
||||
)
|
||||
try:
|
||||
return urllib.request.urlopen(url).read().strip()
|
||||
except urllib.request.URLError:
|
||||
logging.error("unable to open stock exchange url")
|
||||
return None
|
||||
for symbol in self.__symbols.split(","):
|
||||
url = self.__url.format(symbol=symbol, apikey=self.__apikey)
|
||||
try:
|
||||
results.append(json.loads(urllib.request.urlopen(url).read().strip()))
|
||||
except urllib.request.URLError:
|
||||
logging.error("unable to open stock exchange url")
|
||||
return []
|
||||
else:
|
||||
logging.error("unable to retrieve stock exchange rate")
|
||||
return None
|
||||
return []
|
||||
return results
|
||||
|
||||
def update(self):
|
||||
self.__value = self.fetch()
|
||||
self.__values = self.fetch()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
|
@ -8,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")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
76
bumblebee_status/modules/contrib/todoist.py
Normal file
76
bumblebee_status/modules/contrib/todoist.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the nº of Todoist tasks that are due:
|
||||
|
||||
* https://developer.todoist.com/rest/v2/#get-active-tasks
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the Todoist get active tasks query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* todoist.token: Todoist api token, you can get it in https://todoist.com/app/settings/integrations/developer.
|
||||
* todoist.filter: a filter statement defined by Todoist (https://todoist.com/help/articles/introduction-to-filters), eg: "!assigned to: others & (Overdue | due: today)"
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
HOST_API = "https://api.todoist.com"
|
||||
HOST_WEBSITE = "https://todoist.com/app/today"
|
||||
|
||||
TASKS_URL = f"{HOST_API}/rest/v2/tasks"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.todoist))
|
||||
|
||||
self.__user_id = None
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
|
||||
token = self.parameter("token", "")
|
||||
self.__filter = self.parameter("filter", "")
|
||||
|
||||
self.__requests = requests.Session()
|
||||
self.__requests.headers.update({"Authorization": f"Bearer {token}"})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd=f"{cmd} {HOST_WEBSITE}",
|
||||
)
|
||||
|
||||
def todoist(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__label = self.__get_pending_tasks()
|
||||
except Exception:
|
||||
self.__label = "n/a"
|
||||
|
||||
def __get_pending_tasks(self) -> str:
|
||||
params = {"filter": self.__filter} if self.__filter else None
|
||||
|
||||
response = self.__requests.get(TASKS_URL, params=params)
|
||||
data = response.json()
|
||||
|
||||
return str(len(data))
|
78
bumblebee_status/modules/contrib/usage.py
Normal file
78
bumblebee_status/modules/contrib/usage.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Module for ActivityWatch (https://activitywatch.net/)
|
||||
Displays the amount of time the system was used actively.
|
||||
|
||||
Requirements:
|
||||
* sqlite3 module for python
|
||||
* ActivityWatch
|
||||
|
||||
Errors:
|
||||
* when you get 'error: unable to open database file', modify the parameter 'database' to your ActivityWatch database file
|
||||
-> often found by running 'locate aw-server/peewee-sqlite.v2.db'
|
||||
|
||||
Parameters:
|
||||
* usage.database: path to your database file
|
||||
* usage.format: Specify what gets printed to the bar
|
||||
-> use 'HH', 'MM' or 'SS', they will get replaced by the number of hours, minutes and seconds, respectively
|
||||
|
||||
contributed by lasnikr (https://github.com/lasnikr)
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__usage = ""
|
||||
|
||||
def output(self, _):
|
||||
return "{}".format(self.__usage)
|
||||
|
||||
def update(self):
|
||||
database_loc = self.parameter(
|
||||
"database", "~/.local/share/activitywatch/aw-server/peewee-sqlite.v2.db"
|
||||
)
|
||||
home = os.path.expanduser("~")
|
||||
|
||||
database = sqlite3.connect(database_loc.replace("~", home))
|
||||
cursor = database.cursor()
|
||||
|
||||
cursor.execute("SELECT key, id FROM bucketmodel")
|
||||
|
||||
bucket_id = 1
|
||||
|
||||
for tuple in cursor.fetchall():
|
||||
if "aw-watcher-afk" in tuple[1]:
|
||||
bucket_id = tuple[0]
|
||||
|
||||
cursor.execute(
|
||||
f"SELECT duration, datastr FROM eventmodel WHERE bucket_id = {bucket_id} "
|
||||
+ 'AND strftime("%Y,%m,%d", timestamp) = strftime("%Y,%m,%d", "now")'
|
||||
)
|
||||
|
||||
duration = 0
|
||||
|
||||
for tuple in cursor.fetchall():
|
||||
if '{"status": "not-afk"}' in tuple[1]:
|
||||
duration += tuple[0]
|
||||
|
||||
hours = "%.0f" % (duration // 3600)
|
||||
minutes = "%.0f" % ((duration % 3600) // 60)
|
||||
seconds = "%.0f" % (duration % 60)
|
||||
|
||||
formatting = self.parameter("format", "HHh, MMmin")
|
||||
self.__usage = (
|
||||
formatting.replace("HH", hours)
|
||||
.replace("MM", minutes)
|
||||
.replace("SS", seconds)
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -1,4 +1,5 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Displays the VPN profile that is currently in use.
|
||||
|
||||
|
@ -68,7 +69,7 @@ class Module(core.module.Module):
|
|||
|
||||
def vpn_status(self, widget):
|
||||
if self.__connected_vpn_profile is None:
|
||||
return "off"
|
||||
return ""
|
||||
return self.__connected_vpn_profile
|
||||
|
||||
def __on_vpndisconnect(self):
|
||||
|
@ -93,7 +94,7 @@ class Module(core.module.Module):
|
|||
self.__connected_vpn_profile = None
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu()
|
||||
menu = util.popup.menu(self.__config)
|
||||
|
||||
if self.__connected_vpn_profile is not None:
|
||||
menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect)
|
||||
|
|
94
bumblebee_status/modules/contrib/wakatime.py
Normal file
94
bumblebee_status/modules/contrib/wakatime.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""
|
||||
Displays the WakaTime daily/weekly/monthly times:
|
||||
|
||||
* https://wakatime.com/developers#stats
|
||||
|
||||
Uses `xdg-open` or `x-www-browser` to open web-pages.
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Errors:
|
||||
if the Wakatime status query failed, the shown value is `n/a`
|
||||
|
||||
Parameters:
|
||||
* wakatime.token: Wakatime secret api key, you can get it in https://wakatime.com/settings/account.
|
||||
* wakatime.range: Range of the output, default is "Today". Can be one of “Today”, “Yesterday”, “Last 7 Days”, “Last 7 Days from Yesterday”, “Last 14 Days”, “Last 30 Days”, “This Week”, “Last Week”, “This Month”, or “Last Month”.
|
||||
* wakatime.format: Format of the output, default is "digital"
|
||||
Valid inputs are:
|
||||
* "decimal" -> 1.37
|
||||
* "digital" -> 1:22
|
||||
* "seconds" -> 4931.29
|
||||
* "text" -> 1 hr 22 mins
|
||||
* "%H:%M:%S" -> 01:22:31 (or any other valid format)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
import core.decorators
|
||||
import core.input
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
HOST_API = "https://wakatime.com"
|
||||
SUMMARIES_URL = f"{HOST_API}/api/v1/users/current/summaries"
|
||||
UTF8 = "utf-8"
|
||||
FORMAT_PARAMETERS = ["decimal", "digital", "seconds", "text"]
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.wakatime))
|
||||
|
||||
self.background = True
|
||||
self.__label = ""
|
||||
|
||||
self.__output_format = self.parameter("format", "digital")
|
||||
self.__range = self.parameter("range", "Today")
|
||||
|
||||
self.__requests = requests.Session()
|
||||
|
||||
token = self.__encode_to_base_64(self.parameter("token", ""))
|
||||
self.__requests.headers.update({"Authorization": f"Basic {token}"})
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd=f"{cmd} {HOST_API}/dashboard",
|
||||
)
|
||||
|
||||
def wakatime(self, _):
|
||||
return self.__label
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__label = self.__get_waka_time(self.__range)
|
||||
except Exception:
|
||||
self.__label = "n/a"
|
||||
|
||||
def __get_waka_time(self, since_date: str) -> str:
|
||||
response = self.__requests.get(f"{SUMMARIES_URL}?range={since_date}")
|
||||
|
||||
data = response.json()
|
||||
grand_total = data["cumulative_total"]
|
||||
|
||||
if self.__output_format in FORMAT_PARAMETERS:
|
||||
return str(grand_total[self.__output_format])
|
||||
else:
|
||||
total_seconds = int(grand_total["seconds"])
|
||||
return time.strftime(self.__output_format, time.gmtime(total_seconds))
|
||||
|
||||
@staticmethod
|
||||
def __encode_to_base_64(s: str) -> str:
|
||||
return base64.b64encode(s.encode(UTF8)).decode(UTF8)
|
|
@ -5,6 +5,10 @@
|
|||
Requires the following executable:
|
||||
* watson
|
||||
|
||||
Parameters:
|
||||
* watson.format: Output format, defaults to "{project} [{tags}]"
|
||||
Supported fields are: {project}, {tags}, {relative_start}, {absolute_start}
|
||||
|
||||
contributed by `bendardenne <https://github.com/bendardenne>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
@ -26,11 +30,11 @@ class Module(core.module.Module):
|
|||
super().__init__(config, theme, core.widget.Widget(self.text))
|
||||
|
||||
self.__tracking = False
|
||||
self.__project = ""
|
||||
self.__info = {}
|
||||
self.__format = self.parameter("format", "{project} [{tags}]")
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle)
|
||||
|
||||
def toggle(self, widget):
|
||||
self.__project = "hit"
|
||||
if self.__tracking:
|
||||
util.cli.execute("watson stop")
|
||||
else:
|
||||
|
@ -39,20 +43,27 @@ class Module(core.module.Module):
|
|||
|
||||
def text(self, widget):
|
||||
if self.__tracking:
|
||||
return self.__project
|
||||
return self.__format.format(**self.__info)
|
||||
else:
|
||||
return "Paused"
|
||||
|
||||
def update(self):
|
||||
output = util.cli.execute("watson status")
|
||||
if re.match(r"No project started", output):
|
||||
|
||||
m = re.search(r"Project ([^\[\]]+)(?: \[(.+)\])? started (.+) \((.+)\)", output)
|
||||
|
||||
if m:
|
||||
self.__tracking = True
|
||||
self.__info = {
|
||||
"project": m.group(1),
|
||||
"tags": m.group(2) or "",
|
||||
"relative_start": m.group(3),
|
||||
"absolute_start": m.group(4),
|
||||
}
|
||||
else:
|
||||
self.__tracking = False
|
||||
return
|
||||
|
||||
self.__tracking = True
|
||||
m = re.search(r"Project (.+) started", output)
|
||||
self.__project = m.group(1)
|
||||
|
||||
def state(self, widget):
|
||||
return "on" if self.__tracking else "off"
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Parameters:
|
|||
* weather.unit: metric (default), kelvin, imperial
|
||||
* weather.showcity: If set to true, show location information, otherwise hide it (defaults to true)
|
||||
* weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false)
|
||||
* weather.apikey: API key from http://api.openweathermap.org
|
||||
* weather.apikey: API key from https://api.openweathermap.org
|
||||
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
|
@ -116,7 +116,7 @@ class Module(core.module.Module):
|
|||
|
||||
def update(self):
|
||||
try:
|
||||
weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format(
|
||||
weather_url = "https://api.openweathermap.org/data/2.5/weather?appid={}".format(
|
||||
self.__apikey
|
||||
)
|
||||
weather_url = "{}&units={}".format(weather_url, self.__unit)
|
||||
|
|
126
bumblebee_status/modules/contrib/wlrotation.py
Normal file
126
bumblebee_status/modules/contrib/wlrotation.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Shows a widget for each connected screen and allows the user to loop through different orientations.
|
||||
|
||||
Parameters:
|
||||
* wlrotation.display : Name of the output display that will be rotated
|
||||
+ wlrotation.auto : Boolean value if the display should be rotatet automatic by default
|
||||
|
||||
Requires the following executable:
|
||||
* swaymsg
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
import util.cli
|
||||
|
||||
import iio
|
||||
import json
|
||||
from math import degrees, atan2, sqrt
|
||||
from os import environ, path
|
||||
|
||||
possible_orientations = ["normal", "90", "180", "270"]
|
||||
|
||||
class iioValue:
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
self.scale = self.read('scale')
|
||||
self.offset = self.read('offset')
|
||||
|
||||
def read(self, attr):
|
||||
return float(self.channel.attrs[attr].value)
|
||||
|
||||
def value(self):
|
||||
return (self.read('raw') + self.offset) * self.scale
|
||||
|
||||
class iioAccelDevice:
|
||||
def __init__(self):
|
||||
self.ctx = iio.Context() # store ctx pointer
|
||||
d = self.ctx.find_device('accel_3d')
|
||||
self.x = iioValue(d.find_channel('accel_x'))
|
||||
self.y = iioValue(d.find_channel('accel_y'))
|
||||
self.z = iioValue(d.find_channel('accel_z'))
|
||||
|
||||
def orientation(self):
|
||||
"""
|
||||
returns tuple of `[success, value]` where `success` indicates, if an accurate value could be meassured and `value` the sway output api compatible value or `normal` if success is `False`
|
||||
"""
|
||||
x_deg, y_deg, z_deg = self._deg()
|
||||
if abs(z_deg) < 70: # checks if device is angled too shallow
|
||||
if x_deg >= 70: return True, "270"
|
||||
if x_deg <= -70: return True, "90"
|
||||
if abs(x_deg) <= 20:
|
||||
if y_deg < 0: return True, "normal"
|
||||
if y_deg > 0: return True, "180"
|
||||
return False, "normal"
|
||||
|
||||
def _deg(self):
|
||||
gravity = 9.81
|
||||
x, y, z = self.x.value() / gravity, self.y.value() / gravity, self.z.value() / gravity
|
||||
return degrees(atan2(x, sqrt(pow(y, 2) + pow(z, 2)))), degrees(atan2(y, sqrt(pow(z, 2) + pow(x, 2)))), degrees(atan2(z, sqrt(pow(x, 2) + pow(y, 2))))
|
||||
|
||||
class Display():
|
||||
def __init__(self, name, widget, display_data, auto=False):
|
||||
self.name = name
|
||||
self.widget = widget
|
||||
self.accelDevice = iioAccelDevice()
|
||||
self._lock_auto_rotation(not auto)
|
||||
|
||||
self.widget.set("orientation", display_data['transform'])
|
||||
|
||||
core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=self.rotate_90deg)
|
||||
core.input.register(widget, button=core.input.RIGHT_MOUSE, cmd=self.toggle)
|
||||
|
||||
def rotate_90deg(self, event):
|
||||
# compute new orientation based on current orientation
|
||||
current = self.widget.get("orientation")
|
||||
self._set_rotation(possible_orientations[(possible_orientations.index(current) + 1) % len(possible_orientations)])
|
||||
# disable auto rotation
|
||||
self._lock_auto_rotation(True)
|
||||
|
||||
def toggle(self, event):
|
||||
self._lock_auto_rotation(not self.locked)
|
||||
|
||||
def auto_rotate(self):
|
||||
# automagically rotate the display based on sensor values
|
||||
# this is only called if rotation lock is disabled
|
||||
success, value = self.accelDevice.orientation()
|
||||
if success:
|
||||
self._set_rotation(value)
|
||||
|
||||
def _set_rotation(self, new_orientation):
|
||||
self.widget.set("orientation", new_orientation)
|
||||
util.cli.execute("swaymsg 'output {} transform {}'".format(self.name, new_orientation))
|
||||
|
||||
def _lock_auto_rotation(self, locked):
|
||||
self.locked = locked
|
||||
self.widget.set("locked", self.locked)
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.display = None
|
||||
display_filter = self.parameter("display", None)
|
||||
for display in json.loads(util.cli.execute("swaymsg -t get_outputs -r")):
|
||||
name = display['name']
|
||||
if display_filter == None or display_filter == name:
|
||||
self.display = Display(name, self.add_widget(name=name), display, auto=util.format.asbool(self.parameter("auto", False)))
|
||||
break # I assume that it makes only sense to rotate a single screen
|
||||
|
||||
def update(self):
|
||||
if self.display == None:
|
||||
return
|
||||
if self.display.locked:
|
||||
return
|
||||
|
||||
self.display.auto_rotate()
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
state.append("locked" if widget.get("locked", True) else "auto")
|
||||
return state
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
53
bumblebee_status/modules/core/scroll.py
Normal file
53
bumblebee_status/modules/core/scroll.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays two widgets that can be used to scroll the whole status bar
|
||||
|
||||
Parameters:
|
||||
* scroll.width: Width (in number of widgets) to display
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.event
|
||||
|
||||
import util.format
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
self.__offset = 0
|
||||
self.__widgetcount = 0
|
||||
w = self.add_widget(full_text = "<")
|
||||
core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_left)
|
||||
w = self.add_widget(full_text = ">")
|
||||
core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_right)
|
||||
self.__width = util.format.asint(self.parameter("width"))
|
||||
config.set("output.width", self.__width)
|
||||
core.event.register("output.done", self.update_done)
|
||||
|
||||
|
||||
def scroll_left(self, _):
|
||||
if self.__offset > 0:
|
||||
core.event.trigger("output.scroll-left")
|
||||
|
||||
def scroll_right(self, _):
|
||||
if self.__offset + self.__width < self.__widgetcount:
|
||||
core.event.trigger("output.scroll-right")
|
||||
|
||||
def update_done(self, offset, widgetcount):
|
||||
self.__offset = offset
|
||||
self.__widgetcount = widgetcount
|
||||
|
||||
def scroll(self):
|
||||
return False
|
||||
|
||||
def state(self, widget):
|
||||
if widget.id == self.widgets()[0].id:
|
||||
if self.__offset == 0:
|
||||
return ["warning"]
|
||||
elif self.__offset + self.__width >= self.__widgetcount:
|
||||
return ["warning"]
|
||||
return []
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
|
@ -52,7 +52,7 @@ def build_menu(parent, current_directory, callback):
|
|||
)
|
||||
|
||||
else:
|
||||
submenu = util.popup.menu(parent, leave=False)
|
||||
submenu = util.popup.menu(self.__config, parent, leave=False)
|
||||
build_menu(
|
||||
submenu, os.path.join(current_directory, entry.name), callback
|
||||
)
|
||||
|
@ -73,7 +73,7 @@ class Module(core.module.Module):
|
|||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu(leave=False)
|
||||
menu = util.popup.menu(self.__config, leave=False)
|
||||
|
||||
build_menu(menu, self.__path, self.__callback)
|
||||
menu.show(widget, offset_x=self.__offx, offset_y=self.__offy)
|
||||
|
|
|
@ -52,7 +52,19 @@ def execute(
|
|||
raise RuntimeError("{} not found".format(cmd))
|
||||
|
||||
if wait:
|
||||
out, _ = proc.communicate()
|
||||
timeout = 60
|
||||
try:
|
||||
out, _ = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
logging.warning(
|
||||
f"""
|
||||
Communication with process pid={proc.pid} hangs for more
|
||||
than {timeout} seconds.
|
||||
If this is not expected, the process is stale, or
|
||||
you might have run in stdout / stderr deadlock.
|
||||
"""
|
||||
)
|
||||
out, _ = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
err = "{} exited with code {}".format(cmd, proc.returncode)
|
||||
logging.warning(err)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
197
docs/modules.rst
197
docs/modules.rst
|
@ -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!
|
||||
|
|
|
@ -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>`__)
|
||||
|
|
1
requirements/modules/cpu3.txt
Normal file
1
requirements/modules/cpu3.txt
Normal file
|
@ -0,0 +1 @@
|
|||
psutil
|
1
requirements/modules/gitlab.txt
Normal file
1
requirements/modules/gitlab.txt
Normal file
|
@ -0,0 +1 @@
|
|||
requests
|
2
requirements/modules/power-profile.txt
Normal file
2
requirements/modules/power-profile.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
dbus-python
|
||||
power-profiles-daemon
|
BIN
screenshots/gitlab.png
Normal file
BIN
screenshots/gitlab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
screenshots/themes/moonlight-powerline.png
Normal file
BIN
screenshots/themes/moonlight-powerline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
screenshots/todoist.png
Normal file
BIN
screenshots/todoist.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 744 B |
BIN
screenshots/wakatime.png
Normal file
BIN
screenshots/wakatime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
2
setup.py
2
setup.py
|
@ -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.*"])
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%-')
|
||||
|
||||
|
|
6
tests/modules/contrib/test_cpu3.py
Normal file
6
tests/modules/contrib/test_cpu3.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import pytest
|
||||
|
||||
pytest.importorskip("psutil")
|
||||
|
||||
def test_load_module():
|
||||
__import__("modules.contrib.cpu3")
|
70
tests/modules/contrib/test_gitlab.py
Normal file
70
tests/modules/contrib/test_gitlab.py
Normal 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) == []
|
32
tests/modules/contrib/test_power-profile.py
Normal file
32
tests/modules/contrib/test_power-profile.py
Normal 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"
|
58
tests/modules/contrib/test_todoist.py
Normal file
58
tests/modules/contrib/test_todoist.py
Normal 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)'})
|
56
tests/modules/contrib/test_wakatime.py
Normal file
56
tests/modules/contrib/test_wakatime.py
Normal 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')
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": ""}
|
||||
}
|
||||
}
|
||||
|
|
24
themes/moonlight-powerline.json
Normal file
24
themes/moonlight-powerline.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue