[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:
parent
1d25be2059
commit
320827d577
146 changed files with 2509 additions and 2 deletions
0
bumblebee_status/modules/contrib/__init__.py
Normal file
0
bumblebee_status/modules/contrib/__init__.py
Normal file
51
bumblebee_status/modules/contrib/amixer.py
Normal file
51
bumblebee_status/modules/contrib/amixer.py
Normal 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
|
89
bumblebee_status/modules/contrib/apt.py
Normal file
89
bumblebee_status/modules/contrib/apt.py
Normal 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
|
50
bumblebee_status/modules/contrib/arch-update.py
Normal file
50
bumblebee_status/modules/contrib/arch-update.py
Normal 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
|
334
bumblebee_status/modules/contrib/battery-upower.py
Normal file
334
bumblebee_status/modules/contrib/battery-upower.py
Normal 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
|
209
bumblebee_status/modules/contrib/battery.py
Normal file
209
bumblebee_status/modules/contrib/battery.py
Normal 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
|
125
bumblebee_status/modules/contrib/bluetooth.py
Normal file
125
bumblebee_status/modules/contrib/bluetooth.py
Normal 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
|
103
bumblebee_status/modules/contrib/bluetooth2.py
Normal file
103
bumblebee_status/modules/contrib/bluetooth2.py
Normal 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
|
70
bumblebee_status/modules/contrib/brightness.py
Normal file
70
bumblebee_status/modules/contrib/brightness.py
Normal 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
|
117
bumblebee_status/modules/contrib/caffeine.py
Normal file
117
bumblebee_status/modules/contrib/caffeine.py
Normal 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
|
156
bumblebee_status/modules/contrib/cmus.py
Normal file
156
bumblebee_status/modules/contrib/cmus.py
Normal 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
|
154
bumblebee_status/modules/contrib/cpu2.py
Normal file
154
bumblebee_status/modules/contrib/cpu2.py
Normal 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
|
372
bumblebee_status/modules/contrib/currency.py
Normal file
372
bumblebee_status/modules/contrib/currency.py
Normal 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
|
110
bumblebee_status/modules/contrib/datetimetz.py
Normal file
110
bumblebee_status/modules/contrib/datetimetz.py
Normal 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
|
23
bumblebee_status/modules/contrib/datetz.py
Normal file
23
bumblebee_status/modules/contrib/datetz.py
Normal 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
|
149
bumblebee_status/modules/contrib/deadbeef.py
Normal file
149
bumblebee_status/modules/contrib/deadbeef.py
Normal 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
|
84
bumblebee_status/modules/contrib/deezer.py
Normal file
84
bumblebee_status/modules/contrib/deezer.py
Normal 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
|
83
bumblebee_status/modules/contrib/dnf.py
Normal file
83
bumblebee_status/modules/contrib/dnf.py
Normal 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
|
48
bumblebee_status/modules/contrib/docker_ps.py
Normal file
48
bumblebee_status/modules/contrib/docker_ps.py
Normal 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
|
37
bumblebee_status/modules/contrib/dunst.py
Normal file
37
bumblebee_status/modules/contrib/dunst.py
Normal 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"]
|
89
bumblebee_status/modules/contrib/getcrypto.py
Normal file
89
bumblebee_status/modules/contrib/getcrypto.py
Normal 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
|
73
bumblebee_status/modules/contrib/github.py
Normal file
73
bumblebee_status/modules/contrib/github.py
Normal 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
|
60
bumblebee_status/modules/contrib/gpmdp.py
Normal file
60
bumblebee_status/modules/contrib/gpmdp.py
Normal 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
|
95
bumblebee_status/modules/contrib/hddtemp.py
Normal file
95
bumblebee_status/modules/contrib/hddtemp.py
Normal 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
|
28
bumblebee_status/modules/contrib/hostname.py
Normal file
28
bumblebee_status/modules/contrib/hostname.py
Normal 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
|
70
bumblebee_status/modules/contrib/http_status.py
Normal file
70
bumblebee_status/modules/contrib/http_status.py
Normal 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
|
62
bumblebee_status/modules/contrib/indicator.py
Normal file
62
bumblebee_status/modules/contrib/indicator.py
Normal 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
|
24
bumblebee_status/modules/contrib/kernel.py
Normal file
24
bumblebee_status/modules/contrib/kernel.py
Normal 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
|
42
bumblebee_status/modules/contrib/layout-xkbswitch.py
Normal file
42
bumblebee_status/modules/contrib/layout-xkbswitch.py
Normal 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
|
78
bumblebee_status/modules/contrib/layout.py
Normal file
78
bumblebee_status/modules/contrib/layout.py
Normal 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
|
32
bumblebee_status/modules/contrib/libvirtvms.py
Normal file
32
bumblebee_status/modules/contrib/libvirtvms.py
Normal 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
|
59
bumblebee_status/modules/contrib/mocp.py
Normal file
59
bumblebee_status/modules/contrib/mocp.py
Normal 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
|
209
bumblebee_status/modules/contrib/mpd.py
Normal file
209
bumblebee_status/modules/contrib/mpd.py
Normal 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
|
113
bumblebee_status/modules/contrib/network_traffic.py
Normal file
113
bumblebee_status/modules/contrib/network_traffic.py
Normal 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
|
51
bumblebee_status/modules/contrib/notmuch_count.py
Normal file
51
bumblebee_status/modules/contrib/notmuch_count.py
Normal 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
|
82
bumblebee_status/modules/contrib/nvidiagpu.py
Normal file
82
bumblebee_status/modules/contrib/nvidiagpu.py
Normal 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
|
254
bumblebee_status/modules/contrib/octoprint.py
Normal file
254
bumblebee_status/modules/contrib/octoprint.py
Normal 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
|
78
bumblebee_status/modules/contrib/pacman.py
Normal file
78
bumblebee_status/modules/contrib/pacman.py
Normal 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
|
85
bumblebee_status/modules/contrib/pihole.py
Normal file
85
bumblebee_status/modules/contrib/pihole.py
Normal 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
|
129
bumblebee_status/modules/contrib/pomodoro.py
Normal file
129
bumblebee_status/modules/contrib/pomodoro.py
Normal 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
|
68
bumblebee_status/modules/contrib/prime.py
Normal file
68
bumblebee_status/modules/contrib/prime.py
Normal 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
|
108
bumblebee_status/modules/contrib/progress.py
Normal file
108
bumblebee_status/modules/contrib/progress.py
Normal 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
|
28
bumblebee_status/modules/contrib/publicip.py
Normal file
28
bumblebee_status/modules/contrib/publicip.py
Normal 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
|
61
bumblebee_status/modules/contrib/rotation.py
Normal file
61
bumblebee_status/modules/contrib/rotation.py
Normal 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
|
368
bumblebee_status/modules/contrib/rss.py
Normal file
368
bumblebee_status/modules/contrib/rss.py
Normal 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'>★</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
|
148
bumblebee_status/modules/contrib/sensors.py
Normal file
148
bumblebee_status/modules/contrib/sensors.py
Normal 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
|
84
bumblebee_status/modules/contrib/shell.py
Normal file
84
bumblebee_status/modules/contrib/shell.py
Normal 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
|
69
bumblebee_status/modules/contrib/shortcut.py
Normal file
69
bumblebee_status/modules/contrib/shortcut.py
Normal 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
|
127
bumblebee_status/modules/contrib/smartstatus.py
Normal file
127
bumblebee_status/modules/contrib/smartstatus.py
Normal 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
|
149
bumblebee_status/modules/contrib/spaceapi.py
Normal file
149
bumblebee_status/modules/contrib/spaceapi.py
Normal 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
|
95
bumblebee_status/modules/contrib/spotify.py
Normal file
95
bumblebee_status/modules/contrib/spotify.py
Normal 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
|
68
bumblebee_status/modules/contrib/stock.py
Normal file
68
bumblebee_status/modules/contrib/stock.py
Normal 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
|
99
bumblebee_status/modules/contrib/sun.py
Normal file
99
bumblebee_status/modules/contrib/sun.py
Normal 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
|
116
bumblebee_status/modules/contrib/system.py
Normal file
116
bumblebee_status/modules/contrib/system.py
Normal 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
|
41
bumblebee_status/modules/contrib/taskwarrior.py
Normal file
41
bumblebee_status/modules/contrib/taskwarrior.py
Normal 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
|
23
bumblebee_status/modules/contrib/timetz.py
Normal file
23
bumblebee_status/modules/contrib/timetz.py
Normal 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
|
86
bumblebee_status/modules/contrib/title.py
Normal file
86
bumblebee_status/modules/contrib/title.py
Normal 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
|
51
bumblebee_status/modules/contrib/todo.py
Normal file
51
bumblebee_status/modules/contrib/todo.py
Normal 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
|
188
bumblebee_status/modules/contrib/traffic.py
Normal file
188
bumblebee_status/modules/contrib/traffic.py
Normal 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
|
43
bumblebee_status/modules/contrib/twmn.py
Normal file
43
bumblebee_status/modules/contrib/twmn.py
Normal 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
|
28
bumblebee_status/modules/contrib/uptime.py
Normal file
28
bumblebee_status/modules/contrib/uptime.py
Normal 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
|
116
bumblebee_status/modules/contrib/vpn.py
Normal file
116
bumblebee_status/modules/contrib/vpn.py
Normal 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
|
60
bumblebee_status/modules/contrib/watson.py
Normal file
60
bumblebee_status/modules/contrib/watson.py
Normal 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
|
147
bumblebee_status/modules/contrib/weather.py
Normal file
147
bumblebee_status/modules/contrib/weather.py
Normal 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
|
25
bumblebee_status/modules/contrib/xkcd.py
Normal file
25
bumblebee_status/modules/contrib/xkcd.py
Normal 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
|
41
bumblebee_status/modules/contrib/yubikey.py
Normal file
41
bumblebee_status/modules/contrib/yubikey.py
Normal 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
|
235
bumblebee_status/modules/contrib/zpool.py
Normal file
235
bumblebee_status/modules/contrib/zpool.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue