diff --git a/.github/workflows/autotest.yml b/.github/workflows/autotest.yml index ec02237..652bab0 100644 --- a/.github/workflows/autotest.yml +++ b/.github/workflows/autotest.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] 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 | grep -v power | cut -d ' ' -f 1 | sort -u) + pip install $(cat requirements/modules/*.txt | 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.2.0 + - name: Report coverage + uses: paambaati/codeclimate-action@v3.0.0 with: coverageCommand: coverage3 xml debug: true diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 15f1c60..6823857 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,4 @@ version: 2 python: install: - requirements: docs/requirements.txt -build: - os: ubuntu-22.04 - tools: - python: "3" + diff --git a/README.md b/README.md index 2f41627..27031d1 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,10 @@ logo courtesy of [kellya](https://github.com/kellya) - thank you! [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) -![Commits since release](https://img.shields.io/github/commits-since/tobi-wan-kenobi/bumblebee-status/latest) ![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) ![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) -![PyPI version](https://img.shields.io/pypi/v/bumblebee-status) -![Contributors](https://img.shields.io/github/contributors-anon/tobi-wan-kenobi/bumblebee-status) +[![PyPI version](https://badge.fury.io/py/bumblebee-status.svg)](https://badge.fury.io/py/bumblebee-status) [![Tests](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml) - [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) diff --git a/bumblebee-status b/bumblebee-status index a16c747..4a9f7fb 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -68,13 +68,10 @@ def handle_commands(config, update_lock): def handle_events(config, update_lock): while True: - try: - line = sys.stdin.readline().strip(",").strip() - if line == "[": continue - logging.info("input event: {}".format(line)) - process_event(line, config, update_lock) - except Exception as e: - logging.error(e) + line = sys.stdin.readline().strip(",").strip() + if line == "[": continue + logging.info("input event: {}".format(line)) + process_event(line, config, update_lock) def main(): @@ -103,7 +100,6 @@ 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, )) @@ -135,6 +131,7 @@ 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: @@ -152,7 +149,6 @@ 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("[") diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 0b564b3..f191673 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -240,16 +240,11 @@ class Config(util.store.Store): :param filename: path to the file to load """ - def load_config(self, filename, content=None): - if os.path.exists(filename) or content != None: + def load_config(self, filename): + if os.path.exists(filename): log.info("loading {}".format(filename)) tmp = RawConfigParser() - tmp.optionxform = str - - if content: - tmp.read_string(content) - else: - tmp.read(u"{}".format(filename)) + tmp.read(u"{}".format(filename)) if tmp.has_section("module-parameters"): for key, value in tmp.items("module-parameters"): @@ -281,15 +276,6 @@ 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 diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 37a3143..84c3ab5 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -1,6 +1,5 @@ import os import importlib -import importlib.util import logging import threading @@ -113,15 +112,6 @@ 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. diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 9ff3010..1bd5038 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -146,14 +146,11 @@ 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 @@ -226,29 +223,13 @@ 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"] @@ -263,7 +244,6 @@ 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): @@ -294,7 +274,6 @@ class i3(object): def statusline(self): blocks = [] - self.__widgetcount = 0 for module in self.__modules: blocks.extend(self.blocks(module)) return {"blocks": blocks, "suffix": ","} diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index 1426450..d91b060 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -25,7 +25,6 @@ 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", ]) diff --git a/bumblebee_status/modules/contrib/amixer.py b/bumblebee_status/modules/contrib/amixer.py index 6f44e8c..b0fda92 100644 --- a/bumblebee_status/modules/contrib/amixer.py +++ b/bumblebee_status/modules/contrib/amixer.py @@ -4,15 +4,12 @@ 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 `_ - many thanks! input handling contributed by `ardadem `_ - many thanks! - -multiple audio cards contributed by `hugoeustaquio `_ - many thanks! """ import re @@ -29,7 +26,6 @@ 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 @@ -66,7 +62,7 @@ class Module(core.module.Module): self.set_parameter("{}%-".format(self.__change)) def set_parameter(self, parameter): - util.cli.execute("amixer -c {} -q set {} {}".format(self.__card, self.__device, parameter)) + util.cli.execute("amixer -q set {} {}".format(self.__device, parameter)) def volume(self, widget): if self.__level == "n/a": @@ -83,7 +79,7 @@ class Module(core.module.Module): def update(self): try: self.__level = util.cli.execute( - "amixer -c {} get {}".format(self.__card, self.__device) + "amixer get {}".format(self.__device) ) except Exception as e: self.__level = "n/a" diff --git a/bumblebee_status/modules/contrib/aur-update.py b/bumblebee_status/modules/contrib/aur-update.py index 9afdc89..f1b85ea 100644 --- a/bumblebee_status/modules/contrib/aur-update.py +++ b/bumblebee_status/modules/contrib/aur-update.py @@ -31,7 +31,7 @@ class Module(core.module.Module): return self.__format.format(self.__packages) def hidden(self): - return self.__packages == 0 + return self.__packages == 0 and not self.__error def update(self): self.__error = False diff --git a/bumblebee_status/modules/contrib/battery.py b/bumblebee_status/modules/contrib/battery.py index 21951c1..7c65642 100644 --- a/bumblebee_status/modules/contrib/battery.py +++ b/bumblebee_status/modules/contrib/battery.py @@ -130,14 +130,8 @@ class Module(core.module.Module): log.debug("adding new widget for {}".format(battery)) widget = self.add_widget(full_text=self.capacity, name=battery) - 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(): + for w in self.widgets(): + if util.format.asbool(self.parameter("decorate", True)) == False: widget.set("theme.exclude", "suffix") def hidden(self): @@ -153,16 +147,15 @@ 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) ) - elif capacity < 100: - output = "{}%".format(capacity) else: - output = "" + output = "{}%".format(capacity) if ( util.format.asbool(self.parameter("showremaining", True)) @@ -174,16 +167,6 @@ 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) @@ -193,9 +176,6 @@ 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"] @@ -207,10 +187,16 @@ class Module(core.module.Module): charge = self.__manager.charge_any(self._batteries) else: charge = self.__manager.charge(widget.name) - if charge in ["Discharging", "Unknown"]: + if charge == "Discharging": state.append( "discharging-{}".format( - min([5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], key=lambda i: abs(i - capacity)) + 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)) ) ) else: diff --git a/bumblebee_status/modules/contrib/bluetooth.py b/bumblebee_status/modules/contrib/bluetooth.py index 56f5b8b..481ae88 100644 --- a/bumblebee_status/modules/contrib/bluetooth.py +++ b/bumblebee_status/modules/contrib/bluetooth.py @@ -75,15 +75,19 @@ class Module(core.module.Module): def popup(self, widget): """Show a popup menu.""" - menu = util.popup.menu(self.__config) + menu = util.popup.PopupMenu() if self._status == "On": - menu.add_menuitem("Disable Bluetooth", callback=self._toggle) + menu.add_menuitem("Disable Bluetooth") elif self._status == "Off": - menu.add_menuitem("Enable Bluetooth", callback=self._toggle) + menu.add_menuitem("Enable Bluetooth") else: return - menu.show(widget) + # show menu and get return code + ret = menu.show(widget) + if ret == 0: + # first (and only) item selected. + self._toggle() def _toggle(self, widget=None): """Toggle bluetooth state.""" diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index a9742ba..22eae88 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -8,6 +8,7 @@ Parameters: contributed by `martindoublem `_ - many thanks! """ + import os import re import subprocess @@ -21,6 +22,7 @@ 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)) @@ -35,7 +37,7 @@ class Module(core.module.Module): def status(self, widget): """Get status.""" - return self._status if self._status.isdigit() and int(self._status) > 1 else "" + return self._status def update(self): """Update current state.""" @@ -44,7 +46,7 @@ class Module(core.module.Module): ) if state > 0: connected_devices = self.get_connected_devices() - self._status = "{}".format(connected_devices) + self._status = "On - {}".format(connected_devices) else: self._status = "Off" adapters_cmd = "rfkill list | grep Bluetooth" @@ -56,23 +58,31 @@ class Module(core.module.Module): def _toggle(self, widget=None): """Toggle bluetooth state.""" - logging.debug("bt: toggling bluetooth") + if "On" in self._status: + state = "false" + else: + state = "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") + 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) def state(self, widget): """Get current state.""" state = [] - if self._status in [ "No Adapter Found", "Off" ]: + if self._status == "No Adapter Found": state.append("critical") - elif self._status == "0": - state.append("enabled") + elif self._status == "On - 0": + state.append("warning") + elif "On" in self._status and not (self._status == "On - 0"): + state.append("ON") else: - state.append("connected") - state.append("good") - + state.append("critical") return state def get_connected_devices(self): @@ -82,8 +92,12 @@ 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 diff --git a/bumblebee_status/modules/contrib/cpu3.py b/bumblebee_status/modules/contrib/cpu3.py deleted file mode 100644 index 5321012..0000000 --- a/bumblebee_status/modules/contrib/cpu3.py +++ /dev/null @@ -1,158 +0,0 @@ -"""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 ` -based on cpu2 by `` -""" - -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 = '{}'.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 diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py index 1ad43df..f082f1b 100644 --- a/bumblebee_status/modules/contrib/dunstctl.py +++ b/bumblebee_status/modules/contrib/dunstctl.py @@ -28,8 +28,6 @@ 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) diff --git a/bumblebee_status/modules/contrib/gcalendar.py b/bumblebee_status/modules/contrib/gcalendar.py index 6848ba8..efaabf7 100644 --- a/bumblebee_status/modules/contrib/gcalendar.py +++ b/bumblebee_status/modules/contrib/gcalendar.py @@ -3,9 +3,7 @@ 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. @@ -17,7 +15,7 @@ Parameters: Requires these pip packages: * google-api-python-client >= 1.8.0 - * google-auth-httplib2 + * google-auth-httplib2 * google-auth-oauthlib """ @@ -29,12 +27,10 @@ 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 @@ -42,15 +38,11 @@ from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError -# Minutes -update_every = 15 - class Module(core.module.Module): - @core.decorators.every(minutes=update_every) + @core.decorators.every(minutes=15) def __init__(self, config, theme): - super().__init__(config, theme, [core.widget.Widget(self.__datetime), core.widget.Widget(self.__summary)]) - self.__error = False + super().__init__(config, theme, core.widget.Widget(self.first_event)) 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( @@ -68,44 +60,32 @@ class Module(core.module.Module): except Exception: locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8")) - self.__last_update = time.time() - self.__gcalendar_date, self.__gcalendar_summary = self.__fetch_from_calendar() - - def hidden(self): - return self.__error - - def __datetime(self, _): - return self.__gcalendar_date - - @core.decorators.scrollable - def __summary(self, _): - return self.__gcalendar_summary - - - def __fetch_from_calendar(self): + def first_event(self, widget): 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 @@ -145,27 +125,33 @@ class Module(core.module.Module): } ) sorted_list = sorted(event_list, key=lambda t: t["date"]) - next_event = sorted_list[0] - if next_event["date"] >= datetime.datetime.now(datetime.timezone.utc): - if next_event["date"].date() == datetime.datetime.utcnow().date(): - dt = next_event["date"].astimezone()\ - .strftime(f"{self.__time_format}") - else: - dt = next_event["date"].astimezone()\ - .strftime(f"{self.__date_format} {self.__time_format}") - return (dt, next_event["summary"]) + 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." - return (None, "No upcoming events.") except: - self.__error = True + return None - 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 diff --git a/bumblebee_status/modules/contrib/gitlab.py b/bumblebee_status/modules/contrib/gitlab.py deleted file mode 100644 index 24f2eb8..0000000 --- a/bumblebee_status/modules/contrib/gitlab.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/contrib/mpd.py b/bumblebee_status/modules/contrib/mpd.py index 35cb2f6..92568d1 100644 --- a/bumblebee_status/modules/contrib/mpd.py +++ b/bumblebee_status/modules/contrib/mpd.py @@ -42,7 +42,6 @@ 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 `_ - many thanks! @@ -74,12 +73,10 @@ class Module(core.module.Module): self._repeat = False self._tags = defaultdict(lambda: "") - self._hostcmd = "" - if self.parameter("host"): - self._hostcmd = " -h {}".format(self.parameter("host")) - if self.parameter("port"): - self._hostcmd += " -p {}".format(self.parameter("port")) - + if not self.parameter("host"): + self._hostcmd = "" + else: + self._hostcmd = " -h " + self.parameter("host") # Create widgets widget_map = {} diff --git a/bumblebee_status/modules/contrib/pihole.py b/bumblebee_status/modules/contrib/pihole.py index 6e07287..7334abf 100644 --- a/bumblebee_status/modules/contrib/pihole.py +++ b/bumblebee_status/modules/contrib/pihole.py @@ -4,20 +4,13 @@ Parameters: * pihole.address : pi-hole address (e.q: http://192.168.1.3) - - - * 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) - + * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) contributed by `bbernhard `_ - many thanks! """ import requests -import logging + import core.module import core.widget import core.input @@ -29,18 +22,7 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.pihole_status)) self._pihole_address = self.parameter("address", "") - 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_pw_hash = self.parameter("pwhash", "") self._pihole_status = None self._ads_blocked_today = "-" self.update_pihole_status() @@ -60,11 +42,7 @@ class Module(core.module.Module): def update_pihole_status(self): try: - data = requests.get( - self._pihole_address - + "/admin/api.php?summary&auth=" - + self._pihole_secret - ).json() + data = requests.get(self._pihole_address + "/admin/api.php?summary").json() self._pihole_status = True if data["status"] == "enabled" else False self._ads_blocked_today = data["ads_blocked_today"] except Exception as e: @@ -78,13 +56,13 @@ class Module(core.module.Module): req = requests.get( self._pihole_address + "/admin/api.php?disable&auth=" - + self._pihole_secret + + self._pihole_pw_hash ) else: req = requests.get( self._pihole_address + "/admin/api.php?enable&auth=" - + self._pihole_secret + + self._pihole_pw_hash ) if req is not None: if req.status_code == 200: diff --git a/bumblebee_status/modules/contrib/pipewire.py b/bumblebee_status/modules/contrib/pipewire.py deleted file mode 100644 index 86c9b9d..0000000 --- a/bumblebee_status/modules/contrib/pipewire.py +++ /dev/null @@ -1,90 +0,0 @@ -"""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 diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py old mode 100644 new mode 100755 index c405a0e..bd8876c --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -34,7 +34,6 @@ 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( @@ -88,7 +87,7 @@ class Module(core.module.Module): core.input.register(widget, **callback_options) def hidden(self): - return self.__hidden + return self.__hide and self.status() == None def status(self): try: @@ -102,10 +101,6 @@ 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": diff --git a/bumblebee_status/modules/contrib/power-profile.py b/bumblebee_status/modules/contrib/power-profile.py deleted file mode 100644 index 2959391..0000000 --- a/bumblebee_status/modules/contrib/power-profile.py +++ /dev/null @@ -1,99 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/contrib/publicip.py b/bumblebee_status/modules/contrib/publicip.py index 8ae10a0..d79c2a1 100644 --- a/bumblebee_status/modules/contrib/publicip.py +++ b/bumblebee_status/modules/contrib/publicip.py @@ -60,36 +60,43 @@ class Module(core.module.Module): self.__monitor.start() def monitor(self): - __previous_ips = set() - __current_ips = set() + default_route = None + interfaces = None # 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(): - __current_ips.clear() - # Look for any changes to IP addresses + # Look for any changes in the netifaces default route information try: - for interface in netifaces.interfaces(): - try: - __current_ips.add(netifaces.ifaddresses(interface)[2][0]['addr']) - except: - pass + current_default_route = netifaces.gateways()["default"][2] except: - # If not ip address information found clear __current_ips - __current_ips.clear() - - # If a change of any interfaces' IP then flag change - if __current_ips.symmetric_difference(__previous_ips): - __previous_ips = __current_ips.copy() - __information_changed = True + # 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 - # Update if change is flagged - if __information_changed: - __information_changed = False + # 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 self.update() - + # Throttle the calls to netifaces time.sleep(1) @@ -97,11 +104,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): @@ -112,28 +119,14 @@ 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 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" + # Contstruct coordinates string + __lat = "{:.2f}".format(__info["latitude"]) + __lon = "{:.2f}".format(__info["longitude"]) + __coords = __lat + "°N" + "," + " " + __lon + "°E" # Set widget values widget.set("public_ip", __info["public_ip"]) diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index 4aabb5a..566de42 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -41,7 +41,6 @@ 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: @@ -53,7 +52,6 @@ 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, _): diff --git a/bumblebee_status/modules/contrib/stock.py b/bumblebee_status/modules/contrib/stock.py index 6b35bf7..224a5fb 100644 --- a/bumblebee_status/modules/contrib/stock.py +++ b/bumblebee_status/modules/contrib/stock.py @@ -5,9 +5,7 @@ Parameters: * stock.symbols : Comma-separated list of symbols to fetch - * 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" + * stock.change : Should we fetch change in stock value (defaults to True) contributed by `msoulier `_ - many thanks! @@ -24,12 +22,6 @@ 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) @@ -37,41 +29,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.__values = [] - + self.__value = None def value(self, widget): - result = "" + results = [] + if not self.__value: + return "n/a" + data = json.loads(self.__value) - 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 + 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) def fetch(self): - results = [] if self.__symbols: - 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 [] + 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 else: logging.error("unable to retrieve stock exchange rate") - return [] - return results + return None def update(self): - self.__values = self.fetch() + self.__value = self.fetch() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/system.py b/bumblebee_status/modules/contrib/system.py index 8b136a1..79e8846 100644 --- a/bumblebee_status/modules/contrib/system.py +++ b/bumblebee_status/modules/contrib/system.py @@ -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(self.__config) + menu = util.popup.menu() reboot_cmd = self.parameter("reboot", "reboot") shutdown_cmd = self.parameter("shutdown", "shutdown -h now") logout_cmd = self.parameter("logout", "i3exit logout") diff --git a/bumblebee_status/modules/contrib/title.py b/bumblebee_status/modules/contrib/title.py index 1c98b42..055369e 100644 --- a/bumblebee_status/modules/contrib/title.py +++ b/bumblebee_status/modules/contrib/title.py @@ -50,9 +50,8 @@ class Module(core.module.Module): # create a connection with i3ipc self.__i3 = i3ipc.Connection() - # event is called both on focus change and title change, and on workspace change + # event is called both on focus change and title 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() diff --git a/bumblebee_status/modules/contrib/todoist.py b/bumblebee_status/modules/contrib/todoist.py deleted file mode 100644 index 5fae228..0000000 --- a/bumblebee_status/modules/contrib/todoist.py +++ /dev/null @@ -1,76 +0,0 @@ -# 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)) diff --git a/bumblebee_status/modules/contrib/usage.py b/bumblebee_status/modules/contrib/usage.py deleted file mode 100644 index d64e3e1..0000000 --- a/bumblebee_status/modules/contrib/usage.py +++ /dev/null @@ -1,78 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/contrib/vpn.py b/bumblebee_status/modules/contrib/vpn.py index 3381e4f..d9c8793 100644 --- a/bumblebee_status/modules/contrib/vpn.py +++ b/bumblebee_status/modules/contrib/vpn.py @@ -1,5 +1,4 @@ # pylint: disable=C0111,R0903 -# -*- coding: utf-8 -*- """ Displays the VPN profile that is currently in use. @@ -69,7 +68,7 @@ class Module(core.module.Module): def vpn_status(self, widget): if self.__connected_vpn_profile is None: - return "" + return "off" return self.__connected_vpn_profile def __on_vpndisconnect(self): @@ -94,7 +93,7 @@ class Module(core.module.Module): self.__connected_vpn_profile = None def popup(self, widget): - menu = util.popup.menu(self.__config) + menu = util.popup.menu() if self.__connected_vpn_profile is not None: menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect) diff --git a/bumblebee_status/modules/contrib/wakatime.py b/bumblebee_status/modules/contrib/wakatime.py deleted file mode 100644 index 1ac4461..0000000 --- a/bumblebee_status/modules/contrib/wakatime.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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) diff --git a/bumblebee_status/modules/contrib/watson.py b/bumblebee_status/modules/contrib/watson.py index a717342..d7b260b 100644 --- a/bumblebee_status/modules/contrib/watson.py +++ b/bumblebee_status/modules/contrib/watson.py @@ -5,10 +5,6 @@ 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 `_ - many thanks! """ @@ -30,11 +26,11 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.text)) self.__tracking = False - self.__info = {} - self.__format = self.parameter("format", "{project} [{tags}]") + self.__project = "" 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: @@ -43,27 +39,20 @@ class Module(core.module.Module): def text(self, widget): if self.__tracking: - return self.__format.format(**self.__info) + return self.__project else: return "Paused" def update(self): output = util.cli.execute("watson status") - - 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: + if re.match(r"No project started", output): 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" diff --git a/bumblebee_status/modules/contrib/weather.py b/bumblebee_status/modules/contrib/weather.py index 4166995..72e0c27 100644 --- a/bumblebee_status/modules/contrib/weather.py +++ b/bumblebee_status/modules/contrib/weather.py @@ -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 https://api.openweathermap.org + * weather.apikey: API key from http://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! @@ -116,7 +116,7 @@ class Module(core.module.Module): def update(self): try: - weather_url = "https://api.openweathermap.org/data/2.5/weather?appid={}".format( + weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format( self.__apikey ) weather_url = "{}&units={}".format(weather_url, self.__unit) diff --git a/bumblebee_status/modules/contrib/wlrotation.py b/bumblebee_status/modules/contrib/wlrotation.py deleted file mode 100644 index 242c50b..0000000 --- a/bumblebee_status/modules/contrib/wlrotation.py +++ /dev/null @@ -1,126 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index f153a94..09fe487 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -25,7 +25,6 @@ import subprocess import core.module import core.decorators -import core.input import util.cli import util.format @@ -59,8 +58,6 @@ 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()) @@ -91,7 +88,9 @@ class Module(core.module.Module): def _iswlan(self, intf): # wifi, wlan, wlp, seems to work for me - return intf.startswith("w") and not intf.startswith("wwan") + if intf.startswith("w"): + return True + return False def _istunnel(self, intf): return intf.startswith("tun") or intf.startswith("wg") diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 5f74248..431de10 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -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(self.__config) + menu = util.popup.menu() lines = result.splitlines() for line in lines: info = line.split("\t") diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index 63f9e36..94f6994 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -13,8 +13,6 @@ 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. @@ -37,8 +35,6 @@ Requires the following Python module: """ import pulsectl -import logging -import functools import core.module import core.widget @@ -49,18 +45,13 @@ import util.cli import util.graph import util.format -try: - import util.popup -except ImportError as e: - logging.warning("Couldn't import util.popup: %s. Popups won't work!", e) - class Module(core.module.Module): def __init__(self, config, theme, type): super().__init__(config, theme, core.widget.Widget(self.display)) self.background = True self.__type = type - self.__volume = 0 + self.__volume = "n/a" self.__devicename = "n/a" self.__muted = False self.__showbars = util.format.asbool(self.parameter("showbars", False)) @@ -72,11 +63,6 @@ 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 = [ { @@ -122,15 +108,11 @@ 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: @@ -150,22 +132,16 @@ class Module(core.module.Module): for dev in devs: if dev.name == default: return dev - if len(devs) == 0: - return None - return devs[0] # fallback + def process(self, _): with pulsectl.Pulse(self.id + "proc") as pulse: dev = self.get_device(pulse) - if not dev: - self.__volume = 0 - self.__devicename = "n/a" - else: - self.__volume = dev.volume.value_flat - self.__muted = dev.mute - self.__devicename = dev.name + 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") @@ -175,33 +151,9 @@ 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"] - if self.__volume >= .5: - return ["unmuted", "unmuted-high"] - if self.__volume >= .1: - return ["unmuted", "unmuted-mid"] - return ["unmuted", "unmuted-low"] + return ["unmuted"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/scroll.py b/bumblebee_status/modules/core/scroll.py deleted file mode 100644 index b897c5b..0000000 --- a/bumblebee_status/modules/core/scroll.py +++ /dev/null @@ -1,53 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/core/vault.py b/bumblebee_status/modules/core/vault.py index ba316bc..7f3fb75 100644 --- a/bumblebee_status/modules/core/vault.py +++ b/bumblebee_status/modules/core/vault.py @@ -52,7 +52,7 @@ def build_menu(parent, current_directory, callback): ) else: - submenu = util.popup.menu(self.__config, parent, leave=False) + submenu = util.popup.menu(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(self.__config, leave=False) + menu = util.popup.menu(leave=False) build_menu(menu, self.__path, self.__callback) menu.show(widget, offset_x=self.__offx, offset_y=self.__offy) diff --git a/bumblebee_status/util/cli.py b/bumblebee_status/util/cli.py index 4ef25d9..d14dc6a 100644 --- a/bumblebee_status/util/cli.py +++ b/bumblebee_status/util/cli.py @@ -52,19 +52,7 @@ def execute( raise RuntimeError("{} not found".format(cmd)) if wait: - 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() + out, _ = proc.communicate() if proc.returncode != 0: err = "{} exited with code {}".format(cmd, proc.returncode) logging.warning(err) diff --git a/bumblebee_status/util/location.py b/bumblebee_status/util/location.py index 00ac5fd..9cd3a10 100644 --- a/bumblebee_status/util/location.py +++ b/bumblebee_status/util/location.py @@ -18,6 +18,17 @@ __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": { @@ -32,25 +43,14 @@ __sources = [ { "url": "http://ip-api.com/json", "mapping": { - "lat": "latitude", - "lon": "longitude", + "latitude": "lat", + "longitude": "lon", "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", - }, - } ] diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index cd083f7..784a037 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -3,7 +3,6 @@ import logging import tkinter as tk -import tkinter.font as tkFont import functools @@ -11,12 +10,11 @@ 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, config, parent=None, leave=True): + def __init__(self, parent=None, leave=True): self.running = True self.parent = parent @@ -25,7 +23,6 @@ class menu(object): self._root.withdraw() self._menu = tk.Menu(self._root, tearoff=0) self._menu.bind("", self.__on_focus_out) - self._font_size = tkFont.Font(size=config.popup_font_size()) if leave: self._menu.bind("", self.__on_focus_out) @@ -71,7 +68,7 @@ class menu(object): """ def add_cascade(self, menuitem, submenu): - self._menu.add_cascade(label=menuitem, menu=submenu.menu(), font=self._font_size) + self._menu.add_cascade(label=menuitem, menu=submenu.menu()) """Adds an item to the current menu @@ -81,7 +78,7 @@ class menu(object): def add_menuitem(self, menuitem, callback): self._menu.add_command( - label=menuitem, command=functools.partial(self.__on_click, callback), font=self._font_size, + label=menuitem, command=functools.partial(self.__on_click, callback) ) """Adds a separator to the menu in the current location""" diff --git a/docs/introduction.rst b/docs/introduction.rst index 891b14c..3831d4b 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -44,14 +44,6 @@ like this: -t } -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: diff --git a/docs/modules.rst b/docs/modules.rst index 0b3ff9d..85c7a6d 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -264,8 +264,6 @@ 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. @@ -305,14 +303,6 @@ 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 ~~~~~~~~ @@ -426,7 +416,6 @@ 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%) @@ -434,8 +423,6 @@ contributed by `zetxx `_ - many thanks! input handling contributed by `ardadem `_ - many thanks! -multiple audio cards contributed by `hugoeustaquio `_ - many thanks! - .. image:: ../screenshots/amixer.png apt @@ -690,49 +677,6 @@ lacking the aforementioned pattern settings or they have wrong values. contributed by `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 ` -based on cpu2 by `` - currency ~~~~~~~~ @@ -884,9 +828,6 @@ 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 `_ - many thanks! contributed by `joachimmathes `_ - many thanks! @@ -917,9 +858,7 @@ 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. @@ -931,7 +870,7 @@ Parameters: Requires these pip packages: * google-api-python-client >= 1.8.0 - * google-auth-httplib2 + * google-auth-httplib2 * google-auth-oauthlib getcrypto @@ -976,29 +915,6 @@ 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 ~~~~~ @@ -1183,7 +1099,6 @@ 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 `_ - many thanks! @@ -1314,30 +1229,10 @@ 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.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) - + * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) contributed by `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 ~~~~~~~~~ @@ -1657,9 +1552,7 @@ Display a stock quote from finance.yahoo.com Parameters: * stock.symbols : Comma-separated list of symbols to fetch - * 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" + * stock.change : Should we fetch change in stock value (defaults to True) contributed by `msoulier `_ - many thanks! @@ -1694,11 +1587,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') @@ -1797,27 +1690,6 @@ 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 ` -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 ~~~~~~~ @@ -1856,27 +1728,6 @@ contributed by `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 ~~~ @@ -1896,34 +1747,6 @@ Displays the VPN profile that is currently in use. contributed by `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 ~~~~~~ @@ -1932,10 +1755,6 @@ 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 `_ - many thanks! weather @@ -1953,7 +1772,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 https://api.openweathermap.org + * weather.apikey: API key from http://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! diff --git a/docs/themes.rst b/docs/themes.rst index 16f69a6..759ea86 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -97,8 +97,3 @@ 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 `__) diff --git a/requirements/modules/cpu3.txt b/requirements/modules/cpu3.txt deleted file mode 100644 index a4d92cc..0000000 --- a/requirements/modules/cpu3.txt +++ /dev/null @@ -1 +0,0 @@ -psutil diff --git a/requirements/modules/gitlab.txt b/requirements/modules/gitlab.txt deleted file mode 100644 index f229360..0000000 --- a/requirements/modules/gitlab.txt +++ /dev/null @@ -1 +0,0 @@ -requests diff --git a/requirements/modules/power-profile.txt b/requirements/modules/power-profile.txt deleted file mode 100644 index 8f7d255..0000000 --- a/requirements/modules/power-profile.txt +++ /dev/null @@ -1,2 +0,0 @@ -dbus-python -power-profiles-daemon \ No newline at end of file diff --git a/screenshots/gitlab.png b/screenshots/gitlab.png deleted file mode 100644 index d488db0..0000000 Binary files a/screenshots/gitlab.png and /dev/null differ diff --git a/screenshots/themes/moonlight-powerline.png b/screenshots/themes/moonlight-powerline.png deleted file mode 100644 index 025df6b..0000000 Binary files a/screenshots/themes/moonlight-powerline.png and /dev/null differ diff --git a/screenshots/todoist.png b/screenshots/todoist.png deleted file mode 100644 index 66e1bae..0000000 Binary files a/screenshots/todoist.png and /dev/null differ diff --git a/screenshots/wakatime.png b/screenshots/wakatime.png deleted file mode 100644 index a40a865..0000000 Binary files a/screenshots/wakatime.png and /dev/null differ diff --git a/setup.py b/setup.py index a756c8a..a63be64 100755 --- a/setup.py +++ b/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/*")), - ("share/man/man1", glob.glob("man/*.1")), + ("usr/share/man/man1", glob.glob("man/*.1")), ], packages=find_packages(exclude=["tests", "tests.*"]) ) diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 02695cd..762c674 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -113,12 +113,6 @@ 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 diff --git a/tests/modules/contrib/test_amixer.py b/tests/modules/contrib/test_amixer.py index 146d586..4ff37a3 100644 --- a/tests/modules/contrib/test_amixer.py +++ b/tests/modules/contrib/test_amixer.py @@ -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 -c 0 -q set Master,0 toggle') + command.assert_called_once_with('amixer -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 -c 0 -q set Master,0 4%+') + command.assert_called_once_with('amixer -q set Master,0 4%+') command = mocker.patch('util.cli.execute') module.decrease_volume(False) - command.assert_called_once_with('amixer -c 0 -q set Master,0 4%-') + command.assert_called_once_with('amixer -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 -c 0 -q set Master,0 25%+') + command.assert_called_once_with('amixer -q set Master,0 25%+') command = mocker.patch('util.cli.execute') module.decrease_volume(False) - command.assert_called_once_with('amixer -c 0 -q set Master,0 25%-') + command.assert_called_once_with('amixer -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 -c 0 -q set CustomMaster toggle') + command.assert_called_once_with('amixer -q set CustomMaster toggle') command = mocker.patch('util.cli.execute') module.increase_volume(False) - command.assert_called_once_with('amixer -c 0 -q set CustomMaster 4%+') + command.assert_called_once_with('amixer -q set CustomMaster 4%+') command = mocker.patch('util.cli.execute') module.decrease_volume(False) - command.assert_called_once_with('amixer -c 0 -q set CustomMaster 4%-') + command.assert_called_once_with('amixer -q set CustomMaster 4%-') diff --git a/tests/modules/contrib/test_cpu3.py b/tests/modules/contrib/test_cpu3.py deleted file mode 100644 index 9eac30d..0000000 --- a/tests/modules/contrib/test_cpu3.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - -pytest.importorskip("psutil") - -def test_load_module(): - __import__("modules.contrib.cpu3") diff --git a/tests/modules/contrib/test_gitlab.py b/tests/modules/contrib/test_gitlab.py deleted file mode 100644 index 016a2ab..0000000 --- a/tests/modules/contrib/test_gitlab.py +++ /dev/null @@ -1,70 +0,0 @@ -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) == [] diff --git a/tests/modules/contrib/test_power-profile.py b/tests/modules/contrib/test_power-profile.py deleted file mode 100644 index 43cb14f..0000000 --- a/tests/modules/contrib/test_power-profile.py +++ /dev/null @@ -1,32 +0,0 @@ -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" diff --git a/tests/modules/contrib/test_todoist.py b/tests/modules/contrib/test_todoist.py deleted file mode 100644 index 75d42a7..0000000 --- a/tests/modules/contrib/test_todoist.py +++ /dev/null @@ -1,58 +0,0 @@ -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)'}) diff --git a/tests/modules/contrib/test_wakatime.py b/tests/modules/contrib/test_wakatime.py deleted file mode 100644 index 9ee062e..0000000 --- a/tests/modules/contrib/test_wakatime.py +++ /dev/null @@ -1,56 +0,0 @@ -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') diff --git a/tests/util/test_location.py b/tests/util/test_location.py index b86f62e..e04e600 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -14,16 +14,16 @@ def urllib_req(mocker): def secondaryLocation(): return { "country": "Middle Earth", - "lon": "10.0", - "lat": "20.5", - "query": "127.0.0.1", + "longitude": "10.0", + "latitude": "20.5", + "ip": "127.0.0.1", } @pytest.fixture def primaryLocation(): return { - "country": "Rivia", + "country_name": "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"] + assert util.location.country() == primaryLocation["country_name"] 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["lat"], - secondaryLocation["lon"], + secondaryLocation["latitude"], + secondaryLocation["longitude"], ) - assert util.location.public_ip() == secondaryLocation["query"] + assert util.location.public_ip() == secondaryLocation["ip"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json index e008e3f..d199243 100644 --- a/themes/gruvbox-powerline.json +++ b/themes/gruvbox-powerline.json @@ -9,10 +9,6 @@ "fg": "#fbf1c7", "bg": "#cc241d" }, - "good": { - "fg": "#1d2021", - "bg": "#b8bb26" - }, "default-separators": false, "separator-block-width": 0 }, @@ -38,6 +34,16 @@ "bg": "#859900" } }, + "battery": { + "charged": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "AC": { + "fg": "#1d2021", + "bg": "#b8bb26" + } + }, "bluetooth": { "ON": { "fg": "#1d2021", diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 4f48e4d..089fa3c 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -309,9 +309,6 @@ "github": { "prefix": "github" }, - "gitlab": { - "prefix": "gitlab" - }, "deezer": { "prefix": "" }, @@ -410,8 +407,5 @@ "speedtest": { "running": { "prefix": [".", "..", "...", ".."] }, "not-running": { "prefix": "[start]" } - }, - "power-profile": { - "prefix": "profile" } } diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 1b6bb2c..acd1fc3 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -199,14 +199,11 @@ }, "pulseout": { "muted": { - "prefix": "󰝟" + "prefix": "" }, "unmuted": { "prefix": "" - }, - "unmuted-low": { "prefix": "󰕿" }, - "unmuted-mid": { "prefix": "󰖀" }, - "unmuted-high": { "prefix": "󰕾" } + } }, "amixer": { "muted": { @@ -226,40 +223,56 @@ }, "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": { - "connected": { "prefix": "󰂱" }, - "enabled": { "prefix": "󰂯" }, - "critical": { "prefix": "󰂲" } + "ON": { + "prefix": "" + }, + "warning": { + "prefix": "" + }, + "critical": { + "prefix": "" + } }, "battery-upower": { "charged": { @@ -328,46 +341,136 @@ } }, "battery": { - "charged": { "prefix": "󰂄" }, - "AC": { "suffix": "󱐥" }, - "PEN": { "suffix": "󰏪" }, - "charging": { - "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": "" }, - "charging": { - "prefix": [ "", "", "", "", "" ], + "charged": { + "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": "" } + "AC": { + "suffix": "" + }, + "charging": { + "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": "" + } + }, + "battery_all": { + "charged": { + "prefix": "", + "suffix": "" + }, + "AC": { + "suffix": "" + }, + "charging": { + "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": { @@ -470,15 +573,6 @@ "github": { "prefix": "  " }, - "gitlab": { - "prefix": "" - }, - "wakatime": { - "prefix": "\uF017" - }, - "todoist": { - "prefix": "\uF14A" - }, "deezer": { "prefix": "  " }, @@ -580,7 +674,7 @@ } }, "vpn": { - "prefix": "󰖂" + "prefix": "" }, "system": { "prefix": "  " @@ -628,12 +722,5 @@ }, "thunderbird": { "prefix": "" - }, - "power-profile": { - "prefix": "\uF2C1" - }, - "wlrotation": { - "auto": {"prefix": "󰑵"}, - "locked": {"prefix": "󰑸"} } } diff --git a/themes/moonlight-powerline.json b/themes/moonlight-powerline.json deleted file mode 100644 index b46b96a..0000000 --- a/themes/moonlight-powerline.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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" - } - ] -}