[core] restructure to allow PIP packaging

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

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

View file

View file

@ -0,0 +1,520 @@
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (built by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.18 (https://github.com/warner/python-versioneer)
"""Git implementation of _version.py."""
import errno
import os
import re
import subprocess
import sys
def get_keywords():
"""Get the keywords needed to look up the version information."""
# these strings will be replaced by git during git-archive.
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
git_refnames = "$Format:%d$"
git_full = "$Format:%H$"
git_date = "$Format:%ci$"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
class VersioneerConfig:
"""Container for Versioneer configuration parameters."""
def get_config():
"""Create, populate and return the VersioneerConfig() object."""
# these strings are filled in when 'setup.py versioneer' creates
# _version.py
cfg = VersioneerConfig()
cfg.VCS = "git"
cfg.style = "pep440"
cfg.tag_prefix = ""
cfg.parentdir_prefix = "bumblebee"
cfg.versionfile_source = "bumblebee/_version.py"
cfg.verbose = False
return cfg
class NotThisMethod(Exception):
"""Exception raised if a method is not valid for the current scenario."""
LONG_VERSION_PY = {}
HANDLERS = {}
def register_vcs_handler(vcs, method): # decorator
"""Decorator to mark a method as the handler for a particular VCS."""
def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f
return decorate
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
p = None
for c in commands:
try:
dispcmd = str([c] + args)
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
stdout=subprocess.PIPE,
stderr=(subprocess.PIPE if hide_stderr
else None))
break
except EnvironmentError:
e = sys.exc_info()[1]
if e.errno == errno.ENOENT:
continue
if verbose:
print("unable to run %s" % dispcmd)
print(e)
return None, None
else:
if verbose:
print("unable to find command, tried %s" % (commands,))
return None, None
stdout = p.communicate()[0].strip()
if sys.version_info[0] >= 3:
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % dispcmd)
print("stdout was %s" % stdout)
return None, p.returncode
return stdout, p.returncode
def versions_from_parentdir(parentdir_prefix, root, verbose):
"""Try to determine the version from the parent directory name.
Source tarballs conventionally unpack into a directory that includes both
the project name and a version string. We will also support searching up
two directory levels for an appropriately named parent directory
"""
rootdirs = []
for i in range(3):
dirname = os.path.basename(root)
if dirname.startswith(parentdir_prefix):
return {"version": dirname[len(parentdir_prefix):],
"full-revisionid": None,
"dirty": False, "error": None, "date": None}
else:
rootdirs.append(root)
root = os.path.dirname(root) # up a level
if verbose:
print("Tried directories %s but none started with prefix %s" %
(str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@register_vcs_handler("git", "get_keywords")
def git_get_keywords(versionfile_abs):
"""Extract version information from the given file."""
# the code embedded in _version.py can just fetch the value of these
# keywords. When used from setup.py, we don't want to import _version.py,
# so we do it with a regexp instead. This function is not used from
# _version.py.
keywords = {}
try:
f = open(versionfile_abs, "r")
for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["full"] = mo.group(1)
if line.strip().startswith("git_date ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["date"] = mo.group(1)
f.close()
except EnvironmentError:
pass
return keywords
@register_vcs_handler("git", "keywords")
def git_versions_from_keywords(keywords, tag_prefix, verbose):
"""Get version information from git keywords."""
if not keywords:
raise NotThisMethod("no keywords at all, weird")
date = keywords.get("date")
if date is not None:
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
# -like" string, which we must then edit to make compliant), because
# it's been around since git-1.5.3, and it's too difficult to
# discover which version we're using, or to work around using an
# older one.
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("keywords are unexpanded, not using")
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
TAG = "tag: "
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
if not tags:
# Either we're using git < 1.8.3, or there really are no tags. We use
# a heuristic: assume all version tags have a digit. The old git %d
# expansion behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
# between branches and tags. By ignoring refnames without digits, we
# filter out many common branch names like "release" and
# "stabilization", as well as "HEAD" and "master".
tags = set([r for r in refs if re.search(r'\d', r)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
print("likely tags: %s" % ",".join(sorted(tags)))
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return {"version": r,
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": None,
"date": date}
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
return {"version": "0+unknown",
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": "no suitable tags", "date": None}
@register_vcs_handler("git", "pieces_from_vcs")
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
"""Get version from 'git describe' in the root of the source tree.
This only gets called if the git-archive 'subst' keywords were *not*
expanded, and _version.py hasn't already been rewritten with a short
version string, meaning we're inside a checked out source tree.
"""
GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
hide_stderr=True)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
raise NotThisMethod("'git rev-parse --git-dir' returned error")
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
# if there isn't one, this yields HEX[-dirty] (no NUM)
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
"--always", "--long",
"--match", "%s*" % tag_prefix],
cwd=root)
# --long was added in git-1.5.5
if describe_out is None:
raise NotThisMethod("'git describe' failed")
describe_out = describe_out.strip()
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
if full_out is None:
raise NotThisMethod("'git rev-parse' failed")
full_out = full_out.strip()
pieces = {}
pieces["long"] = full_out
pieces["short"] = full_out[:7] # maybe improved later
pieces["error"] = None
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
# TAG might have hyphens.
git_describe = describe_out
# look for -dirty suffix
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
git_describe = git_describe[:git_describe.rindex("-dirty")]
# now we have TAG-NUM-gHEX or HEX
if "-" in git_describe:
# TAG-NUM-gHEX
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
pieces["error"] = ("unable to parse git-describe output: '%s'"
% describe_out)
return pieces
# tag
full_tag = mo.group(1)
if not full_tag.startswith(tag_prefix):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
% (full_tag, tag_prefix))
return pieces
pieces["closest-tag"] = full_tag[len(tag_prefix):]
# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
# commit: short hex revision ID
pieces["short"] = mo.group(3)
else:
# HEX: no tags
pieces["closest-tag"] = None
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
cwd=root)
pieces["distance"] = int(count_out) # total number of commits
# commit date: see ISO-8601 comment in git_versions_from_keywords()
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
cwd=root)[0].strip()
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
return pieces
def plus_or_dot(pieces):
"""Return a + if we don't already have one, else return a ."""
if "+" in pieces.get("closest-tag", ""):
return "."
return "+"
def render_pep440(pieces):
"""Build up version string, with post-release "local version identifier".
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
Exceptions:
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += plus_or_dot(pieces)
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
else:
# exception #1
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
def render_pep440_pre(pieces):
"""TAG[.post.devDISTANCE] -- No -dirty.
Exceptions:
1: no tags. 0.post.devDISTANCE
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += ".post.dev%d" % pieces["distance"]
else:
# exception #1
rendered = "0.post.dev%d" % pieces["distance"]
return rendered
def render_pep440_post(pieces):
"""TAG[.postDISTANCE[.dev0]+gHEX] .
The ".dev0" means dirty. Note that .dev0 sorts backwards
(a dirty tree will appear "older" than the corresponding clean one),
but you shouldn't be releasing software with -dirty anyways.
Exceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += plus_or_dot(pieces)
rendered += "g%s" % pieces["short"]
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += "+g%s" % pieces["short"]
return rendered
def render_pep440_old(pieces):
"""TAG[.postDISTANCE[.dev0]] .
The ".dev0" means dirty.
Eexceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
return rendered
def render_git_describe(pieces):
"""TAG[-DISTANCE-gHEX][-dirty].
Like 'git describe --tags --dirty --always'.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render_git_describe_long(pieces):
"""TAG-DISTANCE-gHEX[-dirty].
Like 'git describe --tags --dirty --always -long'.
The distance/hash is unconditional.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
return {"version": "unknown",
"full-revisionid": pieces.get("long"),
"dirty": None,
"error": pieces["error"],
"date": None}
if not style or style == "default":
style = "pep440" # the default
if style == "pep440":
rendered = render_pep440(pieces)
elif style == "pep440-pre":
rendered = render_pep440_pre(pieces)
elif style == "pep440-post":
rendered = render_pep440_post(pieces)
elif style == "pep440-old":
rendered = render_pep440_old(pieces)
elif style == "git-describe":
rendered = render_git_describe(pieces)
elif style == "git-describe-long":
rendered = render_git_describe_long(pieces)
else:
raise ValueError("unknown style '%s'" % style)
return {"version": rendered, "full-revisionid": pieces["long"],
"dirty": pieces["dirty"], "error": None,
"date": pieces.get("date")}
def get_versions():
"""Get version information or return default if unable to do so."""
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
# __file__, we can work backwards from there to the root. Some
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
# case we can only use expanded keywords.
cfg = get_config()
verbose = cfg.verbose
try:
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
verbose)
except NotThisMethod:
pass
try:
root = os.path.realpath(__file__)
# versionfile_source is the relative path from the top of the source
# tree (where the .git directory might live) to this file. Invert
# this to find the root from __file__.
for i in cfg.versionfile_source.split('/'):
root = os.path.dirname(root)
except NameError:
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to find root of source tree",
"date": None}
try:
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
return render(pieces, cfg.style)
except NotThisMethod:
pass
try:
if cfg.parentdir_prefix:
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
except NotThisMethod:
pass
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to compute version", "date": None}

View file

View file

@ -0,0 +1,240 @@
import os
import ast
from configparser import RawConfigParser
import sys
import glob
import textwrap
import argparse
import logging
import core.theme
import util.store
import util.format
import modules.core
import modules.contrib
log = logging.getLogger(__name__)
MODULE_HELP = "Specify a space-separated list of modules to load. The order of the list determines their order in the i3bar (from left to right). Use <module>:<alias> to provide an alias in case you want to load the same module multiple times, but specify different parameters."
PARAMETER_HELP = (
"Provide configuration parameters in the form of <module>.<key>=<value>"
)
THEME_HELP = "Specify the theme to use for drawing modules"
def all_modules():
"""Return a list of available modules"""
result = {}
for path in [modules.core.__file__, modules.contrib.__file__]:
path = os.path.dirname(path)
for mod in glob.iglob("{}/*.py".format(path)):
result[os.path.basename(mod).replace(".py", "")] = 1
res = list(result.keys())
res.sort()
return res
class print_usage(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
argparse.Action.__init__(self, option_strings, dest, nargs, **kwargs)
self._indent = " " * 2
def __call__(self, parser, namespace, value, option_string=None):
if value == "modules":
self._args = namespace
self._format = "plain"
self.print_modules()
elif value == "modules-rst":
self._args = namespace
self._format = "rst"
self.print_modules()
elif value == "themes":
self.print_themes()
sys.exit(0)
def print_themes(self):
print(", ".join(core.theme.themes()))
def print_modules(self):
basepath = os.path.abspath(
os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
)
rst = {}
for m in all_modules():
try:
module_type = "core"
filename = os.path.join(basepath, "modules", "core", "{}.py".format(m))
if not os.path.exists(filename):
filename = os.path.join(
basepath, "modules", "contrib", "{}.py".format(m)
)
module_type = "contrib"
if not os.path.exists(filename):
log.warning("module {} not found".format(m))
continue
doc = None
with open(filename) as f:
tree = ast.parse(f.read())
doc = ast.get_docstring(tree)
if not doc:
log.warning("failed to find docstring for {}".format(m))
continue
if self._format == "rst":
if os.path.exists(
os.path.join(basepath, "screenshots", "{}.png".format(m))
):
doc = "{}\n\n.. image:: ../screenshots/{}.png".format(doc, m)
rst[module_type] = rst.get(module_type, [])
rst[module_type].append({ "module": m, "content": doc })
else:
print(
textwrap.fill(
"{}:".format(m),
80,
initial_indent=self._indent * 2,
subsequent_indent=self._indent * 2,
)
)
for line in doc.split("\n"):
print(
textwrap.fill(
line,
80,
initial_indent=self._indent * 3,
subsequent_indent=self._indent * 6,
)
)
except Exception as e:
log.warning(e)
if self._format == "rst":
print("List of modules\n===============")
for k in [ "core", "contrib" ]:
print("\n{}\n{}\n".format(k, "-"*len(k)))
for mod in rst[k]:
print("\n{}\n{}\n".format(mod["module"], "~"*len(mod["module"])))
print(mod["content"])
class Config(util.store.Store):
def __init__(self, args):
super(Config, self).__init__()
parser = argparse.ArgumentParser(
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
)
parser.add_argument(
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
)
parser.add_argument(
"-p",
"--parameters",
nargs="+",
action="append",
default=[],
help=PARAMETER_HELP,
)
parser.add_argument("-t", "--theme", default="default", help=THEME_HELP)
parser.add_argument(
"-i",
"--iconset",
default="auto",
help="Specify the name of an iconset to use (overrides theme default)",
)
parser.add_argument(
"-a",
"--autohide",
nargs="+",
default=[],
help="Specify a list of modules to hide when not in warning/error state",
)
parser.add_argument(
"-d", "--debug", action="store_true", help="Add debug fields to i3 output"
)
parser.add_argument(
"-f",
"--logfile",
help="destination for the debug log file, if -d|--debug is specified; defaults to stderr",
)
parser.add_argument(
"-r",
"--right-to-left",
action="store_true",
help="Draw widgets from right to left, rather than left to right (which is the default)",
)
parser.add_argument(
"-l",
"--list",
choices=["modules", "themes", "modules-rst"],
help="Display a list of available themes or available modules, along with their parameters",
action=print_usage,
)
self.__args = parser.parse_args(args)
for cfg in [
"~/.bumblebee-status.conf",
"~/.config/bumblebee-status.conf",
"~/.config/bumblebee-status/config",
]:
cfg = os.path.expanduser(cfg)
self.load_config(cfg)
parameters = [item for sub in self.__args.parameters for item in sub]
for param in parameters:
if not "=" in param:
log.error(
'missing value for parameter "{}" - ignoring this parameter'.format(
param
)
)
continue
key, value = param.split("=", 1)
self.set(key, value)
def load_config(self, filename):
if os.path.exists(filename):
log.info("loading {}".format(filename))
tmp = RawConfigParser()
tmp.read(filename)
if tmp.has_section("module-parameters"):
for key, value in tmp.items("module-parameters"):
self.set(key, value)
def modules(self):
return [item for sub in self.__args.modules for item in sub]
def interval(self, default=1):
return util.format.seconds(self.get("interval", default))
def debug(self):
return self.__args.debug
def reverse(self):
return self.__args.right_to_left
def logfile(self):
return self.__args.logfile
def theme(self):
return self.__args.theme
def iconset(self):
return self.__args.iconset
def autohide(self, name):
return name in self.__args.autohide
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,67 @@
import util.format
def never(init):
def call_init(obj, *args, **kwargs):
init(obj, *args, **kwargs)
if obj.parameter("interval") is None:
obj.set("interval", "never")
return call_init
def every(hours=0, minutes=0, seconds=0):
def decorator_init(init):
def call_init(obj, *args, **kwargs):
init(obj, *args, **kwargs)
if obj.parameter("interval") is None:
obj.set("interval", hours * 3600 + minutes * 60 + seconds)
return call_init
return decorator_init
def scrollable(func):
def wrapper(module, widget):
text = func(module, widget)
if not text:
return text
if text != widget.get("__content__", text):
widget.set("scrolling.start", 0)
widget.set("scrolling.direction", "right")
widget.set("__content__", text)
width = util.format.asint(module.parameter("scrolling.width", 30))
if util.format.asbool(module.parameter("scrolling.makewide", True)):
widget.set("theme.minwidth", "A" * width)
if width < 0 or len(text) <= width:
return text
start = widget.get("scrolling.start", 0)
bounce = util.format.asbool(module.parameter("scrolling.bounce", True))
scroll_speed = util.format.asint(module.parameter("scrolling.speed", 1))
direction = widget.get("scrolling.direction", "right")
if direction == "left":
scroll_speed = -scroll_speed
if start + scroll_speed <= 0: # bounce back
widget.set("scrolling.direction", "right")
next_start = start + scroll_speed
if next_start + width > len(text):
if not bounce:
next_start = 0
else:
next_start = start - scroll_speed
widget.set("scrolling.direction", "left")
widget.set("scrolling.start", next_start)
return text[start : start + width]
return wrapper
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,29 @@
__callbacks = {}
def register(event, callback, *args, **kwargs):
cb = callback
if len(args) + len(kwargs) > 0:
cb = lambda: callback(*args, **kwargs)
__callbacks.setdefault(event, []).append(cb)
def clear():
__callbacks.clear()
def trigger(event, *args, **kwargs):
cb = __callbacks.get(event, [])
if len(cb) == 0:
return False
for callback in cb:
if len(args) + len(kwargs) == 0:
callback()
else:
callback(*args, **kwargs)
return True
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,69 @@
import uuid
import logging
import core.event
import util.cli
LEFT_MOUSE = 1
MIDDLE_MOUSE = 2
RIGHT_MOUSE = 3
WHEEL_UP = 4
WHEEL_DOWN = 5
def button_name(button):
if button == LEFT_MOUSE:
return "left-mouse"
if button == RIGHT_MOUSE:
return "right-mouse"
if button == MIDDLE_MOUSE:
return "middle-mouse"
if button == WHEEL_UP:
return "wheel-up"
if button == WHEEL_DOWN:
return "wheel-down"
return "n/a"
class Object(object):
def __init__(self):
super(Object, self).__init__()
self.id = str(uuid.uuid4())
def __event_id(obj_id, button):
return "{}::{}".format(obj_id, button_name(button))
def __execute(cmd):
try:
util.cli.execute(cmd, wait=False)
except Exception as e:
logging.error("failed to invoke callback: {}".format(e))
def register(obj, button=None, cmd=None):
event_id = __event_id(obj.id if obj is not None else "", button)
logging.debug("registering callback {}".format(event_id))
if callable(cmd):
core.event.register(event_id, cmd)
else:
core.event.register(event_id, lambda _: __execute(cmd))
def trigger(event):
if not "button" in event:
return
triggered = False
for field in ["instance", "name"]:
if not field in event:
continue
if core.event.trigger(__event_id(event[field], event["button"]), event):
triggered = True
if not triggered:
core.event.trigger(__event_id("", event["button"]), event)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,124 @@
import os
import importlib
import logging
import core.input
import core.widget
import core.decorators
try:
error = ModuleNotFoundError("")
except Exception as e:
ModuleNotFoundError = Exception
log = logging.getLogger(__name__)
def load(module_name, config=core.config.Config([]), theme=None):
error = None
module_short, alias = (module_name.split(":") + [module_name])[0:2]
config.set("__alias__", alias)
for namespace in ["core", "contrib"]:
try:
mod = importlib.import_module(
"modules.{}.{}".format(namespace, module_short)
)
log.debug(
"importing {} from {}.{}".format(module_short, namespace, module_short)
)
return getattr(mod, "Module")(config, theme)
except ImportError as e:
log.debug("failed to import {}: {}".format(module_name, e))
error = e
log.fatal("failed to import {}: {}".format(module_name, error))
return Error(config=config, module=module_name, error=error)
class Module(core.input.Object):
def __init__(self, config=core.config.Config([]), theme=None, widgets=[]):
super().__init__()
self.__config = config
self.__widgets = widgets if isinstance(widgets, list) else [widgets]
self.module_name = self.__module__.split(".")[-1]
self.name = self.module_name
self.alias = self.__config.get("__alias__", None)
self.id = self.alias if self.alias else self.name
self.next_update = None
self.theme = theme
for widget in self.__widgets:
widget.module = self
def hidden(self):
return False
def parameter(self, key, default=None):
value = default
for prefix in [self.name, self.module_name, self.alias]:
value = self.__config.get("{}.{}".format(prefix, key), value)
# TODO retrieve from config file
return value
def set(self, key, value):
self.__config.set("{}.{}".format(self.name, key), value)
def update(self):
pass
def update_wrapper(self):
try:
self.update()
except Exception as e:
self.set("interval", 1)
module = Error(config=self.__config, module="error", error=str(e))
self.__widgets = [module.widget()]
self.update = module.update
def widgets(self):
return self.__widgets
def clear_widgets(self):
del self.__widgets[:]
def add_widget(self, full_text="", name=None):
widget = core.widget.Widget(full_text=full_text, name=name, module=self)
self.widgets().append(widget)
return widget
def widget(self, name=None):
if not name:
return self.widgets()[0]
for w in self.widgets():
if w.name == name:
return w
return None
def state(self, widget):
return []
def threshold_state(self, value, warn, crit):
if value > float(self.parameter("critical", crit)):
return "critical"
if value > float(self.parameter("warning", warn)):
return "warning"
return None
class Error(Module):
def __init__(self, module, error, config=core.config.Config([]), theme=None):
super().__init__(config, theme, core.widget.Widget(self.full_text))
self.__module = module
self.__error = error
def full_text(self, widget):
return "{}: {}".format(self.__module, self.__error)
def state(self, widget):
return ["critical"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,245 @@
import sys
import json
import time
import core.theme
import core.event
import util.format
def dump_json(obj):
return obj.dict()
def assign(src, dst, key, src_key=None, default=None):
if not src_key:
if key.startswith("_"):
src_key = key
else:
src_key = key.replace("_", "-") # automagically replace _ with -
for k in src_key if isinstance(src_key, list) else [src_key]:
if k in src:
dst[key] = src[k]
return
if default is not None:
dst[key] = default
class block(object):
__COMMON_THEME_FIELDS = [
"separator",
"separator-block-width",
"default-separators",
"border-top",
"border-left",
"border-right",
"border-bottom",
"fg",
"bg",
"padding",
"prefix",
"suffix",
]
def __init__(self, theme, module, widget):
self.__attributes = {}
for key in self.__COMMON_THEME_FIELDS:
tmp = theme.get(key, widget)
if tmp is not None:
self.__attributes[key] = tmp
self.__attributes["name"] = module.id
self.__attributes["instance"] = widget.id
self.__attributes["prev-bg"] = theme.get("bg", "previous")
def set(self, key, value):
self.__attributes[key] = value
def is_pango(self, attr):
if isinstance(attr, dict) and "pango" in attr:
return True
return False
def pangoize(self, text):
if not self.is_pango(text):
return text
self.__attributes["markup"] = "pango"
attr = dict(text["pango"])
text = attr.get("full_text", "")
if "full_text" in attr:
del attr["full_text"]
result = "<span"
for key, value in attr.items():
result = '{} {}="{}"'.format(result, key, value)
result = "{}>{}</span>".format(result, text)
return result
def dict(self):
result = {}
assign(self.__attributes, result, "full_text", ["full_text", "separator"])
assign(self.__attributes, result, "separator", "default-separators")
if "_decorator" in self.__attributes:
assign(self.__attributes, result, "color", "bg")
assign(self.__attributes, result, "background", "prev-bg")
result["_decorator"] = True
else:
assign(self.__attributes, result, "color", "fg")
assign(self.__attributes, result, "background", "bg")
if "full_text" in self.__attributes:
result["full_text"] = self.pangoize(result["full_text"])
result["full_text"] = self.__format(self.__attributes["full_text"])
for k in [
"name",
"instance",
"separator_block_width",
"border",
"border_top",
"border_bottom",
"border_left",
"border_right",
"markup",
"_raw",
"_suffix",
"_prefix",
"min_width",
"align",
]:
assign(self.__attributes, result, k)
return result
def __pad(self, text):
padding = self.__attributes.get("padding", "")
if not text:
return padding
return "{}{}{}".format(padding, text, padding)
def __format(self, text):
if text is None:
return None
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
self.set("_prefix", prefix)
self.set("_suffix", suffix)
self.set("_raw", text)
return "{}{}{}".format(prefix, text, suffix)
class i3(object):
def __init__(self, theme=core.theme.Theme(), config=core.config.Config([])):
self.__modules = []
self.__content = {}
self.__theme = theme
self.__config = config
core.event.register("update", self.update)
core.event.register("start", self.draw, "start")
core.event.register("draw", self.draw, "statusline")
core.event.register("stop", self.draw, "stop")
def theme(self, new_theme=None):
if new_theme:
self.__theme = new_theme
return self.__theme
def modules(self, modules=None):
if not modules:
return self.__modules
self.__modules = modules if isinstance(modules, list) else [modules]
def draw(self, what, args=None):
cb = getattr(self, what)
data = cb(args) if args else cb()
if "blocks" in data:
sys.stdout.write(json.dumps(data["blocks"], default=dump_json))
if "suffix" in data:
sys.stdout.write(data["suffix"])
sys.stdout.write("\n")
sys.stdout.flush()
def start(self):
return {
"blocks": {"version": 1, "click_events": True},
"suffix": "\n[",
}
def stop(self):
return {"suffix": "\n]"}
def __separator_block(self, module, widget):
if not self.__theme.get("separator"):
return []
blk = block(self.__theme, module, widget)
blk.set("_decorator", True)
return [blk]
def __content_block(self, module, widget):
blk = block(self.__theme, module, widget)
minwidth = widget.theme("minwidth")
if minwidth is not None:
try:
blk.set("min-width", "-" * int(minwidth))
except:
blk.set("min-width", minwidth)
blk.set("align", widget.theme("align"))
blk.set("full_text", self.__content[widget])
if widget.get("pango", False):
blk.set("markup", "pango")
if self.__config.debug():
state = module.state(widget)
if isinstance(state, list):
state = ", ".join(state)
blk.set("__state", state)
return blk
def blocks(self, module):
blocks = []
for widget in module.widgets():
if widget.module and self.__config.autohide(widget.module.name):
if not any(
state in widget.state() for state in ["warning", "critical"]
):
continue
if module.hidden():
continue
blocks.extend(self.__separator_block(module, widget))
blocks.append(self.__content_block(module, widget))
core.event.trigger("next-widget")
return blocks
# TODO: only updates full text, not the state!?
def update(self, affected_modules=None, redraw_only=False):
now = time.time()
for module in self.__modules:
if affected_modules and not module.id in affected_modules:
continue
if not affected_modules and module.next_update:
if module.parameter("interval", "") == "never":
continue
if now < module.next_update:
continue
if not redraw_only:
module.update_wrapper()
if module.parameter("interval", "") != "never":
module.next_update = now + util.format.seconds(
module.parameter("interval", self.__config.interval())
)
for widget in module.widgets():
self.__content[widget] = widget.full_text()
def statusline(self):
blocks = []
for module in self.__modules:
blocks.extend(self.blocks(module))
return {"blocks": blocks, "suffix": ","}
def wait(self, interval):
time.sleep(interval)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,158 @@
import os
import io
import json
import logging
import copy
import glob
import core.event
import util.algorithm
log = logging.getLogger(__name__)
THEME_BASE_DIR = os.path.dirname(os.path.realpath(__file__))
PATHS = [
".",
os.path.join(THEME_BASE_DIR, "../../themes"),
os.path.expanduser("~/.config/bumblebee-status/themes"),
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
]
def themes():
themes_dict = {}
for path in PATHS:
for filename in glob.iglob("{}/*.json".format(path)):
if "test" not in filename:
themes_dict[os.path.basename(filename).replace(".json", "")] = 1
result = list(themes_dict.keys())
result.sort()
return result
def merge_replace(value, new_value, key):
if not isinstance(value, dict):
return new_value
if isinstance(new_value, dict):
util.algorithm.merge(value, new_value)
return value
# right now, merging needs explicit pango support :(
if "pango" in value:
value["pango"]["full_text"] = new_value
return value
class Theme(object):
def __init__(self, name="default", iconset="auto", raw_data=None):
self.name = name
self.__widget_count = 0
self.__previous = {}
self.__current = {}
self.__keywords = {}
self.__value_idx = {}
self.__data = raw_data if raw_data else self.load(name)
for icons in self.__data.get("icons", []):
util.algorithm.merge(self.__data, self.load(icons, "icons"))
if iconset != "auto":
util.algorithm.merge(self.__data, self.load(iconset, "icons"))
for colors in self.__data.get("colors", []):
util.algorithm.merge(self.__keywords, self.load_keywords(colors))
core.event.register("draw", self.__start)
core.event.register("next-widget", self.__next_widget)
def keywords(self):
return self.__keywords
def color(self, name, default=None):
return self.keywords().get(name, default)
def load(self, name, subdir=""):
if isinstance(name, dict):
return name # support plain data
for path in PATHS:
theme_file = os.path.join(path, subdir, "{}.json".format(name))
result = self.__load_json(theme_file)
if result != {}:
return result
raise RuntimeError("unable to find theme {}".format(name))
def __load_json(self, filename):
filename = os.path.expanduser(filename)
if not os.path.isfile(filename):
return {}
with io.open(filename) as data:
return json.load(data)
def load_keywords(self, name):
try:
if isinstance(name, dict):
return name
if name.lower() == "wal":
wal = self.__load_json("~/.cache/wal/colors.json")
result = {}
for field in ["special", "colors"]:
for key in wal.get(field, {}):
result[key] = wal[field][key]
return result
except Exception as e:
log.error("failed to load colors: {}", e)
def __start(self):
self.__widget_count = 0
self.__current.clear()
self.__previous.clear()
for key, value in self.__value_idx.items():
self.__value_idx[key] = value + 1
def __next_widget(self):
self.__widget_count = self.__widget_count + 1
self.__previous = dict(self.__current)
self.__current.clear()
def get(self, key, widget=None, default=None):
if not widget:
widget = core.widget.Widget("")
# special handling
if widget == "previous":
return self.__previous.get(key, None)
value = default
for option in ["defaults", "cycle"]:
if option in self.__data:
tmp = self.__data[option]
if isinstance(tmp, list):
tmp = tmp[self.__widget_count % len(tmp)]
value = merge_replace(value, tmp.get(key, value), key)
if isinstance(value, dict):
value = copy.deepcopy(value)
value = merge_replace(value, self.__data.get(key, value), key)
if widget.module:
value = merge_replace(
value, self.get(widget.module.name, None, {}).get(key, value), key
)
if not key in widget.state():
for state in widget.state():
theme = self.get(state, widget, {})
value = merge_replace(value, theme.get(key, value), key)
if not type(value) in (list, dict):
value = self.__keywords.get(value, value)
if isinstance(value, list):
idx = self.__value_idx.get("{}::{}".format(widget.id, key), 0) % len(value)
self.__value_idx["{}::{}".format(widget.id, key)] = idx
widget.set(key, idx)
value = value[idx]
self.__current[key] = value
return value
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,69 @@
import core.input
import core.decorators
import util.store
import util.format
class Widget(util.store.Store, core.input.Object):
def __init__(self, full_text="", name=None, module=None):
super(Widget, self).__init__()
self.__full_text = full_text
self.module = module
self.name = name
@property
def module(self):
return self.__module
@module.setter
def module(self, module):
self.__module = module
if self.index() < 0:
return
if module:
custom_ids = util.format.aslist(module.parameter("id"))
if len(custom_ids) > self.index():
self.id = custom_ids[self.index()]
def index(self):
if not self.module:
return 0
idx = 0
for w in self.module.widgets():
if w.id == self.id:
return idx
idx = idx + 1
return -1 # not found
def theme(self, attribute):
attr = "theme.{}".format(attribute)
if self.module:
param = util.format.aslist(self.module.parameter(attr))
if param and len(param) > self.index():
return param[self.index()]
return self.get(attr)
def full_text(self, value=None):
if value:
self.__full_text = value
else:
if callable(self.__full_text):
return self.__full_text(self)
return self.__full_text
def state(self):
rv = []
if self.get("state", None):
tmp = self.get("state")
rv = tmp[:] if isinstance(tmp, list) else [tmp]
if self.module:
tmp = self.module.state(self)
rv.extend(tmp if isinstance(tmp, list) else [tmp])
return rv
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,9 @@
import os
import sys
def discover():
libdir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "bumblebee_status"))
sys.path.append(libdir)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,103 @@
"""Displays bluetooth status. Left mouse click launches manager app,
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and
python-dbus to count the number of connections
Parameters:
* bluetooth.manager : application to launch on click (blueman-manager)
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
"""
import os
import re
import subprocess
import dbus
import dbus.mainloop.glib
import logging
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.status))
self.manager = self.parameter("manager", "blueman-manager")
self._status = "Off"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self._bus = dbus.SystemBus()
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle)
def status(self, widget):
"""Get status."""
return self._status
def update(self):
"""Update current state."""
state = len(
subprocess.run(["bluetoothctl", "list"], stdout=subprocess.PIPE).stdout
)
if state > 0:
connected_devices = self.get_connected_devices()
self._status = "On - {}".format(connected_devices)
else:
self._status = "Off"
adapters_cmd = "rfkill list | grep Bluetooth"
if not len(
subprocess.run(adapters_cmd, shell=True, stdout=subprocess.PIPE).stdout
):
self._status = "No Adapter Found"
return
def _toggle(self, widget=None):
"""Toggle bluetooth state."""
if "On" in self._status:
state = "false"
else:
state = "true"
cmd = (
"dbus-send --system --print-reply --dest=org.blueman.Mechanism /org/blueman/mechanism org.blueman.Mechanism.SetRfkillState boolean:%s"
% state
)
logging.debug("bt: toggling bluetooth")
core.util.execute(cmd)
def state(self, widget):
"""Get current state."""
state = []
if self._status == "No Adapter Found":
state.append("critical")
elif self._status == "On - 0":
state.append("warning")
elif "On" in self._status and not (self._status == "On - 0"):
state.append("ON")
else:
state.append("critical")
return state
def get_connected_devices(self):
devices = 0
objects = dbus.Interface(
self._bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager"
).GetManagedObjects()
for path, interfaces in objects.items():
if "org.bluez.Device1" in interfaces:
if dbus.Interface(
self._bus.get_object("org.bluez", path),
"org.freedesktop.DBus.Properties",
).Get("org.bluez.Device1", "Connected"):
devices += 1
return devices
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,60 @@
# pylint: disable=C0111,R0903
"""Displays the status of watson (time-tracking tool)
Requires the following executable:
* watson
contributed by `bendardenne <https://github.com/bendardenne>`_ - many thanks!
"""
import logging
import re
import functools
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.text))
self.__tracking = False
self.__project = ""
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle)
def toggle(self, widget):
self.__project = "hit"
if self.__tracking:
util.cli.execute("watson stop")
else:
util.cli.execute("watson restart")
self.__tracking = not self.__tracking
def text(self, widget):
if self.__tracking:
return self.__project
else:
return "Paused"
def update(self):
output = util.cli.execute("watson status")
if re.match("No project started", output):
self.__tracking = False
return
self.__tracking = True
m = re.search(r"Project (.+) started", output)
self.__project = m.group(1)
def state(self, widget):
return "on" if self.__tracking else "off"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,41 @@
# pylint: disable=C0111,R0903
"""Displays CPU utilization across all CPUs.
Parameters:
* cpu.warning : Warning threshold in % of CPU usage (defaults to 70%)
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
* cpu.format : Format string (defaults to '{:.01f}%')
"""
import psutil
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.utilization))
self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20))
self._utilization = psutil.cpu_percent(percpu=False)
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
)
@property
def _format(self):
return self.parameter("format", "{:.01f}%")
def utilization(self, _):
return self._format.format(self._utilization)
def update(self):
self._utilization = psutil.cpu_percent(percpu=False)
def state(self, _):
return self.threshold_state(self._utilization, 70, 80)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,23 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time.
Parameters:
* date.format: strftime()-compatible formatting string
* date.locale: locale to use rather than the system default
"""
import core.decorators
from .datetime 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"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,45 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time.
Parameters:
* datetime.format: strftime()-compatible formatting string
* datetime.locale: locale to use rather than the system default
"""
from __future__ import absolute_import
import datetime
import locale
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.full_text))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
self._fmt = self.parameter("format", self.default_format())
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 as e:
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
def default_format(self):
return "%x %X"
def full_text(self, widget):
enc = locale.getpreferredencoding()
retval = datetime.datetime.now().strftime(self._fmt)
if hasattr(retval, "decode"):
return retval.decode(enc)
return retval
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,24 @@
# pylint: disable=C0111,R0903
"""Shows that debug is enabled"""
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 "debug"
def state(self, widget):
return "warning"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,65 @@
# pylint: disable=C0111,R0903
"""Shows free diskspace, total diskspace and the percentage of free disk space.
Parameters:
* disk.warning: Warning threshold in % of disk space (defaults to 80%)
* disk.critical: Critical threshold in % of disk space (defaults ot 90%)
* disk.path: Path to calculate disk usage from (defaults to /)
* disk.open: Which application / file manager to launch (default xdg-open)
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
"""
import os
import core.module
import core.widget
import core.input
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.diskspace))
self._path = self.parameter("path", "/")
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
self._used = 0
self._left = 0
self._size = 0
self._percent = 0
core.input.register(
self,
button=core.input.LEFT_MOUSE,
cmd="{} {}".format(self.parameter("open", "xdg-open"), self._path),
)
def diskspace(self, widget):
used_str = util.format.byte(self._used)
size_str = util.format.byte(self._size)
left_str = util.format.byte(self._left)
percent_str = self._percent
return self._format.format(
path=self._path,
used=used_str,
left=left_str,
size=size_str,
percent=percent_str,
)
def update(self):
st = os.statvfs(self._path)
self._size = st.f_blocks * st.f_frsize
self._used = (st.f_blocks - st.f_bfree) * st.f_frsize
self._left = self._size - self._used
self._percent = 100.0 * self._used / self._size
def state(self, widget):
return self.threshold_state(self._percent, 80, 90)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,33 @@
# pylint: disable=C0111,R0903
"""Shows bumblebee-status errors"""
import platform
import core.module
import core.widget
import core.event
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.full_text))
self.__error = ""
self.__state = "critical"
core.event.register("error", self.__set_error)
def full_text(self, widgets):
return self.__error
def __set_error(self, error="n/a", state="critical"):
self.__error = error
self.__state = state
def state(self, widget):
if self.__error:
return [self.__state]
return []
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,77 @@
# pylint: disable=C0111,R0903
"""Print the branch and git status for the
currently focused window.
Requires:
* xcwd
* Python module 'pygit2'
"""
import os
import pygit2
import core.module
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.__error = False
def hidden(self):
return self.__error
def update(self):
state = {}
self.clear_widgets()
try:
directory = util.cli.execute("xcwd").strip()
directory = self.__get_git_root(directory)
repo = pygit2.Repository(directory)
self.add_widget(name="git.main", full_text=repo.head.shorthand)
for filepath, flags in repo.status().items():
if (
flags == pygit2.GIT_STATUS_WT_NEW
or flags == pygit2.GIT_STATUS_INDEX_NEW
):
state["new"] = True
if (
flags == pygit2.GIT_STATUS_WT_DELETED
or flags == pygit2.GIT_STATUS_INDEX_DELETED
):
state["deleted"] = True
if (
flags == pygit2.GIT_STATUS_WT_MODIFIED
or flags == pygit2.GIT_STATUS_INDEX_MODIFIED
):
state["modified"] = True
self.__error = False
if "new" in state:
self.add_widget(name="git.new")
if "modified" in state:
self.add_widget(name="git.modified")
if "deleted" in state:
self.add_widget(name="git.deleted")
except Exception as e:
self.__error = True
def state(self, widget):
return widget.name.split(".")[1]
def __get_git_root(self, directory):
while len(directory) > 1:
if os.path.exists(os.path.join(directory, ".git")):
return directory
directory = "/".join(directory.split("/")[0:-1])
return "/"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,71 @@
# pylint: disable=C0111,R0903
"""Displays the current keyboard layout using libX11
Requires the following library:
* libX11.so.6
and python module:
* xkbgroup
Parameters:
* layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed)
* layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true.
"""
from xkbgroup import *
import logging
log = logging.getLogger(__name__)
import core.module
import core.widget
import core.input
import util.format
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)
self.__show_variant = util.format.asbool(self.parameter("show_variant", True))
def __next_keymap(self, event):
self.__set_keymap(1)
def __prev_keymap(self, event):
self.__set_keymap(-1)
def __set_keymap(self, rotation):
xkb = XKeyboard()
if xkb.groups_count < 2:
return # nothing to do
layouts = xkb.groups_symbols
idx = layouts.index(xkb.group_symbol)
xkb.group_symbol = str(layouts[(idx + rotation) % len(layouts)])
def current_layout(self, widget):
try:
xkb = XKeyboard()
log.debug("group num: {}".format(xkb.group_num))
name = (
xkb.group_name
if util.format.asbool(self.parameter("showname"), False)
else xkb.group_symbol
)
if self.__show_variant:
return (
"{} ({})".format(name, xkb.group_variant)
if xkb.group_variant
else name
)
return name
except Exception as e:
print("got exception: {}".format(e))
return "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,41 @@
# pylint: disable=C0111,R0903
"""Displays system load.
Parameters:
* load.warning : Warning threshold for the one-minute load average (defaults to 70% of the number of CPUs)
* load.critical: Critical threshold for the one-minute load average (defaults to 80% of the number of CPUs)
"""
import os
import multiprocessing
import core.module
import core.input
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.load))
self._load = [0, 0, 0]
try:
self._cpus = multiprocessing.cpu_count()
except NotImplementedError as e:
self._cpus = 1
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
)
def load(self, widget):
return "{:.02f}/{:.02f}/{:.02f}".format(
self._load[0], self._load[1], self._load[2]
)
def update(self):
self._load = os.getloadavg()
def state(self, widget):
return self.threshold_state(self._load[0], self._cpus * 0.7, self._cpus * 0.8)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,77 @@
# pylint: disable=C0111,R0903
"""Displays available RAM, total amount of RAM and percentage available.
Parameters:
* memory.warning : Warning threshold in % of memory used (defaults to 80%)
* memory.critical: Critical threshold in % of memory used (defaults to 90%)
* memory.format: Format string (defaults to '{used}/{total} ({percent:05.02f}%)')
* memory.usedonly: Only show the amount of RAM in use (defaults to False). Same as memory.format='{used}'
"""
import re
import core.module
import core.widget
import core.input
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.memory_usage))
core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
)
@property
def _format(self):
if util.format.asbool(self.parameter("usedonly", False)):
return "{used}"
else:
return self.parameter("format", "{used}/{total} ({percent:05.02f}%)")
def memory_usage(self, widget):
return self._format.format(**self._mem)
def update(self):
data = {}
with open("/proc/meminfo", "r") as f:
for line in f:
tmp = re.split(r"[:\s]+", line)
value = int(tmp[1])
if tmp[2] == "kB":
value = value * 1024
if tmp[2] == "mB":
value = value * 1024 * 1024
if tmp[2] == "gB":
value = value * 1024 * 1024 * 1024
data[tmp[0]] = value
if "MemAvailable" in data:
used = data["MemTotal"] - data["MemAvailable"]
else:
used = (
data["MemTotal"]
- data["MemFree"]
- data["Buffers"]
- data["Cached"]
- data["Slab"]
)
self._mem = {
"total": util.format.byte(data["MemTotal"]),
"available": util.format.byte(data["MemAvailable"]),
"free": util.format.byte(data["MemFree"]),
"used": util.format.byte(used),
"percent": float(used) / float(data["MemTotal"]) * 100.0,
}
def state(self, widget):
if self._mem["percent"] > float(self.parameter("critical", 90)):
return "critical"
if self._mem["percent"] > float(self.parameter("warning", 80)):
return "warning"
return None
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,135 @@
# pylint: disable=C0111,R0903
"""Displays the name, IP address(es) and status of each available network interface.
Requires the following python module:
* netifaces
Parameters:
* nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br')
* nic.include: Comma-separated list of interfaces to include
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
"""
import shutil
import netifaces
import subprocess
import core.module
import core.decorators
import util.cli
import util.format
class Module(core.module.Module):
@core.decorators.every(seconds=10)
def __init__(self, config, theme):
widgets = []
super().__init__(config, theme, widgets)
self._exclude = tuple(
filter(
len,
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(","),
)
)
self._include = self.parameter("include", "").split(",")
self._states = {"include": [], "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._format = self.parameter("format", "{intf} {state} {ip} {ssid}")
self.iwgetid = shutil.which("iwgetid")
self._update_widgets(widgets)
def update(self):
self._update_widgets(self.widgets())
def state(self, widget):
states = []
if widget.get("state") == "down":
states.append("critical")
elif widget.get("state") != "up":
states.append("warning")
intf = widget.get("intf")
iftype = "wireless" if self._iswlan(intf) else "wired"
iftype = "tunnel" if self._istunnel(intf) else iftype
states.append("{}-{}".format(iftype, widget.get("state")))
return states
def _iswlan(self, intf):
# wifi, wlan, wlp, seems to work for me
if intf.startswith("w"):
return True
return False
def _istunnel(self, intf):
return intf.startswith("tun") or intf.startswith("wg")
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 _update_widgets(self, widgets):
self.clear_widgets()
interfaces = [
i for i in netifaces.interfaces() if not i.startswith(self._exclude)
]
interfaces.extend([i for i in netifaces.interfaces() if i in self._include])
for intf in interfaces:
addr = []
state = "down"
for ip in self.get_addresses(intf):
addr.append(ip)
state = "up"
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
widget = self.widget(intf)
if not widget:
widget = self.add_widget(name=intf)
# join/split is used to get rid of multiple whitespaces (in case SSID is not available, for instance
widget.full_text(
" ".join(
self._format.format(
ip=", ".join(addr),
intf=intf,
state=state,
ssid=self.get_ssid(intf),
).split()
)
)
widget.set("intf", intf)
widget.set("state", state)
def get_ssid(self, intf):
if self._iswlan(intf) and self.iwgetid:
return util.cli.execute(
"{} -r {}".format(self.iwgetid, intf), ignore_errors=True
)
return ""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,9 @@
from .pulseaudio import Module
class Module(Module):
def __init__(self, config, theme):
super().__init__(config, theme, "sink")
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,9 @@
from .pulseaudio import Module
class Module(Module):
def __init__(self, config, theme):
super().__init__(config, theme, "source")
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,95 @@
# pylint: disable=C0111,R0903
"""Periodically checks the RTT of a configurable host using ICMP echos
Requires the following executable:
* ping
Parameters:
* ping.address : IP address to check
* ping.timeout : Timeout for waiting for a reply (defaults to 5.0)
* ping.probes : Number of probes to send (defaults to 5)
* ping.warning : Threshold for warning state, in seconds (defaults to 1.0)
* ping.critical: Threshold for critical state, in seconds (defaults to 2.0)
"""
import re
import time
import threading
import core.module
import core.widget
import core.event
import core.decorators
import util.cli
def get_rtt(module, widget):
try:
widget.set("rtt-unreachable", False)
res = util.cli.execute(
"ping -n -q -c {} -W {} {}".format(
widget.get("rtt-probes"),
widget.get("rtt-timeout"),
widget.get("address"),
)
)
for line in res.split("\n"):
if line.startswith(
"{} packets transmitted".format(widget.get("rtt-probes"))
):
m = re.search(r"(\d+)% packet loss", line)
widget.set("packet-loss", m.group(1))
if not line.startswith("rtt"):
continue
m = re.search(r"([0-9\.]+)/([0-9\.]+)/([0-9\.]+)/([0-9\.]+)\s+(\S+)", line)
widget.set("rtt-min", float(m.group(1)))
widget.set("rtt-avg", float(m.group(2)))
widget.set("rtt-max", float(m.group(3)))
widget.set("rtt-unit", m.group(5))
except Exception as e:
widget.set("rtt-unreachable", True)
core.event.trigger("update", [module.id], redraw_only=True)
class Module(core.module.Module):
@core.decorators.every(seconds=60)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.rtt))
widget = self.widget()
widget.set("address", self.parameter("address", "8.8.8.8"))
widget.set("rtt-probes", self.parameter("probes", 5))
widget.set("rtt-timeout", self.parameter("timeout", 5.0))
widget.set("rtt-avg", 0.0)
widget.set("rtt-unit", "")
widget.set("packet-loss", 0)
def rtt(self, widget):
if widget.get("rtt-unreachable"):
return "{}: unreachable".format(widget.get("address"))
return "{}: {:.1f}{} ({}%)".format(
widget.get("address"),
widget.get("rtt-avg"),
widget.get("rtt-unit"),
widget.get("packet-loss"),
)
def state(self, widget):
if widget.get("rtt-unreachable"):
return ["critical"]
return self.threshold_state(widget.get("rtt-avg"), 1000.0, 2000.0)
def update(self):
thread = threading.Thread(target=get_rtt, args=(self, self.widget(),))
thread.start()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,200 @@
# pylint: disable=C0111,R0903
"""Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol.
Aliases: pasink (use this to control output instead of input), pasource
Parameters:
* pulseaudio.autostart: If set to 'true' (default is 'false'), automatically starts the pulseaudio daemon if it is not running
* pulseaudio.percent_change: How much to change volume by when scrolling on the module (default is 2%)
* pulseaudio.limit: Upper limit for setting the volume (default is 0%, which means 'no limit')
Note: If the left and right channels have different volumes, the limit might not be reached exactly.
* pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango;
0 for not showing volume bars (default)
Requires the following executable:
* pulseaudio
* pactl
* pavucontrol
"""
import re
import logging
import core.module
import core.widget
import core.input
import util.cli
import util.graph
import util.format
class Module(core.module.Module):
def __init__(self, config, theme, channel):
super().__init__(config, theme, core.widget.Widget(self.volume))
if util.format.asbool(self.parameter("autostart", False)):
util.cli.execute("pulseaudio --start", ignore_errors=True)
self._change = util.format.asint(
self.parameter("percent_change", "2%").strip("%"), 0, 100
)
self._limit = util.format.asint(self.parameter("limit", "0%").strip("%"), 0)
self._left = 0
self._right = 0
self._mono = 0
self._mute = False
self._failed = False
self._channel = channel
self._showbars = util.format.asbool(self.parameter("showbars", 0))
self._patterns = [
{"expr": "Name:", "callback": (lambda line: False)},
{
"expr": "Mute:",
"callback": (
lambda line: self.mute(False if " no" in line.lower() else True)
),
},
{"expr": "Volume:", "callback": self.getvolume},
]
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd="pavucontrol")
events = [
{"type": "mute", "action": self.toggle, "button": core.input.LEFT_MOUSE},
{
"type": "volume",
"action": self.increase_volume,
"button": core.input.WHEEL_UP,
},
{
"type": "volume",
"action": self.decrease_volume,
"button": core.input.WHEEL_DOWN,
},
]
for event in events:
core.input.register(self, button=event["button"], cmd=event["action"])
def set_volume(self, amount):
util.cli.execute(
"pactl set-{}-{} @DEFAULT_{}@ {}".format(
self._channel, "volume", self._channel.upper(), amount
)
)
def increase_volume(self, event):
if self._limit > 0: # we need to check the limit
left = int(self._left)
right = int(self._right)
if (
left + self._change >= self._limit
or right + self._change >= self._limit
):
if left == right:
# easy case, just set to limit
self.set_volume("{}%".format(self._limit))
return
else:
# don't adjust anymore, since i don't know how to update only one channel
return
self.set_volume("+{}%".format(self._change))
def decrease_volume(self, event):
self.set_volume("-{}%".format(self._change))
def toggle(self, event):
util.cli.execute(
"pactl set-{}-mute @DEFAULT_{}@ toggle".format(
self._channel, self._channel.upper()
)
)
def mute(self, value):
self._mute = value
def getvolume(self, line):
if "mono" in line:
m = re.search(r"mono:.*\s*\/\s*(\d+)%", line)
if m:
self._mono = m.group(1)
else:
m = re.search(r"left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%", line)
if m:
self._left = m.group(1)
self._right = m.group(2)
def _default_device(self):
output = util.cli.execute("pactl info")
pattern = "Default {}: ".format("Sink" if self._channel == "sink" else "Source")
for line in output.split("\n"):
if line.startswith(pattern):
return line.replace(pattern, "")
logging.error("no pulseaudio device found")
return "n/a"
def volume(self, widget):
if self._failed == True:
return "n/a"
if int(self._mono) > 0:
vol = "{}%".format(self._mono)
if self._showbars:
vol = "{} {}".format(vol, util.graph.hbar(float(self._mono)))
return vol
elif self._left == self._right:
vol = "{}%".format(self._left)
if self._showbars:
vol = "{} {}".format(vol, util.graph.hbar(float(self._left)))
return vol
else:
vol = "{}%/{}%".format(self._left, self._right)
if self._showbars:
vol = "{} {}{}".format(
vol,
util.graph.hbar(float(self._left)),
util.graph.hbar(float(self._right)),
)
return vol
def update(self):
try:
self._failed = False
channel = "sinks" if self._channel == "sink" else "sources"
device = self._default_device()
result = util.cli.execute("pactl list {}".format(channel))
found = False
for line in result.split("\n"):
if "Name: {}".format(device) in line:
found = True
continue
if found is False:
continue
for pattern in self._patterns:
if not pattern["expr"] in line:
continue
if pattern["callback"](line) is False and found == True:
return
except Exception as e:
self._failed = True
logging.exception(e)
if util.format.asbool(self.parameter("autostart", False)):
util.cli.execute("pulseaudio --start", ignore_errors=True)
else:
raise e
def state(self, widget):
if self._mute:
return ["warning", "muted"]
if int(self._left) > int(100):
return ["critical", "unmuted"]
return ["unmuted"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,113 @@
# pylint: disable=C0111,R0903
"""Displays the current color temperature of redshift
Requires the following executable:
* redshift
Parameters:
* redshift.location : location provider, either of 'auto' (default), 'geoclue2',
'ipinfo' or 'manual'
'auto' uses whatever redshift is configured to do
* redshift.lat : latitude if location is set to 'manual'
* redshift.lon : longitude if location is set to 'manual'
* redshift.show_transition: information about the transitions (x% day) defaults to True
"""
import re
import threading
import core.module
import core.widget
import core.input
import core.decorators
import util.cli
import util.format
import util.location
def get_redshift_value(module):
widget = module.widget()
location = module.parameter("location", "auto")
lat = module.parameter("lat", None)
lon = module.parameter("lon", None)
# Even if location method is set to manual, if we have no lat or lon,
# fall back to the geoclue2 method.
if location == "manual" and (lat is None or lon is None):
location = "geoclue2"
command = ["redshift", "-p"]
if location == "manual":
command.extend(["-l", "{}:{}".format(lat, lon)])
if location == "geoclue2":
command.extend(["-l", "geoclue2"])
try:
res = util.cli.execute(" ".join(command))
except Exception:
res = ""
widget.set("temp", "n/a")
widget.set("transition", "")
widget.set("state", "day")
for line in res.split("\n"):
line = line.lower()
if "temperature" in line:
widget.set("temp", line.split(" ")[2])
if "period" in line:
state = line.split(" ")[1]
if "day" in state:
widget.set("state", "day")
elif "night" in state:
widget.set("state", "night")
else:
widget.set("state", "transition")
match = re.search(r"(\d+)\.\d+% ([a-z]+)", line)
widget.set(
"transition", "({}% {})".format(match.group(1), match.group(2))
)
core.event.trigger("update", [widget.module.id], redraw_only=True)
class Module(core.module.Module):
@core.decorators.every(seconds=10)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.text))
self.__thread = None
self.show_transition = util.format.asbool(
self.parameter("show_transition", True)
)
if self.parameter("location", "") == "ipinfo":
# override lon/lat with ipinfo
try:
location = util.location.coordinates()
self.set("lat", location[0])
self.set("lon", location[1])
self.set("location", "manual")
except Exception:
# Fall back to geoclue2.
self.set("location", "geoclue2")
self._text = ""
def text(self, widget):
val = widget.get("temp", "n/a")
transition = widget.get("transition", "")
if transition and self.show_transition:
val = "{} {}".format(val, transition)
return val
def update(self):
if self.__thread is not None and self.__thread.isAlive():
return
self.__thread = threading.Thread(target=get_redshift_value, args=(self,))
self.__thread.start()
def state(self, widget):
return widget.get("state", None)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,242 @@
# -*- coding: UTF-8 -*-
"""Displays sensor temperature and CPU frequency
Parameters:
* sensors2.chip: 'sensors -u' compatible filter for chip to display (default to empty - show all chips)
* sensors2.showcpu: Enable or disable CPU frequency display (default: true)
* sensors2.showtemp: Enable or disable temperature display (default: true)
* sensors2.showfan: Enable or disable fan display (default: true)
* sensors2.showother: Enable or display 'other' sensor readings (default: false)
* sensors2.showname: Enable or disable show of sensor name (default: false)
* sensors2.chip_include: Comma-separated list of chip to include (defaults to '' will include all by default, example: 'coretemp,bat')
* sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exlude none by default)
* sensors2.field_include: Comma separated list of chip to include (defaults to '' will include all by default, example: 'temp,fan')
* sensors2.field_exclude: Comma separated list of chip to exclude (defaults to '' will exclude none by default)
* sensors2.chip_field_exclude: Comma separated list of chip field to exclude (defaults to '' will exclude none by default, example: 'coretemp-isa-0000.temp1,coretemp-isa-0000.fan1')
* sensors2.chip_field_include: Comma-separated list of adaper field to include (defaults to '' will include all by default)
"""
import re
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, [])
self.__chip = self.parameter("chip", "")
self.__data = {}
self.__update()
self.__create_widgets()
def update(self):
self.__update()
for widget in self.widgets():
self.__update_widget(widget)
def state(self, widget):
widget_type = widget.get("type", "")
try:
data = self.__data[widget.get("adapter")][widget.get("package")][
widget.get("field")
]
if "crit" in data and float(data["input"]) > float(data["crit"]):
return ["critical", widget_type]
if "max" in data and float(data["input"]) > float(data["max"]):
return ["warning", widget_type]
except:
pass
return [widget_type]
def __create_widgets(self):
show_temp = util.format.asbool(self.parameter("showtemp", True))
show_fan = util.format.asbool(self.parameter("showfan", True))
show_other = util.format.asbool(self.parameter("showother", False))
include_chip = tuple(
filter(len, util.format.aslist(self.parameter("chip_include", "")))
)
exclude_chip = tuple(
filter(len, util.format.aslist(self.parameter("chip_exclude", "")))
)
include_field = tuple(
filter(len, util.format.aslist(self.parameter("field_include", "")))
)
exclude_field = tuple(
filter(len, util.format.aslist(self.parameter("field_exclude", "")))
)
include_chip_field = tuple(
filter(len, util.format.aslist(self.parameter("chip_field_include", "")))
)
exclude_chip_field = tuple(
filter(len, util.format.aslist(self.parameter("chip_field_exclude", "")))
)
if util.format.asbool(self.parameter("showcpu", True)):
widget = self.add_widget(full_text=self.__cpu)
widget.set("type", "cpu")
for adapter in self.__data:
if include_chip or exclude_chip:
if include_chip:
if all([chip not in adapter for chip in include_chip]):
continue
else:
if any([chip in adapter for chip in exclude_chip]):
continue
if include_chip_field:
try:
if all(
[i.split(".")[0] not in adapter for i in include_chip_field]
):
continue
except:
pass
for package in self.__data[adapter]:
if util.format.asbool(self.parameter("showname", False)):
widget = self.add_widget(full_text=package)
widget.set("data", self.__data[adapter][package])
widget.set("package", package)
widget.set("field", "")
widget.set("adapter", adapter)
for field in self.__data[adapter][package]:
if include_field or exclude_field:
if include_field:
if all(
[included not in field for included in include_field]
):
continue
else:
if any([excluded in field for excluded in exclude_field]):
continue
try:
if include_chip_field or exclude_chip_field:
if include_chip_field:
if all(
[
i.split(".")[1] not in field
for i in include_chip_field
if i.split(".")[0] in adapter
]
):
continue
else:
if any(
[
i.split(".")[1] in field
for i in exclude_chip_field
if i.split(".")[0] in adapter
]
):
continue
except:
pass
widget = None
if "temp" in field and show_temp:
# seems to be a temperature
widget = self.add_widget()
widget.set("type", "temp")
if "fan" in field and show_fan:
# seems to be a fan
widget = self.add_widget()
widget.set("type", "fan")
elif show_other:
# everything else
widget = self.add_widget()
widget.set("type", "other")
if widget:
widget.set("package", package)
widget.set("field", field)
widget.set("adapter", adapter)
def __update_widget(self, widget):
if widget.get("field", "") == "":
return # nothing to do
data = self.__data[widget.get("adapter")][widget.get("package")][
widget.get("field")
]
if "temp" in widget.get("field"):
widget.full_text("{:0.01f}°C".format(data["input"]))
elif "fan" in widget.get("field"):
widget.full_text("{:0.0f}RPM".format(data["input"]))
else:
widget.full_text("{:0.0f}".format(data["input"]))
def __update(self):
output = util.cli.execute(
"sensors -u {}".format(self.__chip), ignore_errors=True
)
self.__data = self.__parse(output)
def __parse(self, data):
output = {}
package = ""
adapter = None
chip = None
for line in data.split("\n"):
if "Adapter" in line:
# new adapter
line = line.replace("Adapter: ", "")
output[chip + " " + line] = {}
adapter = chip + " " + line
chip = line # default - line before adapter is always the chip
if not adapter:
continue
key, value = (line.split(":") + ["", ""])[:2]
if not line.startswith(" "):
# assume this starts a new package
if package in output[adapter] and output[adapter][package] == {}:
del output[adapter][package]
output[adapter][key] = {}
package = key
else:
# feature for this chip
try:
name, variant = (key.strip().split("_", 1) + ["", ""])[:2]
if not name in output[adapter][package]:
output[adapter][package][name] = {}
if variant:
output[adapter][package][name][variant] = {}
output[adapter][package][name][variant] = float(value)
except Exception as e:
pass
return output
def __cpu(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)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,24 @@
# pylint: disable=C0111,R0903
"""Draws a widget with configurable text content.
Parameters:
* spacer.text: Widget contents (defaults to empty string)
"""
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.text))
self.__text = self.parameter("text", "")
def text(self, _):
return self.__text
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,15 @@
# pylint: disable=C0111,R0903
"""Test module
"""
import core.widget
import core.module
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config=config, theme=theme, widgets=core.widget.Widget("test"))
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

Some files were not shown because too many files have changed in this diff Show more