[core] restructure to allow PIP packaging

OK - so I have to admit I *hate* the fact that PIP seems to require a
subdirectory named like the library.

But since the PIP package is something really nifty to have (thanks to
@tony again!!!), I updated the codebase to hopefully conform with what
PIP expects. Testruns so far look promising...
This commit is contained in:
tobi-wan-kenobi 2020-05-09 21:22:00 +02:00
parent 1d25be2059
commit 320827d577
146 changed files with 2509 additions and 2 deletions

View file

@ -0,0 +1,51 @@
"""get volume level
Parameters:
* amixer.device: Device to use, defaults to "Master,0"
contributed by `zetxx <https://github.com/zetxx>`_ - many thanks!
"""
import re
import core.module
import core.widget
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.volume))
self.__level = "n/a"
self.__muted = True
device = self.parameter("device", "Master,0")
self._cmdString = "amixer get {}".format(device)
def volume(self, widget):
if self.__level == "n/a":
return self.__level
m = re.search(r"([\d]+)\%", self.__level)
self.__muted = True
if m:
if m.group(1) != "0" and "[on]" in self.__level:
self.__muted = False
return "{}%".format(m.group(1))
else:
return "0%"
def update(self):
try:
self.__level = util.cli.execute(
"amixer get {}".format(self.parameter("device", "Master,0"))
)
except Exception as e:
self.__level = "n/a"
def state(self, widget):
if self.__muted:
return ["warning", "muted"]
return ["unmuted"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,89 @@
# pylint: disable=C0111,R0903
"""Displays APT package update information (<to upgrade>/<to remove >)
Requires the following packages:
* aptitude
contributed by `qba10 <https://github.com/qba10>`_ - many thanks!
"""
import re
import threading
import core.module
import core.widget
import core.decorators
import util.cli
PATTERN = "{} packages upgraded, {} newly installed, {} to remove and {} not upgraded."
def parse_result(to_parse):
# We want to line with the iforamtion about package upgrade
line_to_parse = to_parse.split("\n")[-4]
result = re.search(
"(.+) packages upgraded, (.+) newly installed, (.+) to remove", line_to_parse
)
return int(result.group(1)), int(result.group(3))
def get_apt_check_info(module):
widget = module.widget()
try:
res = util.cli.execute("aptitude full-upgrade --simulate --assume-yes")
widget.set("error", None)
except (RuntimeError, FileNotFoundError) as e:
widget.set("error", "unable to query APT: {}".format(e))
return
to_upgrade = 0
to_remove = 0
try:
to_upgrade, to_remove = parse_result(res)
widget.set("to_upgrade", to_upgrade)
widget.set("to_remove", to_remove)
except Exception as e:
widget.set("error", "parse error: {}".format(e))
core.event.trigger("update", [module.id], redraw_only=True)
class Module(core.module.Module):
@core.decorators.every(minutes=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.updates))
self.__thread = None
def updates(self, widget):
if widget.get("error"):
return widget.get("error")
return "{} to upgrade, {} to remove".format(
widget.get("to_upgrade", 0), widget.get("to_remove", 0)
)
def update(self):
if self.__thread and self.__thread.isAlive():
return
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))
self.__thread.start()
def state(self, widget):
cnt = 0
ret = "good"
for t in ["to_upgrade", "to_remove"]:
cnt += widget.get(t, 0)
if cnt > 50:
ret = "critical"
elif cnt > 0:
ret = "warning"
if widget.get("error"):
ret = "critical"
return ret
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,50 @@
"""Check updates to Arch Linux.
Requires the following executable:
* checkupdates (from pacman-contrib)
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
"""
import logging
import core.module
import core.widget
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.utilization))
self.__packages = 0
self.__error = False
@property
def __format(self):
return self.parameter("format", "Update Arch: {}")
def utilization(self, widget):
return self.__format.format(self.__packages)
def hidden(self):
return self.__packages == 0 and not self.__error
def update(self):
try:
result = util.cli.execute("checkupdates")
self.__packages = len(result.split("\n")) - 1
self.__error = False
except Exception as e:
logging.exception(e)
self.__error = True
def state(self, widget):
if self.__error:
return "warning"
return self.threshold_state(self.__packages, 1, 100)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,334 @@
# UPowerManger Class Copyright (C) 2017 Oscar Svensson (wogscpar) under MIT licence from upower-python
"""Displays battery status, remaining percentage and charging information.
Parameters:
* battery-upower.warning : Warning threshold in % of remaining charge (defaults to 20)
* battery-upower.critical : Critical threshold in % of remaining charge (defaults to 10)
* battery-upower.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
"""
import dbus
import logging
import core.module
import core.widget
import core.input
import util.format
class UPowerManager:
def __init__(self):
self.UPOWER_NAME = "org.freedesktop.UPower"
self.UPOWER_PATH = "/org/freedesktop/UPower"
self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties"
self.bus = dbus.SystemBus()
def detect_devices(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
devices = upower_interface.EnumerateDevices()
return devices
def get_display_device(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
dispdev = upower_interface.GetDisplayDevice()
return dispdev
def get_critical_action(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
critical_action = upower_interface.GetCriticalAction()
return critical_action
def get_device_percentage(self, battery):
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
return battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Percentage")
def get_full_device_information(self, battery):
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
hasHistory = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "HasHistory"
)
hasStatistics = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "HasStatistics"
)
isPresent = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "IsPresent"
)
isRechargable = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "IsRechargeable"
)
online = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Online")
powersupply = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "PowerSupply"
)
capacity = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Capacity")
energy = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Energy")
energyempty = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "EnergyEmpty"
)
energyfull = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "EnergyFull"
)
energyfulldesign = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "EnergyFullDesign"
)
energyrate = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "EnergyRate"
)
luminosity = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "Luminosity"
)
percentage = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "Percentage"
)
temperature = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "Temperature"
)
voltage = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Voltage")
timetoempty = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "TimeToEmpty"
)
timetofull = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "TimeToFull"
)
iconname = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "IconName")
model = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Model")
nativepath = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "NativePath"
)
serial = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Serial")
vendor = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Vendor")
state = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State")
technology = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "Technology"
)
battype = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Type")
warninglevel = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "WarningLevel"
)
updatetime = battery_proxy_interface.Get(
self.UPOWER_NAME + ".Device", "UpdateTime"
)
information_table = {
"HasHistory": hasHistory,
"HasStatistics": hasStatistics,
"IsPresent": isPresent,
"IsRechargeable": isRechargable,
"Online": online,
"PowerSupply": powersupply,
"Capacity": capacity,
"Energy": energy,
"EnergyEmpty": energyempty,
"EnergyFull": energyfull,
"EnergyFullDesign": energyfulldesign,
"EnergyRate": energyrate,
"Luminosity": luminosity,
"Percentage": percentage,
"Temperature": temperature,
"Voltage": voltage,
"TimeToEmpty": timetoempty,
"TimeToFull": timetofull,
"IconName": iconname,
"Model": model,
"NativePath": nativepath,
"Serial": serial,
"Vendor": vendor,
"State": state,
"Technology": technology,
"Type": battype,
"WarningLevel": warninglevel,
"UpdateTime": updatetime,
}
return information_table
def is_lid_present(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
is_lid_present = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsPresent"))
return is_lid_present
def is_lid_closed(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
is_lid_closed = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsClosed"))
return is_lid_closed
def on_battery(self):
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
on_battery = bool(upower_interface.Get(self.UPOWER_NAME, "OnBattery"))
return on_battery
def has_wakeup_capabilities(self):
upower_proxy = self.bus.get_object(
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
)
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
has_wakeup_capabilities = bool(
upower_interface.Get(self.UPOWER_NAME + ".Wakeups", "HasCapability")
)
return has_wakeup_capabilities
def get_wakeups_data(self):
upower_proxy = self.bus.get_object(
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
)
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups")
data = upower_interface.GetData()
return data
def get_wakeups_total(self):
upower_proxy = self.bus.get_object(
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
)
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups")
data = upower_interface.GetTotal()
return data
def is_loading(self, battery):
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State"))
if state == 1:
return True
else:
return False
def get_state(self, battery):
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State"))
if state == 0:
return "Unknown"
elif state == 1:
return "Loading"
elif state == 2:
return "Discharging"
elif state == 3:
return "Empty"
elif state == 4:
return "Fully charged"
elif state == 5:
return "Pending charge"
elif state == 6:
return "Pending discharge"
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.capacity))
try:
self.power = UPowerManager()
self.device = self.power.get_display_device()
except Exception as e:
logging.exception("unable to get battery display device: {}".format(str(e)))
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics"
)
self._showremaining = util.format.asbool(self.parameter("showremaining", True))
def capacity(self, widget):
widget.set("capacity", -1)
widget.set("ac", False)
output = "n/a"
try:
capacity = int(self.power.get_device_percentage(self.device))
capacity = capacity if capacity < 100 else 100
widget.set("capacity", capacity)
output = "{}%".format(capacity)
widget.set("theme.minwidth", "100%")
except Exception as e:
logging.exception("unable to get battery capacity: {}".format(str(e)))
if self._showremaining:
try:
p = self.power # an alias to make each line of code shorter
proxy = p.bus.get_object(p.UPOWER_NAME, self.device)
interface = dbus.Interface(proxy, p.DBUS_PROPERTIES)
state = int(interface.Get(p.UPOWER_NAME + ".Device", "State"))
# state: 1 => charging, 2 => discharging, other => don't care
remain = int(
interface.Get(
p.UPOWER_NAME + ".Device",
["TimeToFull", "TimeToEmpty"][state - 1],
)
)
remain = util.format.duration(remain, compact=True, unit=True)
output = "{} {}".format(output, remain)
except IndexError:
pass
except Exception as e:
logging.exception(
"unable to get battery remaining time: {}".format(str(e))
)
return output
def state(self, widget):
state = []
capacity = widget.get("capacity", -1)
if capacity < 0:
return ["critical", "unknown"]
if capacity < int(self.parameter("critical", 10)):
state.append("critical")
elif capacity < int(self.parameter("warning", 20)):
state.append("warning")
if widget.get("ac"):
state.append("AC")
else:
charge = "Unknown"
try:
charge = self.power.get_state(self.device)
except Exception as e:
logging.exception("unable to get charge value: {}".format(str(e)))
if charge == "Discharging":
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))
)
)
else:
if capacity > 95:
state.append("charged")
else:
state.append("charging")
return state
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,209 @@
# pylint: disable=C0111,R0903
"""Displays battery status, remaining percentage and charging information.
Parameters:
* battery.device : Comma-separated list of battery devices to read information from (defaults to auto for auto-detection)
* battery.warning : Warning threshold in % of remaining charge (defaults to 20)
* battery.critical : Critical threshold in % of remaining charge (defaults to 10)
* battery.showdevice : If set to 'true', add the device name to the widget (defaults to False)
* battery.decorate : If set to 'false', hides additional icons (charging, etc.) (defaults to True)
* battery.showpowerconsumption: If set to 'true', show current power consumption (defaults to False)
* battery.compact-devices : If set to 'true', compacts multiple batteries into a single entry (default to False)
(partially) contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
"""
import os
import glob
import logging
log = logging.getLogger(__name__)
try:
import power
except ImportError:
log.warning('unable to import module "power": Time estimates will not be available')
import core.module
import core.input
import util.format
class BatteryManager(object):
def remaining(self):
try:
estimate = power.PowerManagement().get_time_remaining_estimate()
# do not show remaining if on AC
if estimate == power.common.TIME_REMAINING_UNLIMITED:
return None
return estimate * 60 # return value in seconds
except Exception as e:
return -1
return -1
def read(self, battery, component, default=None):
path = "/sys/class/power_supply/{}".format(battery)
if not os.path.exists(path):
return default
try:
with open("{}/{}".format(path, component)) as f:
return f.read().strip()
except IOError:
return "n/a"
return default
def capacity(self, battery):
capacity = self.read(battery, "capacity", 100)
if capacity != "n/a":
capacity = int(capacity)
return capacity if capacity < 100 else 100
def capacity_all(self, batteries):
now = 0
full = 0
for battery in batteries:
try:
with open(
"/sys/class/power_supply/{}/energy_full".format(battery)
) as f:
full += int(f.read())
with open("/sys/class/power_supply/{}/energy_now".format(battery)) as f:
now += int(f.read())
except IOError:
return "n/a"
return int(float(now) / float(full) * 100.0)
def isac(self, battery):
path = "/sys/class/power_supply/{}".format(battery)
return not os.path.exists(path)
def isac_any(self, batteries):
for battery in batteries:
if self.isac(battery):
return True
return False
def consumption(self, battery):
consumption = self.read(battery, "power_now", "n/a")
if consumption == "n/a":
return "n/a"
return "{}W".format(int(consumption) / 1000000)
def charge(self, battery):
return self.read(battery, "status", "n/a")
def charge_any(self, batteries):
for battery in batteries:
if self.charge(battery) == "Discharging":
return "Discharging"
return "Charged"
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.__manager = BatteryManager()
self._batteries = util.format.aslist(self.parameter("device", "auto"))
if self._batteries[0] == "auto":
self._batteries = [
os.path.basename(battery)
for battery in glob.glob("/sys/class/power_supply/BAT*")
]
if len(self._batteries) == 0:
raise Exceptions("no batteries configured/found")
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics"
)
if util.format.asbool(self.parameter("compact-devices", False)):
widget = self.add_widget(
full_text=self.capacity, name="all-batteries"
)
else:
for battery in self._batteries:
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:
widget.set("theme.exclude", "suffix")
def capacity(self, widget):
if widget.name == "all-batteries":
capacity = self.__manager.capacity_all(self._batteries)
else:
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:
output = "{}%".format(capacity)
if (
util.format.asbool(self.parameter("showremaining", True))
and self.__manager.charge(widget.name) == "Discharging"
):
remaining = self.__manager.remaining()
if remaining >= 0:
output = "{} {}".format(
output, util.format.duration(remaining, compact=True, unit=True)
)
if util.format.asbool(self.parameter("showdevice", False)):
output = "{} ({})".format(output, widget.name)
return output
def state(self, widget):
state = []
capacity = widget.get("capacity")
if capacity < 0:
log.debug("battery state: {}".format(state))
return ["critical", "unknown"]
if capacity < int(self.parameter("critical", 10)):
state.append("critical")
elif capacity < int(self.parameter("warning", 20)):
state.append("warning")
if widget.get("ac"):
state.append("AC")
else:
if widget.name == "all-batteries":
charge = self.__manager.charge_any(self._batteries)
else:
charge = self.__manager.charge(widget.name)
if charge == "Discharging":
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))
)
)
else:
if capacity > 95:
state.append("charged")
else:
state.append("charging")
return state
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,125 @@
"""Displays bluetooth status (Bluez). Left mouse click launches manager app,
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state.
Parameters:
* bluetooth.device : the device to read state from (default is hci0)
* bluetooth.manager : application to launch on click (blueman-manager)
* bluetooth.dbus_destination : dbus destination (defaults to org.blueman.Mechanism)
* bluetooth.dbus_destination_path : dbus destination path (defaults to /)
* bluetooth.right_click_popup : use popup menu when right-clicked (defaults to True)
contributed by `brunosmmm <https://github.com/brunosmmm>`_ - many thanks!
"""
import os
import re
import logging
import core.module
import core.widget
import core.input
import util.cli
import util.format
import util.popup
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.status))
device = self.parameter("device", "hci0")
self.manager = self.parameter("manager", "blueman-manager")
self._path = "/sys/class/bluetooth/{}".format(device)
self._status = "Off"
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager)
# determine whether to use pop-up menu or simply toggle the device on/off
right_click_popup = util.format.asbool(
self.parameter("right_click_popup", True)
)
if right_click_popup:
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.popup)
else:
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle)
def status(self, widget):
"""Get status."""
return self._status
def update(self):
"""Update current state."""
if not os.path.exists(self._path):
self._status = "?"
return
# search for whichever rfkill directory available
try:
dirnames = next(os.walk(self._path))[1]
for dirname in dirnames:
m = re.match(r"rfkill[0-9]+", dirname)
if m is not None:
with open(os.path.join(self._path, dirname, "state"), "r") as f:
state = int(f.read())
if state == 1:
self._status = "On"
else:
self._status = "Off"
return
except IOError:
self._status = "?"
def popup(self, widget):
"""Show a popup menu."""
menu = util.popup.PopupMenu()
if self._status == "On":
menu.add_menuitem("Disable Bluetooth")
elif self._status == "Off":
menu.add_menuitem("Enable Bluetooth")
else:
return
# 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."""
if self._status == "On":
state = "false"
else:
state = "true"
dst = self.parameter("dbus_destination", "org.blueman.Mechanism")
dst_path = self.parameter("dbus_destination_path", "/")
cmd = (
"dbus-send --system --print-reply --dest={}"
" {} org.blueman.Mechanism.SetRfkillState"
" boolean:{}".format(dst, dst_path, state)
)
logging.debug("bt: toggling bluetooth")
util.cli.execute(cmd)
def state(self, widget):
"""Get current state."""
state = []
if self._status == "?":
state = ["unknown"]
elif self._status == "On":
state = ["ON"]
else:
state = ["OFF"]
return state
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,103 @@
"""Displays bluetooth status. Left mouse click launches manager app,
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and
python-dbus to count the number of connections
Parameters:
* bluetooth.manager : application to launch on click (blueman-manager)
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
"""
import os
import re
import subprocess
import dbus
import dbus.mainloop.glib
import logging
import core.module
import core.widget
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))
self.manager = self.parameter("manager", "blueman-manager")
self._status = "Off"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self._bus = dbus.SystemBus()
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle)
def status(self, widget):
"""Get status."""
return self._status
def update(self):
"""Update current state."""
state = len(
subprocess.run(["bluetoothctl", "list"], stdout=subprocess.PIPE).stdout
)
if state > 0:
connected_devices = self.get_connected_devices()
self._status = "On - {}".format(connected_devices)
else:
self._status = "Off"
adapters_cmd = "rfkill list | grep Bluetooth"
if not len(
subprocess.run(adapters_cmd, shell=True, stdout=subprocess.PIPE).stdout
):
self._status = "No Adapter Found"
return
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")
core.util.execute(cmd)
def state(self, widget):
"""Get current state."""
state = []
if self._status == "No Adapter Found":
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")
else:
state.append("critical")
return state
def get_connected_devices(self):
devices = 0
objects = dbus.Interface(
self._bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager"
).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"):
devices += 1
return devices
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,70 @@
# pylint: disable=C0111,R0903
"""Displays the brightness of a display
Parameters:
* brightness.step: The amount of increase/decrease on scroll in % (defaults to 2)
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
"""
import glob
import shutil
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(seconds=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.brightness))
self.__brightness = "n/a"
self.__readcmd = None
step = self.parameter("step", 2)
if shutil.which("light"):
self.__readcmd = self.__light
self.register_cmd("light -A {}%".format(step), "light -U {}%".format(step))
elif shutil.which("brightnessctl"):
self.__readcmd = self.__brightnessctl
self.register_cmd(
"brightnessctl s {}%+".format(step), "brightnessctl s {}%-".format(step)
)
else:
self.__readcmd = self.__xbacklight
self.register_cmd(
"xbacklight +{}%".format(step), "xbacklight -{}%".format(step)
)
def register_cmd(self, up_cmd, down_cmd):
core.input.register(self, button=core.input.WHEEL_UP, cmd=up_cmd)
core.input.register(self, button=core.input.WHEEL_DOWN, cmd=down_cmd)
def brightness(self, widget):
return self.__brightness
def __light(self):
return util.cli.execute("light").strip()
def __brightnessctl(self):
m = util.cli.execute("brightnessctl m").strip()
g = util.cli.execute("brightnessctl g").strip()
return float(g) / float(m) * 100.0
def __xbacklight(self):
return util.cli.execute("xbacklight -get").strip()
def update(self):
try:
self.__brightness = "{:3.0f}%".format(float(self.__readcmd()))
except:
self.__brightness = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,117 @@
# pylint: disable=C0111,R0903,W0212
"""Enable/disable automatic screen locking.
Requires the following executables:
* xdg-screensaver
* xdotool
* xprop (as dependency for xdotool)
* notify-send
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
"""
import logging
import os
import shutil
import psutil
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=10)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(""))
self.__active = False
self.__xid = None
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__toggle)
def __check_requirements(self):
requirements = ["xdotool", "xprop", "xdg-screensaver"]
missing = []
for tool in requirements:
if not shutil.which(tool):
missing.append(tool)
return missing
def __get_i3bar_xid(self):
xid = (
util.cli.execute("xdotool search --class 'i3bar'")
.partition("\n")[0]
.strip()
)
if xid.isdigit():
return xid
logging.warning("Module caffeine: xdotool couldn't get X window ID of 'i3bar'.")
return None
def __notify(self):
if not shutil.which("notify-send"):
return
if self.__active:
util.cli.execute("notify-send 'Consuming caffeine'")
else:
util.cli.execute("notify-send 'Out of coffee'")
def _suspend_screensaver(self):
self.__xid = self.__get_i3bar_xid()
if self.__xid is None:
return False
pid = os.fork()
if pid == 0:
os.setsid()
util.cli.execute("xdg-screensaver suspend {}".format(self.__xid))
os._exit(0)
else:
os.waitpid(pid, 0)
return True
def __resume_screensaver(self):
success = True
xprop_path = shutil.which("xprop")
pids = [
p.pid
for p in psutil.process_iter()
if p.cmdline() == [xprop_path, "-id", str(self.__xid), "-spy"]
]
for pid in pids:
try:
os.kill(pid, 9)
except OSError:
success = False
return success
def state(self, _):
if self.__active:
return "activated"
return "deactivated"
def __toggle(self, _):
missing = self.__check_requirements()
if missing:
logging.warning("Could not run caffeine - missing %s!", ", ".join(missing))
return
self.__active = not self.__active
if self.__active:
success = self._suspend_screensaver()
else:
success = self.__resume_screensaver()
if success:
self.__notify()
else:
self.__active = not self.__active
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,156 @@
# pylint: disable=C0111,R0903
"""Displays information about the current song in cmus.
Requires the following executable:
* cmus-remote
Parameters:
* cmus.format: Format string for the song information. Tag values can be put in curly brackets (i.e. {artist})
Additional tags:
* {file} - full song file name
* {file1} - song file name without path prefix
if {file} = '/foo/bar.baz', then {file1} = 'bar.baz'
* {file2} - song file name without path prefix and extension suffix
if {file} = '/foo/bar.baz', then {file2} = 'bar'
* cmus.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles cmus.prev, cmus.next, cmus.shuffle and cmus.repeat, and the main display with play/pause function cmus.main.
* cmus.server: The address of the cmus server, either a UNIX socket or host[:port]. Connects to the local instance by default.
* cmus.passwd: The password to use for the TCP/IP connection.
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
"""
from collections import defaultdict
import os
import string
import core.module
import core.input
import core.decorators
import util.cli
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self._layout = self.parameter(
"layout", "cmus.prev cmus.main cmus.next cmus.shuffle cmus.repeat"
)
self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}")
self._server = self.parameter("server", None)
self._passwd = self.parameter("passwd", None)
self._status = None
self._shuffle = False
self._repeat = False
self._tags = defaultdict(lambda: "")
# Create widgets
widget_map = {}
for widget_name in self._layout.split():
widget = self.add_widget(name=widget_name)
self._cmd = "cmus-remote"
if self._server is not None:
self._cmd = "{cmd} --server {server}".format(
cmd=self._cmd, server=self._server
)
if self._passwd is not None:
self._cmd = "{cmd} --passwd {passwd}".format(
cmd=self._cmd, passwd=self._passwd
)
if widget_name == "cmus.prev":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "{cmd} -r".format(cmd=self._cmd),
}
elif widget_name == "cmus.main":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "{cmd} -u".format(cmd=self._cmd),
}
widget.full_text(self.description)
elif widget_name == "cmus.next":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "{cmd} -n".format(cmd=self._cmd),
}
elif widget_name == "cmus.shuffle":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "{cmd} -S".format(cmd=self._cmd),
}
elif widget_name == "cmus.repeat":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "{cmd} -R".format(cmd=self._cmd),
}
else:
raise KeyError(
"The cmus module does not support a {widget_name!r} widget".format(
widget_name=widget_name
)
)
# Register input callbacks
for widget, callback_options in widget_map.items():
core.input.register(widget, **callback_options)
def hidden(self):
return self._status is None
@core.decorators.scrollable
def description(self, widget):
return string.Formatter().vformat(self._fmt, (), self._tags)
def update(self):
self._load_song()
def state(self, widget):
returns = {
"cmus.shuffle": "shuffle-on" if self._shuffle else "shuffle-off",
"cmus.repeat": "repeat-on" if self._repeat else "repeat-off",
"cmus.prev": "prev",
"cmus.next": "next",
}
return returns.get(widget.name, self._status)
def _eval_line(self, line):
if line.startswith("file "):
full_file = line[5:]
file1 = os.path.basename(full_file)
file2 = os.path.splitext(file1)[0]
self._tags.update({"file": full_file})
self._tags.update({"file1": file1})
self._tags.update({"file2": file2})
return
name, key, value = (line.split(" ", 2) + [None, None])[:3]
if name == "status":
self._status = key
if name == "tag":
self._tags.update({key: value})
if name in ["duration", "position"]:
self._tags.update({name: util.format.duration(int(key))})
if name == "set" and key == "repeat":
self._repeat = value == "true"
if name == "set" and key == "shuffle":
self._shuffle = value == "true"
def _load_song(self):
info = ""
try:
info = util.cli.execute("{cmd} -Q".format(cmd=self._cmd))
except RuntimeError:
self._status = None
self._tags = defaultdict(lambda: "")
for line in info.split("\n"):
self._eval_line(line)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,154 @@
"""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:
* cpu2.layout: Space-separated list of widgets to add.
Possible widgets are:
* cpu2.maxfreq
* cpu2.cpuload
* cpu2.coresload
* cpu2.temp
* cpu2.fanspeed
* cpu2.colored: 1 for colored per core load graph, 0 for mono (default)
* cpu2.temp_pattern: pattern to look for in the output of 'sensors -u';
required if cpu2.temp widged is used
* cpu2.fan_pattern: pattern to look for in the output of 'sensors -u';
required if cpu2.fanspeed widged is used
Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're
lacking the aforementioned pattern settings or they have wrong values.
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
"""
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", "cpu2.maxfreq cpu2.cpuload cpu2.coresload cpu2.temp cpu2.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 == "cpu2.maxfreq":
widget = self.add_widget(name=widget_name, full_text=self.maxfreq)
widget.set("type", "freq")
elif widget_name == "cpu2.cpuload":
widget = self.add_widget(name=widget_name, full_text=self.cpuload)
widget.set("type", "load")
elif widget_name == "cpu2.coresload":
widget = self.add_widget(name=widget_name, full_text=self.coresload)
widget.set("type", "loads")
elif widget_name == "cpu2.temp":
widget = self.add_widget(name=widget_name, full_text=self.temp)
widget.set("type", "temp")
elif widget_name == "cpu2.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_pattern = self.parameter("temp_pattern")
if self.__temp_pattern is None:
self.__temp = "n/a"
self.__fan_pattern = self.parameter("fan_pattern")
if self.__fan_pattern is None:
self.__fan = "n/a"
# maxfreq is loaded only once at startup
if "cpu2.maxfreq" in self.__widget_names:
self.__maxfreq = psutil.cpu_freq().max / 1000
def maxfreq(self, _):
return "{:.2f}GHz".format(self.__maxfreq)
def cpuload(self, _):
return "{:>3}%".format(self.__cpuload)
def add_color(self, bar):
"""add color as pango markup to a bar"""
if bar in ["", ""]:
color = self.theme.color("green", "green")
elif bar in ["", ""]:
color = self.theme.color("yellow", "yellow")
elif bar in ["", ""]:
color = self.theme.color("orange", "orange")
elif bar in ["", ""]:
color = self.theme.color("red", "red")
colored_bar = '<span foreground="{}">{}</span>'.format(color, bar)
return colored_bar
def coresload(self, _):
mono_bars = [util.graph.hbar(x) for x in self.__coresload]
if not self.__colored:
return "".join(mono_bars)
colored_bars = [self.add_color(x) for x in mono_bars]
return "".join(colored_bars)
def temp(self, _):
if self.__temp == "n/a" or self.__temp == 0:
return "n/a"
return "{}°C".format(self.__temp)
def fanspeed(self, _):
if self.__fanspeed == "n/a":
return "n/a"
return "{}RPM".format(self.__fanspeed)
def _parse_sensors_output(self):
output = util.cli.execute("sensors -u")
lines = output.split("\n")
temp = "n/a"
fan = "n/a"
temp_line = None
fan_line = None
for line in lines:
if self.__temp_pattern is not None and self.__temp_pattern in line:
temp_line = line
if self.__fan_pattern is not None and self.__fan_pattern in line:
fan_line = line
if temp_line is not None and fan_line is not None:
break
if temp_line is not None:
temp = round(float(temp_line.split(":")[1].strip()))
if fan_line is not None:
fan = int(fan_line.split(":")[1].strip()[:-4])
return temp, fan
def update(self):
if "cpu2.maxfreq" in self.__widget_names:
self.__maxfreq = psutil.cpu_freq().max / 1000
if "cpu2.cpuload" in self.__widget_names:
self.__cpuload = round(psutil.cpu_percent(percpu=False))
if "cpu2.coresload" in self.__widget_names:
self.__coresload = psutil.cpu_percent(percpu=True)
if "cpu2.temp" in self.__widget_names or "cpu2.fanspeed" in self.__widget_names:
self.__temp, self.__fanspeed = self._parse_sensors_output()
def state(self, widget):
"""for having per-widget icons"""
return [widget.get("type", "")]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,372 @@
# -*- coding: UTF-8 -*-
# pylint: disable=C0111,R0903
"""Displays currency exchange rates. Currently, displays currency between GBP and USD/EUR only.
Requires the following python packages:
* requests
Parameters:
* currency.interval: Interval in minutes between updates, default is 1.
* currency.source: Source currency (ex. 'GBP', 'EUR'). Defaults to 'auto', which infers the local one from IP address.
* currency.destination: Comma-separated list of destination currencies (defaults to 'USD,EUR')
* currency.sourceformat: String format for source formatting; Defaults to '{}: {}' and has two variables,
the base symbol and the rate list
* currency.destinationdelimiter: Delimiter used for separating individual rates (defaults to '|')
Note: source and destination names right now must correspond to the names used by the API of https://markets.ft.com
contributed by `AntouanK <https://github.com/AntouanK>`_ - many thanks!
"""
import requests
try:
from babel.numbers import format_currency
except ImportError:
format_currency = None
import json
import os
import core.module
import core.widget
import core.decorators
import util.format
import util.location
SYMBOL = {"GBP": "£", "EUR": "", "USD": "$", "JPY": "¥", "KRW": ""}
DEFAULT_DEST = "USD,EUR,auto"
DEFAULT_SRC = "GBP"
API_URL = "https://markets.ft.com/data/currencies/ajax/conversion?baseCurrency={}&comparison={}"
def load_country_to_currency():
return [
{"country": "Afghanistan", "currency_code": "AFN"},
{"country": "Albania", "currency_code": "ALL"},
{"country": "Algeria", "currency_code": "DZD"},
{"country": "American Samoa", "currency_code": "USD"},
{"country": "Andorra", "currency_code": "EUR"},
{"country": "Angola", "currency_code": "AOA"},
{"country": "Anguilla", "currency_code": "XCD"},
{"country": "Antarctica", "currency_code": "XCD"},
{"country": "Antigua and Barbuda", "currency_code": "XCD"},
{"country": "Argentina", "currency_code": "ARS"},
{"country": "Armenia", "currency_code": "AMD"},
{"country": "Aruba", "currency_code": "AWG"},
{"country": "Australia", "currency_code": "AUD"},
{"country": "Austria", "currency_code": "EUR"},
{"country": "Azerbaijan", "currency_code": "AZN"},
{"country": "Bahamas", "currency_code": "BSD"},
{"country": "Bahrain", "currency_code": "BHD"},
{"country": "Bangladesh", "currency_code": "BDT"},
{"country": "Barbados", "currency_code": "BBD"},
{"country": "Belarus", "currency_code": "BYR"},
{"country": "Belgium", "currency_code": "EUR"},
{"country": "Belize", "currency_code": "BZD"},
{"country": "Benin", "currency_code": "XOF"},
{"country": "Bermuda", "currency_code": "BMD"},
{"country": "Bhutan", "currency_code": "BTN"},
{"country": "Bolivia", "currency_code": "BOB"},
{"country": "Bosnia and Herzegovina", "currency_code": "BAM"},
{"country": "Botswana", "currency_code": "BWP"},
{"country": "Bouvet Island", "currency_code": "NOK"},
{"country": "Brazil", "currency_code": "BRL"},
{"country": "British Indian Ocean Territory", "currency_code": "USD"},
{"country": "Brunei", "currency_code": "BND"},
{"country": "Bulgaria", "currency_code": "BGN"},
{"country": "Burkina Faso", "currency_code": "XOF"},
{"country": "Burundi", "currency_code": "BIF"},
{"country": "Cambodia", "currency_code": "KHR"},
{"country": "Cameroon", "currency_code": "XAF"},
{"country": "Canada", "currency_code": "CAD"},
{"country": "Cape Verde", "currency_code": "CVE"},
{"country": "Cayman Islands", "currency_code": "KYD"},
{"country": "Central African Republic", "currency_code": "XAF"},
{"country": "Chad", "currency_code": "XAF"},
{"country": "Chile", "currency_code": "CLP"},
{"country": "China", "currency_code": "CNY"},
{"country": "Christmas Island", "currency_code": "AUD"},
{"country": "Cocos (Keeling) Islands", "currency_code": "AUD"},
{"country": "Colombia", "currency_code": "COP"},
{"country": "Comoros", "currency_code": "KMF"},
{"country": "Congo", "currency_code": "XAF"},
{"country": "Cook Islands", "currency_code": "NZD"},
{"country": "Costa Rica", "currency_code": "CRC"},
{"country": "Croatia", "currency_code": "HRK"},
{"country": "Cuba", "currency_code": "CUP"},
{"country": "Cyprus", "currency_code": "EUR"},
{"country": "Czech Republic", "currency_code": "CZK"},
{"country": "Denmark", "currency_code": "DKK"},
{"country": "Djibouti", "currency_code": "DJF"},
{"country": "Dominica", "currency_code": "XCD"},
{"country": "Dominican Republic", "currency_code": "DOP"},
{"country": "East Timor", "currency_code": "USD"},
{"country": "Ecuador", "currency_code": "ECS"},
{"country": "Egypt", "currency_code": "EGP"},
{"country": "El Salvador", "currency_code": "SVC"},
{"country": "England", "currency_code": "GBP"},
{"country": "Equatorial Guinea", "currency_code": "XAF"},
{"country": "Eritrea", "currency_code": "ERN"},
{"country": "Estonia", "currency_code": "EUR"},
{"country": "Ethiopia", "currency_code": "ETB"},
{"country": "Falkland Islands", "currency_code": "FKP"},
{"country": "Faroe Islands", "currency_code": "DKK"},
{"country": "Fiji Islands", "currency_code": "FJD"},
{"country": "Finland", "currency_code": "EUR"},
{"country": "France", "currency_code": "EUR"},
{"country": "French Guiana", "currency_code": "EUR"},
{"country": "French Polynesia", "currency_code": "XPF"},
{"country": "French Southern territories", "currency_code": "EUR"},
{"country": "Gabon", "currency_code": "XAF"},
{"country": "Gambia", "currency_code": "GMD"},
{"country": "Georgia", "currency_code": "GEL"},
{"country": "Germany", "currency_code": "EUR"},
{"country": "Ghana", "currency_code": "GHS"},
{"country": "Gibraltar", "currency_code": "GIP"},
{"country": "Greece", "currency_code": "EUR"},
{"country": "Greenland", "currency_code": "DKK"},
{"country": "Grenada", "currency_code": "XCD"},
{"country": "Guadeloupe", "currency_code": "EUR"},
{"country": "Guam", "currency_code": "USD"},
{"country": "Guatemala", "currency_code": "QTQ"},
{"country": "Guinea", "currency_code": "GNF"},
{"country": "Guinea-Bissau", "currency_code": "CFA"},
{"country": "Guyana", "currency_code": "GYD"},
{"country": "Haiti", "currency_code": "HTG"},
{"country": "Heard Island and McDonald Islands", "currency_code": "AUD"},
{"country": "Holy See (Vatican City State)", "currency_code": "EUR"},
{"country": "Honduras", "currency_code": "HNL"},
{"country": "Hong Kong", "currency_code": "HKD"},
{"country": "Hungary", "currency_code": "HUF"},
{"country": "Iceland", "currency_code": "ISK"},
{"country": "India", "currency_code": "INR"},
{"country": "Indonesia", "currency_code": "IDR"},
{"country": "Iran", "currency_code": "IRR"},
{"country": "Iraq", "currency_code": "IQD"},
{"country": "Ireland", "currency_code": "EUR"},
{"country": "Israel", "currency_code": "ILS"},
{"country": "Italy", "currency_code": "EUR"},
{"country": "Ivory Coast", "currency_code": "XOF"},
{"country": "Jamaica", "currency_code": "JMD"},
{"country": "Japan", "currency_code": "JPY"},
{"country": "Jordan", "currency_code": "JOD"},
{"country": "Kazakhstan", "currency_code": "KZT"},
{"country": "Kenya", "currency_code": "KES"},
{"country": "Kiribati", "currency_code": "AUD"},
{"country": "Kuwait", "currency_code": "KWD"},
{"country": "Kyrgyzstan", "currency_code": "KGS"},
{"country": "Laos", "currency_code": "LAK"},
{"country": "Latvia", "currency_code": "LVL"},
{"country": "Lebanon", "currency_code": "LBP"},
{"country": "Lesotho", "currency_code": "LSL"},
{"country": "Liberia", "currency_code": "LRD"},
{"country": "Libyan Arab Jamahiriya", "currency_code": "LYD"},
{"country": "Liechtenstein", "currency_code": "CHF"},
{"country": "Lithuania", "currency_code": "LTL"},
{"country": "Luxembourg", "currency_code": "EUR"},
{"country": "Macao", "currency_code": "MOP"},
{"country": "North Macedonia", "currency_code": "MKD"},
{"country": "Madagascar", "currency_code": "MGF"},
{"country": "Malawi", "currency_code": "MWK"},
{"country": "Malaysia", "currency_code": "MYR"},
{"country": "Maldives", "currency_code": "MVR"},
{"country": "Mali", "currency_code": "XOF"},
{"country": "Malta", "currency_code": "EUR"},
{"country": "Marshall Islands", "currency_code": "USD"},
{"country": "Martinique", "currency_code": "EUR"},
{"country": "Mauritania", "currency_code": "MRO"},
{"country": "Mauritius", "currency_code": "MUR"},
{"country": "Mayotte", "currency_code": "EUR"},
{"country": "Mexico", "currency_code": "MXN"},
{"country": "Micronesia, Federated States of", "currency_code": "USD"},
{"country": "Moldova", "currency_code": "MDL"},
{"country": "Monaco", "currency_code": "EUR"},
{"country": "Mongolia", "currency_code": "MNT"},
{"country": "Montserrat", "currency_code": "XCD"},
{"country": "Morocco", "currency_code": "MAD"},
{"country": "Mozambique", "currency_code": "MZN"},
{"country": "Myanmar", "currency_code": "MMR"},
{"country": "Namibia", "currency_code": "NAD"},
{"country": "Nauru", "currency_code": "AUD"},
{"country": "Nepal", "currency_code": "NPR"},
{"country": "Netherlands", "currency_code": "EUR"},
{"country": "Netherlands Antilles", "currency_code": "ANG"},
{"country": "New Caledonia", "currency_code": "XPF"},
{"country": "New Zealand", "currency_code": "NZD"},
{"country": "Nicaragua", "currency_code": "NIO"},
{"country": "Niger", "currency_code": "XOF"},
{"country": "Nigeria", "currency_code": "NGN"},
{"country": "Niue", "currency_code": "NZD"},
{"country": "Norfolk Island", "currency_code": "AUD"},
{"country": "North Korea", "currency_code": "KPW"},
{"country": "Northern Ireland", "currency_code": "GBP"},
{"country": "Northern Mariana Islands", "currency_code": "USD"},
{"country": "Norway", "currency_code": "NOK"},
{"country": "Oman", "currency_code": "OMR"},
{"country": "Pakistan", "currency_code": "PKR"},
{"country": "Palau", "currency_code": "USD"},
{"country": "Palestine", "currency_code": null},
{"country": "Panama", "currency_code": "PAB"},
{"country": "Papua New Guinea", "currency_code": "PGK"},
{"country": "Paraguay", "currency_code": "PYG"},
{"country": "Peru", "currency_code": "PEN"},
{"country": "Philippines", "currency_code": "PHP"},
{"country": "Pitcairn", "currency_code": "NZD"},
{"country": "Poland", "currency_code": "PLN"},
{"country": "Portugal", "currency_code": "EUR"},
{"country": "Puerto Rico", "currency_code": "USD"},
{"country": "Qatar", "currency_code": "QAR"},
{"country": "Reunion", "currency_code": "EUR"},
{"country": "Romania", "currency_code": "RON"},
{"country": "Russian Federation", "currency_code": "RUB"},
{"country": "Rwanda", "currency_code": "RWF"},
{"country": "Saint Helena", "currency_code": "SHP"},
{"country": "Saint Kitts and Nevis", "currency_code": "XCD"},
{"country": "Saint Lucia", "currency_code": "XCD"},
{"country": "Saint Pierre and Miquelon", "currency_code": "EUR"},
{"country": "Saint Vincent and the Grenadines", "currency_code": "XCD"},
{"country": "Samoa", "currency_code": "WST"},
{"country": "San Marino", "currency_code": "EUR"},
{"country": "Sao Tome and Principe", "currency_code": "STD"},
{"country": "Saudi Arabia", "currency_code": "SAR"},
{"country": "Scotland", "currency_code": "GBP"},
{"country": "Senegal", "currency_code": "XOF"},
{"country": "Seychelles", "currency_code": "SCR"},
{"country": "Sierra Leone", "currency_code": "SLL"},
{"country": "Singapore", "currency_code": "SGD"},
{"country": "Slovakia", "currency_code": "EUR"},
{"country": "Slovenia", "currency_code": "EUR"},
{"country": "Solomon Islands", "currency_code": "SBD"},
{"country": "Somalia", "currency_code": "SOS"},
{"country": "South Africa", "currency_code": "ZAR"},
{
"country": "South Georgia and the South Sandwich Islands",
"currency_code": "GBP",
},
{"country": "South Korea", "currency_code": "KRW"},
{"country": "South Sudan", "currency_code": "SSP"},
{"country": "Spain", "currency_code": "EUR"},
{"country": "SriLanka", "currency_code": "LKR"},
{"country": "Sudan", "currency_code": "SDG"},
{"country": "Suriname", "currency_code": "SRD"},
{"country": "Svalbard and Jan Mayen", "currency_code": "NOK"},
{"country": "Swaziland", "currency_code": "SZL"},
{"country": "Sweden", "currency_code": "SEK"},
{"country": "Switzerland", "currency_code": "CHF"},
{"country": "Syria", "currency_code": "SYP"},
{"country": "Tajikistan", "currency_code": "TJS"},
{"country": "Tanzania", "currency_code": "TZS"},
{"country": "Thailand", "currency_code": "THB"},
{"country": "The Democratic Republic of Congo", "currency_code": "CDF"},
{"country": "Togo", "currency_code": "XOF"},
{"country": "Tokelau", "currency_code": "NZD"},
{"country": "Tonga", "currency_code": "TOP"},
{"country": "Trinidad and Tobago", "currency_code": "TTD"},
{"country": "Tunisia", "currency_code": "TND"},
{"country": "Turkey", "currency_code": "TRY"},
{"country": "Turkmenistan", "currency_code": "TMT"},
{"country": "Turks and Caicos Islands", "currency_code": "USD"},
{"country": "Tuvalu", "currency_code": "AUD"},
{"country": "Uganda", "currency_code": "UGX"},
{"country": "Ukraine", "currency_code": "UAH"},
{"country": "United Arab Emirates", "currency_code": "AED"},
{"country": "United Kingdom", "currency_code": "GBP"},
{"country": "United States", "currency_code": "USD"},
{"country": "United States Minor Outlying Islands", "currency_code": "USD"},
{"country": "Uruguay", "currency_code": "UYU"},
{"country": "Uzbekistan", "currency_code": "UZS"},
{"country": "Vanuatu", "currency_code": "VUV"},
{"country": "Venezuela", "currency_code": "VEF"},
{"country": "Vietnam", "currency_code": "VND"},
{"country": "Virgin Islands, British", "currency_code": "USD"},
{"country": "Virgin Islands, U.S.", "currency_code": "USD"},
{"country": "Wales", "currency_code": "GBP"},
{"country": "Wallis and Futuna", "currency_code": "XPF"},
{"country": "Western Sahara", "currency_code": "MAD"},
{"country": "Yemen", "currency_code": "YER"},
{"country": "Yugoslavia", "currency_code": null},
{"country": "Zambia", "currency_code": "ZMW"},
{"country": "Zimbabwe", "currency_code": "ZWD"},
]
class Module(core.module.Module):
@core.decorators.every(minutes=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.price))
self.__data = []
src = self.parameter("source", DEFAULT_SRC)
if src == "auto":
self.__base = self.find_local_currency()
else:
self.__base = src
self.__symbols = []
for d in util.format.aslist(self.parameter("destination", DEFAULT_DEST)):
if d == "auto":
new = self.find_local_currency()
else:
new = d
if new != self.__base:
self.__symbols.append(new)
def price(self, widget):
if len(self.__data) == 0:
return "?"
rates = []
for sym, rate in self.__data:
rate_float = float(rate.replace(",", ""))
if format_currency:
rates.append(format_currency(rate_float, sym))
else:
rate = self.fmt_rate(rate)
rates.append("{}{}".format(rate, SYMBOL[sym] if sym in SYMBOL else sym))
basefmt = "{}".format(self.parameter("sourceformat", "{}={}"))
ratefmt = "{}".format(self.parameter("destinationdelimiter", "="))
if format_currency:
base_val = format_currency(1, self.__base)
else:
base_val = "1{}".format(
SYMBOL[self.__base] if self.__base in SYMBOL else self.__base
)
return basefmt.format(base_val, ratefmt.join(rates))
def update(self):
self.__data = []
for symbol in self.__symbols:
url = API_URL.format(self.__base, symbol)
try:
response = requests.get(url).json()
self.__data.append((symbol, response["data"]["exchangeRate"]))
except Exception:
pass
def find_local_currency(self):
"""Use geolocation lookup to find local currency"""
try:
country = util.location.country()
currency_map = load_country_to_currency()
return currency_map.get(country, DEFAULT_SRC)
except:
return DEFAULT_SRC
def fmt_rate(self, rate):
float_rate = float(rate.replace(",", ""))
if not 0.01 < float_rate < 100:
ret = rate
else:
ret = "%.3g" % float_rate
return ret
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,110 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time with timezone options.
Parameters:
* datetimetz.format : strftime()-compatible formatting string
* datetimetz.timezone : IANA timezone name
* datetz.format : alias for datetimetz.format
* timetz.format : alias for datetimetz.format
* timetz.timezone : alias for datetimetz.timezone
* datetimetz.locale : locale to use rather than the system default
* datetz.locale : alias for datetimetz.locale
* timetz.locale : alias for datetimetz.locale
* timetz.timezone : alias for datetimetz.timezone
contributed by `frankzhao <https://github.com/frankzhao>`_ - many thanks!
"""
from __future__ import absolute_import
import datetime
import locale
import logging
import pytz
import tzlocal
import core.module
import core.widget
import core.input
import util.format
def default_format(module):
default = "%x %X %Z"
if module == "datetz":
default = "%x %Z"
if module == "timetz":
default = "%X %Z"
return default
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.get_time))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_tz)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.prev_tz)
self.__fmt = self.parameter("format", self.default_format())
default_timezone = ""
try:
default_timezone = tzlocal.get_localzone().zone
except Exception as e:
logging.error("unable to get default timezone: {}".format(str(e)))
try:
self._timezones = util.format.aslist(
self.parameter("timezone", default_timezone)
)
except:
self._timezones = [default_timezone]
self._current_tz = 0
l = locale.getdefaultlocale()
if not l or l == (None, None):
l = ("en_US", "UTF-8")
lcl = self.parameter("locale", ".".join(l))
try:
locale.setlocale(locale.LC_TIME, lcl.split("."))
except Exception:
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
def default_format(self):
return "%x %X %Z"
def get_time(self, widget):
try:
try:
tz = pytz.timezone(self._timezones[self._current_tz].strip())
retval = (
datetime.datetime.now(tz=tzlocal.get_localzone())
.astimezone(tz)
.strftime(self.__fmt)
)
except pytz.exceptions.UnknownTimeZoneError:
retval = "[Unknown timezone: {}]".format(
self._timezones[self._current_tz].strip()
)
except Exception as e:
logging.error("unable to get time: {}".format(str(e)))
retval = "[n/a]"
enc = locale.getpreferredencoding()
if hasattr(retval, "decode"):
return retval.decode(enc)
return retval
def next_tz(self, event):
next_timezone = self._current_tz + 1
if next_timezone >= len(self._timezones):
next_timezone = 0 # wraparound
self._current_tz = next_timezone
def prev_tz(self, event):
previous_timezone = self._current_tz - 1
if previous_timezone < 0:
previous_timezone = 0 # wraparound
self._current_tz = previous_timezone
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,23 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time.
Parameters:
* date.format: strftime()-compatible formatting string
* date.locale: locale to use rather than the system default
"""
import core.decorators
from .datetimetz import Module
class Module(Module):
@core.decorators.every(hours=1)
def __init__(self, config, theme):
super().__init__(config, theme)
def default_format(self):
return "%x %Z"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,149 @@
# pylint: disable=C0111,R0903
"""Displays the current song being played in DeaDBeeF and provides
some media control bindings.
Left click toggles pause, scroll up skips the current song, scroll
down returns to the previous song.
Requires the following library:
* subprocess
Parameters:
* deadbeef.format: Format string (defaults to '{artist} - {title}')
Available values are: {artist}, {title}, {album}, {length},
{trackno}, {year}, {comment},
{copyright}, {time}
This is deprecated, but much simpler.
* deadbeef.tf_format: A foobar2000 title formatting-style format string.
These can be much more sophisticated than the standard
format strings. This is off by default, but specifying
any tf_format will enable it. If both deadbeef.format
and deadbeef.tf_format are specified, deadbeef.tf_format
takes priority.
* deadbeef.tf_format_if_stopped: Controls whether or not the tf_format format
string should be displayed even if no song is paused or
playing. This could be useful if you want to implement
your own stop strings with the built in logic. Any non-
null value will enable this (by default the module will
hide itself when the player is stopped).
* deadbeef.previous: Change binding for previous song (default is left click)
* deadbeef.next: Change binding for next song (default is right click)
* deadbeef.pause: Change binding for toggling pause (default is middle click)
Available options for deadbeef.previous, deadbeef.next and deadbeef.pause are:
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
contributed by `joshbarrass <https://github.com/joshbarrass>`_ - many thanks!
"""
import sys
import subprocess
import logging
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.deadbeef))
buttons = {
"LEFT_CLICK": core.input.LEFT_MOUSE,
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
"SCROLL_UP": core.input.WHEEL_UP,
"SCROLL_DOWN": core.input.WHEEL_DOWN,
}
self._song = ""
self._format = self.parameter("format", "{artist} - {title}")
self._tf_format = self.parameter("tf_format", "")
self._show_tf_when_stopped = util.format.asbool(
self.parameter("tf_format_if_stopped", False)
)
prev_button = self.parameter("previous", "LEFT_CLICK")
next_button = self.parameter("next", "RIGHT_CLICK")
pause_button = self.parameter("pause", "MIDDLE_CLICK")
self.now_playing = "deadbeef --nowplaying %a;%t;%b;%l;%n;%y;%c;%r;%e"
self.now_playing_tf = "deadbeef --nowplaying-tf "
cmd = "deadbeef "
core.input.register(self, button=buttons[prev_button], cmd=cmd + "--prev")
core.input.register(self, button=buttons[next_button], cmd=cmd + "--next")
core.input.register(
self, button=buttons[pause_button], cmd=cmd + "--play-pause"
)
# modify the tf_format if we don't want it to show on stop
# this adds conditions to the query itself, rather than
# polling to see if deadbeef is running
# doing this reduces the number of calls we have to make
if self._tf_format and not self._show_tf_when_stopped:
self._tf_format = "$if($or(%isplaying%,%ispaused%),{query})".format(
query=self._tf_format
)
@core.decorators.scrollable
def deadbeef(self, widget):
return self.string_song
def hidden(self):
return self.string_song == ""
def update(self):
widgets = self.widgets()
try:
if self._tf_format == "": # no tf format set, use the old style
return self.update_standard(widgets)
return self.update_tf(widgets)
except Exception as e:
logging.exception(e)
self._song = "error"
def update_tf(self, widgets):
## ensure that deadbeef is actually running
## easiest way to do this is to check --nowplaying for
## the string 'nothing'
if util.cli.execute(self.now_playing) == "nothing":
self._song = ""
return
## perform the actual query -- these can be much more sophisticated
data = util.cli.execute(self.now_playing_tf + self._tf_format)
self._song = data
def update_standard(self, widgets):
data = util.cli.execute(self.now_playing)
if data == "nothing":
self._song = ""
else:
data = data.split(";")
self._song = self._format.format(
artist=data[0],
title=data[1],
album=data[2],
length=data[3],
trackno=data[4],
year=data[5],
comment=data[6],
copyright=data[7],
time=data[8],
)
@property
def string_song(self):
"""\
Returns the current song as a string, either as a unicode() (Python <
3) or a regular str() (Python >= 3)
"""
if sys.version_info.major < 3:
return unicode(self._song)
return str(self._song)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,84 @@
# pylint: disable=C0111,R0903
"""Displays the current song being played
Requires the following library:
* python-dbus
Parameters:
* deezer.format: Format string (defaults to '{artist} - {title}')
Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus}
* deezer.previous: Change binding for previous song (default is left click)
* deezer.next: Change binding for next song (default is right click)
* deezer.pause: Change binding for toggling pause (default is middle click)
Available options for deezer.previous, deezer.next and deezer.pause are:
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
contributed by `wwmoraes <https://github.com/wwmoraes>`_ - many thanks!
"""
import dbus
import core.module
import core.widget
import core.input
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.deezer))
buttons = {
"LEFT_CLICK": core.input.LEFT_MOUSE,
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
"SCROLL_UP": core.input.WHEEL_UP,
"SCROLL_DOWN": core.input.WHEEL_DOWN,
}
self._song = ""
self._format = self.parameter("format", "{artist} - {title}")
prev_button = self.parameter("previous", "LEFT_CLICK")
next_button = self.parameter("next", "RIGHT_CLICK")
pause_button = self.parameter("pause", "MIDDLE_CLICK")
cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.deezer \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous")
core.input.register(self, button=buttons[next_button], cmd=cmd + "Next")
core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause")
def deezer(self, widget):
return str(self._song)
def hidden(self):
return str(self._song) == ""
def update(self):
try:
bus = dbus.SessionBus()
deezer = bus.get_object(
"org.mpris.MediaPlayer2.deezer", "/org/mpris/MediaPlayer2"
)
deezer_iface = dbus.Interface(deezer, "org.freedesktop.DBus.Properties")
props = deezer_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
playback_status = str(
deezer_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
)
self._song = self._format.format(
album=str(props.get("xesam:album")),
title=str(props.get("xesam:title")),
artist=",".join(props.get("xesam:artist")),
trackNumber=str(props.get("xesam:trackNumber")),
playbackStatus="\u25B6"
if playback_status == "Playing"
else "\u258D\u258D"
if playback_status == "Paused"
else "",
)
except Exception:
self._song = ""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,83 @@
# pylint: disable=C0111,R0903
"""Displays DNF package update information (<security>/<bugfixes>/<enhancements>/<other>)
Requires the following executable:
* dnf
Parameters:
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
"""
import threading
import core.event
import core.module
import core.widget
import core.decorators
import util.cli
def get_dnf_info(widget):
res = util.cli.execute("dnf updateinfo", ignore_errors=True)
security = 0
bugfixes = 0
enhancements = 0
other = 0
for line in res.split("\n"):
if not line.startswith(" "):
continue
elif "ecurity" in line:
for s in line.split():
if s.isdigit():
security += int(s)
elif "ugfix" in line:
for s in line.split():
if s.isdigit():
bugfixes += int(s)
elif "hancement" in line:
for s in line.split():
if s.isdigit():
enhancements += int(s)
else:
for s in line.split():
if s.isdigit():
other += int(s)
widget.set("security", security)
widget.set("bugfixes", bugfixes)
widget.set("enhancements", enhancements)
widget.set("other", other)
core.event.trigger("update", [widget.module.id], redraw_only=True)
class Module(core.module.Module):
@core.decorators.every(minutes=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.updates))
def updates(self, widget):
result = []
for t in ["security", "bugfixes", "enhancements", "other"]:
result.append(str(widget.get(t, 0)))
return "/".join(result)
def update(self):
thread = threading.Thread(target=get_dnf_info, args=(self.widget(),))
thread.start()
def state(self, widget):
cnt = 0
for t in ["security", "bugfixes", "enhancements", "other"]:
cnt += widget.get(t, 0)
if cnt == 0:
return "good"
if cnt > 50 or widget.get("security", 0) > 0:
return "critical"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
"""Displays the number of docker containers running
Requires the following python packages:
* docker
contributed by `jlopezzarza <https://github.com/jlopezzarza>`_ - many thanks!
"""
import docker
from requests.exceptions import ConnectionError
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.docker_info))
self.__info = ""
def state(self, widget):
state = []
if self.__info == "OK - 0":
state.append("warning")
elif self.__info in ["n/a", "off"]:
state.append("critical")
return state
def docker_info(self, widget):
try:
cli = docker.DockerClient(base_url="unix://var/run/docker.sock")
cli.ping()
self.__info = "OK - {}".format(
len(cli.containers.list(filters={"status": "running"}))
)
except ConnectionError:
self.__info = "off"
except Exception:
self.__info = "n/a"
return self.__info
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,37 @@
# pylint: disable=C0111,R0903
"""Toggle dunst notifications.
contributed by `eknoes <https://github.com/eknoes>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(""))
self._paused = False
# Make sure that dunst is currently not paused
util.cli.execute("killall -s SIGUSR2 dunst", ignore_errors=True)
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status)
def toggle_status(self, event):
self._paused = not self._paused
try:
if self._paused:
util.cli.execute("killall -s SIGUSR1 dunst")
else:
util.cli.execute("killall -s SIGUSR2 dunst")
except:
self._paused = not self._paused # toggling failed
def state(self, widget):
if self._paused:
return ["muted", "warning"]
return ["unmuted"]

View file

@ -0,0 +1,89 @@
# pylint: disable=C0111,R0903
"""Displays the price of a cryptocurrency.
Requires the following python packages:
* requests
Parameters:
* getcrypto.interval: Interval in seconds for updating the price, default is 120, less than that will probably get your IP banned.
* getcrypto.getbtc: 0 for not getting price of BTC, 1 for getting it (default).
* getcrypto.geteth: 0 for not getting price of ETH, 1 for getting it (default).
* getcrypto.getltc: 0 for not getting price of LTC, 1 for getting it (default).
* getcrypto.getcur: Set the currency to display the price in, usd is the default.
contributed by `Ryunaq <https://github.com/Ryunaq>`_ - many thanks!
"""
import requests
from requests.exceptions import RequestException
import time
import core.module
import core.widget
import core.input
import core.decorators
import util.format
def getfromkrak(coin, currency):
abbrev = {
"Btc": ["xbt", "XXBTZ"],
"Eth": ["eth", "XETHZ"],
"Ltc": ["ltc", "XLTCZ"],
}
data = abbrev.get(coin, None)
if not data:
return
epair = "{}{}".format(data[0], currency)
tickname = "{}{}".format(data[1], currency.upper())
try:
krakenget = requests.get(
"https://api.kraken.com/0/public/Ticker?pair=" + epair
).json()
except (RequestException, Exception):
return "No connection"
if not "result" in krakenget:
return "No data"
kethusdask = float(krakenget["result"][tickname]["a"][0])
kethusdbid = float(krakenget["result"][tickname]["b"][0])
return coin + ": " + str((kethusdask + kethusdbid) / 2)[0:6]
class Module(core.module.Module):
@core.decorators.every(minutes=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.curprice))
self.__curprice = ""
self.__getbtc = util.format.asbool(self.parameter("getbtc", True))
self.__geteth = util.format.asbool(self.parameter("geteth", True))
self.__getltc = util.format.asbool(self.parameter("getltc", True))
self.__getcur = self.parameter("getcur", "usd")
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="xdg-open https://cryptowat.ch/"
)
def curprice(self, widget):
return self.__curprice
def update(self):
currency = self.__getcur
btcprice, ethprice, ltcprice = "", "", ""
if self.__getbtc:
btcprice = getfromkrak("Btc", currency)
if self.__geteth:
ethprice = getfromkrak("Eth", currency)
if self.__getltc:
ltcprice = getfromkrak("Ltc", currency)
self.__curprice = (
btcprice
+ " " * (self.__getbtc * self.__geteth)
+ ethprice
+ " " * (self.__getltc * max(self.__getbtc, self.__geteth))
+ ltcprice
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,73 @@
# pylint: disable=C0111,R0903
"""Displays the unread GitHub notifications for a GitHub user
Requires the following library:
* requests
Parameters:
* github.token: GitHub user access token, the token needs to have the 'notifications' scope.
* github.interval: Interval in minutes between updates, default is 5.
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
"""
import shutil
import requests
import core.module
import core.widget
import core.decorators
import core.input
class Module(core.module.Module):
@core.decorators.every(minutes=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.github))
self.__count = 0
self.__requests = requests.Session()
self.__requests.headers.update(
{"Authorization": "token {}".format(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="{} https://github.com/notifications".format(cmd),
)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
def github(self, _):
return str(self.__count)
def update(self):
try:
self.__count = 0
url = "https://api.github.com/notifications"
while True:
notifications = self.__requests.get(url)
self.__count += len(
list(
filter(
lambda notification: notification["unread"],
notifications.json(),
)
)
)
next_link = notifications.links.get("next")
if next_link is not None:
url = next_link.get("url")
else:
break
except Exception:
self.__count = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,60 @@
# pylint: disable=C0111,R0903
"""Displays information about the current song in Google Play music player.
Requires the following executable:
* gpmdp-remote
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
widgets = [
core.widget.Widget(name="gpmdp.prev"),
core.widget.Widget(name="gpmdp.main", full_text=self.description),
core.widget.Widget(name="gpmdp.next"),
]
super().__init__(config, theme, widgets)
core.input.register(
widgets[0], button=core.input.LEFT_MOUSE, cmd="playerctl previous"
)
core.input.register(
widgets[1], button=core.input.LEFT_MOUSE, cmd="playerctl play-pause"
)
core.input.register(
widgets[2], button=core.input.LEFT_MOUSE, cmd="playerctl next"
)
self.__status = None
self.__tags = None
def description(self, widget):
return self.__tags if self.__tags else "n/a"
def update(self):
self.__load_song()
def state(self, widget):
if widget.name == "gpmdp.prev":
return "prev"
if widget.name == "gpmdp.next":
return "next"
return self.__status
def __load_song(self):
info = util.cli.execute("gpmdp-remote current", ignore_errors=True)
status = util.cli.execute("gpmdp-remote status", ignore_errors=True)
self.__status = status.split("\n")[0].lower()
self.__tags = info.split("\n")[0]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
"""Fetch hard drive temeperature data from a hddtemp daemon
that runs on localhost and default port (7634)
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
"""
import socket
import core.module
import core.widget
HOST = "localhost"
PORT = 7634
CHUNK_SIZE = 1024
RECORD_SIZE = 5
SEPARATOR = "|"
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.hddtemps))
self.__hddtemps = self.__get_hddtemps()
def hddtemps(self, _):
return self.__hddtemps
def __fetch_data(self):
"""fetch data from hddtemp service"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((HOST, PORT))
data = ""
while True:
chunk = sock.recv(CHUNK_SIZE)
if chunk:
data += str(chunk)
else:
break
return data
except (AttributeError, socket.error) as e:
pass
@staticmethod
def __get_parts(data):
"""
split data using | separator and remove first item
(because the first item is empty)
"""
parts = data.split("|")[1:]
return parts
@staticmethod
def __partition_parts(parts):
"""
partition parts: one device record is five (5) items
"""
per_disk = [
parts[i : i + RECORD_SIZE] for i in range(len(parts))[::RECORD_SIZE]
]
return per_disk
@staticmethod
def __get_name_and_temp(device_record):
"""
get device name (without /dev part, to save space on bar)
and temperature (in °C) as tuple
"""
device_name = device_record[0].split("/")[-1]
device_temp = device_record[2]
return (device_name, device_temp)
@staticmethod
def __get_hddtemp(device_record):
name, temp = device_record
hddtemp = "{}+{}°C".format(name, temp)
return hddtemp
def __get_hddtemps(self):
data = self.__fetch_data()
if data is None:
return "n/a"
parts = self.__get_parts(data)
per_disk = self.__partition_parts(parts)
names_and_temps = [self.__get_name_and_temp(x) for x in per_disk]
hddtemps = [self.__get_hddtemp(x) for x in names_and_temps]
return SEPARATOR.join(hddtemps)
def update(self):
self.__hddtemps = self.__get_hddtemps()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,28 @@
# pylint: disable=C0111,R0903
"""Displays the system hostname.
contributed by `varkokonyi <https://github.com/varkokonyi>`_ - many thanks!
"""
import platform
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__hname = ""
def output(self, _):
return self.__hname + " " + "\uf233"
def update(self):
self.__hname = platform.node()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,70 @@
# pylint: disable=C0111,R0903
"""Display HTTP status code
Parameters:
* http__status.label: Prefix label (optional)
* http__status.target: Target to retrieve the HTTP status from
* http__status.expect: Expected HTTP status
contributed by `valkheim <https://github.com/valkheim>`_ - many thanks!
"""
from requests import head
import psutil
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
UNK = "UNK"
@core.decorators.every(seconds=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__label = self.parameter("label")
self.__target = self.parameter("target")
self.__expect = self.parameter("expect", "200")
def labelize(self, s):
if self.__label is None:
return s
return "{}: {}".format(self.__label, s)
def getStatus(self):
try:
res = head(self.__target)
except Exception as e:
print(e)
return self.UNK
else:
status = str(res.status_code)
return status
def getOutput(self):
if self.__status == self.__expect:
return self.labelize(self.__status)
else:
reason = " != {}".format(self.__expect)
return self.labelize("{}{}".format(self.__status, reason))
def output(self, widget):
return self.__output
def update(self):
self.__status = self.getStatus()
self.__output = self.getOutput()
def state(self, widget):
if self.__status == self.UNK:
return "warning"
if self.__status != self.__expect:
return "critical"
return self.__output
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,62 @@
# pylint: disable=C0111,R0903
"""Displays the indicator status, for numlock, scrolllock and capslock
Parameters:
* indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock')
* indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning')
contributed by `freed00m <https://github.com/freed00m>`_ - many thanks!
"""
import core.module
import util.cli
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.__include = tuple(
filter(
len, util.format.aslist(self.parameter("include", "NumLock,CapsLock"))
)
)
self.__signalType = (
self.parameter("signaltype")
if not self.parameter("signaltype") is None
else "warning"
)
def update(self):
status_line = ""
for line in (
util.cli.execute("xset q", ignore_errors=True).replace(" ", "").split("\n")
):
if "capslock" in line.lower():
status_line = line
break
for indicator in self.__include:
widget = self.widget(indicator)
if not widget:
widget = self.add_widget(name=indicator, full_text=indicator)
widget.set(
"status",
True
if "{}:on".format(indicator.lower()) in status_line.lower()
else False,
)
def state(self, widget):
states = []
if widget.get("status", False):
states.append(self.__signalType)
else:
states.append("normal")
return states
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,24 @@
# pylint: disable=C0111,R0903
"""Shows Linux kernel version information
contributed by `pierre87 <https://github.com/pierre87>`_ - many thanks!
"""
import platform
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.full_text))
def full_text(self, widgets):
return platform.release()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,42 @@
"""Displays and changes the current keyboard layout
Requires the following executable:
* xkb-switch
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
"""
import core.module
import core.widget
import core.decorators
import core.input
import util.cli
class Module(core.module.Module):
@core.decorators.every(seconds=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.current_layout))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
self.__current_layout = self.__get_current_layout()
def current_layout(self, _):
return self.__current_layout
def __next_keymap(self, event):
util.cli.execute("xkb-switch -n", ignore_errors=True)
def __get_current_layout(self):
try:
res = util.cli.execute("xkb-switch")
return res.split("\n")[0]
except RuntimeError:
return ["n/a"]
def update(self):
self.__current_layout = self.__get_current_layout()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,78 @@
# pylint: disable=C0111,R0903
"""Displays and changes the current keyboard layout
Requires the following executable:
* setxkbmap
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.current_layout))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_keymap)
def __next_keymap(self, event):
self._set_keymap(1)
def __prev_keymap(self, event):
self._set_keymap(-1)
def _set_keymap(self, rotation):
layouts = self.get_layouts()
if len(layouts) == 1:
return # nothing to do
layouts = layouts[rotation:] + layouts[:rotation]
layout_list = []
variant_list = []
for l in layouts:
tmp = l.split(":")
layout_list.append(tmp[0])
variant_list.append(tmp[1] if len(tmp) > 1 else "")
util.cli.execute(
"setxkbmap -layout {} -variant {}".format(
",".join(layout_list), ",".join(variant_list)
),
ignore_errors=True,
)
def get_layouts(self):
try:
res = util.cli.execute("setxkbmap -query")
except RuntimeError:
return ["n/a"]
layouts = []
variants = []
for line in res.split("\n"):
if not line:
continue
if "layout" in line:
layouts = line.split(":")[1].strip().split(",")
if "variant" in line:
variants = line.split(":")[1].strip().split(",")
result = []
for idx, layout in enumerate(layouts):
if len(variants) > idx and variants[idx]:
layout = "{}:{}".format(layout, variants[idx])
result.append(layout)
return result if len(result) > 0 else ["n/a"]
def current_layout(self, widget):
layouts = self.get_layouts()
return layouts[0]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,32 @@
"""Displays count of running libvirt VMs.
Required the following python packages:
* libvirt
contributed by `maxpivo <https://github.com/maxpivo>`_ - many thanks!
"""
import sys
import libvirt
import core.module
import core.widget
import core.input
import core.decorators
class Module(core.module.Module):
@core.decorators.every(seconds=10)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.status))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="virt-manager")
def status(self, _):
conn = libvirt.openReadOnly(None)
if conn == None:
return "Failed to open connection to the hypervisor"
return "VMs %s" % (conn.numOfDomains())
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,59 @@
# pylint: disable=C0111,R0903
# -*- coding: utf-8 -*-
"""Displays information about the current song in mocp. Left click toggles play/pause. Right click toggles shuffle.
Requires the following executable:
* mocp
Parameters:
* mocp.format: Format string for the song information. Replace string sequences with the actual information:
* %state State
* %file File
* %title Title, includes track, artist, song title and album
* %artist Artist
* %song SongTitle
* %album Album
* %tt TotalTime
* %tl TimeLeft
* %ts TotalSec
* %ct CurrentTime
* %cs CurrentSec
* %b Bitrate
* %r Sample rate
contributed by `chrugi <https://github.com/chrugi>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.description))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="mocp -G")
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd="mocp -t shuffle")
self.__format = self.parameter("format", "%state %artist - %song | %ct/%tt")
self.__running = False
def description(self, widget):
return self.__info if self.__running == True else "Music On Console Player"
def update(self):
self.__load_song()
def __load_song(self):
try:
self.__info = util.cli.execute("mocp -Q '{}'".format(self.__format)).strip()
self.__running = True
except RuntimeError:
self.__running = False
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,209 @@
# pylint: disable=C0111,R0903
# -*- coding: utf-8 -*-
"""Displays information about the current song in mpd.
Requires the following executable:
* mpc
Parameters:
* mpd.format: Format string for the song information.
Supported tags (see `man mpc` for additional information)
* {name}
* {artist}
* {album}
* {albumartist}
* {comment}
* {composer}
* {date}
* {originaldate}
* {disc}
* {genre}
* {performer}
* {title}
* {track}
* {time}
* {file}
* {id}
* {prio}
* {mtime}
* {mdate}
Additional tags:
* {position} - position of currently playing song
not to be confused with %position% mpc tag
* {duration} - duration of currently playing song
* {file1} - song file name without path prefix
if {file} = '/foo/bar.baz', then {file1} = 'bar.baz'
* {file2} - song file name without path prefix and extension suffix
if {file} = '/foo/bar.baz', then {file2} = 'bar'
* mpd.host: MPD host to connect to. (mpc behaviour by default)
* mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main.
contributed by `alrayyes <https://github.com/alrayyes>`_ - many thanks!
"""
from collections import defaultdict
import string
import os
import core.module
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self._layout = self.parameter(
"layout", "mpd.prev mpd.main mpd.next mpd.shuffle mpd.repeat"
)
self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}")
self._status = None
self._shuffle = False
self._repeat = False
self._tags = defaultdict(lambda: "")
if not self.parameter("host"):
self._hostcmd = ""
else:
self._hostcmd = " -h " + self.parameter("host")
# Create widgets
widget_map = {}
for widget_name in self._layout.split():
widget = self.add_widget(name=widget_name)
if widget_name == "mpd.prev":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "mpc prev" + self._hostcmd,
}
elif widget_name == "mpd.main":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "mpc toggle" + self._hostcmd,
}
widget.full_text(self.description)
elif widget_name == "mpd.next":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "mpc next" + self._hostcmd,
}
elif widget_name == "mpd.shuffle":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "mpc random" + self._hostcmd,
}
elif widget_name == "mpd.repeat":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": "mpc repeat" + self._hostcmd,
}
else:
raise KeyError(
"The mpd module does not support a {widget_name!r} widget".format(
widget_name=widget_name
)
)
# Register input callbacks
for widget, callback_options in widget_map.items():
core.input.register(widget, **callback_options)
def hidden(self):
return self._status is None
@core.decorators.scrollable
def description(self, widget):
return string.Formatter().vformat(self._fmt, (), self._tags)
def update(self):
self._load_song()
def state(self, widget):
if widget.name == "mpd.shuffle":
return "shuffle-on" if self._shuffle else "shuffle-off"
if widget.name == "mpd.repeat":
return "repeat-on" if self._repeat else "repeat-off"
if widget.name == "mpd.prev":
return "prev"
if widget.name == "mpd.next":
return "next"
return self._status
def _load_song(self):
info = ""
tags = [
"name",
"artist",
"album",
"albumartist",
"comment",
"composer",
"date",
"originaldate",
"disc",
"genre",
"performer",
"title",
"track",
"time",
"file",
"id",
"prio",
"mtime",
"mdate",
]
joinedtags = "\n".join(["tag {0} %{0}%".format(tag) for tag in tags])
info = util.cli.execute(
'mpc -f "{}"{}'.format(joinedtags, self._hostcmd), ignore_errors=True
)
self._tags = defaultdict(lambda: "")
self._status = None
for line in info.split("\n"):
if line.startswith("[playing]"):
self._status = "playing"
elif line.startswith("[paused]"):
self._status = "paused"
if line.startswith("["):
timer = line.split()[2]
position = timer.split("/")[0]
dur = timer.split("/")[1]
duration = dur.split(" ")[0]
self._tags.update({"position": position})
self._tags.update({"duration": duration})
if line.startswith("volume"):
value = line.split(" ", 2)[1:]
for option in value:
if option.startswith("repeat: on"):
self._repeat = True
elif option.startswith("repeat: off"):
self._repeat = False
elif option.startswith("random: on"):
self._shuffle = True
elif option.startswith("random: off"):
self._shuffle = False
if line.startswith("tag"):
key, value = line.split(" ", 2)[1:]
self._tags.update({key: value})
if key == "file":
self._tags.update({"file1": os.path.basename(value)})
self._tags.update(
{"file2": os.path.splitext(os.path.basename(value))[0]}
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,113 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Displays network traffic
* No extra configuration needed
contributed by `izn <https://github.com/izn>`_ - many thanks!
"""
import psutil
import netifaces
import core.module
import core.widget
import util.format
WIDGET_NAME = "network_traffic"
class Module(core.module.Module):
def __init__(self, config, theme):
widgets = [
core.widget.Widget(
module=self,
name="{0}.rx".format(WIDGET_NAME),
full_text=self.download_rate,
),
core.widget.Widget(
module=self,
name="{0}.tx".format(WIDGET_NAME),
full_text=self.upload_rate,
),
]
super().__init__(config, theme, widgets)
self.widgets()[0].set("theme.minwidth", "0000000KiB/s")
self.widgets()[1].set("theme.minwidth", "0000000KiB/s")
try:
self._bandwidth = BandwidthInfo()
self._rate_recv = "?"
self._rate_sent = "?"
self._bytes_recv = self._bandwidth.bytes_recv()
self._bytes_sent = self._bandwidth.bytes_sent()
except Exception:
""" We do not want do explode anything """
pass
def state(self, widget):
"""Return the widget state"""
if widget.name == "{}.rx".format(WIDGET_NAME):
return "rx"
elif widget.name == "{}.tx".format(WIDGET_NAME):
return "tx"
return None
def update(self):
try:
bytes_recv = self._bandwidth.bytes_recv()
bytes_sent = self._bandwidth.bytes_sent()
self._rate_recv = bytes_recv - self._bytes_recv
self._rate_sent = bytes_sent - self._bytes_sent
self._bytes_recv, self._bytes_sent = bytes_recv, bytes_sent
except Exception:
""" We do not want do explode anything """
pass
def download_rate(self, _):
return "{}/s".format(util.format.byte(self._rate_recv))
def upload_rate(self, _):
return "{}/s".format(util.format.byte(self._rate_sent))
class BandwidthInfo(object):
"""Get received/sent bytes from network adapter"""
def bytes_recv(self):
"""Return received bytes"""
return self.bandwidth().bytes_recv
def bytes_sent(self):
"""Return sent bytes"""
return self.bandwidth().bytes_sent
def bandwidth(self):
"""Return bandwidth information"""
io_counters = self.io_counters()
return io_counters[self.default_network_adapter()]
@classmethod
def default_network_adapter(cls):
"""Return default active network adapter"""
gateway = netifaces.gateways()["default"]
if not gateway:
raise "No default gateway found"
return gateway[netifaces.AF_INET][1]
@classmethod
def io_counters(cls):
"""Return IO counters"""
return psutil.net_io_counters(pernic=True)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,51 @@
# pylint: disable=C0111,R0903
"""Displays the result of a notmuch count query
default : unread emails which path do not contained 'Trash' (notmuch count 'tag:unread AND NOT path:/.*Trash.*/')
Parameters:
* notmuch_count.query: notmuch count query to show result
Errors:
if the notmuch query failed, the shown value is -1
Dependencies:
notmuch (https://notmuchmail.org/)
contributed by `abdoulayeYATERA <https://github.com/abdoulayeYATERA>`_ - many thanks!
"""
import os
import core.module
import core.widget
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__notmuch_count_query = self.parameter(
"query", "tag:unread AND NOT path:/.*Trash.*/"
)
def output(self, widget):
return self.__notmuch_count
def state(self, widgets):
if self.__notmuch_count == 0:
return "empty"
return "items"
def update(self):
try:
self.__notmuch_count = util.cli.execute(
"notmuch count {}".format(self.__notmuch_count_query)
).strip()
except Exception:
self.__notmuch_count = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""Displays GPU name, temperature and memory usage.
Parameters:
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem}
Requires nvidia-smi
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
"""
import core.module
import core.widget
import util.cli
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.utilization))
self.__utilization = "Not found: 0 0/0"
def utilization(self, widget):
return self.__utilization
def hidden(self):
return "not found" in self.__utilization
def update(self):
sp = util.cli.execute("nvidia-smi -q", ignore_errors=True)
title = ""
usedMem = ""
totalMem = ""
temp = ""
name = "not found"
clockMem = ""
clockGpu = ""
fanspeed = ""
for item in sp.split("\n"):
try:
key, val = item.split(":")
key, val = key.strip(), val.strip()
if title == "Clocks":
if key == "Graphics":
clockGpu = val.split(" ")[0]
elif key == "Memory":
clockMem = val.split(" ")[0]
if title == "FB Memory Usage":
if key == "Total":
totalMem = val.split(" ")[0]
elif key == "Used":
usedMem = val.split(" ")[0]
elif key == "GPU Current Temp":
temp = val.split(" ")[0]
elif key == "Product Name":
name = val
elif key == "Fan Speed":
fanspeed = val.split(" ")[0]
except:
title = item.strip()
str_format = self.parameter(
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
)
self.__utilization = str_format.format(
name=name,
temp=temp,
mem_used=usedMem,
mem_total=totalMem,
clock_gpu=clockGpu,
clock_mem=clockMem,
fanspeed=fanspeed,
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,254 @@
# pylint: disable=C0111,R0903
"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar.
Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled).
Parameters:
* octoprint.address : Octoprint address (e.q: http://192.168.1.3)
* octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface)
* octoprint.webcam : Set to True if a webcam is connected (default: False)
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
"""
import urllib
import logging
import threading
import queue
import tkinter as tk
from io import BytesIO
from PIL import Image, ImageTk
import requests
import simplejson
import core.module
import core.widget
import core.input
def get_frame(url):
img_bytes = b""
stream = urllib.request.urlopen(url)
while True:
img_bytes += stream.read(1024)
a = img_bytes.find(b"\xff\xd8")
b = img_bytes.find(b"\xff\xd9")
if a != -1 and b != -1:
jpg = img_bytes[a : b + 2]
img_bytes = img_bytes[b + 2 :]
img = Image.open(BytesIO(jpg))
return img
return None
class WebcamImagesWorker(threading.Thread):
def __init__(self, url, queue):
threading.Thread.__init__(self)
self.__url = url
self.__queue = queue
self.__running = True
def run(self):
while self.__running:
img = get_frame(self.__url)
self.__queue.put(img)
def stop(self):
self.__running = False
class Module(core.module.Module):
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.octoprint_status))
self.__octoprint_state = "Unknown"
self.__octoprint_address = self.parameter("address", "")
self.__octoprint_api_token = self.parameter("apitoken", "")
self.__octoprint_webcam = self.parameter("webcam", False)
self.__webcam_images_worker = None
self.__webcam_image_url = self.__octoprint_address + "/webcam/?action=stream"
self.__webcam_images_queue = None
self.__printer_bed_temperature = "-"
self.__tool1_temperature = "-"
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
def octoprint_status(self, widget):
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown":
return self.__octoprint_state
return (
self.__octoprint_state
+ " | B: "
+ str(self.__printer_bed_temperature)
+ "°C"
+ " | T1: "
+ str(self.__tool1_temperature)
+ "°C"
)
def __get(self, endpoint):
url = self.__octoprint_address + "/api/" + endpoint
headers = {"X-Api-Key": self.__octoprint_api_token}
resp = requests.get(url, headers=headers)
try:
return resp.json(), resp.status_code
except simplejson.errors.JSONDecodeError:
return None, resp.status_code
def __get_printer_bed_temperature(self):
printer_info, status_code = self.__get("printer")
if status_code == 200:
return (
printer_info["temperature"]["bed"]["actual"],
printer_info["temperature"]["bed"]["target"],
)
return None, None
def __get_octoprint_state(self):
job_info, status_code = self.__get("job")
return job_info["state"] if status_code == 200 else "Unknown"
def __get_tool_temperatures(self):
tool_temperatures = []
printer_info, status_code = self.__get("printer")
if status_code == 200:
temperatures = printer_info["temperature"]
tool_id = 0
while True:
try:
tool = temperatures["tool" + str(tool_id)]
tool_temperatures.append((tool["actual"], tool["target"]))
except KeyError:
break
tool_id += 1
return tool_temperatures
def update(self):
try:
self.__octoprint_state = self.__get_octoprint_state()
actual_temp, _ = self.__get_printer_bed_temperature()
if actual_temp is None:
actual_temp = "-"
self.__printer_bed_temperature = str(actual_temp)
tool_temps = self.__get_tool_temperatures()
if len(tool_temps) > 0:
self.__tool1_temperature = tool_temps[0][0]
else:
self.__tool1_temperature = "-"
except Exception as e:
logging.exception("Couldn't get data")
def __refresh_image(self, root, webcam_image, webcam_image_container):
try:
img = self.__webcam_images_queue.get()
webcam_image = ImageTk.PhotoImage(img)
webcam_image_container.config(image=webcam_image)
except queue.Empty as e:
pass
except Exception as e:
logging.exception("Couldn't refresh image")
root.after(5, self.__refresh_image, root, webcam_image, webcam_image_container)
def __refresh_temperatures(
self, root, printer_bed_temperature_label, tools_temperature_label
):
actual_bed_temp, target_bed_temp = self.__get_printer_bed_temperature()
if actual_bed_temp is None:
actual_bed_temp = "-"
if target_bed_temp is None:
target_bed_temp = "-"
bed_temp = "Bed: " + str(actual_bed_temp) + "/" + str(target_bed_temp) + " °C"
printer_bed_temperature_label.config(text=bed_temp)
tool_temperatures = self.__get_tool_temperatures()
tools_temp = "Tools: "
if len(tool_temperatures) == 0:
tools_temp += "-/- °C"
else:
for i, tool_temperature in enumerate(tool_temperatures):
tools_temp += (
str(tool_temperature[0]) + "/" + str(tool_temperature[1]) + "°C"
)
if i != len(tool_temperatures) - 1:
tools_temp += "\t"
tools_temperature_label.config(text=tools_temp)
root.after(
500,
self.__refresh_temperatures,
root,
printer_bed_temperature_label,
tools_temperature_label,
)
def __show_popup(self, widget):
root = tk.Tk()
root.attributes("-type", "dialog")
root.title("Octoprint")
frame = tk.Frame(root)
if self.__octoprint_webcam:
# load first image synchronous before popup is shown, otherwise tkinter isn't able to layout popup properly
img = get_frame(self.__webcam_image_url)
webcam_image = ImageTk.PhotoImage(img)
webcam_image_container = tk.Button(frame, image=webcam_image)
webcam_image_container.pack()
self.__webcam_images_queue = queue.Queue()
self.__webcam_images_worker = WebcamImagesWorker(
self.__webcam_image_url, self.__webcam_images_queue
)
self.__webcam_images_worker.start()
else:
logging.debug(
"Not using webcam, as webcam is disabled. Enable with --webcam."
)
frame.pack()
temperatures_label = tk.Label(frame, text="Temperatures", font=("", 25))
temperatures_label.pack()
printer_bed_temperature_label = tk.Label(
frame, text="Bed: -/- °C", font=("", 15)
)
printer_bed_temperature_label.pack()
tools_temperature_label = tk.Label(frame, text="Tools: -/- °C", font=("", 15))
tools_temperature_label.pack()
root.after(10, self.__refresh_image, root, webcam_image, webcam_image_container)
root.after(
500,
self.__refresh_temperatures,
root,
printer_bed_temperature_label,
tools_temperature_label,
)
root.bind("<Destroy>", self.__on_close_popup)
root.eval("tk::PlaceWindow . center")
root.mainloop()
def __on_close_popup(self, event):
self.__webcam_images_queue = None
self.__webcam_images_worker.stop()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,78 @@
# pylint: disable=C0111,R0903
"""Displays update information per repository for pacman.
Parameters:
* pacman.sum: If you prefere displaying updates with a single digit (defaults to 'False')
Requires the following executables:
* fakeroot
* pacman
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
"""
import os
import threading
import core.module
import core.widget
import core.decorators
import util.cli
import util.format
# list of repositories.
# the last one should always be other
repos = ["core", "extra", "community", "multilib", "testing", "other"]
def get_pacman_info(widget, path):
cmd = "{}/../../bin/pacman-updates".format(path)
if not os.path.exists(cmd):
cmd = "/usr/share/bumblebee-status/bin/pacman-update"
result = util.cli.execute(cmd, ignore_errors=True)
count = len(repos) * [0]
for line in result.splitlines():
if line.startswith(("http", "rsync")):
for i in range(len(repos) - 1):
if "/" + repos[i] + "/" in line:
count[i] += 1
break
else:
result[-1] += 1
for i in range(len(repos)):
widget.set(repos[i], count[i])
core.event.trigger("update", [widget.module.id], redraw_only=True)
class Module(core.module.Module):
@core.decorators.every(minutes=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.updates))
def updates(self, widget):
if util.format.asbool(self.parameter("sum")):
return str(sum(map(lambda x: widget.get(x, 0), repos)))
return "/".join(map(lambda x: str(widget.get(x, 0)), repos))
def update(self):
path = os.path.dirname(os.path.abspath(__file__))
thread = threading.Thread(target=get_pacman_info, args=(self.widget(), path))
thread.start()
def state(self, widget):
weightedCount = sum(
map(lambda x: (len(repos) - x[0]) * widget.get(x[1], 0), enumerate(repos))
)
if weightedCount < 10:
return "good"
return self.threshold_state(weightedCount, 100, 150)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,85 @@
# pylint: disable=C0111,R0903
"""Displays the pi-hole status (up/down) together with the number of ads that were blocked today
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)
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
"""
import requests
import core.module
import core.widget
import core.input
class Module(core.module.Module):
@core.decorators.every(minutes=1)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.pihole_status))
self._pihole_address = self.parameter("address", "")
self._pihole_pw_hash = self.parameter("pwhash", "")
self._pihole_status = None
self._ads_blocked_today = "-"
self.update_pihole_status()
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd=self.toggle_pihole_status
)
def pihole_status(self, widget):
if self._pihole_status is None:
return "pi-hole unknown"
return "pi-hole {}".format(
"up {} blocked".format(self._ads_blocked_today)
if self._pihole_status
else "down"
)
def update_pihole_status(self):
try:
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:
self._pihole_status = None
def toggle_pihole_status(self, widget):
if self._pihole_status is not None:
try:
req = None
if self._pihole_status:
req = requests.get(
self._pihole_address
+ "/admin/api.php?disable&auth="
+ self._pihole_pw_hash
)
else:
req = requests.get(
self._pihole_address
+ "/admin/api.php?enable&auth="
+ self._pihole_pw_hash
)
if req is not None:
if req.status_code == 200:
status = req.json()["status"]
self._pihole_status = False if status == "disabled" else True
except:
pass
def update(self):
self.update_pihole_status()
def state(self, widget):
if self._pihole_status is None:
return []
elif self._pihole_status:
return ["enabled"]
return ["disabled", "warning"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,129 @@
# pylint: disable=C0111,R0903
"""Display and run a Pomodoro timer.
Left click to start timer, left click again to pause.
Right click will cancel the timer.
Parameters:
* pomodoro.work: The work duration of timer in minutes (defaults to 25)
* pomodoro.break: The break duration of timer in minutes (defaults to 5)
* pomodoro.format: Timer display format with '%m' and '%s' for minutes and seconds (defaults to '%m:%s')
Examples: '%m min %s sec', '%mm', '', 'timer'
* pomodoro.notify: Notification command to run when timer ends/starts (defaults to nothing)
Example: 'notify-send 'Time up!''. If you want to chain multiple commands,
please use an external wrapper script and invoke that. The module itself does
not support command chaining (see https://github.com/tobi-wan-kenobi/bumblebee-status/issues/532
for a detailled explanation)
contributed by `martindoublem <https://github.com/martindoublem>`_, inspired by `karthink <https://github.com/karthink>`_ - many thanks!
"""
from __future__ import absolute_import
import datetime
from math import ceil
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.text))
# Parameters
self.__work_period = int(self.parameter("work", 25))
self.__break_period = int(self.parameter("break", 5))
self.__time_format = self.parameter("format", "%m:%s")
self.__notify_cmd = self.parameter("notify", "")
# TODO: Handle time formats more gracefully. This is kludge.
self.display_seconds_p = False
self.display_minutes_p = False
if "%s" in self.__time_format:
self.display_seconds_p = True
if "%m" in self.__time_format:
self.display_minutes_p = True
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
self.time = None
self.pomodoro = {"state": "OFF", "type": ""}
self.__text = self.remaining_time_str() + self.pomodoro["type"]
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd=self.timer_play_pause
)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.timer_reset)
def remaining_time_str(self):
if self.display_seconds_p and self.display_minutes_p:
minutes, seconds = divmod(self.remaining_time.seconds, 60)
if not self.display_seconds_p:
minutes = ceil(self.remaining_time.seconds / 60)
seconds = 0
if not self.display_minutes_p:
minutes = 0
seconds = self.remaining_time.seconds
minutes = "{:2d}".format(minutes)
seconds = "{:02d}".format(seconds)
return self.__time_format.replace("%m", minutes).replace("%s", seconds) + " "
def text(self, widget):
return "{}".format(self.__text)
def update(self):
if self.pomodoro["state"] == "ON":
timediff = datetime.datetime.now() - self.time
if timediff.seconds >= 0:
self.remaining_time -= timediff
self.time = datetime.datetime.now()
if self.remaining_time.total_seconds() <= 0:
self.notify()
if self.pomodoro["type"] == "Work":
self.pomodoro["type"] = "Break"
self.remaining_time = datetime.timedelta(
minutes=self.__break_period
)
elif self.pomodoro["type"] == "Break":
self.pomodoro["type"] = "Work"
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
self.__text = self.remaining_time_str() + self.pomodoro["type"]
def notify(self):
if self.__notify_cmd:
util.cli.execute(self.__notify_cmd)
def timer_play_pause(self, widget):
if self.pomodoro["state"] == "OFF":
self.pomodoro = {"state": "ON", "type": "Work"}
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
self.time = datetime.datetime.now()
elif self.pomodoro["state"] == "ON":
self.pomodoro["state"] = "PAUSED"
self.remaining_time -= datetime.datetime.now() - self.time
self.time = datetime.datetime.now()
elif self.pomodoro["state"] == "PAUSED":
self.pomodoro["state"] = "ON"
self.time = datetime.datetime.now()
def timer_reset(self, widget):
if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "PAUSED":
self.pomodoro = {"state": "OFF", "type": ""}
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
def state(self, widget):
state = []
state.append(self.pomodoro["state"].lower())
if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "OFF":
state.append(self.pomodoro["type"].lower())
return state
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,68 @@
# pylint: disable=C0111,R0903
"""Displays and changes the current selected prime video card
Left click will call 'sudo prime-select nvidia'
Right click will call 'sudo prime-select nvidia'
Running these commands without a password requires editing your sudoers file
(always use visudo, it's very easy to make a mistake and get locked out of your computer!)
sudo visudo -f /etc/sudoers.d/prime
Then put a line like this in there:
user ALL=(ALL) NOPASSWD: /usr/bin/prime-select
If you can't figure out the sudoers thing, then don't worry, it's still really useful.
Parameters:
* prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel')
* prime.intelstring: String to use when intel is selected (defaults to 'intel')
Requires the following executable:
* prime-select
contributed by `jeffeb3 <https://github.com/jeffeb3>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.query))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__chooseNvidia)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__chooseIntel)
self.nvidiastring = self.parameter("nvidiastring", "nv")
self.intelstring = self.parameter("intelstring", "it")
def __chooseNvidia(self, event):
util.cli.execute("sudo prime-select nvidia")
def __chooseIntel(self, event):
util.cli.execute("sudo prime-select intel")
def query(self, widget):
try:
res = util.cli.execute("prime-select query")
except RuntimeError:
return "n/a"
for line in res.split("\n"):
if not line:
continue
if "nvidia" in line:
return self.nvidiastring
if "intel" in line:
return self.intelstring
return "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,108 @@
"""
Show progress for cp, mv, dd, ...
Parameters:
* progress.placeholder: Text to display while no process is running (defaults to 'n/a')
* progress.barwidth: Width of the progressbar if it is used (defaults to 8)
* progress.format: Format string (defaults to '{bar} {cmd} {arg}')
Available values are: {bar} {pid} {cmd} {arg} {percentage} {quantity} {speed} {time}
* progress.barfilledchar: Character used to draw the filled part of the bar (defaults to '#'), notice that it can be a string
* progress.baremptychar: Character used to draw the empty part of the bar (defaults to '-'), notice that it can be a string
Requires the following executable:
* progress
contributed by `remi-dupre <https://github.com/remi-dupre>`_ - many thanks!
"""
import core.module
import core.widget
import util.cli
import util.format
import re
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.get_progress_text))
self.__active = False
def get_progress_text(self, widget):
if self.update_progress_info(widget):
width = util.format.asint(self.parameter("barwidth", 8))
count = round((width * widget.get("per")) / 100)
filledchar = self.parameter("barfilledchar", "#")
emptychar = self.parameter("baremptychar", "-")
bar = "[{}{}]".format(filledchar * count, emptychar * (width - count))
str_format = self.parameter("format", "{bar} {cmd} {arg}")
return str_format.format(
bar=bar,
pid=widget.get("pid"),
cmd=widget.get("cmd"),
arg=widget.get("arg"),
percentage=widget.get("per"),
quantity=widget.get("qty"),
speed=widget.get("spd"),
time=widget.get("tim"),
)
else:
return self.parameter("placeholder", "n/a")
def update_progress_info(self, widget):
"""Update widget's informations about the copy"""
if not self.__active:
return
# These regex extracts following groups:
# 1. pid
# 2. command
# 3. arguments
# 4. progress (xx.x formated)
# 5. quantity (.. unit / .. unit formated)
# 6. speed
# 7. time remaining
extract_nospeed = re.compile(
"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\)\n.*"
)
extract_wtspeed = re.compile(
"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\) (\d*\.\d .*) remaining (\d*:\d*:\d*)\n.*"
)
try:
raw = util.cli.execute("progress -qW 0.1")
result = extract_wtspeed.match(raw)
if not result:
# Abord speed measures
raw = util.cli.execute("progress -q")
result = extract_nospeed.match(raw)
widget.set("spd", "???.? B/s")
widget.set("tim", "??:??:??")
else:
widget.set("spd", result.group(6))
widget.set("tim", result.group(7))
widget.set("pid", int(result.group(1)))
widget.set("cmd", result.group(2))
widget.set("arg", result.group(3))
widget.set("per", float(result.group(4)))
widget.set("qty", result.group(5))
return True
except Exception:
return False
def update(self):
self.__active = bool(util.cli.execute("progress -q"))
def state(self, widget):
if self.__active:
return "copying"
return "pending"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,28 @@
"""Displays public IP address
"""
import core.module
import core.widget
import core.decorators
import util.location
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.public_ip))
self.__ip = ""
def public_ip(self, widget):
return self.__ip
def update(self):
try:
self.__ip = util.location.public_ip()
except Exception:
self.__ip = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,61 @@
# pylint: disable=C0111,R0903
"""Shows a widget for each connected screen and allows the user to loop through different orientations.
Requires the following executable:
* xrandr
"""
import core.module
import core.input
import util.cli
possible_orientations = ["normal", "left", "inverted", "right"]
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
def update(self):
widgets = self.widgets()
for line in util.cli.execute("xrandr -q").split("\n"):
if not " connected" in line:
continue
display = line.split(" ", 2)[0]
orientation = "normal"
for curr_orient in possible_orientations:
if (line.split(" ")).count(curr_orient) > 1:
orientation = curr_orient
break
widget = self.widget(display)
if not widget:
widget = self.add_widget(full_text=display, name=display)
core.input.register(
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
)
widget.set("orientation", orientation)
widgets.append(widget)
def state(self, widget):
return widget.get("orientation", "normal")
def __toggle(self, event):
widget = self.widget_by_id(event["instance"])
# compute new orientation based on current orientation
idx = possible_orientations.index(widget.get("orientation"))
idx = (idx + 1) % len(possible_orientations)
new_orientation = possible_orientations[idx]
widget.set("orientation", new_orientation)
util.cli.execute(
"xrandr --output {} --rotation {}".format(widget.name, new_orientation)
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,368 @@
# pylint: disable=C0111,R0903
"""RSS news ticker
Fetches rss news items and shows these as a news ticker.
Left-clicking will open the full story in a browser.
New stories are highlighted.
Parameters:
* rss.feeds : Space-separated list of RSS URLs
* rss.length : Maximum length of the module, default is 60
contributed by `lonesomebyte537 <https://github.com/lonesomebyte537>`_ - many thanks!
"""
import feedparser
import webbrowser
import time
import os
import tempfile
import logging
import random
import re
import json
import core.module
import core.widget
import core.input
# pylint: disable=too-many-instance-attributes
class Module(core.module.Module):
REFRESH_DELAY = 600
SCROLL_SPEED = 3
LAYOUT_STYLES_ITEMS = [[1, 1, 1], [3, 3, 2], [2, 3, 3], [3, 2, 3]]
HISTORY_FILENAME = ".config/i3/rss.hist"
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.ticker_update))
self._feeds = self.parameter(
"feeds", "https://www.espn.com/espn/rss/news"
).split(" ")
self._feeds_to_update = []
self._response = ""
self._max_title_length = int(self.parameter("length", 60))
self._items = []
self._current_item = None
self._ticker_offset = 0
self._pre_delay = 0
self._post_delay = 0
self._state = []
self._newspaper_filename = tempfile.mktemp(".html")
self._last_refresh = 0
self._last_update = 0
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self._open)
core.input.register(
self, button=core.input.RIGHT_MOUSE, cmd=self._create_newspaper
)
self._history = {"ticker": {}, "newspaper": {}}
self._load_history()
def _load_history(self):
if os.path.isfile(self.HISTORY_FILENAME):
self._history = json.loads(open(self.HISTORY_FILENAME, "r").read())
def _update_history(self, group):
sources = set([i["source"] for i in self._items])
self._history[group] = dict(
[
[s, [i["title"] for i in self._items if i["source"] == s]]
for s in sources
]
)
def _save_history(self):
if not os.path.exists(os.path.dirname(self.HISTORY_FILENAME)):
os.makedirs(os.path.dirname(self.HISTORY_FILENAME))
open(self.HISTORY_FILENAME, "w").write(json.dumps(self._history))
def _check_history(self, items, group):
for i in items:
i["new"] = not (
i["source"] in self._history[group]
and i["title"] in self._history[group][i["source"]]
)
def _open(self, _):
if self._current_item:
webbrowser.open(self._current_item["link"])
def _check_for_image(self, entry):
image = next(
iter([l["href"] for l in entry["links"] if l["rel"] == "enclosure"]), None
)
if not image and "media_content" in entry:
try:
media = sorted(
entry["media_content"],
key=lambda i: i["height"] if "height" in i else 0,
reverse=True,
)
image = next(
iter([i["url"] for i in media if i["medium"] == "image"]), None
)
except Exception:
pass
if not image:
match = re.search(
r"<img[^>]*src\s*=['\']*([^\s^>^'^\']*)['\']*", entry["summary"]
)
if match:
image = match.group(1)
return image if image else ""
def _remove_tags(self, txt):
return re.sub("<[^>]*>", "", txt)
def _create_item(self, entry, url, feed):
return {
"title": self._remove_tags(entry["title"].replace("\n", " ")),
"link": entry["link"],
"new": True,
"source": url,
"summary": self._remove_tags(entry["summary"]),
"feed": feed,
"image": self._check_for_image(entry),
"published": time.mktime(entry.published_parsed)
if hasattr(entry, "published_parsed")
else 0,
}
def _update_items_from_feed(self, url):
parser = feedparser.parse(url)
new_items = [
self._create_item(entry, url, parser["feed"]["title"])
for entry in parser["entries"]
]
# Check history
self._check_history(new_items, "ticker")
# Remove the previous items
self._items = [i for i in self._items if i["source"] != url]
# Add the new items
self._items.extend(new_items)
# Sort the items on publish date
self._items.sort(key=lambda i: i["published"], reverse=True)
def _check_for_refresh(self):
if self._feeds_to_update:
# Update one feed at a time to not overload this update cycle
url = self._feeds_to_update.pop()
self._update_items_from_feed(url)
if not self._feeds_to_update:
self._update_history("ticker")
self._save_history()
if not self._current_item:
self._next_item()
elif time.time() - self._last_refresh >= self.REFRESH_DELAY:
# Populate the list with feeds to update
self._feeds_to_update = self._feeds[:]
# Update the refresh time
self._last_refresh = time.time()
def _next_item(self):
self._ticker_offset = 0
self._pre_delay = 2
self._post_delay = 4
if not self._items:
return
# Index of the current element
idx = (
self._items.index(self._current_item)
if self._current_item in self._items
else -1
)
# First show new items, else show next
new_items = [i for i in self._items if i["new"]]
self._current_item = next(
iter(new_items), self._items[(idx + 1) % len(self._items)]
)
def _check_scroll_done(self):
# Check if the complete title has been shown
if self._ticker_offset + self._max_title_length > len(
self._current_item["title"]
):
# Do not immediately show next item after scroll
self._post_delay -= 1
if self._post_delay == 0:
self._current_item["new"] = False
# Mark the previous item as 'old'
self._next_item()
else:
# Increase scroll position
self._ticker_offset += self.SCROLL_SPEED
def ticker_update(self, _):
# Only update the ticker once a second
now = time.time()
if now - self._last_update < 1:
return self._response
self._last_update = now
self._check_for_refresh()
# If no items were retrieved, return an empty string
if not self._current_item:
return " " * self._max_title_length
# Prepare a substring of the item title
self._response = self._current_item["title"][
self._ticker_offset : self._ticker_offset + self._max_title_length
]
# Add spaces if too short
self._response = self._response.ljust(self._max_title_length)
# Do not immediately scroll
if self._pre_delay > 0:
# Change state during pre_delay for new items
if self._current_item["new"]:
self._state = ["warning"]
self._pre_delay -= 1
return self._response
self._state = []
self._check_scroll_done()
return self._response
def state(self, _):
return self._state
def _create_news_element(self, item, overlay_title):
try:
timestr = (
"" if item["published"] == 0 else str(time.ctime(item["published"]))
)
except Exception as exc:
logging.error(str(exc))
raise e
element = "<div class='item' onclick=window.open('" + item["link"] + "')>"
element += "<div class='titlecontainer'>"
element += (
" <img "
+ ("" if item["image"] else "class='noimg' ")
+ "src='"
+ item["image"]
+ "'>"
)
element += (
" <div class='title"
+ (" overlay" if overlay_title else "")
+ "'>"
+ ("<span class='star'>&#x2605;</span>" if item["new"] else "")
+ item["title"]
+ "</div>"
)
element += "</div>"
element += "<div class='summary'>" + item["summary"] + "</div>"
element += (
"<div class='info'><span class='author'>"
+ item["feed"]
+ "</span><span class='published'>"
+ timestr
+ "</span></div>"
)
element += "</div>"
return element
def _create_news_section(self, newspaper_items):
style = random.randint(0, 3)
section = "<table><tr class='style" + str(style) + "'>"
for i in range(0, 3):
section += "<td><div class='itemcontainer'>"
for _ in range(0, self.LAYOUT_STYLES_ITEMS[style][i]):
if newspaper_items:
section += self._create_news_element(
newspaper_items[0], self.LAYOUT_STYLES_ITEMS[style][i] != 3
)
del newspaper_items[0]
section += "</div></td>"
section += "</tr></table>"
return section
def _create_newspaper(self, _):
content = ""
newspaper_items = self._items[:]
self._check_history(newspaper_items, "newspaper")
# Make sure new items are always listed first, independent of publish date
newspaper_items.sort(
key=lambda i: i["published"] + (10000000 if i["new"] else 0), reverse=True
)
while newspaper_items:
content += self._create_news_section(newspaper_items)
open(self._newspaper_filename, "w").write(
HTML_TEMPLATE.replace("[[CONTENT]]", content)
)
webbrowser.open("file://" + self._newspaper_filename)
self._update_history("newspaper")
self._save_history()
HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
var images = document.getElementsByTagName('img');
// Remove very small images
for(var i = 0; i < images.length; i++) {
if (images[i].naturalWidth<50 || images[i].naturalHeight<50) {
images[i].src = ''
images[i].className+=' noimg'
}
}
}
</script>
</head>
<style>
body {background: #eee; font-family: Helvetica neue;}
td {background: #fff; height: 100%;}
tr.style0 td {width: 33%;}
tr.style1 td {width: 20%;}
tr.style1 td:last-child {width: 60%;}
tr.style2 td {width: 20%;}
tr.style2 td:first-child {width: 60%;}
tr.style3 td {width: 20%;}
tr.style3 td:nth-child(2) {width: 60%;}
img {width: 100%; display: block; }
img.noimg {min-height:250px; background: #1299c8;}
#content {width: 1500px; margin: auto; background: #eee; padding: 1px;}
#newspapertitle {text-align: center; font-size: 60px; font-family: Arial Black; background: #1299c8; font-style: Italic; padding: 10px; color: #fff; }
.star {color: #ffa515; font-size: 24px;}
.section {display: flex;}
.column {display: flex;}
.itemcontainer {width: 100%; height: 100%; position: relative; display: inline-table;}
.item {cursor: pointer; }
.titlecontainer {position: relative;}
.title.overlay {font-family: Arial; position: absolute; bottom: 10px; color: #fff; font-weight: bold; text-align: right; max-width: 75%; right: 10px; font-size: 23px; text-shadow: 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000, -1px 0 0 #000;}
.title:not(.overlay) {font-weight: bold; padding: 0px 10px;}
.summary {color: #444; padding: 10px 10px 0px 10px; font-family: Times new roman; font-size: 18px; flex: 1;max-height: 105px; overflow: hidden;}
.info {color: #aaa; font-family: arial; font-size: 13px; padding: 10px;}
.published {float: right;}
</style>
<body>
<div id='content'>
<div id='newspapertitle'>Bumblebee Daily</div>
[[CONTENT]]
</div>
</body>
</html>"""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,148 @@
# -*- coding: UTF-8 -*-
# pylint: disable=C0111,R0903
"""Displays sensor temperature
Parameters:
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
be: 'coretemp-isa-00000/Core 0/temp1_input' (defaults to 'false')
* sensors.match: (fallback) Line to match against output of 'sensors -u' (default: temp1_input)
* sensors.match_pattern: (fallback) Line to match against before temperature is read (no default)
* sensors.match_number: (fallback) which of the matches you want (default -1: last match).
* sensors.show_freq: whether to show CPU frequency. (default: true)
contributed by `mijoharas <https://github.com/mijoharas>`_ - many thanks!
"""
import re
import json
import logging
log = logging.getLogger(__name__)
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.temperature))
self._temperature = "unknown"
self._mhz = "n/a"
self._match_number = int(self.parameter("match_number", "-1"))
self._match_pattern = self.parameter("match_pattern", None)
self._pattern = re.compile(
r"^\s*{}:\s*([\d.]+)$".format(self.parameter("match", "temp1_input")),
re.MULTILINE,
)
self._json = util.format.asbool(self.parameter("json", False))
self._freq = util.format.asbool(self.parameter("show_freq", True))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
self.determine_method()
def determine_method(self):
if self.parameter("path") != None and self._json == False:
self.use_sensors = False # use thermal zone
else:
# try to use output of sensors -u
try:
output = util.cli.execute("sensors -u")
self.use_sensors = True
log.debug("Sensors command available")
except FileNotFoundError as e:
log.info(
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
)
self.use_sensors = False
def _get_temp_from_sensors(self):
if self._json == True:
try:
output = json.loads(util.cli.execute("sensors -j"))
for key in self.parameter("path").split("/"):
output = output[key]
return int(float(output))
except Exception as e:
logging.error("unable to read sensors: {}".format(str(e)))
return "unknown"
else:
output = util.cli.execute("sensors -u")
if self._match_pattern:
temp_pattern = self.parameter("match", "temp1_input")
match = re.search(
r"{}.+{}:\s*([\d.]+)$".format(self._match_pattern, temp_pattern),
output.replace("\n", ""),
)
if match:
return int(float(match.group(1)))
else:
return "unknown"
match = self._pattern.findall(output)
if match:
return int(float(match[self._match_number]))
return "unknown"
def get_temp(self):
if self.use_sensors:
temperature = self._get_temp_from_sensors()
log.debug("Retrieve temperature from sensors -u")
else:
try:
temperature = open(
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp")
).read()[:2]
log.debug("retrieved temperature from /sys/class/")
# TODO: Iterate through all thermal zones to determine the correct one and use its value
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal
except IOError:
temperature = "unknown"
log.info("Can not determine temperature, please install lm-sensors")
return temperature
def get_mhz(self):
mhz = None
try:
output = open(
"/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"
).read()
mhz = int(float(output) / 1000.0)
except:
output = open("/proc/cpuinfo").read()
m = re.search(r"cpu MHz\s+:\s+(\d+)", output)
if m:
mhz = int(m.group(1))
else:
m = re.search(r"BogoMIPS\s+:\s+(\d+)", output)
if m:
return "{} BogoMIPS".format(int(m.group(1)))
if not mhz:
return "n/a"
if mhz < 1000:
return "{} MHz".format(mhz)
else:
return "{:0.01f} GHz".format(float(mhz) / 1000.0)
def temperature(self, _):
if self._freq:
return "{}°c @ {}".format(self._temperature, self._mhz)
else:
return "{}°c".format(self._temperature)
def update(self):
self._temperature = self.get_temp()
if self._freq:
self._mhz = self.get_mhz()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,84 @@
# pylint: disable=C0111,R0903,W1401
""" Execute command in shell and print result
Few command examples:
'ping -c 1 1.1.1.1 | grep -Po '(?<=time=)\d+(\.\d+)? ms''
'echo 'BTC=$(curl -s rate.sx/1BTC | grep -Po \'^\d+\')USD''
'curl -s https://wttr.in/London?format=%l+%t+%h+%w'
'pip3 freeze | wc -l'
'any_custom_script.sh | grep arguments'
Parameters:
* shell.command: Command to execute
Use single parentheses if evaluating anything inside (sh-style)
For example shell.command='echo $(date +'%H:%M:%S')'
But NOT shell.command='echo $(date +'%H:%M:%S')'
Second one will be evaluated only once at startup
* shell.interval: Update interval in seconds
(defaults to 1s == every bumblebee-status update)
* shell.async: Run update in async mode. Won't run next thread if
previous one didn't finished yet. Useful for long
running scripts to avoid bumblebee-status freezes
(defaults to False)
contributed by `rrhuffy <https://github.com/rrhuffy>`_ - many thanks!
"""
import os
import subprocess
import threading
import core.module
import core.widget
import core.input
import util.format
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.get_output))
self.__command = self.parameter("command", 'echo "no command configured"')
self.__async = util.format.asbool(self.parameter("async"))
if self.__async:
self.__output = "please wait..."
self.__current_thread = threading.Thread()
# LMB and RMB will update output regardless of timer
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
def set_output(self, value):
self.__output = value
def get_output(self, _):
return self.__output
def update(self):
# 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, ignore_errors=True).strip()
return
# if previous thread didn't end yet then don't do anything
if self.__current_thread.is_alive():
return
# spawn new thread to execute command and pass callback method to get output from it
self.__current_thread = threading.Thread(
target=lambda obj, cmd: obj.set_output(
util.cli.execute(cmd, ignore_errors=True)
),
args=(self, self.__command),
)
self.__current_thread.start()
def state(self, _):
if self.__output == "no command configured":
return "warning"
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,69 @@
# pylint: disable=C0112,R0903
"""Shows a widget per user-defined shortcut and allows to define the behaviour
when clicking on it.
For more than one shortcut, the commands and labels are strings separated by
a demiliter (; semicolon by default).
For example in order to create two shortcuts labeled A and B with commands
cmdA and cmdB you could do:
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B'
Parameters:
* shortcut.cmds : List of commands to execute
* shortcut.labels: List of widgets' labels (text)
* shortcut.delim : Commands and labels delimiter (; semicolon by default)
contributed by `cacyss0807 <https://github.com/cacyss0807>`_ - many thanks!
"""
import logging
LINK = "https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
LABEL = "Click me"
import core.module
import core.input
import core.decorators
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.__labels = self.parameter("labels", "{}".format(LABEL))
self.__cmds = self.parameter("cmds", "firefox {}".format(LINK))
self.__delim = self.parameter("delim", ";")
self.update_widgets()
def update_widgets(self):
""" Creates a set of widget per user define shortcut."""
cmds = self.__cmds.split(self.__delim)
labels = self.__labels.split(self.__delim)
# to be on the safe side create as many widgets as there are data (cmds or labels)
num_shortcuts = min(len(cmds), len(labels))
# report possible problem as a warning
if len(cmds) is not len(labels):
logging.warning(
"shortcut: the number of commands does not match "
"the number of provided labels."
)
logging.warning("cmds : %s, labels : %s", cmds, labels)
for idx in range(0, num_shortcuts):
cmd = cmds[idx]
label = labels[idx]
widget = self.add_widget(full_text=label)
core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=cmd)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,127 @@
# -*- coding: UTF-8 -*-
# smart function inspired by py-SMART https://github.com/freenas/py-SMART
# under Copyright (C) 2015 Marc Herndon and GPL2
"""Displays HDD smart status of different drives or all drives
Parameters:
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles')
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
"""
import os
import shutil
import core.module
import core.decorators
import util.cli
import util.format
class Module(core.module.Module):
@core.decorators.every(minutes=5)
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.devices = self.list_devices()
self.display = self.parameter("display", "combined")
self.drives = self.parameter("drives", "sda")
self.show_names = util.format.asbool(self.parameter("show_names", True))
self.create_widgets()
def create_widgets(self):
if self.display == "combined":
widget = self.add_widget()
widget.set("device", "combined")
widget.set("assessment", self.combined())
self.output(widget)
else:
for device in self.devices:
if self.display == "singles" and device not in self.drives:
continue
widget = self.add_widget()
widget.set("device", device)
widget.set("assessment", self.smart(device))
self.output(widget)
def update(self):
for widget in self.widgets():
device = widget.get("device")
if device == "combined":
widget.set("assessment", self.combined())
self.output(widget)
else:
widget.set("assessment", self.smart(device))
self.output(widget)
def output(self, widget):
device = widget.get("device")
assessment = widget.get("assessment")
if self.show_names:
widget.full_text("{}: {}".format(device, assessment))
else:
widget.full_text("{}".format(assessment))
def state(self, widget):
states = []
assessment = widget.get("assessment")
if assessment == "Pre-fail":
states.append("warning")
if assessment == "Fail":
states.append("critical")
return states
def combined(self):
for device in self.devices:
result = self.smart(device)
if result == "Fail":
return "Fail"
if result == "Pre-fail":
return "Pre-fail"
return "OK"
def list_devices(self):
for (root, folders, files) in os.walk("/dev"):
if root == "/dev":
devices = {
"".join(filter(lambda i: i.isdigit() == False, file))
for file in files
if "sd" in file
}
nvme = {
file for file in files if ("nvme0n" in file and "p" not in file)
}
devices.update(nvme)
return devices
def smart(self, disk_name):
smartctl = shutil.which("smartctl")
assessment = None
output = util.cli.execute(
"sudo {} --health {}".format(smartctl, os.path.join("/dev/", disk_name))
)
output = output.split("\n")
line = output[4]
if "SMART" in line:
if any([i in line for i in ["PASSED", "OK"]]):
assessment = "OK"
else:
assessment = "Fail"
if assessment == "OK":
output = util.cli.execute(
"sudo {} -A {}".format(smartctl, os.path.join("/dev/", disk_name))
)
output = output.split("\n")
for line in output:
if "Pre-fail" in line:
assessment = "Pre-fail"
return assessment
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,149 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=C0111,R0903
"""Displays the state of a Space API endpoint
Space API is an API for hackspaces based on JSON. See spaceapi.io for
an example.
Requires the following libraries:
* requests
* regex
Parameters:
* spaceapi.url: String representation of the api endpoint
* spaceapi.format: Format string for the output
Format Strings:
* Format strings are indicated by double %%
* They represent a leaf in the JSON tree, layers seperated by '.'
* Boolean values can be overwritten by appending '%true%false'
in the format string
* Example: to reference 'open' in '{'state':{'open': true}}'
you would write '%%state.open%%', if you also want
to say 'Open/Closed' depending on the boolean you
would write '%%state.open%Open%Closed%%'
contributed by `rad4day <https://github.com/rad4day>`_ - many thanks!
"""
import requests
import threading
import re
import json
import core.module
import core.widget
import core.input
import core.decorators
def formatStringBuilder(s, json):
"""
Parses Format Strings
Parameter:
s -> format string
json -> the spaceapi response object
"""
identifiers = re.findall("%%.*?%%", s)
for i in identifiers:
ic = i[2:-2] # Discard %%
j = ic.split("%")
# Only neither of, or both true AND false may be overwritten
if len(j) != 3 and len(j) != 1:
return "INVALID FORMAT STRING"
if len(j) == 1: # no overwrite
s = s.replace(i, json[j[0]])
elif json[j[0]]: # overwrite for True
s = s.replace(i, j[1])
else: # overwrite for False
s = s.replace(i, j[2])
return s
class Module(core.module.Module):
@core.decorators.every(minutes=15)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.getState))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__forceReload)
self.__data = {}
self.__error = None
self.__thread = None
# The URL representing the api endpoint
self.__url = self.parameter("url", default="http://club.entropia.de/spaceapi")
self._format = self.parameter(
"format", default="%%space%%: %%state.open%Open%Closed%%"
)
def state(self, widget):
try:
if self.__error is not None:
return ["critical"]
elif self.__data["state.open"]:
return ["warning"]
else:
return []
except KeyError:
return ["critical"]
def update(self):
if not self.__thread or self.__thread.is_alive() == False:
self.__thread = threading.Thread(target=self.get_api_async, args=())
self.__thread.start()
def getState(self, widget):
text = self._format
if self.__error is not None:
text = self.__error
else:
try:
text = formatStringBuilder(self._format, self.__data)
except KeyError:
text = "KeyError"
return text
def get_api_async(self):
try:
with requests.get(self.__url, timeout=10) as request:
# Can't implement error handling for python2.7 if I use
# request.json() as it uses simplejson in newer versions
self.__data = self.__flatten(json.loads(request.text))
self.__error = None
except requests.exceptions.Timeout:
self.__error = "Timeout"
except requests.exceptions.HTTPError:
self.__error = "HTTP Error"
except ValueError:
self.__error = "Not a JSON response"
core.event.trigger("update", [self.id], redraw_only=True)
# left_mouse_button handler
def __forceReload(self, event):
if self.__thread:
self.__thread.raise_exception()
self.__error = "RELOADING"
core.event.trigger("update", [self.id], redraw_only=True)
# Flattens the JSON structure recursively, e.g. ['space']['open']
# becomes ['space.open']
def __flatten(self, json):
out = {}
for key in json:
value = json[key]
if type(value) is dict:
flattened_key = self.__flatten(value)
for fk in flattened_key:
out[key + "." + fk] = flattened_key[fk]
else:
out[key] = value
return out
# Author: Tobias Manske <tobias@chaoswg.xyz>
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,95 @@
# pylint: disable=C0111,R0903
"""Displays the current song being played
Requires the following library:
* python-dbus
Parameters:
* spotify.format: Format string (defaults to '{artist} - {title}')
Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus}
* spotify.previous: Change binding for previous song (default is left click)
* spotify.next: Change binding for next song (default is right click)
* spotify.pause: Change binding for toggling pause (default is middle click)
Available options for spotify.previous, spotify.next and spotify.pause are:
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
"""
import sys
import dbus
import core.module
import core.widget
import core.input
import core.decorators
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.spotify))
buttons = {
"LEFT_CLICK": core.input.LEFT_MOUSE,
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
"SCROLL_UP": core.input.WHEEL_UP,
"SCROLL_DOWN": core.input.WHEEL_DOWN,
}
self.__song = ""
self.__format = self.parameter("format", "{artist} - {title}")
prev_button = self.parameter("previous", "LEFT_CLICK")
next_button = self.parameter("next", "RIGHT_CLICK")
pause_button = self.parameter("pause", "MIDDLE_CLICK")
cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous")
core.input.register(self, button=buttons[next_button], cmd=cmd + "Next")
core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause")
@core.decorators.scrollable
def spotify(self, widget):
return self.string_song
def hidden(self):
return self.string_song == ""
def update(self):
try:
bus = dbus.SessionBus()
spotify = bus.get_object(
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
)
spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties")
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
playback_status = str(
spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
)
self.__song = self.__format.format(
album=str(props.get("xesam:album")),
title=str(props.get("xesam:title")),
artist=",".join(props.get("xesam:artist")),
trackNumber=str(props.get("xesam:trackNumber")),
playbackStatus="\u25B6"
if playback_status == "Playing"
else "\u258D\u258D"
if playback_status == "Paused"
else "",
)
except Exception:
self.__song = ""
@property
def string_song(self):
if sys.version_info.major < 3:
return unicode(self.__song)
return str(self.__song)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,68 @@
# -*- coding: UTF-8 -*-
# pylint: disable=C0111,R0903
"""Display a stock quote from worldtradingdata.com
Requires the following python packages:
* requests
Parameters:
* stock.symbols : Comma-separated list of symbols to fetch
* stock.change : Should we fetch change in stock value (defaults to True)
contributed by `msoulier <https://github.com/msoulier>`_ - many thanks!
"""
import json
import urllib.request
import logging
import core.module
import core.widget
import core.decorators
import util.format
class Module(core.module.Module):
@core.decorators.every(hours=1)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.value))
self.__symbols = self.parameter("symbols", "")
self.__change = util.format.asbool(self.parameter("change", True))
self.__value = None
def value(self, widget):
results = []
if not self.__value:
return "n/a"
data = json.loads(self.__value)
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):
if self.__symbols:
url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols="
url += (
self.__symbols
+ "&fields=regularMarketPrice,currency,regularMarketChange"
)
return urllib.request.urlopen(url).read().strip()
else:
logging.error("unable to retrieve stock exchange rate")
return None
def update(self):
self.__value = self.fetch()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,99 @@
# pylint: disable=C0111,R0903
"""Displays sunrise and sunset times
Requires the following python packages:
* requests
* suntime
Parameters:
* cpu.lat : Latitude of your location
* cpu.lon : Longitude of your location
(if none of those are set, location is determined automatically via location APIs)
contributed by `lonesomebyte537 <https://github.com/lonesomebyte537>`_ - many thanks!
"""
from suntime import Sun, SunTimeException
import requests
from dateutil.tz import tzlocal
import datetime
import core.module
import core.widget
import core.decorators
import util.location
class Module(core.module.Module):
@core.decorators.every(hours=1)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.suntimes))
lat = self.parameter("lat", None)
lon = self.parameter("lon", None)
self.__sun = None
if not lat or not lon:
lat, lon = util.location.coordinates()
if lat and lon:
self.__sun = Sun(float(lat), float(lon))
def suntimes(self, _):
if self.__sunset and self.__sunrise:
if self.__isup:
return "\u21A7{} \u21A5{}".format(
self.__sunset.strftime("%H:%M"), self.__sunrise.strftime("%H:%M")
)
return "\u21A5{} \u21A7{}".format(
self.__sunrise.strftime("%H:%M"), self.__sunset.strftime("%H:%M")
)
return "n/a"
def __calculate_times(self):
self.__isup = False
order_matters = True
try:
self.__sunrise = self.__sun.get_local_sunrise_time()
except SunTimeException:
self.__sunrise = "no sunrise"
order_matters = False
try:
self.__sunset = self.__sun.get_local_sunset_time()
except SunTimeException:
self.__sunset = "no sunset"
order_matters = False
if not order_matters:
return
now = datetime.datetime.now(tz=tzlocal())
if now > self.__sunset:
tomorrow = (now + datetime.timedelta(days=1)).date()
try:
self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow)
self.__sunset = self.__sun.get_local_sunset_time(tomorrow)
except SunTimeException:
self.__sunrise = "no sunrise"
self.__sunset = "no sunset"
elif now > self.__sunrise:
tomorrow = (now + datetime.timedelta(days=1)).date()
try:
self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow)
except SunTimeException:
self.__sunrise = "no sunrise"
return
self.__isup = True
def update(self):
self.__calculate_times()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# pylint: disable=C0111,R0903
""" system module
adds the possibility to
* shutdown
* 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.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')
* system.switch_user: specify a command for switching the user (defaults to 'i3exit switch_user')
* system.lock: specify a command for locking the screen (defaults to 'i3exit lock')
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
"""
import logging
import functools
try:
import tkinter as tk
from tkinter import messagebox as tkmessagebox
except ImportError:
logging.warning("failed to import tkinter - bumblebee popups won't work!")
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
import util.popup
import util.format
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.text))
self.__confirm = util.format.asbool(self.parameter("confirm", True))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
def text(self, widget):
return ""
def __on_command(self, header, text, command):
do_it = True
if self.__confirm:
root = tk.Tk()
root.withdraw()
root.focus_set()
do_it = tkmessagebox.askyesno(header, text)
root.destroy()
if do_it:
util.cli.execute(command)
def popup(self, widget):
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")
switch_user_cmd = self.parameter("switch_user", "i3exit switch_user")
lock_cmd = self.parameter("lock", "i3exit lock")
suspend_cmd = self.parameter("suspend", "i3exit suspend")
hibernate_cmd = self.parameter("hibernate", "i3exit hibernate")
menu.add_menuitem(
"shutdown",
callback=functools.partial(
self.__on_command, "Shutdown", "Shutdown?", shutdown_cmd
),
)
menu.add_menuitem(
"reboot",
callback=functools.partial(
self.__on_command, "Reboot", "Reboot?", reboot_cmd
),
)
menu.add_menuitem(
"log out",
callback=functools.partial(
self.__on_command, "Log out", "Log out?", "i3exit logout"
),
)
# don't ask for these
menu.add_menuitem(
"switch user", callback=functools.partial(util.cli.execute, switch_user_cmd)
)
menu.add_menuitem(
"lock", callback=functools.partial(util.cli.execute, lock_cmd)
)
menu.add_menuitem(
"suspend", callback=functools.partial(util.cli.execute, suspend_cmd)
)
menu.add_menuitem(
"hibernate", callback=functools.partial(util.cli.execute, hibernate_cmd)
)
menu.show(widget, 0, 0)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,41 @@
"""Displays the number of pending tasks in TaskWarrior.
Requires the following library:
* taskw
Parameters:
* taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc)
contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
"""
from taskw import TaskWarrior
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__pending_tasks = "0"
def update(self):
"""Return a string with the number of pending tasks from TaskWarrior."""
try:
taskrc = self.parameter("taskrc", "~/.taskrc")
w = TaskWarrior(config_filename=taskrc)
pending_tasks = w.filter_tasks({"status": "pending"})
self.__pending_tasks = str(len(pending_tasks))
except:
self.__pending_tasks = "n/a"
def output(self, _):
"""Format the task counter to output in bumblebee."""
return "{}".format(self.__pending_tasks)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,23 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time.
Parameters:
* time.format: strftime()-compatible formatting string
* time.locale: locale to use rather than the system default
"""
import core.decorators
from .datetimetz import Module
class Module(Module):
@core.decorators.every(seconds=59) # ensures one update per minute
def __init__(self, config, theme):
super().__init__(config, theme)
def default_format(self):
return "%X %Z"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,86 @@
# pylint: disable=C0111,R0903
"""Displays focused i3 window title.
Requirements:
* i3ipc
Parameters:
* title.max : Maximum character length for title before truncating. Defaults to 64.
* title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'.
* title.scroll : Boolean flag for scrolling title. Defaults to False
contributed by `UltimatePancake <https://github.com/UltimatePancake>`_ - many thanks!
"""
import threading
try:
import i3ipc
except ImportError:
pass
no_title = "n/a"
import core.module
import core.decorators
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
# parsing of parameters
self.__scroll = util.format.asbool(self.parameter("scroll", False))
self.__max = int(self.parameter("max", 64))
self.__placeholder = self.parameter("placeholder", "...")
self.__title = ""
# set output of the module
self.add_widget(
full_text=self.__scrolling_focused_title
if self.__scroll
else self.__focused_title
)
# create a connection with i3ipc
self.__i3 = i3ipc.Connection()
# event is called both on focus change and title change
self.__i3.on("window", lambda __p_i3, __p_e: self.__pollTitle())
# begin listening for events
threading.Thread(target=self.__i3.main).start()
# initialize the first title
self.__pollTitle()
def __focused_title(self, widget):
return self.__title
@core.decorators.scrollable
def __scrolling_focused_title(self, widget):
return self.__full_title
def __pollTitle(self):
"""Updating current title."""
try:
self.__full_title = self.__i3.get_tree().find_focused().name
except:
self.__full_title = no_title
if self.__full_title is None:
self.__full_title = no_title
if not self.__scroll:
# cut the text if it is too long
if len(self.__full_title) > self.__max:
self.__title = self.__full_title[
0 : self.__max - len(self.__placeholder)
]
self.__title = "{}{}".format(self.__title, self.__placeholder)
else:
self.__title = self.__full_title
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,51 @@
# pylint: disable=C0111,R0903
"""Displays the number of todo items from a text file
Parameters:
* todo.file: File to read TODOs from (defaults to ~/Documents/todo.txt)
contributed by `codingo <https://github.com/codingo>`_ - many thanks!
"""
import os.path
import core.module
import core.widget
import core.input
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
self.__todos = self.count_items()
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc)
)
def output(self, widget):
return str(self.__todos)
def update(self):
self.__todos = self.count_items()
def state(self, widgets):
if self.__todos == 0:
return "empty"
return "items"
def count_items(self):
try:
i = -1
with open(self.__doc) as f:
for i, l in enumerate(f):
pass
return i + 1
except Exception:
return 0
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,188 @@
# pylint: disable=C0111,R0903
"""Displays network IO for interfaces.
Parameters:
* traffic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth')
* traffic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
* traffic.showname: If set to False, hide network interface name (defaults to True)
* traffic.format: Format string for download/upload speeds.
Defaults to '{:.2f}'
* traffic.graphlen: Graph lenth in seconds. Positive even integer. Each
char shows 2 seconds. If set, enables up/down traffic
graphs
contributed by `meain <https://github.com/meain>`_ - many thanks!
"""
import re
import time
import psutil
import netifaces
import core.module
import util.format
import util.graph
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self._exclude = tuple(
filter(
len,
util.format.aslist(
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth")
),
)
)
self._status = ""
self._showname = util.format.asbool(self.parameter("showname", True))
self._format = self.parameter("format", "{:.2f}")
self._prev = {}
self._states = {}
self._lastcheck = 0
self._states["include"] = []
self._states["exclude"] = []
for state in tuple(
filter(len, util.format.aslist(self.parameter("states", "")))
):
if state[0] == "^":
self._states["exclude"].append(state[1:])
else:
self._states["include"].append(state)
self._graphlen = int(self.parameter("graphlen", 0))
if self._graphlen > 0:
self._graphdata = {}
self._first_run = True
self._update_widgets()
def state(self, widget):
if "traffic.rx" in widget.name:
return "rx"
if "traffic.tx" in widget.name:
return "tx"
return self._status
def update(self):
self._update_widgets()
def create_widget(self, name, txt=None, attributes={}):
widget = self.add_widget(name=name, full_text=txt)
for key in attributes:
widget.set(key, attributes[key])
return widget
def get_addresses(self, intf):
retval = []
try:
for ip in netifaces.ifaddresses(intf).get(netifaces.AF_INET, []):
if ip.get("addr", "") != "":
retval.append(ip.get("addr"))
except Exception:
return []
return retval
def get_minwidth_str(self):
"""
computes theme.minwidth string
based on traffic.format and traffic.graphlen parameters
"""
minwidth_str = ""
if self._graphlen > 0:
graph_len = int(self._graphlen / 2)
graph_prefix = "0" * graph_len
minwidth_str += graph_prefix
minwidth_str += "1000"
try:
length = int(re.match("{:\.(\d+)f}", self._format).group(1))
if length > 0:
minwidth_str += "." + "0" * length
except AttributeError:
# return default value
return "1000.00KiB/s"
finally:
minwidth_str += "KiB/s"
return minwidth_str
def _update_widgets(self):
interfaces = [
i for i in netifaces.interfaces() if not i.startswith(self._exclude)
]
self.clear_widgets()
counters = psutil.net_io_counters(pernic=True)
now = time.time()
timediff = now - (self._lastcheck if self._lastcheck else now)
if timediff <= 0:
timediff = 1
self._lastcheck = now
for interface in interfaces:
if self._graphlen > 0:
if interface not in self._graphdata:
self._graphdata[interface] = {
"rx": [0] * self._graphlen,
"tx": [0] * self._graphlen,
}
if not interface:
interface = "lo"
state = "down"
if len(self.get_addresses(interface)) > 0:
state = "up"
elif util.format.asbool(self.parameter("hide_down", True)):
continue
if len(self._states["exclude"]) > 0 and state in self._states["exclude"]:
continue
if (
len(self._states["include"]) > 0
and state not in self._states["include"]
):
continue
data = {
"rx": counters[interface].bytes_recv,
"tx": counters[interface].bytes_sent,
}
name = "traffic-{}".format(interface)
if self._showname:
self.create_widget(name, interface)
for direction in ["rx", "tx"]:
name = "traffic.{}-{}".format(direction, interface)
widget = self.create_widget(
name,
attributes={"theme.minwidth": self.get_minwidth_str()},
)
prev = self._prev.get(name, 0)
bspeed = (int(data[direction]) - int(prev)) / timediff
speed = util.format.byte(bspeed, self._format)
txtspeed = "{0}/s".format(speed)
if self._graphlen > 0:
# skip first value returned by psutil, because it is
# giant and ruins the grapth ratio until it gets pushed
# out of saved list
if self._first_run is True:
self._first_run = False
else:
self._graphdata[interface][direction] = self._graphdata[
interface
][direction][1:]
self._graphdata[interface][direction].append(bspeed)
txtspeed = "{}{}".format(
util.graph.braille(self._graphdata[interface][direction]),
txtspeed,
)
widget.full_text(txtspeed)
self._prev[name] = data[direction]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,43 @@
# pylint: disable=C0111,R0903
"""Toggle twmn notifications.
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(""))
self.__paused = False
# Make sure that twmn is currently not paused
util.cli.execute("killall -SIGUSR2 twmnd", ignore_errors=True)
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status)
def toggle_status(self, event):
self.__paused = not self.__paused
try:
if self.__paused:
util.cli.execute("systemctl --user start twmnd")
else:
util.cli.execute("systemctl --user stop twmnd")
except:
self.__paused = not self.__paused # toggling failed
def state(self, widget):
if self.__paused:
return ["muted"]
return ["unmuted"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,28 @@
# pylint: disable=C0111,R0903
"""Displays the system uptime.
contributed by `ccoors <https://github.com/ccoors>`_ - many thanks!
"""
from datetime import timedelta
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.__uptime = ""
def output(self, _):
return "{}".format(self.__uptime)
def update(self):
with open("/proc/uptime", "r") as f:
uptime_seconds = int(float(f.readline().split()[0]))
self.__uptime = timedelta(seconds=uptime_seconds)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,116 @@
# pylint: disable=C0111,R0903
""" Displays the VPN profile that is currently in use.
Left click opens a popup menu that lists all available VPN profiles and allows to establish
a VPN connection using that profile.
Prerequisites:
* tk python library (usually python-tk or python3-tk, depending on your distribution)
* nmcli needs to be installed and configured properly.
To quickly test, whether nmcli is working correctly, type 'nmcli -g NAME,TYPE,DEVICE con' which
lists all the connection profiles that are configured. Make sure that your VPN profile is in that list!
e.g: to import a openvpn profile via nmcli:
`sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn>`
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
"""
import logging
import functools
import core.module
import core.widget
import core.input
import util.cli
import util.popup
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.vpn_status))
self.__connected_vpn_profile = None
self.__selected_vpn_profile = None
res = util.cli.execute("nmcli -g NAME,TYPE c")
lines = res.splitlines()
self.__vpn_profiles = []
for line in lines:
info = line.split(":")
try:
if self.__isvpn(info[1]):
self.__vpn_profiles.append(info[0])
except:
pass
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
def __isvpn(self, connection_type):
return connection_type in ["vpn", "wireguard"]
def update(self):
try:
res = util.cli.execute("nmcli -g NAME,TYPE,DEVICE con")
lines = res.splitlines()
self.__connected_vpn_profile = None
for line in lines:
info = line.split(":")
if self.__isvpn(info[1]) and info[2] != "":
self.__connected_vpn_profile = info[0]
except Exception as e:
logging.exception("Could not get VPN status")
self.__connected_vpn_profile = None
def vpn_status(self, widget):
if self.__connected_vpn_profile is None:
return "off"
return self.__connected_vpn_profile
def __on_vpndisconnect(self):
try:
util.cli.execute(
"nmcli c down '{vpn}'".format(vpn=self.__connected_vpn_profile)
)
self.__connected_vpn_profile = None
except Exception as e:
logging.exception("Could not disconnect VPN connection")
def __on_vpnconnect(self, name):
self.__selected_vpn_profile = name
try:
util.cli.execute(
"nmcli c up '{vpn}'".format(vpn=self.__selected_vpn_profile)
)
self.__connected_vpn_profile = name
except Exception as e:
logging.exception("Could not establish VPN connection")
self.__connected_vpn_profile = None
def popup(self, widget):
menu = util.popup.menu()
if self.__connected_vpn_profile is not None:
menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect)
for vpn_profile in self.__vpn_profiles:
if (
self.__connected_vpn_profile is not None
and self.__connected_vpn_profile == vpn_profile
):
continue
menu.add_menuitem(
vpn_profile,
callback=functools.partial(self.__on_vpnconnect, vpn_profile),
)
menu.show(widget)
def state(self, widget):
return []
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,60 @@
# pylint: disable=C0111,R0903
"""Displays the status of watson (time-tracking tool)
Requires the following executable:
* watson
contributed by `bendardenne <https://github.com/bendardenne>`_ - many thanks!
"""
import logging
import re
import functools
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.text))
self.__tracking = False
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:
util.cli.execute("watson restart")
self.__tracking = not self.__tracking
def text(self, widget):
if self.__tracking:
return self.__project
else:
return "Paused"
def update(self):
output = util.cli.execute("watson status")
if re.match("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"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,147 @@
# -*- coding: UTF-8 -*-
# pylint: disable=C0111,R0903
"""Displays the temperature on the current location based on the ip
Requires the following python packages:
* requests
Parameters:
* weather.location: Set location, defaults to 'auto' for getting location automatically from a web service
If set to a comma-separated list, left-click and right-click can be used to rotate the locations.
Locations should be city names or city ids.
* 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
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.format
import util.location
import re
import requests
from requests.exceptions import RequestException
class Module(core.module.Module):
@core.decorators.every(minutes=15)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__temperature = 0
self.__apikey = self.parameter("apikey", "af7bfe22287c652d032a3064ffa44088")
self.__location = util.format.aslist(self.parameter("location", "auto"))
self.__index = 0
self.__showcity = util.format.asbool(self.parameter("showcity", True))
self.__showminmax = util.format.asbool(self.parameter("showminmax", False))
self.__unit = self.parameter("unit", "metric")
self.__valid = False
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd=self.__next_location
)
core.input.register(
self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_location
)
def __next_location(self, event):
self.__index = (self.__index + 1) % len(self.__location)
self.update()
def __prev_location(self, event):
self.__index = (
len(self.__location) - 1 if self.__index <= 0 else self.__index - 1
)
self.update()
def temperature(self):
return util.format.astemperature(self.__temperature, self.__unit)
def tempmin(self):
return util.format.astemperature(self.__tempmin, self.__unit)
def tempmax(self):
return util.format.astemperature(self.__tempmax, self.__unit)
def city(self):
city = re.sub("[_-]", " ", self.__city)
return "{} ".format(city)
def output(self, widget):
if not self.__valid:
return "?"
if self.__showminmax:
self.__showcity = False
return (
self.city()
+ self.temperature()
+ " Hi:"
+ self.tempmax()
+ " Lo:"
+ self.tempmin()
)
elif self.__showcity:
return self.city() + self.temperature()
else:
return self.temperature()
def state(self, widget):
if self.__valid:
if "thunderstorm" in self.__weather:
return ["thunder"]
elif "drizzle" in self.__weather:
return ["rain"]
elif "rain" in self.__weather:
return ["rain"]
elif "snow" in self.__weather:
return ["snow"]
elif "sleet" in self.__weather:
return ["sleet"]
elif "clear" in self.__weather:
return ["clear"]
elif "cloud" in self.__weather:
return ["clouds"]
return []
def update(self):
try:
weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format(
self.__apikey
)
weather_url = "{}&units={}".format(weather_url, self.__unit)
if self.__location[self.__index] == "auto":
coord = util.location.coordinates()
weather_url = "{url}&lat={lat}&lon={lon}".format(
url=weather_url, lat=coord[0], lon=coord[1]
)
elif self.__location[self.__index].isdigit():
weather_url = "{url}&id={id}".format(
url=weather_url, id=self.__location[self.__index]
)
else:
weather_url = "{url}&q={city}".format(
url=weather_url, city=self.__location[self.__index]
)
weather = requests.get(weather_url).json()
self.__city = weather["name"]
self.__temperature = int(weather["main"]["temp"])
self.__tempmin = int(weather["main"]["temp_min"])
self.__tempmax = int(weather["main"]["temp_max"])
self.__weather = weather["weather"][0]["main"].lower()
self.__valid = True
except Exception:
self.__valid = False
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,25 @@
# pylint: disable=C0111,R0903
"""Opens a random xkcd comic in the browser.
contributed by `whzup <https://github.com/whzup>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import core.decorators
class Module(core.module.Module):
@core.decorators.never
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget("xkcd"))
core.input.register(
self,
button=core.input.LEFT_MOUSE,
cmd="xdg-open https://c.xkcd.com/random/comic/",
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,41 @@
# pylint: disable=C0111,R0903
"""Shows yubikey information
Requires: https://github.com/Yubico/python-yubico
The output indicates that a YubiKey is not connected or it displays
the corresponding serial number.
contributed by `EmmaTinten <https://github.com/EmmaTinten>`_ - many thanks!
"""
import yubico
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.keystate))
self.__keystate = "No YubiKey"
def keystate(self, widget):
return self.__keystate
def update(self):
try:
self.__keystate = "YubiKey: " + str(
yubico.find_yubikey(debug=False).serial()
)
except yubico.yubico_exception.YubicoError:
self.__keystate = "No YubiKey"
except Exception:
self.__keystate = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,235 @@
"""Displays info about zpools present on the system
Parameters:
* zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools
is displayed. (Default: '')
* zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse},
{status}, {shortstatus}, {fragpercent}, {deduppercent} are supported.
(Default: '{name} {used}/{size} ({percentfree}%)')
* zpool.showio: Show also widgets detailing current read and write I/O (Default: true)
* zpool.ioformat: Format string for I/O widget, tags {ops} (operations per seconds) and {band}
(bandwidth) are supported. (Default: '{band}')
* zpool.warnfree: Warn if free space is below this percentage (Default: 10)
* zpool.sudo: Use sudo when calling the `zpool` binary. (Default: false)
Option `zpool.sudo` is intended for Linux users using zfsonlinux older than 0.7.0: In pre-0.7.0
releases of zfsonlinux regular users couldn't invoke even informative commands such as
`zpool list`. If this option is true, command `zpool list` is invoked with sudo. If this option
is used, the following (or ekvivalent) must be added to the `sudoers(5)`:
```
<username/ALL> ALL = (root) NOPASSWD: /usr/bin/zpool list
```
Be aware of security implications of doing this!
contributed by `adam-dej <https://github.com/adam-dej>`_ - many thanks!
"""
import time
import logging
from pkg_resources import parse_version
log = logging.getLogger(__name__)
import core.module
import util.cli
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self._includelist = set(
filter(
lambda x: len(x) > 0,
util.format.aslist(self.parameter("list", default="")),
)
)
self._format = self.parameter(
"format", default="{name} {shortstatus} {used}/{size} " + "({percentfree}%)"
)
self._usesudo = util.format.asbool(self.parameter("sudo", default=False))
self._showio = util.format.asbool(self.parameter("showio", default=True))
self._ioformat = self.parameter("ioformat", default="{band}")
self._warnfree = int(self.parameter("warnfree", default=10))
def update(self):
self.clear_widgets()
zfs_version_path = "/sys/module/zfs/version"
# zpool list -H: List all zpools, use script mode (no headers and tabs as separators).
try:
with open(zfs_version_path, "r") as zfs_mod_version:
zfs_version = zfs_mod_version.readline().rstrip().split("-")[0]
except IOError:
# ZFS isn't installed or the module isn't loaded, stub the version
zfs_version = "0.0.0"
logging.error(
"ZFS version information not found at {}, check the module is loaded.".format(
zfs_version_path
)
)
raw_zpools = util.cli.execute(
("sudo " if self._usesudo else "") + "zpool list -H"
).split("\n")
for raw_zpool in raw_zpools:
try:
# Ignored fields (assigned to _) are 'expandsz' and 'altroot', also 'ckpoint' in ZFS 0.8.0+
if parse_version(zfs_version) < parse_version("0.8.0"):
(
name,
size,
alloc,
free,
_,
frag,
cap,
dedup,
health,
_,
) = raw_zpool.split("\t")
else:
(
name,
size,
alloc,
free,
_,
_,
frag,
cap,
dedup,
health,
_,
) = raw_zpool.split("\t")
cap = cap.rstrip("%")
percentuse = int(cap)
percentfree = 100 - percentuse
# There is a command, zpool iostat, which is however blocking and was therefore
# causing issues.
# Instead, we read file `/proc/spl/kstat/zfs/<poolname>/io` which contains
# cumulative I/O statistics since boot (or pool creation). We store these values
# (and timestamp) during each widget update, and during the next widget update we
# use them to compute delta of transferred bytes, and using the last and current
# timestamp the rate at which they have been transferred.
with open("/proc/spl/kstat/zfs/{}/io".format(name), "r") as f:
# Third row provides data we need, we are interested in the first 4 values.
# More info about this file can be found here:
# https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h#L580
# The 4 values are:
# nread, nwritten, reads, writes
iostat = list(map(int, f.readlines()[2].split()[:4]))
except (ValueError, IOError):
# Unable to parse info about this pool, skip it
continue
if self._includelist and name not in self._includelist:
continue
widget = self.widget(name)
if not widget:
widget = self.add_widget(name=name)
widget.set("last_iostat", [0, 0, 0, 0])
widget.set("last_timestamp", 0)
delta_iostat = [b - a for a, b in zip(iostat, widget.get("last_iostat"))]
widget.set("last_iostat", iostat)
# From docs:
# > Note that even though the time is always returned as a floating point number, not
# > all systems provide time with a better precision than 1 second.
# Be aware that that may affect the precision of reported I/O
# Also, during one update cycle the reported I/O may be garbage if the system time
# was changed.
timestamp = time.time()
delta_timestamp = widget.get("last_timestamp") - timestamp
widget.set("last_timestamp", time.time())
# abs is there because sometimes the result is -0
rate_iostat = [abs(x / delta_timestamp) for x in delta_iostat]
nread, nwritten, reads, writes = rate_iostat
# theme.minwidth is not set since these values are not expected to change
# rapidly
widget.full_text(
self._format.format(
name=name,
used=alloc,
left=free,
size=size,
percentfree=percentfree,
percentuse=percentuse,
status=health,
shortstatus=self._shortstatus(health),
fragpercent=frag,
deduppercent=dedup,
)
)
widget.set("state", health)
widget.set("percentfree", percentfree)
widget.set("visited", True)
if self._showio:
wname, rname = [name + x for x in ["__write", "__read"]]
widget_w = self.widget(wname)
widget_r = self.widget(rname)
if not widget_w or not widget_r:
widget_r = self.add_widget(name=rname)
widget_w = self.add_widget(name=wname)
for w in [widget_r, widget_w]:
w.set(
"theme.minwidth",
self._ioformat.format(
ops=9999, band=util.format.bytefmt(999.99 * (1024 ** 2))
),
)
widget_w.full_text(
self._ioformat.format(
ops=round(writes), band=util.format.bytefmt(nwritten)
)
)
widget_r.full_text(
self._ioformat.format(
ops=round(reads), band=util.format.bytefmt(nread)
)
)
def state(self, widget):
if widget.name.endswith("__read"):
return "poolread"
elif widget.name.endswith("__write"):
return "poolwrite"
state = widget.get("state")
if state == "FAULTED":
return [state, "critical"]
elif state == "DEGRADED" or widget.get("percentfree") < self._warnfree:
return [state, "warning"]
return state
@staticmethod
def _shortstatus(status):
# From `zpool(8)`, section Device Failure and Recovery:
# A pool's health status is described by one of three states: online, degraded, or faulted.
# An online pool has all devices operating normally. A degraded pool is one in which one
# or more devices have failed, but the data is still available due to a redundant
# configuration. A faulted pool has corrupted metadata, or one or more faulted devices, and
# insufficient replicas to continue functioning.
shortstate = {
"DEGRADED": "DEG",
"FAULTED": "FLT",
"ONLINE": "ONL",
}
try:
return shortstate[status]
except KeyError:
return ""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4