diff --git a/.github/workflows/autotest.yml b/.github/workflows/autotest.yml index 4877a70..ec02237 100644 --- a/.github/workflows/autotest.yml +++ b/.github/workflows/autotest.yml @@ -29,7 +29,7 @@ jobs: python -m pip install --upgrade pip pip install -U coverage pytest pytest-mock freezegun pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) + pip install $(cat requirements/modules/*.txt | grep -v power | cut -d ' ' -f 1 | sort -u) - name: Install Code Climate dependency run: | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 63fb402..15f1c60 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,4 +6,4 @@ python: build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3" diff --git a/bumblebee_status/modules/contrib/battery.py b/bumblebee_status/modules/contrib/battery.py index 7c65642..21951c1 100644 --- a/bumblebee_status/modules/contrib/battery.py +++ b/bumblebee_status/modules/contrib/battery.py @@ -130,8 +130,14 @@ class Module(core.module.Module): log.debug("adding new widget for {}".format(battery)) widget = self.add_widget(full_text=self.capacity, name=battery) - for w in self.widgets(): - if util.format.asbool(self.parameter("decorate", True)) == False: + try: + with open("/sys/class/power_supply/{}/model_name".format(battery)) as f: + widget.set("pen", ("Pen" in f.read().strip())) + except Exception: + pass + + if util.format.asbool(self.parameter("decorate", True)) == False: + for widget in self.widgets(): widget.set("theme.exclude", "suffix") def hidden(self): @@ -147,15 +153,16 @@ class Module(core.module.Module): capacity = self.__manager.capacity(widget.name) widget.set("capacity", capacity) widget.set("ac", self.__manager.isac_any(self._batteries)) - widget.set("theme.minwidth", "100%") # Read power conumption if util.format.asbool(self.parameter("showpowerconsumption", False)): output = "{}% ({})".format( capacity, self.__manager.consumption(widget.name) ) - else: + elif capacity < 100: output = "{}%".format(capacity) + else: + output = "" if ( util.format.asbool(self.parameter("showremaining", True)) @@ -167,6 +174,16 @@ class Module(core.module.Module): output, util.format.duration(remaining, compact=True, unit=True) ) +# if bumblebee.util.asbool(self.parameter("rate", True)): +# try: +# with open("{}/power_now".format(widget.name)) as f: +# rate = (float(f.read())/1000000) +# if rate > 0: +# output = "{} {:.2f}w".format(output, rate) +# except Exception: +# pass + + if util.format.asbool(self.parameter("showdevice", False)): output = "{} ({})".format(output, widget.name) @@ -176,6 +193,9 @@ class Module(core.module.Module): state = [] capacity = widget.get("capacity") + if widget.get("pen"): + state.append("PEN") + if capacity < 0: log.debug("battery state: {}".format(state)) return ["critical", "unknown"] @@ -187,16 +207,10 @@ class Module(core.module.Module): charge = self.__manager.charge_any(self._batteries) else: charge = self.__manager.charge(widget.name) - if charge == "Discharging": + if charge in ["Discharging", "Unknown"]: state.append( "discharging-{}".format( - min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity)) - ) - ) - elif charge == "Unknown": - state.append( - "unknown-{}".format( - min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity)) + min([5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], key=lambda i: abs(i - capacity)) ) ) else: diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index 22eae88..a9742ba 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -8,7 +8,6 @@ Parameters: contributed by `martindoublem `_ - many thanks! """ - import os import re import subprocess @@ -22,7 +21,6 @@ import core.input import util.cli - class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.status)) @@ -37,7 +35,7 @@ class Module(core.module.Module): def status(self, widget): """Get status.""" - return self._status + return self._status if self._status.isdigit() and int(self._status) > 1 else "" def update(self): """Update current state.""" @@ -46,7 +44,7 @@ class Module(core.module.Module): ) if state > 0: connected_devices = self.get_connected_devices() - self._status = "On - {}".format(connected_devices) + self._status = "{}".format(connected_devices) else: self._status = "Off" adapters_cmd = "rfkill list | grep Bluetooth" @@ -58,31 +56,23 @@ class Module(core.module.Module): def _toggle(self, widget=None): """Toggle bluetooth state.""" - if "On" in self._status: - state = "false" - else: - state = "true" - - cmd = ( - "dbus-send --system --print-reply --dest=org.blueman.Mechanism /org/blueman/mechanism org.blueman.Mechanism.SetRfkillState boolean:%s" - % state - ) - logging.debug("bt: toggling bluetooth") - util.cli.execute(cmd, ignore_errors=True) + + SetRfkillState = self._bus.get_object("org.blueman.Mechanism", "/org/blueman/mechanism").get_dbus_method("SetRfkillState", dbus_interface="org.blueman.Mechanism") + SetRfkillState(self._status == "Off") def state(self, widget): """Get current state.""" state = [] - if self._status == "No Adapter Found": + if self._status in [ "No Adapter Found", "Off" ]: state.append("critical") - elif self._status == "On - 0": - state.append("warning") - elif "On" in self._status and not (self._status == "On - 0"): - state.append("ON") + elif self._status == "0": + state.append("enabled") else: - state.append("critical") + state.append("connected") + state.append("good") + return state def get_connected_devices(self): @@ -92,12 +82,8 @@ class Module(core.module.Module): ).GetManagedObjects() for path, interfaces in objects.items(): if "org.bluez.Device1" in interfaces: - if dbus.Interface( - self._bus.get_object("org.bluez", path), - "org.freedesktop.DBus.Properties", - ).Get("org.bluez.Device1", "Connected"): + if dbus.Interface(self._bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties", ).Get("org.bluez.Device1", "Connected"): devices += 1 return devices - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/power-profile.py b/bumblebee_status/modules/contrib/power-profile.py new file mode 100644 index 0000000..2959391 --- /dev/null +++ b/bumblebee_status/modules/contrib/power-profile.py @@ -0,0 +1,99 @@ +# pylint: disable=C0111,R0903 +""" +Displays the current Power-Profile active + + +Left-Click or Right-Click as well as Scrolling up / down changes the active Power-Profile + +Prerequisites: + * dbus-python + * power-profiles-daemon +""" + +import dbus +import core.module +import core.widget +import core.input + + +class PowerProfileManager: + def __init__(self): + self.POWER_PROFILES_NAME = "net.hadess.PowerProfiles" + self.POWER_PROFILES_PATH = "/net/hadess/PowerProfiles" + self.PP_PROPERTIES_CURRENT_POWER_PROFILE = "ActiveProfile" + self.PP_PROPERTIES_ALL_POWER_PROFILES = "Profiles" + + self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties" + bus = dbus.SystemBus() + pp_proxy = bus.get_object(self.POWER_PROFILES_NAME, self.POWER_PROFILES_PATH) + self.pp_interface = dbus.Interface(pp_proxy, self.DBUS_PROPERTIES) + + def get_current_power_profile(self): + return self.pp_interface.Get( + self.POWER_PROFILES_NAME, self.PP_PROPERTIES_CURRENT_POWER_PROFILE + ) + + def __get_all_power_profile_names(self): + power_profiles = self.pp_interface.Get( + self.POWER_PROFILES_NAME, self.PP_PROPERTIES_ALL_POWER_PROFILES + ) + power_profiles_names = [] + for pp in power_profiles: + power_profiles_names.append(pp["Profile"]) + + return power_profiles_names + + def next_power_profile(self, event): + all_pp_names = self.__get_all_power_profile_names() + current_pp_index = self.__get_current_pp_index() + next_index = 0 + if current_pp_index != (len(all_pp_names) - 1): + next_index = current_pp_index + 1 + + self.pp_interface.Set( + self.POWER_PROFILES_NAME, + self.PP_PROPERTIES_CURRENT_POWER_PROFILE, + all_pp_names[next_index], + ) + + def prev_power_profile(self, event): + all_pp_names = self.__get_all_power_profile_names() + current_pp_index = self.__get_current_pp_index() + last_index = len(all_pp_names) - 1 + if current_pp_index is not 0: + last_index = current_pp_index - 1 + + self.pp_interface.Set( + self.POWER_PROFILES_NAME, + self.PP_PROPERTIES_CURRENT_POWER_PROFILE, + all_pp_names[last_index], + ) + + def __get_current_pp_index(self): + all_pp_names = self.__get_all_power_profile_names() + current_pp = self.get_current_power_profile() + return all_pp_names.index(current_pp) + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.full_text)) + self.pp_manager = PowerProfileManager() + core.input.register( + self, button=core.input.WHEEL_UP, cmd=self.pp_manager.next_power_profile + ) + core.input.register( + self, button=core.input.WHEEL_DOWN, cmd=self.pp_manager.prev_power_profile + ) + core.input.register( + self, button=core.input.LEFT_MOUSE, cmd=self.pp_manager.next_power_profile + ) + core.input.register( + self, button=core.input.RIGHT_MOUSE, cmd=self.pp_manager.prev_power_profile + ) + + def full_text(self, widgets): + return self.pp_manager.get_current_power_profile() + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index e8461b9..4aabb5a 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -41,6 +41,7 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.get_output)) self.__command = self.parameter("command", 'echo "no command configured"') + self.__command = os.path.expanduser(self.__command) self.__async = util.format.asbool(self.parameter("async")) if self.__async: @@ -52,6 +53,7 @@ class Module(core.module.Module): def set_output(self, value): self.__output = value + core.event.trigger("update", [self.id], redraw_only=True) @core.decorators.scrollable def get_output(self, _): @@ -61,7 +63,6 @@ class Module(core.module.Module): # if requested then run not async version and just execute command in this thread if not self.__async: self.__output = util.cli.execute(self.__command, shell=True, ignore_errors=True).strip() - core.event.trigger("update", [self.id], redraw_only=True) return # if previous thread didn't end yet then don't do anything diff --git a/bumblebee_status/modules/contrib/vpn.py b/bumblebee_status/modules/contrib/vpn.py index ae8e748..3381e4f 100644 --- a/bumblebee_status/modules/contrib/vpn.py +++ b/bumblebee_status/modules/contrib/vpn.py @@ -1,4 +1,5 @@ # pylint: disable=C0111,R0903 +# -*- coding: utf-8 -*- """ Displays the VPN profile that is currently in use. @@ -68,7 +69,7 @@ class Module(core.module.Module): def vpn_status(self, widget): if self.__connected_vpn_profile is None: - return "off" + return "" return self.__connected_vpn_profile def __on_vpndisconnect(self): diff --git a/bumblebee_status/modules/contrib/wlrotation.py b/bumblebee_status/modules/contrib/wlrotation.py new file mode 100644 index 0000000..242c50b --- /dev/null +++ b/bumblebee_status/modules/contrib/wlrotation.py @@ -0,0 +1,126 @@ +# pylint: disable=C0111,R0903 +# -*- coding: utf-8 -*- + +"""Shows a widget for each connected screen and allows the user to loop through different orientations. + +Parameters: + * wlrotation.display : Name of the output display that will be rotated + + wlrotation.auto : Boolean value if the display should be rotatet automatic by default + +Requires the following executable: + * swaymsg +""" + +import core.module +import core.input +import util.cli + +import iio +import json +from math import degrees, atan2, sqrt +from os import environ, path + +possible_orientations = ["normal", "90", "180", "270"] + +class iioValue: + def __init__(self, channel): + self.channel = channel + self.scale = self.read('scale') + self.offset = self.read('offset') + + def read(self, attr): + return float(self.channel.attrs[attr].value) + + def value(self): + return (self.read('raw') + self.offset) * self.scale + +class iioAccelDevice: + def __init__(self): + self.ctx = iio.Context() # store ctx pointer + d = self.ctx.find_device('accel_3d') + self.x = iioValue(d.find_channel('accel_x')) + self.y = iioValue(d.find_channel('accel_y')) + self.z = iioValue(d.find_channel('accel_z')) + + def orientation(self): + """ + returns tuple of `[success, value]` where `success` indicates, if an accurate value could be meassured and `value` the sway output api compatible value or `normal` if success is `False` + """ + x_deg, y_deg, z_deg = self._deg() + if abs(z_deg) < 70: # checks if device is angled too shallow + if x_deg >= 70: return True, "270" + if x_deg <= -70: return True, "90" + if abs(x_deg) <= 20: + if y_deg < 0: return True, "normal" + if y_deg > 0: return True, "180" + return False, "normal" + + def _deg(self): + gravity = 9.81 + x, y, z = self.x.value() / gravity, self.y.value() / gravity, self.z.value() / gravity + return degrees(atan2(x, sqrt(pow(y, 2) + pow(z, 2)))), degrees(atan2(y, sqrt(pow(z, 2) + pow(x, 2)))), degrees(atan2(z, sqrt(pow(x, 2) + pow(y, 2)))) + +class Display(): + def __init__(self, name, widget, display_data, auto=False): + self.name = name + self.widget = widget + self.accelDevice = iioAccelDevice() + self._lock_auto_rotation(not auto) + + self.widget.set("orientation", display_data['transform']) + + core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=self.rotate_90deg) + core.input.register(widget, button=core.input.RIGHT_MOUSE, cmd=self.toggle) + + def rotate_90deg(self, event): + # compute new orientation based on current orientation + current = self.widget.get("orientation") + self._set_rotation(possible_orientations[(possible_orientations.index(current) + 1) % len(possible_orientations)]) + # disable auto rotation + self._lock_auto_rotation(True) + + def toggle(self, event): + self._lock_auto_rotation(not self.locked) + + def auto_rotate(self): + # automagically rotate the display based on sensor values + # this is only called if rotation lock is disabled + success, value = self.accelDevice.orientation() + if success: + self._set_rotation(value) + + def _set_rotation(self, new_orientation): + self.widget.set("orientation", new_orientation) + util.cli.execute("swaymsg 'output {} transform {}'".format(self.name, new_orientation)) + + def _lock_auto_rotation(self, locked): + self.locked = locked + self.widget.set("locked", self.locked) + +class Module(core.module.Module): + @core.decorators.every(seconds=1) + def __init__(self, config, theme): + super().__init__(config, theme, []) + + self.display = None + display_filter = self.parameter("display", None) + for display in json.loads(util.cli.execute("swaymsg -t get_outputs -r")): + name = display['name'] + if display_filter == None or display_filter == name: + self.display = Display(name, self.add_widget(name=name), display, auto=util.format.asbool(self.parameter("auto", False))) + break # I assume that it makes only sense to rotate a single screen + + def update(self): + if self.display == None: + return + if self.display.locked: + return + + self.display.auto_rotate() + + def state(self, widget): + state = [] + state.append("locked" if widget.get("locked", True) else "auto") + return state + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 09fe487..f153a94 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -25,6 +25,7 @@ import subprocess import core.module import core.decorators +import core.input import util.cli import util.format @@ -58,6 +59,8 @@ class Module(core.module.Module): self.iw = shutil.which("iw") self._update_widgets(widgets) + core.input.register(self, button=core.input.LEFT_MOUSE, cmd='wifi-menu') + core.input.register(self, button=core.input.RIGHT_MOUSE, cmd='nm-connection-editor') def update(self): self._update_widgets(self.widgets()) @@ -88,9 +91,7 @@ class Module(core.module.Module): def _iswlan(self, intf): # wifi, wlan, wlp, seems to work for me - if intf.startswith("w"): - return True - return False + return intf.startswith("w") and not intf.startswith("wwan") def _istunnel(self, intf): return intf.startswith("tun") or intf.startswith("wg") diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py index fd77c26..63f9e36 100644 --- a/bumblebee_status/modules/core/pulsectl.py +++ b/bumblebee_status/modules/core/pulsectl.py @@ -198,6 +198,10 @@ class Module(core.module.Module): def state(self, _): if self.__muted: return ["warning", "muted"] - return ["unmuted"] + if self.__volume >= .5: + return ["unmuted", "unmuted-high"] + if self.__volume >= .1: + return ["unmuted", "unmuted-mid"] + return ["unmuted", "unmuted-low"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/docs/modules.rst b/docs/modules.rst index c7a78de..0b3ff9d 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -264,6 +264,8 @@ Parameters: * pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running * pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%) * pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') + * pulsectl.popup-filter: Comma-separated list of device strings (if the device name contains it) to exclude + from the default device popup menu (e.g. Monitor for sources) * pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango; 'false' for not showing volume bars (default) * pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. @@ -424,6 +426,7 @@ Requires the following executable: * amixer Parameters: + * amixer.card: Sound Card to use (default is 0) * amixer.device: Device to use (default is Master,0) * amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) @@ -431,6 +434,8 @@ 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 @@ -685,6 +690,49 @@ 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 ~~~~~~~~ @@ -836,6 +884,9 @@ be running. Scripts will be executed when dunst gets unpaused. Requires: * dunst v1.5.0+ +Parameters: + * dunstctl.disabled(Boolean): dunst state on start + contributed by `cristianmiranda `_ - many thanks! contributed by `joachimmathes `_ - many thanks! @@ -866,7 +917,9 @@ Displays first upcoming event in google calendar. Events that are set as 'all-day' will not be shown. Requires credentials.json from a google api application where the google calendar api is installed. -On first time run the browser will open and google will ask for permission for this app to access the google calendar and then save a .gcalendar_token.json file to the credentials_path directory which stores this permission. +On first time run the browser will open and google will ask for permission for this app to access +the google calendar and then save a .gcalendar_token.json file to the credentials_path directory +which stores this permission. A refresh is done every 15 minutes. @@ -878,7 +931,7 @@ Parameters: Requires these pip packages: * google-api-python-client >= 1.8.0 - * google-auth-httplib2 + * google-auth-httplib2 * google-auth-oauthlib getcrypto @@ -923,6 +976,29 @@ contributed by: .. image:: ../screenshots/github.png +gitlab +~~~~~~ + +Displays the GitLab todo count: + + * https://docs.gitlab.com/ee/user/todos.html + * https://docs.gitlab.com/ee/api/todos.html + +Uses `xdg-open` or `x-www-browser` to open web-pages. + +Requires the following library: + * requests + +Errors: + if the GitLab todo query failed, the shown value is `n/a` + +Parameters: + * gitlab.token: GitLab personal access token, the token needs to have the "read_api" scope. + * gitlab.host: Host of the GitLab instance, default is "gitlab.com". + * gitlab.actions: Comma separated actions to be parsed (e.g.: gitlab.actions=assigned,approval_required) + +.. image:: ../screenshots/gitlab.png + gpmdp ~~~~~ @@ -1107,6 +1183,7 @@ Parameters: if {file} = '/foo/bar.baz', then {file2} = 'bar' * mpd.host: MPD host to connect to. (mpc behaviour by default) + * mpd.port: MPD port to connect to. (mpc behaviour by default) * mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main. contributed by `alrayyes `_ - many thanks! @@ -1126,9 +1203,7 @@ network_traffic ~~~~~~~~~~~~~~~ Displays network traffic - -Requires the following library: - * netifaces + * No extra configuration needed contributed by `izn `_ - many thanks! @@ -1155,7 +1230,7 @@ nvidiagpu Displays GPU name, temperature and memory usage. Parameters: - * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{mem_used}/{mem_total} MiB') + * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct} Requires nvidia-smi @@ -1239,12 +1314,19 @@ Displays the pi-hole status (up/down) together with the number of ads that were Parameters: * pihole.address : pi-hole address (e.q: http://192.168.1.3) - * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) + + + * pihole.apitoken : pi-hole API token (can be obtained in the pi-hole webinterface (Settings -> API) + + OR (deprecated!) + + * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) + contributed by `bbernhard `_ - many thanks! pipewire -~~~~~~~ +~~~~~~~~ get volume level or control it @@ -1575,7 +1657,9 @@ Display a stock quote from finance.yahoo.com Parameters: * stock.symbols : Comma-separated list of symbols to fetch - * stock.change : Should we fetch change in stock value (defaults to True) + * stock.apikey : API key created on https://alphavantage.co + * stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}" + * stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent" contributed by `msoulier `_ - many thanks! @@ -1610,11 +1694,11 @@ adds the possibility to * reboot the system. - + Per default a confirmation dialog is shown before the actual action is performed. - + Parameters: - * system.confirm: show confirmation dialog before performing any action (default: true) + * system.confirm: show confirmation dialog before performing any action (default: true) * system.reboot: specify a reboot command (defaults to 'reboot') * system.shutdown: specify a shutdown command (defaults to 'shutdown -h now') * system.logout: specify a logout command (defaults to 'i3exit logout') @@ -1713,6 +1797,27 @@ Parameters: * todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed) Based on the todo module by `codingo ` +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 ~~~~~~~ @@ -1751,6 +1856,27 @@ 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 ~~~ @@ -1770,6 +1896,34 @@ 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 ~~~~~~ @@ -1778,6 +1932,10 @@ Displays the status of watson (time-tracking tool) Requires the following executable: * watson +Parameters: + * watson.format: Output format, defaults to "{project} [{tags}]" + Supported fields are: {project}, {tags}, {relative_start}, {absolute_start} + contributed by `bendardenne `_ - many thanks! weather @@ -1795,7 +1953,7 @@ Parameters: * weather.unit: metric (default), kelvin, imperial * weather.showcity: If set to true, show location information, otherwise hide it (defaults to true) * weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false) - * weather.apikey: API key from http://api.openweathermap.org + * weather.apikey: API key from https://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! diff --git a/requirements/modules/power-profile.txt b/requirements/modules/power-profile.txt new file mode 100644 index 0000000..8f7d255 --- /dev/null +++ b/requirements/modules/power-profile.txt @@ -0,0 +1,2 @@ +dbus-python +power-profiles-daemon \ No newline at end of file diff --git a/tests/modules/contrib/test_power-profile.py b/tests/modules/contrib/test_power-profile.py new file mode 100644 index 0000000..43cb14f --- /dev/null +++ b/tests/modules/contrib/test_power-profile.py @@ -0,0 +1,32 @@ +from unittest.mock import patch, MagicMock +import unittest +import pytest + +import core.config +import modules.contrib.power_profile + +pytest.importorskip("dbus") + + +def build_powerprofile_module(): + config = core.config.Config([]) + return modules.contrib.power_profile.Module(config=config, theme=None) + + +class TestPowerProfileUnit(unittest.TestCase): + def __get_mock_dbus_get_method(self, mock_system_bus): + return ( + mock_system_bus.return_value.get_object.return_value.get_dbus_method.return_value + ) + + def test_load_module(self): + __import__("modules.contrib.power-profile") + + @patch("dbus.SystemBus") + def test_full_text(self, mock_system_bus): + mock_get = self.__get_mock_dbus_get_method(mock_system_bus) + mock_get.return_value = "balanced" + + module = build_powerprofile_module() + module.update() + assert module.widgets()[0].full_text() == "balanced" diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json index d199243..e008e3f 100644 --- a/themes/gruvbox-powerline.json +++ b/themes/gruvbox-powerline.json @@ -9,6 +9,10 @@ "fg": "#fbf1c7", "bg": "#cc241d" }, + "good": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, "default-separators": false, "separator-block-width": 0 }, @@ -34,16 +38,6 @@ "bg": "#859900" } }, - "battery": { - "charged": { - "fg": "#1d2021", - "bg": "#b8bb26" - }, - "AC": { - "fg": "#1d2021", - "bg": "#b8bb26" - } - }, "bluetooth": { "ON": { "fg": "#1d2021", diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index d353386..4f48e4d 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -410,5 +410,8 @@ "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 60f8f51..1b6bb2c 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -199,11 +199,14 @@ }, "pulseout": { "muted": { - "prefix": "" + "prefix": "󰝟" }, "unmuted": { "prefix": "" - } + }, + "unmuted-low": { "prefix": "󰕿" }, + "unmuted-mid": { "prefix": "󰖀" }, + "unmuted-high": { "prefix": "󰕾" } }, "amixer": { "muted": { @@ -223,7 +226,7 @@ }, "pulsein": { "muted": { - "prefix": "" + "prefix": "󰍭" }, "unmuted": { "prefix": "" @@ -241,46 +244,22 @@ "prefix": "\uf17c" }, "nic": { - "wireless-up": { - "prefix": "" - }, - "wireless-down": { - "prefix": "" - }, - "wired-up": { - "prefix": "" - }, - "wired-down": { - "prefix": "" - }, - "tunnel-up": { - "prefix": "" - }, - "tunnel-down": { - "prefix": "" - } + "wireless-up": { "prefix": "" }, + "wireless-down": { "prefix": "睊" }, + "wired-up": { "prefix": "" }, + "wired-down": { "prefix": "" }, + "tunnel-up": { "prefix": "嬨" }, + "tunnel-down": { "prefix": "嬨" } }, "bluetooth": { - "ON": { - "prefix": "" - }, - "OFF": { - "prefix": "" - }, - "?": { - "prefix": "" - } + "ON": { "prefix": "󰂯" }, + "OFF": { "prefix": "󰂲" }, + "?": { "prefix": "󰂱" } }, "bluetooth2": { - "ON": { - "prefix": "" - }, - "warning": { - "prefix": "" - }, - "critical": { - "prefix": "" - } + "connected": { "prefix": "󰂱" }, + "enabled": { "prefix": "󰂯" }, + "critical": { "prefix": "󰂲" } }, "battery-upower": { "charged": { @@ -349,136 +328,46 @@ } }, "battery": { - "charged": { - "prefix": "", - "suffix": "" - }, - "AC": { - "suffix": "" - }, + "charged": { "prefix": "󰂄" }, + "AC": { "suffix": "󱐥" }, + "PEN": { "suffix": "󰏪" }, "charging": { - "prefix": [ - "", - "", - "", - "", - "" - ], - "suffix": "" + "prefix": [ "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅" ], + "suffix": "" }, - "discharging-10": { - "prefix": "", - "suffix": "" - }, - "discharging-25": { - "prefix": "", - "suffix": "" - }, - "discharging-50": { - "prefix": "", - "suffix": "" - }, - "discharging-80": { - "prefix": "", - "suffix": "" - }, - "discharging-100": { - "prefix": "", - "suffix": "" - }, - "unlimited": { - "prefix": "", - "suffix": "" - }, - "estimate": { - "prefix": "" - }, - "unknown-10": { - "prefix": "", - "suffix": "" - }, - "unknown-25": { - "prefix": "", - "suffix": "" - }, - "unknown-50": { - "prefix": "", - "suffix": "" - }, - "unknown-80": { - "prefix": "", - "suffix": "" - }, - "unknown-100": { - "prefix": "", - "suffix": "" - } + "discharging-05": { "prefix": "󰂎", "suffix": "" }, + "discharging-10": { "prefix": "󰁺", "suffix": "" }, + "discharging-20": { "prefix": "󰁻", "suffix": "" }, + "discharging-30": { "prefix": "󰁼", "suffix": "" }, + "discharging-40": { "prefix": "󰁽", "suffix": "" }, + "discharging-50": { "prefix": "󰁾", "suffix": "" }, + "discharging-60": { "prefix": "󰁿", "suffix": "" }, + "discharging-70": { "prefix": "󰂀", "suffix": "" }, + "discharging-80": { "prefix": "󰂁", "suffix": "" }, + "discharging-90": { "prefix": "󰂂", "suffix": "" }, + "discharging-100": { "prefix": "󰁹" }, + "unlimited": { "prefix": "", "suffix": "" }, + "estimate": { "prefix": "" } }, "battery_all": { - "charged": { - "prefix": "", - "suffix": "" - }, - "AC": { - "suffix": "" - }, + "charged": { "prefix": "", "suffix": "" }, + "AC": { "suffix": "" }, "charging": { - "prefix": [ - "", - "", - "", - "", - "" - ], + "prefix": [ "", "", "", "", "" ], "suffix": "" }, - "discharging-10": { - "prefix": "", - "suffix": "" - }, - "discharging-25": { - "prefix": "", - "suffix": "" - }, - "discharging-50": { - "prefix": "", - "suffix": "" - }, - "discharging-80": { - "prefix": "", - "suffix": "" - }, - "discharging-100": { - "prefix": "", - "suffix": "" - }, - "unlimited": { - "prefix": "", - "suffix": "" - }, - "estimate": { - "prefix": "" - }, - "unknown-10": { - "prefix": "", - "suffix": "" - }, - "unknown-25": { - "prefix": "", - "suffix": "" - }, - "unknown-50": { - "prefix": "", - "suffix": "" - }, - "unknown-80": { - "prefix": "", - "suffix": "" - }, - "unknown-100": { - "prefix": "", - "suffix": "" - } + "discharging-10": { "prefix": "", "suffix": "" }, + "discharging-25": { "prefix": "", "suffix": "" }, + "discharging-50": { "prefix": "", "suffix": "" }, + "discharging-80": { "prefix": "", "suffix": "" }, + "discharging-100": { "prefix": "", "suffix": "" }, + "unlimited": { "prefix": "", "suffix": "" }, + "estimate": { "prefix": "" }, + "unknown-10": { "prefix": "", "suffix": "" }, + "unknown-25": { "prefix": "", "suffix": "" }, + "unknown-50": { "prefix": "", "suffix": "" }, + "unknown-80": { "prefix": "", "suffix": "" }, + "unknown-100": { "prefix": "", "suffix": "" } }, "caffeine": { "activated": { @@ -691,7 +580,7 @@ } }, "vpn": { - "prefix": "" + "prefix": "󰖂" }, "system": { "prefix": "  " @@ -739,5 +628,12 @@ }, "thunderbird": { "prefix": "" + }, + "power-profile": { + "prefix": "\uF2C1" + }, + "wlrotation": { + "auto": {"prefix": "󰑵"}, + "locked": {"prefix": "󰑸"} } }